From 19f93f569a7ff0b4c0649e5e9d446d9dd4401632 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 31 May 2024 00:14:34 +0800 Subject: [PATCH 001/111] complete flow --- fendermint/app/options/src/genesis.rs | 22 + fendermint/app/src/app.rs | 48 +- fendermint/app/src/cmd/genesis.rs | 16 + fendermint/app/src/lib.rs | 2 + fendermint/vm/genesis/src/lib.rs | 1 + .../vm/interpreter/src/fvm/state/genesis.rs | 11 + .../vm/interpreter/src/fvm/state/snapshot.rs | 4 +- fendermint/vm/interpreter/src/genesis.rs | 785 ++++++++++++++++++ fendermint/vm/interpreter/src/lib.rs | 1 + 9 files changed, 846 insertions(+), 44 deletions(-) create mode 100644 fendermint/vm/interpreter/src/genesis.rs diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 3da1794bc..2b8e682a2 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -147,6 +147,28 @@ pub enum GenesisIpcCommands { Gateway(GenesisIpcGatewayArgs), /// Fetch the genesis parameters of a subnet from the parent. FromParent(Box), + /// Seal the genesis state from the genesis parameter file + SealState(SealGenesisArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct SealGenesisArgs { + /// The built in actors bundle path + #[arg(long, short)] + pub builtin_actors_path: PathBuf, + + /// The custom actors bundle path + #[arg(long, short)] + pub custom_actors_path: PathBuf, + + /// The ipc artifacts output path. If you are using ipc-monorepo, it should be the `out` folder + /// of `make build` + #[arg(long, short)] + pub ipc_artifacts_path: PathBuf, + + /// The sealed, i.e. finalized genesis state CAR file dump path + #[arg(long, short)] + pub sealed_genesis_path: PathBuf, } #[derive(Args, Debug, Clone)] diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 81d67073f..f4a734a4f 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize}; use tendermint::abci::request::CheckTxKind; use tendermint::abci::{request, response}; use tracing::instrument; +use fendermint_vm_interpreter::genesis::read_genesis_car; use crate::events::{NewBlock, ProposalProcessed}; use crate::AppExitCode; @@ -136,8 +137,10 @@ where /// Path to the Wasm bundle. /// /// Only loaded once during genesis; later comes from the [`StateTree`]. + #[deprecated] builtin_actors_bundle: PathBuf, /// Path to the custom actor WASM bundle. + #[deprecated] custom_actors_bundle: PathBuf, /// Block height where we should gracefully stop the node halt_height: i64, @@ -447,29 +450,6 @@ where /// Called once upon genesis. async fn init_chain(&self, request: request::InitChain) -> AbciResult { - let bundle = &self.builtin_actors_bundle; - let bundle = std::fs::read(bundle) - .map_err(|e| anyhow!("failed to load builtin bundle CAR from {bundle:?}: {e}"))?; - - let custom_actors_bundle = &self.custom_actors_bundle; - let custom_actors_bundle = std::fs::read(custom_actors_bundle).map_err(|e| { - anyhow!("failed to load custom actor bundle CAR from {custom_actors_bundle:?}: {e}") - })?; - - let state = FvmGenesisState::new( - self.state_store_clone(), - self.multi_engine.clone(), - &bundle, - &custom_actors_bundle, - ) - .await - .context("failed to create genesis state")?; - - tracing::info!( - manifest_root = format!("{}", state.manifest_data_cid), - "pre-genesis state created" - ); - let genesis_bytes = request.app_state_bytes.to_vec(); let genesis_hash = fendermint_vm_message::cid(&genesis_bytes).context("failed to compute genesis CID")?; @@ -477,15 +457,8 @@ where // Make it easy to spot any discrepancies between nodes. tracing::info!(genesis_hash = genesis_hash.to_string(), "genesis"); - let (state, out) = self - .interpreter - .init(state, genesis_bytes) - .await - .context("failed to init from genesis")?; - - let state_root = state.commit().context("failed to commit genesis state")?; - let validators = - to_validator_updates(out.validators).context("failed to convert validators")?; + let (validators, state_params) = read_genesis_car(&genesis_bytes, &self.state_store).await?; + let validators = to_validator_updates(validators).context("failed to convert validators")?; // Let's pretend that the genesis state is that of a fictive block at height 0. // The record will be stored under height 1, and the record after the application @@ -502,16 +475,7 @@ where let app_state = AppState { block_height: height, oldest_state_height: height, - 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, - }, + state_params, }; let response = response::InitChain { diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index 183a89cb7..d188ba6f5 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -14,6 +14,7 @@ use fendermint_vm_genesis::{ ipc, Account, Actor, ActorMeta, Collateral, Genesis, Multisig, PermissionMode, SignerAddr, Validator, ValidatorKey, }; +use fendermint_vm_interpreter::genesis::GenesisCreator; use crate::cmd; use crate::options::genesis::*; @@ -93,6 +94,8 @@ cmd! { set_ipc_gateway(&genesis_file, args), GenesisIpcCommands::FromParent(args) => new_genesis_from_parent(&genesis_file, args).await, + GenesisIpcCommands::SealState(args) => + seal_state(&genesis_file, args).await, } } } @@ -279,6 +282,19 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh }) } +async fn seal_state(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::Result<()> { + let genesis = read_genesis(genesis_file)?; + + let genesis_creator = GenesisCreator::new( + args.builtin_actors_path.clone(), + args.custom_actors_path.clone(), + args.ipc_artifacts_path.clone(), + args.sealed_genesis_path.clone(), + ); + + genesis_creator.create(genesis).await +} + async fn new_genesis_from_parent( genesis_file: &PathBuf, args: &GenesisFromParentArgs, diff --git a/fendermint/app/src/lib.rs b/fendermint/app/src/lib.rs index 29c83a384..d0972423d 100644 --- a/fendermint/app/src/lib.rs +++ b/fendermint/app/src/lib.rs @@ -1,3 +1,5 @@ +extern crate core; + // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT mod app; diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index ae87b4a1d..ab2c1d136 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -7,6 +7,7 @@ use anyhow::anyhow; use fvm_shared::bigint::{BigInt, Integer}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use std::path::PathBuf; use fendermint_actor_eam::PermissionModeParams; use fvm_shared::version::NetworkVersion; diff --git a/fendermint/vm/interpreter/src/fvm/state/genesis.rs b/fendermint/vm/interpreter/src/fvm/state/genesis.rs index f43b6c4fd..da90cbe0a 100644 --- a/fendermint/vm/interpreter/src/fvm/state/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/state/genesis.rs @@ -177,6 +177,17 @@ where } } + /// Flush the data to the block store. Returns the state root cid and the underlying state store. + pub fn finalize(self) -> anyhow::Result<(Cid, DB)> { + match self.stage { + Stage::Tree(_) => Err(anyhow!("invalid finalize state")), + Stage::Exec(exec_state) => match exec_state.commit()? { + (_, _, true) => bail!("FVM parameters are not expected to be updated in genesis"), + (cid, _, _) => Ok((cid, self.store)), + }, + } + } + /// Replaces the built in actor with custom actor. This assumes the system actor is already /// created, else it would throw an error. pub fn replace_builtin_actor( diff --git a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs index 0801ade9e..7f70c5cbc 100644 --- a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs +++ b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs @@ -213,7 +213,7 @@ where } #[pin_project::pin_project] -struct StateTreeStreamer { +pub(crate) struct StateTreeStreamer { /// The list of cids to pull from the blockstore #[pin] dfs: VecDeque, @@ -286,7 +286,7 @@ fn walk_ipld_cids(ipld: Ipld, dfs: &mut VecDeque) { } } -fn derive_cid(t: &T) -> anyhow::Result<(Cid, Vec)> { +pub(crate) fn derive_cid(t: &T) -> anyhow::Result<(Cid, Vec)> { let bytes = fvm_ipld_encoding::to_vec(&t)?; let cid = Cid::new_v1(DAG_CBOR, Code::Blake2b256.digest(&bytes)); Ok((cid, bytes)) diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs new file mode 100644 index 000000000..5b804639d --- /dev/null +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -0,0 +1,785 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::collections::{BTreeSet, HashMap}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; + +use anyhow::{anyhow, Context}; +use cid::Cid; +use ethers::abi::Tokenize; +use ethers::core::types as et; +use fendermint_actor_eam::PermissionModeParams; +use fendermint_eth_hardhat::{Hardhat, FQN}; +use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; +use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; +use fendermint_vm_actor_interface::{ + account, burntfunds, chainmetadata, cron, eam, init, ipc, reward, system, EMPTY_ARR, +}; +use fendermint_vm_core::{chainid, Timestamp}; +use fendermint_vm_genesis::{ActorMeta, Genesis, Power, PowerScale, Validator}; +use fvm::engine::MultiEngine; +use fvm_ipld_blockstore::Blockstore; +use fvm_ipld_car::{CarHeader, CarReader}; +use fvm_ipld_encoding::CborStore; +use fvm_shared::chainid::ChainID; +use fvm_shared::econ::TokenAmount; +use fvm_shared::version::NetworkVersion; +use ipc_actors_abis::i_diamond::FacetCut; +use num_traits::Zero; + +use crate::fvm::state::snapshot::{derive_cid, StateTreeStreamer}; +use crate::fvm::state::{FvmGenesisState, FvmStateParams}; +use crate::fvm::store::memory::MemoryBlockstore; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use tokio_stream::StreamExt; +use tokio_util::compat::TokioAsyncWriteCompatExt; + +/// The sealed genesis state metadata +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct GenesisMetadata { + pub state_params: FvmStateParams, + pub validators: Vec>, +} + +impl GenesisMetadata { + fn new(state_root: Cid, out: FvmGenesisOutput) -> GenesisMetadata { + let 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, + }; + + GenesisMetadata { + state_params, + validators: out.validators, + } + } +} + +pub async fn read_genesis_car(bytes: &[u8], store: &DB) -> anyhow::Result<(Vec>, FvmStateParams)> { + let car_reader = CarReader::new(bytes).await?; + + let roots = car_reader.read_into(store).await?; + + if roots.len() != 1 { + return Err(anyhow!("invalid genesis car, should have 1 root cid")); + } + + let metadata_cid = roots[0]; + let metadata = if let Some(metadata) = store.get_cbor::(&metadata_cid)? { + metadata + } else { + return Err(anyhow!("invalid genesis car, metadata not found")); + }; + + Ok((metadata.validators, metadata.state_params)) +} + +/// The output of genesis creation +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FvmGenesisOutput { + pub chain_id: ChainID, + pub timestamp: Timestamp, + pub network_version: NetworkVersion, + pub base_fee: TokenAmount, + pub power_scale: PowerScale, + pub circ_supply: TokenAmount, + pub validators: Vec>, +} + +pub struct GenesisCreator { + /// Hardhat like util to deploy ipc contracts + hardhat: Hardhat, + /// The built in actors bundle path + builtin_actors_path: PathBuf, + /// The custom actors bundle path + custom_actors_path: PathBuf, + /// The CAR path to flush the sealed genesis state + sealed_out_path: PathBuf, +} + +impl GenesisCreator { + pub fn new( + builtin_actors_path: PathBuf, + custom_actors_path: PathBuf, + ipc_artifacts_path: PathBuf, + sealed_out_path: PathBuf, + ) -> Self { + Self { + hardhat: Hardhat::new(ipc_artifacts_path), + builtin_actors_path, + custom_actors_path, + sealed_out_path, + } + } + /// Initialize actor states from the Genesis parameters + pub async fn create(&self, genesis: Genesis) -> anyhow::Result<()> { + let mut state = self.init_state().await?; + let out = self.populate_state(&mut state, genesis)?; + let (state_root, store) = state.finalize()?; + self.write_car(state_root, out, store).await + } + + async fn write_car( + &self, + state_root: Cid, + out: FvmGenesisOutput, + store: MemoryBlockstore, + ) -> anyhow::Result<()> { + let file = tokio::fs::File::create(&self.sealed_out_path).await?; + + let metadata = GenesisMetadata::new(state_root, out); + + let streamer = StateTreeStreamer::new(state_root, store); + let (metadata_cid, metadata_bytes) = derive_cid(&metadata)?; + + // create the target car header with the metadata cid as the only root + let car = CarHeader::new(vec![metadata_cid], 1); + + // create the stream to stream all the data into the car file + let mut streamer = tokio_stream::iter(vec![(metadata_cid, metadata_bytes)]).merge(streamer); + + let write_task = tokio::spawn(async move { + let mut write = file.compat_write(); + car.write_stream_async(&mut Pin::new(&mut write), &mut streamer) + .await + }); + + write_task.await??; + + Ok(()) + } + + async fn init_state(&self) -> anyhow::Result> { + let bundle = std::fs::read(&self.builtin_actors_path).with_context(|| { + format!( + "failed to read bundle: {}", + self.builtin_actors_path.to_string_lossy() + ) + })?; + + let custom_actors_bundle = std::fs::read(&self.custom_actors_path).with_context(|| { + format!( + "failed to read custom actors_bundle: {}", + self.custom_actors_path.to_string_lossy() + ) + })?; + + let store = MemoryBlockstore::new(); + + FvmGenesisState::new( + store, + Arc::new(MultiEngine::new(1)), + &bundle, + &custom_actors_bundle, + ) + .await + .context("failed to create genesis state") + } + + fn populate_state( + &self, + state: &mut FvmGenesisState, + genesis: Genesis, + ) -> anyhow::Result { + // NOTE: We could consider adding the chain ID to the interpreter + // and rejecting genesis if it doesn't match the expectation, + // but the Tendermint genesis file also has this field, and + // presumably Tendermint checks that its peers have the same. + let chain_id = chainid::from_str_hashed(&genesis.chain_name)?; + + // Convert validators to CometBFT power scale. + let validators = genesis + .validators + .iter() + .cloned() + .map(|vc| vc.map_power(|c| c.into_power(genesis.power_scale))) + .collect(); + + // Currently we just pass them back as they are, but later we should + // store them in the IPC actors; or in case of a snapshot restore them + // from the state. + let out = FvmGenesisOutput { + chain_id, + timestamp: genesis.timestamp, + network_version: genesis.network_version, + circ_supply: circ_supply(&genesis), + base_fee: genesis.base_fee, + power_scale: genesis.power_scale, + validators, + }; + + // STAGE 0: Declare the built-in EVM contracts we'll have to deploy. + + // Pre-defined IDs for top-level Ethereum contracts. + let mut eth_builtin_ids = BTreeSet::new(); + let mut eth_root_contracts = Vec::new(); + let mut eth_contracts = EthContractMap::default(); + + // Only allocate IDs if the contracts are deployed. + if genesis.ipc.is_some() { + eth_contracts.extend(IPC_CONTRACTS.clone()); + } + + eth_builtin_ids.extend(eth_contracts.values().map(|c| c.actor_id)); + eth_root_contracts.extend(eth_contracts.keys()); + eth_root_contracts.extend( + eth_contracts + .values() + .flat_map(|c| c.facets.iter().map(|f| f.name)), + ); + // Collect dependencies of the main IPC actors. + let mut eth_libs = self + .hardhat + .dependencies( + ð_root_contracts + .iter() + .map(|n| (contract_src(n), *n)) + .collect::>(), + ) + .context("failed to collect EVM contract dependencies")?; + + // Only keep library dependencies, not contracts with constructors. + eth_libs.retain(|(_, d)| !eth_contracts.contains_key(d.as_str())); + + // STAGE 1: First we initialize native built-in actors. + + // System actor + state + .create_builtin_actor( + system::SYSTEM_ACTOR_CODE_ID, + system::SYSTEM_ACTOR_ID, + &system::State { + builtin_actors: state.manifest_data_cid, + }, + TokenAmount::zero(), + None, + ) + .context("failed to create system actor")?; + + // Init actor + let (init_state, addr_to_id) = init::State::new( + state.store(), + genesis.chain_name.clone(), + &genesis.accounts, + ð_builtin_ids, + eth_libs.len() as u64, + ) + .context("failed to create init state")?; + + state + .create_builtin_actor( + init::INIT_ACTOR_CODE_ID, + init::INIT_ACTOR_ID, + &init_state, + TokenAmount::zero(), + None, + ) + .context("failed to create init actor")?; + + // Cron actor + state + .create_builtin_actor( + cron::CRON_ACTOR_CODE_ID, + cron::CRON_ACTOR_ID, + &cron::State { + entries: vec![], // TODO: Maybe with the IPC. + }, + TokenAmount::zero(), + None, + ) + .context("failed to create cron actor")?; + + // Ethereum Account Manager (EAM) actor + state + .create_builtin_actor( + eam::EAM_ACTOR_CODE_ID, + eam::EAM_ACTOR_ID, + &EMPTY_ARR, + TokenAmount::zero(), + None, + ) + .context("failed to create EAM actor")?; + + // Burnt funds actor (it's just an account). + state + .create_builtin_actor( + account::ACCOUNT_ACTOR_CODE_ID, + burntfunds::BURNT_FUNDS_ACTOR_ID, + &account::State { + address: burntfunds::BURNT_FUNDS_ACTOR_ADDR, + }, + TokenAmount::zero(), + None, + ) + .context("failed to create burnt funds actor")?; + + // A placeholder for the reward actor, beause I don't think + // using the one in the builtin actors library would be appropriate. + // This effectively burns the miner rewards. Better than panicking. + state + .create_builtin_actor( + account::ACCOUNT_ACTOR_CODE_ID, + reward::REWARD_ACTOR_ID, + &account::State { + address: reward::REWARD_ACTOR_ADDR, + }, + TokenAmount::zero(), + None, + ) + .context("failed to create reward actor")?; + + // STAGE 1b: Then we initialize the in-repo custom actors. + + // Initialize the chain metadata actor which handles saving metadata about the chain + // (e.g. block hashes) which we can query. + let chainmetadata_state = fendermint_actor_chainmetadata::State::new( + &state.store(), + fendermint_actor_chainmetadata::DEFAULT_LOOKBACK_LEN, + )?; + state + .create_custom_actor( + fendermint_actor_chainmetadata::CHAINMETADATA_ACTOR_NAME, + chainmetadata::CHAINMETADATA_ACTOR_ID, + &chainmetadata_state, + TokenAmount::zero(), + None, + ) + .context("failed to create chainmetadata actor")?; + + let eam_state = fendermint_actor_eam::State::new( + state.store(), + PermissionModeParams::from(genesis.eam_permission_mode), + )?; + state + .replace_builtin_actor( + eam::EAM_ACTOR_NAME, + eam::EAM_ACTOR_ID, + fendermint_actor_eam::IPC_EAM_ACTOR_NAME, + &eam_state, + TokenAmount::zero(), + None, + ) + .context("failed to replace built in eam actor")?; + + // STAGE 2: Create non-builtin accounts which do not have a fixed ID. + + // The next ID is going to be _after_ the accounts, which have already been assigned an ID by the `Init` actor. + // The reason we aren't using the `init_state.next_id` is because that already accounted for the multisig accounts. + let mut next_id = init::FIRST_NON_SINGLETON_ADDR + addr_to_id.len() as u64; + + for a in genesis.accounts { + let balance = a.balance; + match a.meta { + ActorMeta::Account(acct) => { + state + .create_account_actor(acct, balance, &addr_to_id) + .context("failed to create account actor")?; + } + ActorMeta::Multisig(ms) => { + state + .create_multisig_actor(ms, balance, &addr_to_id, next_id) + .context("failed to create multisig actor")?; + next_id += 1; + } + } + } + + // STAGE 3: Initialize the FVM and create built-in FEVM actors. + + state + .init_exec_state( + out.timestamp, + out.network_version, + out.base_fee.clone(), + out.circ_supply.clone(), + out.chain_id.into(), + out.power_scale, + ) + .context("failed to init exec state")?; + + let mut deployer = ContractDeployer::::new(&self.hardhat, ð_contracts); + + // Deploy Ethereum libraries. + for (lib_src, lib_name) in eth_libs { + deployer.deploy_library(state, &mut next_id, lib_src, &lib_name)?; + } + + if let Some(ipc_params) = genesis.ipc { + // IPC Gateway actor. + let gateway_addr = { + use ipc::gateway::ConstructorParameters; + + let params = ConstructorParameters::new(ipc_params.gateway, genesis.validators) + .context("failed to create gateway constructor")?; + + let facets = deployer + .facets(ipc::gateway::CONTRACT_NAME) + .context("failed to collect gateway facets")?; + + deployer.deploy_contract(state, ipc::gateway::CONTRACT_NAME, (facets, params))? + }; + + // IPC SubnetRegistry actor. + { + use ipc::registry::ConstructorParameters; + + let mut facets = deployer + .facets(ipc::registry::CONTRACT_NAME) + .context("failed to collect registry facets")?; + + let getter_facet = facets.remove(0); + let manager_facet = facets.remove(0); + let rewarder_facet = facets.remove(0); + let checkpointer_facet = facets.remove(0); + let pauser_facet = facets.remove(0); + let diamond_loupe_facet = facets.remove(0); + let diamond_cut_facet = facets.remove(0); + let ownership_facet = facets.remove(0); + + debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); + + let params = ConstructorParameters { + gateway: gateway_addr, + getter_facet: getter_facet.facet_address, + manager_facet: manager_facet.facet_address, + rewarder_facet: rewarder_facet.facet_address, + pauser_facet: pauser_facet.facet_address, + checkpointer_facet: checkpointer_facet.facet_address, + diamond_cut_facet: diamond_cut_facet.facet_address, + diamond_loupe_facet: diamond_loupe_facet.facet_address, + ownership_facet: ownership_facet.facet_address, + subnet_getter_selectors: getter_facet.function_selectors, + subnet_manager_selectors: manager_facet.function_selectors, + subnet_rewarder_selectors: rewarder_facet.function_selectors, + subnet_checkpointer_selectors: checkpointer_facet.function_selectors, + subnet_pauser_selectors: pauser_facet.function_selectors, + subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, + subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, + subnet_actor_ownership_selectors: ownership_facet.function_selectors, + creation_privileges: 0, + }; + + deployer.deploy_contract(state, ipc::registry::CONTRACT_NAME, (facets, params))?; + }; + } + + Ok(out) + } +} + +fn contract_src(name: &str) -> PathBuf { + PathBuf::from(format!("{name}.sol")) +} + +struct ContractDeployer<'a, DB> { + hardhat: &'a Hardhat, + top_contracts: &'a EthContractMap, + // Assign dynamic ID addresses to libraries, but use fixed addresses for the top level contracts. + lib_addrs: HashMap, + phantom_db: PhantomData, +} + +impl<'a, DB> ContractDeployer<'a, DB> +where + DB: Blockstore + 'static + Clone, +{ + pub fn new(hardhat: &'a Hardhat, top_contracts: &'a EthContractMap) -> Self { + Self { + hardhat, + top_contracts, + lib_addrs: Default::default(), + phantom_db: PhantomData, + } + } + + /// Deploy a library contract with a dynamic ID and no constructor. + pub fn deploy_library( + &mut self, + state: &mut FvmGenesisState, + next_id: &mut u64, + lib_src: impl AsRef, + lib_name: &str, + ) -> anyhow::Result<()> { + let fqn = self.hardhat.fqn(lib_src.as_ref(), lib_name); + + let bytecode = self + .hardhat + .bytecode(&lib_src, lib_name, &self.lib_addrs) + .with_context(|| format!("failed to load library bytecode {fqn}"))?; + + let eth_addr = state + .create_evm_actor(*next_id, bytecode) + .with_context(|| format!("failed to create library actor {fqn}"))?; + + let id_addr = et::Address::from(EthAddress::from_id(*next_id).0); + let eth_addr = et::Address::from(eth_addr.0); + + tracing::info!( + actor_id = next_id, + ?eth_addr, + ?id_addr, + fqn, + "deployed Ethereum library" + ); + + // We can use the masked ID here or the delegated address. + // Maybe the masked ID is quicker because it doesn't need to be resolved. + self.lib_addrs.insert(fqn, id_addr); + + *next_id += 1; + + Ok(()) + } + + /// Construct the bytecode of a top-level contract and deploy it with some constructor parameters. + pub fn deploy_contract( + &self, + state: &mut FvmGenesisState, + contract_name: &str, + constructor_params: T, + ) -> anyhow::Result + where + T: Tokenize, + { + let contract = self.top_contract(contract_name)?; + let contract_id = contract.actor_id; + let contract_src = contract_src(contract_name); + + let bytecode = self + .hardhat + .bytecode(contract_src, contract_name, &self.lib_addrs) + .with_context(|| format!("failed to load {contract_name} bytecode"))?; + + let eth_addr = state + .create_evm_actor_with_cons(contract_id, &contract.abi, bytecode, constructor_params) + .with_context(|| format!("failed to create {contract_name} actor"))?; + + let id_addr = et::Address::from(EthAddress::from_id(contract_id).0); + let eth_addr = et::Address::from(eth_addr.0); + + tracing::info!( + actor_id = contract_id, + ?eth_addr, + ?id_addr, + contract_name, + "deployed Ethereum contract" + ); + + // The Ethereum address is more usable inside the EVM than the ID address. + Ok(eth_addr) + } + + /// Collect Facet Cuts for the diamond pattern, where the facet address comes from already deployed library facets. + pub fn facets(&self, contract_name: &str) -> anyhow::Result> { + let contract = self.top_contract(contract_name)?; + let mut facet_cuts = Vec::new(); + + for facet in contract.facets.iter() { + let facet_name = facet.name; + let facet_src = contract_src(facet_name); + let facet_fqn = self.hardhat.fqn(&facet_src, facet_name); + + let facet_addr = self + .lib_addrs + .get(&facet_fqn) + .ok_or_else(|| anyhow!("facet {facet_name} has not been deployed"))?; + + let method_sigs = facet + .abi + .functions() + .filter(|f| f.signature() != "init(bytes)") + .map(|f| f.short_signature()) + .collect(); + + let facet_cut = FacetCut { + facet_address: *facet_addr, + action: 0, // Add + function_selectors: method_sigs, + }; + + facet_cuts.push(facet_cut); + } + + Ok(facet_cuts) + } + + fn top_contract(&self, contract_name: &str) -> anyhow::Result<&EthContract> { + self.top_contracts + .get(contract_name) + .ok_or_else(|| anyhow!("unknown top contract name: {contract_name}")) + } +} + +/// Sum of balances in the genesis accounts. +fn circ_supply(g: &Genesis) -> TokenAmount { + g.accounts + .iter() + .fold(TokenAmount::zero(), |s, a| s + a.balance.clone()) +} +// +// #[cfg(test)] +// mod tests { +// use std::{str::FromStr, sync::Arc}; +// +// use cid::Cid; +// use fendermint_vm_genesis::{ipc::IpcParams, Genesis}; +// use fvm::engine::MultiEngine; +// use quickcheck::Arbitrary; +// use tendermint_rpc::{MockClient, MockRequestMethodMatcher}; +// +// use crate::{ +// fvm::{ +// bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, +// state::ipc::GatewayCaller, +// store::memory::MemoryBlockstore, +// upgrades::UpgradeScheduler, +// FvmMessageInterpreter, +// }, +// GenesisInterpreter, +// }; +// +// use super::FvmGenesisState; +// +// #[tokio::test] +// async fn load_genesis() { +// let genesis = make_genesis(); +// let bundle = read_bundle(); +// let custom_actors_bundle = read_custom_actors_bundle(); +// let interpreter = make_interpreter(); +// +// let multi_engine = Arc::new(MultiEngine::default()); +// let store = MemoryBlockstore::new(); +// +// let state = FvmGenesisState::new(store, multi_engine, &bundle, &custom_actors_bundle) +// .await +// .expect("failed to create state"); +// +// let (mut state, out) = interpreter +// .init(state, genesis.clone()) +// .await +// .expect("failed to create actors"); +// +// assert_eq!(out.validators.len(), genesis.validators.len()); +// +// // Try calling a method on the IPC Gateway. +// let exec_state = state.exec_state().expect("should be in exec stage"); +// let caller = GatewayCaller::default(); +// +// let period = caller +// .bottom_up_check_period(exec_state) +// .expect("error calling the gateway"); +// +// assert_eq!(period, genesis.ipc.unwrap().gateway.bottom_up_check_period); +// +// let _state_root = state.commit().expect("failed to commit"); +// } +// +// #[tokio::test] +// async fn load_genesis_deterministic() { +// let genesis = make_genesis(); +// let bundle = read_bundle(); +// let custom_actors_bundle = read_custom_actors_bundle(); +// let interpreter = make_interpreter(); +// let multi_engine = Arc::new(MultiEngine::default()); +// +// // Create a couple of states and load the same thing. +// let mut outputs = Vec::new(); +// for _ in 0..3 { +// let store = MemoryBlockstore::new(); +// let state = +// FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) +// .await +// .expect("failed to create state"); +// +// let (state, out) = interpreter +// .init(state, genesis.clone()) +// .await +// .expect("failed to create actors"); +// +// let state_root_hash = state.commit().expect("failed to commit"); +// outputs.push((state_root_hash, out)); +// } +// +// for out in &outputs[1..] { +// assert_eq!(out.0, outputs[0].0, "state root hash is different"); +// } +// } +// +// // This is a sort of canary test, if it fails means something changed in the way we do genesis, +// // which is probably fine, but it's better to know about it, and if anybody doesn't get the same +// // then we might have some non-determinism. +// #[ignore] // I see a different value on CI than locally. +// #[tokio::test] +// async fn load_genesis_known() { +// let genesis_json = "{\"chain_name\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"timestamp\":1073250,\"network_version\":18,\"base_fee\":\"1000\",\"power_scale\":3,\"validators\":[{\"public_key\":\"BLX9ojqB+8Z26aMmKoCRb3Te6AnSU6zY8hPcf1X5Q69XCNaHVcRxzYO2xx7o/2vgdS7nkDTMRRbkDGzy+FYdAFc=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BFcOveVieknZiscWsfXa06aGbBkKeucBycd/w0N1QHlaZfa/5dJcH7D0hvcdfv3B2Rv1OPuxo1PkgsEbWegWKcA=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BEP30ykovfrQp3zo+JVRvDVL2emC+Ju1Kpox3zMVYZyFKvYt64qyN/HOVjridDrkEsnQU8BVen4Aegja4fBZ+LU=\",\"power\":\"1000000000000000000\"}],\"accounts\":[{\"meta\":{\"Account\":{\"owner\":\"f410fggjevhgketpz6gw6ordusynlgcd5piyug4aomuq\"}},\"balance\":\"1000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410frbdnwklaitcjsqe7swjwp5naple6vthq4woyfry\"}},\"balance\":\"2000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410fxo4lih4n2acr3oadalidwqjgoqkzhp5dw3zwkvy\"}},\"balance\":\"1000000000000000000\"}],\"ipc\":{\"gateway\":{\"subnet_id\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"bottom_up_check_period\":30,\"msg_fee\":\"1000000000000\",\"majority_percentage\":60,\"active_validators_limit\":100}}}"; +// +// let genesis: Genesis = serde_json::from_str(genesis_json).expect("failed to parse genesis"); +// +// let bundle = read_bundle(); +// let custom_actors_bundle = read_custom_actors_bundle(); +// let interpreter = make_interpreter(); +// let multi_engine = Arc::new(MultiEngine::default()); +// +// let store = MemoryBlockstore::new(); +// let state = +// FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) +// .await +// .expect("failed to create state"); +// +// let (state, _) = interpreter +// .init(state, genesis.clone()) +// .await +// .expect("failed to create actors"); +// +// let state_root_hash = state.commit().expect("failed to commit"); +// +// let expected_root_hash = +// Cid::from_str("bafy2bzacedebgy4j7qnh2v2x4kkr2jqfkryql5ookbjrwge6dbrr24ytlqnj4") +// .unwrap(); +// +// assert_eq!(state_root_hash, expected_root_hash); +// } +// +// fn make_genesis() -> Genesis { +// let mut g = quickcheck::Gen::new(5); +// let mut genesis = Genesis::arbitrary(&mut g); +// +// // Make sure we have IPC enabled. +// genesis.ipc = Some(IpcParams::arbitrary(&mut g)); +// genesis +// } +// +// fn make_interpreter( +// ) -> FvmMessageInterpreter> { +// let (client, _) = MockClient::new(MockRequestMethodMatcher::default()); +// FvmMessageInterpreter::new( +// client, +// None, +// contracts_path(), +// 1.05, +// 1.05, +// false, +// UpgradeScheduler::new(), +// ) +// } +// +// fn read_bundle() -> Vec { +// std::fs::read(bundle_path()).expect("failed to read bundle") +// } +// +// fn read_custom_actors_bundle() -> Vec { +// std::fs::read(custom_actors_bundle_path()).expect("failed to read custom actor bundle") +// } +// } diff --git a/fendermint/vm/interpreter/src/lib.rs b/fendermint/vm/interpreter/src/lib.rs index 42dab76b8..afa1b43db 100644 --- a/fendermint/vm/interpreter/src/lib.rs +++ b/fendermint/vm/interpreter/src/lib.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; pub mod bytes; pub mod chain; pub mod fvm; +pub mod genesis; pub mod signed; #[cfg(feature = "arb")] From fcdc70fef4fe638d614c2da4d6d4dd2fad162d22 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 3 Jun 2024 11:02:01 +0800 Subject: [PATCH 002/111] convert fendermint to cometbft genesis --- fendermint/app/options/src/genesis.rs | 3 +++ fendermint/app/src/app.rs | 8 +++++--- fendermint/app/src/cmd/genesis.rs | 4 ++-- fendermint/vm/genesis/src/lib.rs | 1 - fendermint/vm/interpreter/src/genesis.rs | 20 +++++++++++++------- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 2b8e682a2..b374824a6 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -133,6 +133,9 @@ pub struct GenesisAddValidatorArgs { #[derive(Args, Debug)] pub struct GenesisIntoTendermintArgs { + /// Sealed genesis file that is the initial app bytes for cometbft + #[arg(long, short)] + pub sealed: PathBuf, /// Output file name for the Tendermint genesis JSON file. #[arg(long, short)] pub out: PathBuf, diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index f4a734a4f..bb8455aef 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -25,6 +25,7 @@ use fendermint_vm_interpreter::fvm::state::{ }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput, PowerUpdates}; +use fendermint_vm_interpreter::genesis::read_genesis_car; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, @@ -42,7 +43,6 @@ use serde::{Deserialize, Serialize}; use tendermint::abci::request::CheckTxKind; use tendermint::abci::{request, response}; use tracing::instrument; -use fendermint_vm_interpreter::genesis::read_genesis_car; use crate::events::{NewBlock, ProposalProcessed}; use crate::AppExitCode; @@ -457,8 +457,10 @@ where // Make it easy to spot any discrepancies between nodes. tracing::info!(genesis_hash = genesis_hash.to_string(), "genesis"); - let (validators, state_params) = read_genesis_car(&genesis_bytes, &self.state_store).await?; - let validators = to_validator_updates(validators).context("failed to convert validators")?; + let (validators, state_params) = + read_genesis_car(&genesis_bytes, &self.state_store).await?; + let validators = + to_validator_updates(validators).context("failed to convert validators")?; // Let's pretend that the genesis state is that of a fictive block at height 0. // The record will be stored under height 1, and the record after the application diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index d188ba6f5..c6e705370 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -215,7 +215,7 @@ fn set_eam_permissions( fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> anyhow::Result<()> { let genesis = read_genesis(genesis_file)?; - let genesis_json = serde_json::to_value(&genesis)?; + let app_state = hex::encode(std::fs::read(&args.sealed)?); let chain_id: u64 = chainid::from_str_hashed(&genesis.chain_name)?.into(); let chain_id = chain_id.to_string(); @@ -250,7 +250,7 @@ fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> // Hopefully leaving this empty will skip validation, // otherwise we have to run the genesis in memory here and now. app_hash: tendermint::AppHash::default(), - app_state: genesis_json, + app_state: serde_json::Value::String(app_state), }; let tmg_json = serde_json::to_string_pretty(&tmg)?; std::fs::write(&args.out, tmg_json)?; diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index ab2c1d136..ae87b4a1d 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -7,7 +7,6 @@ use anyhow::anyhow; use fvm_shared::bigint::{BigInt, Integer}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::path::PathBuf; use fendermint_actor_eam::PermissionModeParams; use fvm_shared::version::NetworkVersion; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 5b804639d..0e8cc9ad2 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -42,13 +42,13 @@ use tokio_util::compat::TokioAsyncWriteCompatExt; /// The sealed genesis state metadata #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -pub struct GenesisMetadata { +struct GenesisMetadata { pub state_params: FvmStateParams, pub validators: Vec>, } impl GenesisMetadata { - fn new(state_root: Cid, out: FvmGenesisOutput) -> GenesisMetadata { + fn new(state_root: Cid, out: GenesisOutput) -> GenesisMetadata { let state_params = FvmStateParams { state_root, timestamp: out.timestamp, @@ -67,7 +67,10 @@ impl GenesisMetadata { } } -pub async fn read_genesis_car(bytes: &[u8], store: &DB) -> anyhow::Result<(Vec>, FvmStateParams)> { +pub async fn read_genesis_car( + bytes: &[u8], + store: &DB, +) -> anyhow::Result<(Vec>, FvmStateParams)> { let car_reader = CarReader::new(bytes).await?; let roots = car_reader.read_into(store).await?; @@ -88,7 +91,7 @@ pub async fn read_genesis_car(bytes: &[u /// The output of genesis creation #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FvmGenesisOutput { +pub struct GenesisOutput { pub chain_id: ChainID, pub timestamp: Timestamp, pub network_version: NetworkVersion, @@ -134,7 +137,7 @@ impl GenesisCreator { async fn write_car( &self, state_root: Cid, - out: FvmGenesisOutput, + out: GenesisOutput, store: MemoryBlockstore, ) -> anyhow::Result<()> { let file = tokio::fs::File::create(&self.sealed_out_path).await?; @@ -143,6 +146,7 @@ impl GenesisCreator { let streamer = StateTreeStreamer::new(state_root, store); let (metadata_cid, metadata_bytes) = derive_cid(&metadata)?; + tracing::info!("generated genesis metadata header cid: {}", metadata_cid); // create the target car header with the metadata cid as the only root let car = CarHeader::new(vec![metadata_cid], 1); @@ -158,6 +162,8 @@ impl GenesisCreator { write_task.await??; + tracing::info!("written sealed genesis state to file"); + Ok(()) } @@ -192,7 +198,7 @@ impl GenesisCreator { &self, state: &mut FvmGenesisState, genesis: Genesis, - ) -> anyhow::Result { + ) -> anyhow::Result { // NOTE: We could consider adding the chain ID to the interpreter // and rejecting genesis if it doesn't match the expectation, // but the Tendermint genesis file also has this field, and @@ -210,7 +216,7 @@ impl GenesisCreator { // Currently we just pass them back as they are, but later we should // store them in the IPC actors; or in case of a snapshot restore them // from the state. - let out = FvmGenesisOutput { + let out = GenesisOutput { chain_id, timestamp: genesis.timestamp, network_version: genesis.network_version, From 3d8f00f29c5bb15d4030ac213e16cc53a2818788 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 3 Jun 2024 11:15:40 +0800 Subject: [PATCH 003/111] remove deprecated fields --- fendermint/app/settings/src/lib.rs | 12 +----------- fendermint/app/src/app.rs | 17 ----------------- fendermint/app/src/cmd/run.rs | 2 -- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/fendermint/app/settings/src/lib.rs b/fendermint/app/settings/src/lib.rs index e83f53c05..e05a1866e 100644 --- a/fendermint/app/settings/src/lib.rs +++ b/fendermint/app/settings/src/lib.rs @@ -262,10 +262,6 @@ pub struct Settings { snapshots_dir: PathBuf, /// Solidity contracts. contracts_dir: PathBuf, - /// Builtin-actors CAR file. - builtin_actors_bundle: PathBuf, - /// Custom actors CAR file. - custom_actors_bundle: PathBuf, /// Where to reach CometBFT for queries or broadcasting transactions. tendermint_rpc_url: Url, @@ -289,13 +285,7 @@ pub struct Settings { } impl Settings { - home_relative!( - data_dir, - snapshots_dir, - contracts_dir, - builtin_actors_bundle, - custom_actors_bundle - ); + home_relative!(data_dir, snapshots_dir, contracts_dir); /// Load the default configuration from a directory, /// then potential overrides specific to the run mode, diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index bb8455aef..c45f40271 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -1,7 +1,6 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT use std::future::Future; -use std::path::PathBuf; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; @@ -107,12 +106,6 @@ pub struct AppConfig { pub state_hist_namespace: S::Namespace, /// Size of state history to keep; 0 means unlimited. pub state_hist_size: u64, - /// Path to the Wasm bundle. - /// - /// Only loaded once during genesis; later comes from the [`StateTree`]. - pub builtin_actors_bundle: PathBuf, - /// Path to the custom actor WASM bundle. - pub custom_actors_bundle: PathBuf, /// Block height where we should gracefully stop the node pub halt_height: i64, } @@ -134,14 +127,6 @@ where state_store: Arc, /// Wasm engine cache. multi_engine: Arc, - /// Path to the Wasm bundle. - /// - /// Only loaded once during genesis; later comes from the [`StateTree`]. - #[deprecated] - builtin_actors_bundle: PathBuf, - /// Path to the custom actor WASM bundle. - #[deprecated] - custom_actors_bundle: PathBuf, /// Block height where we should gracefully stop the node halt_height: i64, /// Namespace to store app state. @@ -195,8 +180,6 @@ where db: Arc::new(db), state_store: Arc::new(state_store), multi_engine: Arc::new(MultiEngine::new(1)), - builtin_actors_bundle: config.builtin_actors_bundle, - custom_actors_bundle: config.custom_actors_bundle, halt_height: config.halt_height, namespace: config.app_namespace, state_hist: KVCollection::new(config.state_hist_namespace), diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index 28f8b9a97..1fcc1d1ab 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -290,8 +290,6 @@ async fn run(settings: Settings) -> anyhow::Result<()> { app_namespace: ns.app, state_hist_namespace: ns.state_hist, state_hist_size: settings.db.state_hist_size, - builtin_actors_bundle: settings.builtin_actors_bundle(), - custom_actors_bundle: settings.custom_actors_bundle(), halt_height: settings.halt_height, }, db, From 8a8efd6023f1f0a6b428221905b18af37818b4ee Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 3 Jun 2024 14:56:00 +0800 Subject: [PATCH 004/111] fix cursor --- fendermint/app/src/app.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index c45f40271..4c07781e1 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -441,7 +441,7 @@ where tracing::info!(genesis_hash = genesis_hash.to_string(), "genesis"); let (validators, state_params) = - read_genesis_car(&genesis_bytes, &self.state_store).await?; + read_genesis_car(genesis_bytes, &self.state_store).await?; let validators = to_validator_updates(validators).context("failed to convert validators")?; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 0e8cc9ad2..b0c5a54b5 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -11,6 +11,7 @@ use anyhow::{anyhow, Context}; use cid::Cid; use ethers::abi::Tokenize; use ethers::core::types as et; +use futures_util::io::Cursor; use fendermint_actor_eam::PermissionModeParams; use fendermint_eth_hardhat::{Hardhat, FQN}; use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; @@ -23,7 +24,7 @@ use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Genesis, Power, PowerScale, Validator}; use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_car::{CarHeader, CarReader}; +use fvm_ipld_car::{CarHeader, load_car}; use fvm_ipld_encoding::CborStore; use fvm_shared::chainid::ChainID; use fvm_shared::econ::TokenAmount; @@ -68,12 +69,10 @@ impl GenesisMetadata { } pub async fn read_genesis_car( - bytes: &[u8], + bytes: Vec, store: &DB, ) -> anyhow::Result<(Vec>, FvmStateParams)> { - let car_reader = CarReader::new(bytes).await?; - - let roots = car_reader.read_into(store).await?; + let roots = load_car(store, Cursor::new(&bytes)).await?; if roots.len() != 1 { return Err(anyhow!("invalid genesis car, should have 1 root cid")); From 3890de409ad7f35b73f3ac8c5812392aca317f2f Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 3 Jun 2024 15:47:42 +0800 Subject: [PATCH 005/111] fix genesis app bytes --- fendermint/app/src/app.rs | 12 +++++++++--- fendermint/vm/interpreter/src/genesis.rs | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 4c07781e1..a89cedb8c 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -373,6 +373,13 @@ where // It's really the empty state tree that would be the best indicator. !(height == 0 && params.timestamp.0 == 0 && params.network_version == NetworkVersion::V0) } + + fn parse_genesis_app_bytes(bytes: &[u8]) -> Result> { + match serde_json::from_slice(bytes)? { + serde_json::Value::String(s) => Ok(hex::decode(s)?), + _ => Err(anyhow!("invalid app state json")), + } + } } // NOTE: The `Application` interface doesn't allow failures at the moment. The protobuf @@ -433,15 +440,14 @@ where /// Called once upon genesis. async fn init_chain(&self, request: request::InitChain) -> AbciResult { - let genesis_bytes = request.app_state_bytes.to_vec(); + let genesis_bytes = Self::parse_genesis_app_bytes(&request.app_state_bytes)?; let genesis_hash = fendermint_vm_message::cid(&genesis_bytes).context("failed to compute genesis CID")?; // Make it easy to spot any discrepancies between nodes. tracing::info!(genesis_hash = genesis_hash.to_string(), "genesis"); - let (validators, state_params) = - read_genesis_car(genesis_bytes, &self.state_store).await?; + let (validators, state_params) = read_genesis_car(genesis_bytes, &self.state_store).await?; let validators = to_validator_updates(validators).context("failed to convert validators")?; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index b0c5a54b5..7ee3edd4f 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -11,7 +11,6 @@ use anyhow::{anyhow, Context}; use cid::Cid; use ethers::abi::Tokenize; use ethers::core::types as et; -use futures_util::io::Cursor; use fendermint_actor_eam::PermissionModeParams; use fendermint_eth_hardhat::{Hardhat, FQN}; use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; @@ -22,9 +21,10 @@ use fendermint_vm_actor_interface::{ }; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Genesis, Power, PowerScale, Validator}; +use futures_util::io::Cursor; use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_car::{CarHeader, load_car}; +use fvm_ipld_car::{load_car, CarHeader}; use fvm_ipld_encoding::CborStore; use fvm_shared::chainid::ChainID; use fvm_shared::econ::TokenAmount; From 14e6deaaeb90e00d0ddd63523d7202430adc47e5 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 3 Jun 2024 22:04:08 +0800 Subject: [PATCH 006/111] fmt --- fendermint/app/options/src/genesis.rs | 4 ++-- fendermint/app/src/cmd/genesis.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index b374824a6..8f4fc0e1f 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -169,9 +169,9 @@ pub struct SealGenesisArgs { #[arg(long, short)] pub ipc_artifacts_path: PathBuf, - /// The sealed, i.e. finalized genesis state CAR file dump path + /// The sealed genesis state output path, i.e. finalized genesis state CAR file dump path #[arg(long, short)] - pub sealed_genesis_path: PathBuf, + pub output_path: PathBuf, } #[derive(Args, Debug, Clone)] diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index c6e705370..ee3fcf150 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -289,7 +289,7 @@ async fn seal_state(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::R args.builtin_actors_path.clone(), args.custom_actors_path.clone(), args.ipc_artifacts_path.clone(), - args.sealed_genesis_path.clone(), + args.output_path.clone(), ); genesis_creator.create(genesis).await diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 7ee3edd4f..a3bc1c54c 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -125,6 +125,7 @@ impl GenesisCreator { sealed_out_path, } } + /// Initialize actor states from the Genesis parameters pub async fn create(&self, genesis: Genesis) -> anyhow::Result<()> { let mut state = self.init_state().await?; From 4dc0482b04e3d73201c85fa48425d877de4eac61 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 4 Jun 2024 16:43:46 +0800 Subject: [PATCH 007/111] minor changes --- fendermint/app/options/src/genesis.rs | 4 ++-- fendermint/app/src/cmd/genesis.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 30 +++++++++++++----------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 8f4fc0e1f..7a115c197 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -164,10 +164,10 @@ pub struct SealGenesisArgs { #[arg(long, short)] pub custom_actors_path: PathBuf, - /// The ipc artifacts output path. If you are using ipc-monorepo, it should be the `out` folder + /// The solidity artifacts output path. If you are using ipc-monorepo, it should be the `out` folder /// of `make build` #[arg(long, short)] - pub ipc_artifacts_path: PathBuf, + pub artifacts_path: PathBuf, /// The sealed genesis state output path, i.e. finalized genesis state CAR file dump path #[arg(long, short)] diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index ee3fcf150..a512cedb3 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -288,7 +288,7 @@ async fn seal_state(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::R let genesis_creator = GenesisCreator::new( args.builtin_actors_path.clone(), args.custom_actors_path.clone(), - args.ipc_artifacts_path.clone(), + args.artifacts_path.clone(), args.output_path.clone(), ); diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index a3bc1c54c..49b177341 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -115,11 +115,11 @@ impl GenesisCreator { pub fn new( builtin_actors_path: PathBuf, custom_actors_path: PathBuf, - ipc_artifacts_path: PathBuf, + artifacts_path: PathBuf, sealed_out_path: PathBuf, ) -> Self { Self { - hardhat: Hardhat::new(ipc_artifacts_path), + hardhat: Hardhat::new(artifacts_path), builtin_actors_path, custom_actors_path, sealed_out_path, @@ -229,19 +229,17 @@ impl GenesisCreator { // STAGE 0: Declare the built-in EVM contracts we'll have to deploy. // Pre-defined IDs for top-level Ethereum contracts. - let mut eth_builtin_ids = BTreeSet::new(); - let mut eth_root_contracts = Vec::new(); - let mut eth_contracts = EthContractMap::default(); + let mut all_contracts = Vec::new(); + let mut top_level_contracts = EthContractMap::default(); // Only allocate IDs if the contracts are deployed. if genesis.ipc.is_some() { - eth_contracts.extend(IPC_CONTRACTS.clone()); + top_level_contracts.extend(IPC_CONTRACTS.clone()); } - eth_builtin_ids.extend(eth_contracts.values().map(|c| c.actor_id)); - eth_root_contracts.extend(eth_contracts.keys()); - eth_root_contracts.extend( - eth_contracts + all_contracts.extend(top_level_contracts.keys()); + all_contracts.extend( + top_level_contracts .values() .flat_map(|c| c.facets.iter().map(|f| f.name)), ); @@ -249,7 +247,7 @@ impl GenesisCreator { let mut eth_libs = self .hardhat .dependencies( - ð_root_contracts + &all_contracts .iter() .map(|n| (contract_src(n), *n)) .collect::>(), @@ -257,7 +255,7 @@ impl GenesisCreator { .context("failed to collect EVM contract dependencies")?; // Only keep library dependencies, not contracts with constructors. - eth_libs.retain(|(_, d)| !eth_contracts.contains_key(d.as_str())); + eth_libs.retain(|(_, d)| !top_level_contracts.contains_key(d.as_str())); // STAGE 1: First we initialize native built-in actors. @@ -279,7 +277,10 @@ impl GenesisCreator { state.store(), genesis.chain_name.clone(), &genesis.accounts, - ð_builtin_ids, + &top_level_contracts + .values() + .map(|c| c.actor_id) + .collect::>(), eth_libs.len() as u64, ) .context("failed to create init state")?; @@ -415,7 +416,8 @@ impl GenesisCreator { ) .context("failed to init exec state")?; - let mut deployer = ContractDeployer::::new(&self.hardhat, ð_contracts); + let mut deployer = + ContractDeployer::::new(&self.hardhat, &top_level_contracts); // Deploy Ethereum libraries. for (lib_src, lib_name) in eth_libs { From 02b183f0e8d286169593e574e1bf1adbc8cfdb6c Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:20:24 +0800 Subject: [PATCH 008/111] Fix cicd (#1023) --- fendermint/testing/smoke-test/scripts/init.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fendermint/testing/smoke-test/scripts/init.sh b/fendermint/testing/smoke-test/scripts/init.sh index 1ccbcc77d..c3fa8d63a 100755 --- a/fendermint/testing/smoke-test/scripts/init.sh +++ b/fendermint/testing/smoke-test/scripts/init.sh @@ -7,6 +7,7 @@ set -e KEYS_DIR=/data/keys CMT_DIR=/data/${NODE_NAME}/cometbft GENESIS_FILE=/data/genesis.json +SEALED_GENESIS_FILE=/data/sealed.car # Create a genesis file fendermint \ @@ -63,10 +64,20 @@ fendermint \ --msg-fee 10 \ --majority-percentage 66 +# Seal the genesis state +fendermint \ + genesis --genesis-file $GENESIS_FILE \ + ipc \ + seal-state \ + --builtin-actors-path /fendermint/bundle.car \ + --custom-actors-path /fendermint/custom_actors_bundle.car \ + --artifacts-path /fendermint/contracts \ + --output-path "${SEALED_GENESIS_FILE}" + # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json + into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" # Convert FM validator key to CMT fendermint \ From d88face4499a23054c8757f9126d5b8dced061b1 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:20:11 +0800 Subject: [PATCH 009/111] Fix cicd (#1025) --- fendermint/testing/graph-test/scripts/init.sh | 13 ++++++++++++- fendermint/testing/snapshot-test/scripts/init.sh | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/fendermint/testing/graph-test/scripts/init.sh b/fendermint/testing/graph-test/scripts/init.sh index 461ccfa3e..8fc376fb1 100755 --- a/fendermint/testing/graph-test/scripts/init.sh +++ b/fendermint/testing/graph-test/scripts/init.sh @@ -7,6 +7,7 @@ set -e KEYS_DIR=/data/keys CMT_DIR=/data/${NODE_NAME}/cometbft GENESIS_FILE=/data/genesis.json +SEALED_GENESIS_FILE=/data/sealed.car # Create a genesis file fendermint \ @@ -47,10 +48,20 @@ fendermint \ genesis --genesis-file $GENESIS_FILE \ add-validator --public-key $KEYS_DIR/$VALIDATOR_NAME.pk --power 1 +# Seal the genesis state +fendermint \ + genesis --genesis-file $GENESIS_FILE \ + ipc \ + seal-state \ + --builtin-actors-path /fendermint/bundle.car \ + --custom-actors-path /fendermint/custom_actors_bundle.car \ + --artifacts-path /fendermint/contracts \ + --output-path "${SEALED_GENESIS_FILE}" + # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json + into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" # Copy the default validator key cp $KEYS_DIR/$VALIDATOR_NAME.priv_validator_key.json \ diff --git a/fendermint/testing/snapshot-test/scripts/init.sh b/fendermint/testing/snapshot-test/scripts/init.sh index 011bbfa75..9e75058d9 100755 --- a/fendermint/testing/snapshot-test/scripts/init.sh +++ b/fendermint/testing/snapshot-test/scripts/init.sh @@ -7,6 +7,7 @@ set -e KEYS_DIR=/data/keys CMT_DIR=/data/${NODE_NAME}/cometbft GENESIS_FILE=/data/genesis.json +SEALED_GENESIS_FILE=/data/sealed.car # Create a genesis file fendermint \ @@ -42,10 +43,20 @@ fendermint \ genesis --genesis-file $GENESIS_FILE \ add-validator --public-key $KEYS_DIR/$VALIDATOR_NAME.pk --power 1 +# Seal the genesis state +fendermint \ + genesis --genesis-file $GENESIS_FILE \ + ipc \ + seal-state \ + --builtin-actors-path /fendermint/bundle.car \ + --custom-actors-path /fendermint/custom_actors_bundle.car \ + --artifacts-path /fendermint/contracts \ + --output-path "${SEALED_GENESIS_FILE}" + # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json + into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" # Copy the default validator key cp $KEYS_DIR/$VALIDATOR_NAME.priv_validator_key.json \ From 2a4d428eec495dee3a6a8f00cdff32a58c3e68d6 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:38:56 +0800 Subject: [PATCH 010/111] Fix cicd (#1027) Co-authored-by: Eric Tu <6364934+ec2@users.noreply.github.com> Co-authored-by: Mikers Co-authored-by: raulk Co-authored-by: Shashank Trivedi <100513286+lordshashank@users.noreply.github.com> --- .github/workflows/contracts-prettier.yaml | 4 +- .github/workflows/contracts-test.yaml | 6 +- Cargo.lock | 2 + contracts/.gitignore | 1 + contracts/package-lock.json | 61 ++++++++++++++++--- contracts/package.json | 29 ++++++--- contracts/remappings.json | 6 +- contracts/remappings.txt | 6 +- contracts/scripts/npm-postpack.js | 11 ++++ contracts/scripts/npm-prepack.js | 16 +++++ contracts/sdk/IpcContractUpgradeable.sol | 2 +- contracts/src/gateway/GatewayManagerFacet.sol | 2 +- .../src/gateway/GatewayMessengerFacet.sol | 2 +- .../gateway/router/TopDownFinalityFacet.sol | 2 +- .../src/gateway/router/XnetMessagingFacet.sol | 2 +- contracts/src/lib/AccountHelper.sol | 2 +- contracts/src/lib/CrossMsgHelper.sol | 2 +- contracts/src/lib/LibGateway.sol | 2 +- contracts/src/lib/LibGatewayActorStorage.sol | 2 +- contracts/test/IntegrationTestBase.sol | 2 +- .../test/integration/GatewayDiamond.t.sol | 2 +- .../integration/GatewayDiamondToken.t.sol | 2 +- .../test/integration/L2GatewayDiamond.t.sol | 2 +- contracts/test/integration/MultiSubnet.t.sol | 2 +- .../test/integration/SubnetActorDiamond.t.sol | 2 +- contracts/test/sdk/IpcContract.t.sol | 2 +- contracts/test/unit/AccountHelper.t.sol | 2 +- fendermint/app/src/app.rs | 2 + fendermint/testing/graph-test/Makefile.toml | 2 +- .../subgraph/graph-node/docker-compose.yaml | 2 + fendermint/testing/materializer/Cargo.toml | 1 + .../testing/materializer/src/docker/node.rs | 18 +++++- .../testing/materializer/tests/docker.rs | 2 +- .../tests/docker_tests/standalone.rs | 4 ++ .../vm/interpreter/src/fvm/store/memory.rs | 10 +-- fendermint/vm/interpreter/src/genesis.rs | 2 + ipc/cli/Cargo.toml | 1 + ipc/cli/src/main.rs | 8 ++- specs/supply-sources.md | 45 ++++++++++++++ 39 files changed, 220 insertions(+), 53 deletions(-) create mode 100644 contracts/scripts/npm-postpack.js create mode 100644 contracts/scripts/npm-prepack.js create mode 100644 specs/supply-sources.md diff --git a/.github/workflows/contracts-prettier.yaml b/.github/workflows/contracts-prettier.yaml index 1dd4e9bb5..b105c791c 100644 --- a/.github/workflows/contracts-prettier.yaml +++ b/.github/workflows/contracts-prettier.yaml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: ${{ github.ref }} - name: Set up node.js uses: actions/setup-node@v4 with: - node-version: '21' + node-version: "21" - name: Run formatter run: cd contracts && make fmt - name: Check diff clean diff --git a/.github/workflows/contracts-test.yaml b/.github/workflows/contracts-test.yaml index 6d52bc31a..4602b13c4 100644 --- a/.github/workflows/contracts-test.yaml +++ b/.github/workflows/contracts-test.yaml @@ -12,19 +12,19 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.ref }} submodules: recursive - name: Install python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: "3.10" - name: Install abi run: pip install eth_abi - name: Install lcov and genhtml - run: sudo apt-get update && sudo apt-get -y install lcov + run: sudo apt-get update && sudo apt-get -y install lcov - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 diff --git a/Cargo.lock b/Cargo.lock index e372d6f95..540f6c41b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3088,6 +3088,7 @@ dependencies = [ "fendermint_vm_core", "fendermint_vm_encoding", "fendermint_vm_genesis", + "fendermint_vm_interpreter", "fendermint_vm_message", "futures", "fvm_shared", @@ -5039,6 +5040,7 @@ dependencies = [ "tokio", "tokio-tungstenite 0.18.0", "toml 0.7.8", + "tracing-subscriber", "url", "zeroize", ] diff --git a/contracts/.gitignore b/contracts/.gitignore index 84ef71d4e..80a64e5f1 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -7,6 +7,7 @@ crytic-export/ broadcast/ out/ binding/src +contracts/ node_modules diff --git a/contracts/package-lock.json b/contracts/package-lock.json index 54ab8eb06..4bd21db3e 100644 --- a/contracts/package-lock.json +++ b/contracts/package-lock.json @@ -1,12 +1,12 @@ { - "name": "ipc-solidity-actors", - "version": "0.0.1", + "name": "@consensus-shipyard/ipc-contracts", + "version": "1.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ipc-solidity-actors", - "version": "0.0.1", + "name": "@consensus-shipyard/ipc-contracts", + "version": "1.0.0-alpha.0", "license": "MIT OR Apache-2.0", "dependencies": { "@solidity-parser/parser": "^0.16.2", @@ -18,10 +18,14 @@ "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.1", "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "5.0.1", "@typechain/ethers-v5": "^11.1.2", "@typechain/hardhat": "^7.0.0", "dotenv": "^16.0.1", + "elliptic-curve-solidity": "github:witnet/elliptic-curve-solidity#3475478", "ethers": "^5.7.0", + "fevmate": "github:wadealexc/fevmate#6a80e98", + "fs-extra": "^11.2.0", "hardhat-contract-sizer": "^2.6.1", "hardhat-deploy": "^0.11.14", "hardhat-deploy-ethers": "^0.3.0-beta.13", @@ -1452,6 +1456,12 @@ "hardhat": "^2.0.0" } }, + "node_modules/@openzeppelin/contracts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.1.tgz", + "integrity": "sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w==", + "dev": true + }, "node_modules/@scure/base": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", @@ -1652,6 +1662,21 @@ "typechain": "^8.2.0" } }, + "node_modules/@typechain/hardhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@types/bn.js": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", @@ -2681,6 +2706,11 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/elliptic-curve-solidity": { + "version": "0.2.5", + "resolved": "git+ssh://git@github.com/witnet/elliptic-curve-solidity.git#347547890840fd501809dfe0b855206407136ec0", + "dev": true + }, "node_modules/elliptic/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", @@ -2932,6 +2962,20 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fevmate": { + "version": "1.0.3", + "resolved": "git+ssh://git@github.com/wadealexc/fevmate.git#6a80e989847fd563df21360176cbfd5d08aaabbb", + "dev": true, + "dependencies": { + "@openzeppelin/contracts": "^4.8.1" + } + }, + "node_modules/fevmate/node_modules/@openzeppelin/contracts": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.6.tgz", + "integrity": "sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==", + "dev": true + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3022,18 +3066,17 @@ "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==" }, "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=14.14" } }, "node_modules/fs.realpath": { diff --git a/contracts/package.json b/contracts/package.json index ee45a946f..4b2b0c497 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,32 +1,47 @@ { - "name": "ipc-solidity-actors", - "version": "0.0.1", + "name": "@consensus-shipyard/ipc-contracts", + "version": "1.0.0-alpha.0", "description": "This repository includes the reference implementation of all the actors (i.e. smart contracts) responsible for the operation of the IPC (i.e. Inter-Planetary Consensus) protocol. These actors are written in Solidity and target FileCoin’s FEVM.", - "main": "index.js", + "author": "ConsensusLab, Protocol Labs, Filecoin Core Devs, Limechain", "directories": { "lib": "lib", "test": "test" }, + "files": [ + "contracts", + "sdk", + "README.md", + "LICENSE-APACHE", + "LICENSE-MIT" + ], "scripts": { + "prepack": "node scripts/npm-prepack.js", + "postpack": "node scripts/npm-postpack.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/consensus-shipyard/ipc-solidity-actors.git" + "url": "git+https://github.com/consensus-shipyard/ipc.git" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" }, - "author": "Limechain team, ConsensusLab, Protocol Labs, Filecoin Core Devs", "license": "MIT OR Apache-2.0", "bugs": { - "url": "https://github.com/consensus-shipyard/ipc-solidity-actors/issues" + "url": "https://github.com/consensus-shipyard/ipc/issues" }, - "homepage": "https://github.com/consensus-shipyard/ipc-solidity-actors/", + "homepage": "https://github.com/consensus-shipyard/ipc/", "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.1", "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "5.0.1", "@typechain/ethers-v5": "^11.1.2", "@typechain/hardhat": "^7.0.0", "dotenv": "^16.0.1", + "elliptic-curve-solidity": "github:witnet/elliptic-curve-solidity#3475478", "ethers": "^5.7.0", + "fevmate": "github:wadealexc/fevmate#6a80e98", + "fs-extra": "^11.2.0", "hardhat-contract-sizer": "^2.6.1", "hardhat-deploy": "^0.11.14", "hardhat-deploy-ethers": "^0.3.0-beta.13", diff --git a/contracts/remappings.json b/contracts/remappings.json index dcad5f3e6..00b36a5a8 100644 --- a/contracts/remappings.json +++ b/contracts/remappings.json @@ -3,8 +3,8 @@ "ds-test/=lib/forge-std/lib/ds-test/src/", "forge-std/=lib/forge-std/src/", "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/", - "fevmate=lib/fevmate/contracts", - "murky/=lib/murky/src", - "elliptic-curve-solidity/=lib/elliptic-curve-solidity" + "fevmate/=lib/fevmate/", + "murky/=lib/murky/src/", + "elliptic-curve-solidity/=lib/elliptic-curve-solidity/" ] } diff --git a/contracts/remappings.txt b/contracts/remappings.txt index c3e074384..1725e0403 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,6 +1,6 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ -fevmate=lib/fevmate/contracts -murky/=lib/murky/src -elliptic-curve-solidity/=lib/elliptic-curve-solidity \ No newline at end of file +fevmate/=lib/fevmate/ +murky/=lib/murky/src/ +elliptic-curve-solidity/=lib/elliptic-curve-solidity/ diff --git a/contracts/scripts/npm-postpack.js b/contracts/scripts/npm-postpack.js new file mode 100644 index 000000000..4388051b6 --- /dev/null +++ b/contracts/scripts/npm-postpack.js @@ -0,0 +1,11 @@ +// A cleanup script after npm pack is done. +// This script is plugged into the npm build through package.json scripts. +const fs = require('fs-extra') +const path = require('path') + +const contractsDir = path.join(__dirname, '../contracts') + +// Make sure the destination directory is empty and exists +fs.remove(contractsDir) + +console.log('Cleanup after pack is done.') diff --git a/contracts/scripts/npm-prepack.js b/contracts/scripts/npm-prepack.js new file mode 100644 index 000000000..1de1318a5 --- /dev/null +++ b/contracts/scripts/npm-prepack.js @@ -0,0 +1,16 @@ +// A script to place contracts under the conventional contracts/ directory before publishing. +// This script is plugged into the npm build through package.json scripts. +const fs = require('fs-extra') +const path = require('path') + +// Define source and destination directories +const srcDir = path.join(__dirname, '../src') +const contractsDir = path.join(__dirname, '../contracts') + +// Make sure the destination directory is empty and exists +fs.emptyDirSync(contractsDir) + +// Copy from src to contracts +fs.copySync(srcDir, contractsDir, { overwrite: true }) + +console.log('Preparation for pack is done.') diff --git a/contracts/sdk/IpcContractUpgradeable.sol b/contracts/sdk/IpcContractUpgradeable.sol index bbf30434d..4b58f07dd 100644 --- a/contracts/sdk/IpcContractUpgradeable.sol +++ b/contracts/sdk/IpcContractUpgradeable.sol @@ -29,7 +29,7 @@ abstract contract IpcExchangeUpgradeable is Initializable, IIpcHandler, OwnableU function __IpcExchangeUpgradeable_init(address gatewayAddr_) public onlyInitializing { gatewayAddr = gatewayAddr_; - __Ownable_init(); + __Ownable_init(msg.sender); __ReentrancyGuard_init(); } diff --git a/contracts/src/gateway/GatewayManagerFacet.sol b/contracts/src/gateway/GatewayManagerFacet.sol index bab0a72ba..e8a434830 100644 --- a/contracts/src/gateway/GatewayManagerFacet.sol +++ b/contracts/src/gateway/GatewayManagerFacet.sol @@ -12,7 +12,7 @@ import {AlreadyRegisteredSubnet, CannotReleaseZero, MethodNotAllowed, NotEnoughF import {LibGateway} from "../lib/LibGateway.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; diff --git a/contracts/src/gateway/GatewayMessengerFacet.sol b/contracts/src/gateway/GatewayMessengerFacet.sol index 7f410f28e..4739b4eb7 100644 --- a/contracts/src/gateway/GatewayMessengerFacet.sol +++ b/contracts/src/gateway/GatewayMessengerFacet.sol @@ -8,7 +8,7 @@ import {SubnetID, SupplyKind, IPCAddress} from "../structs/Subnet.sol"; import {InvalidXnetMessage, InvalidXnetMessageReason, CannotSendCrossMsgToItself, MethodNotAllowed} from "../errors/IPCErrors.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {LibGateway} from "../lib/LibGateway.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {SupplySourceHelper} from "../lib/SupplySourceHelper.sol"; import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; import {FvmAddressHelper} from "../lib/FvmAddressHelper.sol"; diff --git a/contracts/src/gateway/router/TopDownFinalityFacet.sol b/contracts/src/gateway/router/TopDownFinalityFacet.sol index 3e1088793..b29f7e203 100644 --- a/contracts/src/gateway/router/TopDownFinalityFacet.sol +++ b/contracts/src/gateway/router/TopDownFinalityFacet.sol @@ -6,7 +6,7 @@ import {ParentFinality} from "../../structs/CrossNet.sol"; import {PermissionMode, Validator, ValidatorInfo, StakingChangeRequest, Membership} from "../../structs/Subnet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {ParentValidatorsTracker, ValidatorSet} from "../../structs/Subnet.sol"; import {LibValidatorTracking, LibValidatorSet} from "../../lib/LibStaking.sol"; diff --git a/contracts/src/gateway/router/XnetMessagingFacet.sol b/contracts/src/gateway/router/XnetMessagingFacet.sol index 6f3424287..9504def4f 100644 --- a/contracts/src/gateway/router/XnetMessagingFacet.sol +++ b/contracts/src/gateway/router/XnetMessagingFacet.sol @@ -8,7 +8,7 @@ import {IPCMsgType} from "../../enums/IPCMsgType.sol"; import {SubnetActorGetterFacet} from "../../subnet/SubnetActorGetterFacet.sol"; import {Subnet} from "../../structs/Subnet.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {SupplySourceHelper} from "../../lib/SupplySourceHelper.sol"; diff --git a/contracts/src/lib/AccountHelper.sol b/contracts/src/lib/AccountHelper.sol index 8757c860e..12212b381 100644 --- a/contracts/src/lib/AccountHelper.sol +++ b/contracts/src/lib/AccountHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; /// @title Helper library for checking account type /// @author LimeChain team diff --git a/contracts/src/lib/CrossMsgHelper.sol b/contracts/src/lib/CrossMsgHelper.sol index d9b12b97c..05a9a4782 100644 --- a/contracts/src/lib/CrossMsgHelper.sol +++ b/contracts/src/lib/CrossMsgHelper.sol @@ -8,7 +8,7 @@ import {SubnetID, IPCAddress} from "../structs/Subnet.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {FvmAddressHelper} from "../lib/FvmAddressHelper.sol"; import {FvmAddress} from "../structs/FvmAddress.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {SupplySource} from "../structs/Subnet.sol"; import {SupplySourceHelper} from "./SupplySourceHelper.sol"; diff --git a/contracts/src/lib/LibGateway.sol b/contracts/src/lib/LibGateway.sol index 81a0d9a17..3713658a2 100644 --- a/contracts/src/lib/LibGateway.sol +++ b/contracts/src/lib/LibGateway.sol @@ -10,7 +10,7 @@ import {CallMsg, IpcMsgKind, IpcEnvelope, OutcomeType, BottomUpMsgBatch, BottomU import {Membership} from "../structs/Subnet.sol"; import {CannotSendCrossMsgToItself, MethodNotAllowed, MaxMsgsPerBatchExceeded, InvalidXnetMessage ,OldConfigurationNumber, NotRegisteredSubnet, InvalidActorAddress, ParentFinalityAlreadyCommitted, InvalidXnetMessageReason} from "../errors/IPCErrors.sol"; import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SupplySourceHelper} from "../lib/SupplySourceHelper.sol"; diff --git a/contracts/src/lib/LibGatewayActorStorage.sol b/contracts/src/lib/LibGatewayActorStorage.sol index 64108acf2..a1888cc00 100644 --- a/contracts/src/lib/LibGatewayActorStorage.sol +++ b/contracts/src/lib/LibGatewayActorStorage.sol @@ -7,7 +7,7 @@ import {BottomUpCheckpoint, BottomUpMsgBatch, IpcEnvelope, ParentFinality} from import {SubnetID, Subnet, ParentValidatorsTracker} from "../structs/Subnet.sol"; import {Membership} from "../structs/Subnet.sol"; import {AccountHelper} from "../lib/AccountHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; struct GatewayActorStorage { diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 6fb050add..093b5c650 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -13,7 +13,7 @@ import {SubnetID, SupplyKind, PermissionMode, PermissionMode, Subnet, SupplySour import {SubnetIDHelper} from "../src/lib/SubnetIDHelper.sol"; import {FvmAddressHelper} from "../src/lib/FvmAddressHelper.sol"; import {CrossMsgHelper} from "../src/lib/CrossMsgHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {GatewayDiamond} from "../src/GatewayDiamond.sol"; import {SubnetActorDiamond} from "../src/SubnetActorDiamond.sol"; import {GatewayGetterFacet} from "../src/gateway/GatewayGetterFacet.sol"; diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index aa33a321f..04f6da90c 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -18,7 +18,7 @@ import {SubnetID, Subnet, IPCAddress, Validator, StakingChange, StakingChangeReq import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol"; import {FvmAddressHelper} from "../../src/lib/FvmAddressHelper.sol"; import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {GatewayDiamond, FunctionNotFound} from "../../src/GatewayDiamond.sol"; import {GatewayGetterFacet} from "../../src/gateway/GatewayGetterFacet.sol"; import {GatewayManagerFacet} from "../../src/gateway/GatewayManagerFacet.sol"; diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 17ae01c5f..f119fedc7 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -13,7 +13,7 @@ import {FvmAddressHelper} from "../../src/lib/FvmAddressHelper.sol"; import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol"; import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol"; import {SupplySourceHelper} from "../../src/lib/SupplySourceHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {GatewayDiamond} from "../../src/GatewayDiamond.sol"; import {LibGateway} from "../../src/lib/LibGateway.sol"; import {MockIpcContract, TestUtils} from "../helpers/TestUtils.sol"; diff --git a/contracts/test/integration/L2GatewayDiamond.t.sol b/contracts/test/integration/L2GatewayDiamond.t.sol index f230d8725..4008b9245 100644 --- a/contracts/test/integration/L2GatewayDiamond.t.sol +++ b/contracts/test/integration/L2GatewayDiamond.t.sol @@ -21,7 +21,7 @@ import {DiamondCutFacet} from "../../src/diamond/DiamondCutFacet.sol"; import {IntegrationTestBase} from "../IntegrationTestBase.sol"; import {L2GatewayActorDiamond} from "../IntegrationTestPresets.sol"; import {TestUtils} from "../helpers/TestUtils.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 670785ab9..764f383e0 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -29,7 +29,7 @@ import {DiamondCutFacet} from "../../src/diamond/DiamondCutFacet.sol"; import {IntegrationTestBase, RootSubnetDefinition, TestSubnetDefinition} from "../IntegrationTestBase.sol"; import {L2GatewayActorDiamond, L1GatewayActorDiamond} from "../IntegrationTestPresets.sol"; import {TestUtils, MockIpcContract, MockIpcContractPayable, MockIpcContractRevert, MockIpcContractFallback} from "../helpers/TestUtils.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 5b64ee7c8..73442ba87 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -30,7 +30,7 @@ import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol" import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; import {SubnetActorRewardFacet} from "../../src/subnet/SubnetActorRewardFacet.sol"; import {DiamondCutFacet} from "../../src/diamond/DiamondCutFacet.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {LibStaking} from "../../src/lib/LibStaking.sol"; import {LibDiamond} from "../../src/lib/LibDiamond.sol"; import {Pausable} from "../../src/lib/LibPausable.sol"; diff --git a/contracts/test/sdk/IpcContract.t.sol b/contracts/test/sdk/IpcContract.t.sol index c5d791d01..18f31c727 100644 --- a/contracts/test/sdk/IpcContract.t.sol +++ b/contracts/test/sdk/IpcContract.t.sol @@ -11,7 +11,7 @@ import {SubnetID, Subnet, IPCAddress, Validator} from "../../src/structs/Subnet. import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol"; import {FvmAddressHelper} from "../../src/lib/FvmAddressHelper.sol"; import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol"; -import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {IpcExchange} from "../../sdk/IpcContract.sol"; import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol"; import {IGateway} from "../../src/interfaces/IGateway.sol"; diff --git a/contracts/test/unit/AccountHelper.t.sol b/contracts/test/unit/AccountHelper.t.sol index 017c97bdf..f0e8ffc3e 100644 --- a/contracts/test/unit/AccountHelper.t.sol +++ b/contracts/test/unit/AccountHelper.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import "forge-std/Test.sol"; -import "fevmate/utils/FilAddress.sol"; +import "fevmate/contracts/utils/FilAddress.sol"; import "../../src/lib/AccountHelper.sol"; contract AccountHelperTest is Test { diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index a89cedb8c..df6a3e524 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -451,6 +451,8 @@ where let validators = to_validator_updates(validators).context("failed to convert validators")?; + tracing::info!(state_params = serde_json::to_string(&state_params)?); + // Let's pretend that the genesis state is that of a fictive block at height 0. // The record will be stored under height 1, and the record after the application // of the actual block 1 will be at height 2, so they are distinct records. diff --git a/fendermint/testing/graph-test/Makefile.toml b/fendermint/testing/graph-test/Makefile.toml index f47b7c4bb..3fe2c024e 100644 --- a/fendermint/testing/graph-test/Makefile.toml +++ b/fendermint/testing/graph-test/Makefile.toml @@ -57,7 +57,7 @@ npm run graph-node echo "Waiting for Graph node to start..." # Notice that subgraph node does takes a while to spin up -sleep 30s +sleep 30 npm run create-local npm run deploy-local diff --git a/fendermint/testing/graph-test/subgraph/graph-node/docker-compose.yaml b/fendermint/testing/graph-test/subgraph/graph-node/docker-compose.yaml index 4888cd5d4..8b0d49e4a 100644 --- a/fendermint/testing/graph-test/subgraph/graph-node/docker-compose.yaml +++ b/fendermint/testing/graph-test/subgraph/graph-node/docker-compose.yaml @@ -2,6 +2,7 @@ version: '3' services: graph-node: image: graphprotocol/graph-node:v0.27.0 + platform: linux/amd64 ports: - '8000:8000' - '8001:8001' @@ -24,6 +25,7 @@ services: GRAPH_ETHEREUM_GENESIS_BLOCK_NUMBER: 1 ipfs: image: ipfs/go-ipfs:v0.10.0 + platform: linux/amd64 ports: - '5001:5001' postgres: diff --git a/fendermint/testing/materializer/Cargo.toml b/fendermint/testing/materializer/Cargo.toml index b43b09702..fabce48d2 100644 --- a/fendermint/testing/materializer/Cargo.toml +++ b/fendermint/testing/materializer/Cargo.toml @@ -44,6 +44,7 @@ fendermint_vm_core = { path = "../../vm/core" } fendermint_vm_genesis = { path = "../../vm/genesis" } fendermint_vm_encoding = { path = "../../vm/encoding" } fendermint_vm_message = { path = "../../vm/message" } +fendermint_vm_interpreter = { path = "../../vm/interpreter" } fendermint_testing = { path = "..", optional = true } diff --git a/fendermint/testing/materializer/src/docker/node.rs b/fendermint/testing/materializer/src/docker/node.rs index f64035a72..cfa9c79b9 100644 --- a/fendermint/testing/materializer/src/docker/node.rs +++ b/fendermint/testing/materializer/src/docker/node.rs @@ -205,6 +205,21 @@ impl DockerNode { export_file(keys_dir.join(COMETBFT_NODE_ID), cometbft_node_id)?; + fendermint_runner + .run_cmd( + "genesis \ + --genesis-file /fendermint/genesis.json \ + ipc \ + seal-state \ + --builtin-actors-path /fendermint/bundle.car \ + --custom-actors-path /fendermint/custom_actors_bundle.car \ + --artifacts-path /fendermint/contracts \ + --output-path /cometbft/config/sealed.json \ + ", + ) + .await + .context("failed to seal genesis state")?; + // Convert fendermint genesis to cometbft. fendermint_runner .run_cmd( @@ -212,6 +227,7 @@ impl DockerNode { --genesis-file /fendermint/genesis.json \ into-tendermint \ --out /cometbft/config/genesis.json \ + --sealed /cometbft/config/sealed.json \ ", ) .await @@ -511,7 +527,7 @@ impl DockerNode { } if let Some(client) = self.ethapi_http_provider()? { - if let Err(e) = client.get_chainid().await { + if let Err(e) = client.get_block(1).await { continue; } } diff --git a/fendermint/testing/materializer/tests/docker.rs b/fendermint/testing/materializer/tests/docker.rs index c6910f8cc..04038a183 100644 --- a/fendermint/testing/materializer/tests/docker.rs +++ b/fendermint/testing/materializer/tests/docker.rs @@ -166,7 +166,7 @@ async fn wait_for_startup(testnet: &DockerTestnet) -> anyhow::Result { } if let Some(client) = dnode.ethapi_http_provider()? { - if let Err(e) = client.get_chainid().await { + if let Err(e) = client.get_block(1).await { eprintln!("EthAPI on {name} still fails: {e}"); continue 'startup; } diff --git a/fendermint/testing/materializer/tests/docker_tests/standalone.rs b/fendermint/testing/materializer/tests/docker_tests/standalone.rs index 38b1fd05d..805943889 100644 --- a/fendermint/testing/materializer/tests/docker_tests/standalone.rs +++ b/fendermint/testing/materializer/tests/docker_tests/standalone.rs @@ -66,6 +66,8 @@ async fn test_sent_tx_found_in_mempool() { .await .context("failed to set up middleware")?; + eprintln!("middleware ready, pending tests"); + // Create the simplest transaction possible: send tokens between accounts. let to: H160 = charlie.eth_addr().into(); let transfer = Eip1559TransactionRequest::new().to(to).value(1); @@ -77,6 +79,8 @@ async fn test_sent_tx_found_in_mempool() { let tx_hash = pending.tx_hash(); + eprintln!("sent pending txn {:?}", tx_hash); + // We expect that the transaction is pending, however it should not return an error. match middleware.get_transaction(tx_hash).await { Ok(Some(_)) => {} diff --git a/fendermint/vm/interpreter/src/fvm/store/memory.rs b/fendermint/vm/interpreter/src/fvm/store/memory.rs index d49b9eb3b..9ad8a4d86 100644 --- a/fendermint/vm/interpreter/src/fvm/store/memory.rs +++ b/fendermint/vm/interpreter/src/fvm/store/memory.rs @@ -24,11 +24,6 @@ impl MemoryBlockstore { } impl Blockstore for MemoryBlockstore { - fn has(&self, k: &Cid) -> Result { - let guard = self.blocks.read().unwrap(); - Ok(guard.contains_key(k)) - } - fn get(&self, k: &Cid) -> Result>> { let guard = self.blocks.read().unwrap(); Ok(guard.get(k).cloned()) @@ -39,4 +34,9 @@ impl Blockstore for MemoryBlockstore { guard.insert(*k, block.into()); Ok(()) } + + fn has(&self, k: &Cid) -> Result { + let guard = self.blocks.read().unwrap(); + Ok(guard.contains_key(k)) + } } diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 49b177341..951bec82a 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -142,6 +142,8 @@ impl GenesisCreator { ) -> anyhow::Result<()> { let file = tokio::fs::File::create(&self.sealed_out_path).await?; + tracing::info!(state_root = state_root.to_string(), "state root"); + let metadata = GenesisMetadata::new(state_root, out); let streamer = StateTreeStreamer::new(state_root, store); diff --git a/ipc/cli/Cargo.toml b/ipc/cli/Cargo.toml index 9aae53cf5..6dc4e71d5 100644 --- a/ipc/cli/Cargo.toml +++ b/ipc/cli/Cargo.toml @@ -46,3 +46,4 @@ ipc-wallet = { workspace = true } ipc-provider = { workspace = true } ipc-api = { workspace = true } ipc-types = { workspace = true } +tracing-subscriber.workspace = true diff --git a/ipc/cli/src/main.rs b/ipc/cli/src/main.rs index 8d183e364..fc6a154ba 100644 --- a/ipc/cli/src/main.rs +++ b/ipc/cli/src/main.rs @@ -1,9 +1,15 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: MIT +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, EnvFilter}; + #[tokio::main] async fn main() { - env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .init(); if let Err(e) = ipc_cli::cli().await { log::error!("main process failed: {e:#}"); diff --git a/specs/supply-sources.md b/specs/supply-sources.md new file mode 100644 index 000000000..418d901fe --- /dev/null +++ b/specs/supply-sources.md @@ -0,0 +1,45 @@ +# Supply Sources + +## Subnet native coin + +Every subnet has a native coin. This is the economic unit that’s passed when sending value in transactions, and it’s used to settle gas payments (both burns and premiums). This coin is externally defined by configuring the “supply source” at the parent, and the subnet itself has no internal knowledge of what the supply is, nor does it need to (thanks to IPC’s decoupled architecture). + +## Supply sources + +The `SupplySource` of a subnet is configured at subnet creation time. It determines which currency in the parent chain will turn into the native circulating supply of the subnet. The `SupplySource` can’t be modified at the later stage, so make sure you choose it wisely. + +It can be one of two kinds (as per the `SupplyKind` enum): + +- `Native` (default) +- `ERC20` + +The `Native` supply source indicates that the subnet adopts the native coin of the parent as its own native circulating supply currency. `Native` has transitive properties. + +- if an L2 subnet is anchored to *Filecoin* as its root chain, the subnet’s currency will be *FIL*. +- if an L3 subnet is anchored to an L2 whose circulating supply is *FIL*, the L3 will adopt *FIL* too (transitive property). + +Conversely, the `ERC20` supply source indicates that the subnet adopts an arbitrary ERC20-compliant token residing at the parent as its own native circulating supply currency. Consequently, the total supply of the subnet is controlled by the ERC20 token at the parent. This choice is highly desirable when the developer wants to kickstart a custom cryptoeconomy within their subnet. + +When specifying the `ERC20` supply kind, the `SupplySource` must specify `tokenAddress` - the address of the smart contract implementing the ERC20 interface. This token must exist at subnet creation time. Counterfactual token deployments are disallowed. + +## Implementation notes + +### **Creating subnet with ERC20 `SupplySource`** + +In order to create a subnet with non-default `SupplySource` , in the IPC CLI `subnet create` command the subnet owner needs to specify: + +- `--supply-source-kind erc20` +- `--supply_source_address
` + +In order to make sure the specified contract exists `[SupplySourceHelper#validate](https://github.com/consensus-shipyard/ipc/blob/main/contracts/src/lib/SupplySourceHelper.sol#L29)` function is called which executed basic sanity check using `[IERC20#balanceOf](https://github.com/consensus-shipyard/ipc/blob/main/contracts/src/lib/SupplySourceHelper.sol#L38)` which reverts if the contract doesn’t exist or doesn’t implement `balanceOf`. + +### Sending funds to the subnet depending on the `SupplySource` + +Funds can be sent to the subnet’s receiver address using + +- `GatewayManagerFacet#fund(subnetID, to)` if the `Native` supply source is used +- `GatewayManagerFacet#fundWithToken(subnetID, to, amount)` if the `ERC20` supply source is used + +`GatewayManagerFacet#fundWithToken` locks a specified amount into custody using `IERC20#safeTransferFrom`. + +Both `fund` and `fundWithToken` functions commit a top-down message to be processed by the subnet in order to increase the recipient’s balance. From 01c8bf1224ee2b06a598812b44544dd65bad551b Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 6 Aug 2024 00:04:41 +0800 Subject: [PATCH 011/111] add block gas limit --- Cargo.lock | 20 ++++ Cargo.toml | 1 + fendermint/actors/gas/Cargo.toml | 34 +++++++ fendermint/actors/gas/src/lib.rs | 157 +++++++++++++++++++++++++++++++ fendermint/vm/core/src/gas.rs | 16 ++++ fendermint/vm/core/src/lib.rs | 1 + 6 files changed, 229 insertions(+) create mode 100644 fendermint/actors/gas/Cargo.toml create mode 100644 fendermint/actors/gas/src/lib.rs create mode 100644 fendermint/vm/core/src/gas.rs diff --git a/Cargo.lock b/Cargo.lock index ecebaa242..8c289c5cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2835,6 +2835,26 @@ dependencies = [ "serde", ] +[[package]] +name = "fendermint_actor_gas" +version = "0.1.0" +dependencies = [ + "anyhow", + "cid", + "fil_actors_evm_shared", + "fil_actors_runtime", + "frc42_dispatch", + "fvm_ipld_blockstore", + "fvm_ipld_encoding", + "fvm_shared", + "hex-literal 0.4.1", + "log", + "multihash 0.18.1", + "num-derive 0.3.3", + "num-traits", + "serde", +] + [[package]] name = "fendermint_actors" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bb28ffb43..b01e0d35a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "fendermint/vm/*", "fendermint/actors", "fendermint/actors/chainmetadata", + "fendermint/actors/gas", ] [workspace.package] diff --git a/fendermint/actors/gas/Cargo.toml b/fendermint/actors/gas/Cargo.toml new file mode 100644 index 000000000..b6d551181 --- /dev/null +++ b/fendermint/actors/gas/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "fendermint_actor_gas" +description = "Builtin transaction gas tracking actor for IPC" +license.workspace = true +edition.workspace = true +authors.workspace = true +version = "0.1.0" + +[lib] +## lib is necessary for integration tests +## cdylib is necessary for Wasm build +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = { workspace = true } +cid = { workspace = true } +fil_actors_runtime = { workspace = true } +fvm_ipld_blockstore = { workspace = true } +fvm_ipld_encoding = { workspace = true } +fvm_shared = { workspace = true } +log = { workspace = true } +multihash = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true } +hex-literal = { workspace = true } +frc42_dispatch = { workspace = true } + +[dev-dependencies] +fil_actors_evm_shared = { workspace = true } +fil_actors_runtime = { workspace = true, features = ["test_utils"] } + +[features] +fil-actor = ["fil_actors_runtime/fil-actor"] diff --git a/fendermint/actors/gas/src/lib.rs b/fendermint/actors/gas/src/lib.rs new file mode 100644 index 000000000..f88dd89c4 --- /dev/null +++ b/fendermint/actors/gas/src/lib.rs @@ -0,0 +1,157 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fil_actors_runtime::runtime::{ActorCode, Runtime}; +use fil_actors_runtime::ActorError; +use fil_actors_runtime::SYSTEM_ACTOR_ADDR; +use fvm_ipld_blockstore::Blockstore; +use fvm_ipld_encoding::ipld_block::IpldBlock; +use fvm_ipld_encoding::tuple::*; +use fvm_shared::{MethodNum, METHOD_CONSTRUCTOR}; +use num_derive::FromPrimitive; + +#[cfg(feature = "fil-actor")] +fil_actors_runtime::wasm_trampoline!(IPCEamActor); + +pub const IPC_GAS_ACTOR_NAME: &str = "gas"; + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct State { + block_gas_limit: Gas, +} + +pub struct IPCGasActor; + +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum Method { + Constructor = METHOD_CONSTRUCTOR, + SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), + BlockGasLimit = frc42_dispatch::method_hash!("BlockGasLimit"), +} + +type Gas = u64; + +impl IPCGasActor { + /// Creates the actor + pub fn constructor(rt: &impl Runtime, args: ConstructorParams) -> Result<(), ActorError> { + let st = State { + block_gas_limit: args.block_gas_limit, + }; + rt.create(&st)?; + + Ok(()) + } + + fn set_block_gas_limit(rt: &impl Runtime, limit: Gas) -> Result<(), ActorError> { + if rt.message().caller() != SYSTEM_ACTOR_ADDR { + return Err(ActorError::forbidden("not system actor".into())); + } + + rt.transaction(|st: &mut State, _rt| { + st.block_gas_limit = limit; + Ok(()) + })?; + + Ok(()) + } + + fn block_gas_limit(rt: &impl Runtime) -> Result { + Ok(rt.state::()?.block_gas_limit) + } +} + +impl ActorCode for IPCGasActor { + type Methods = Method; + + fn name() -> &'static str { + IPC_GAS_ACTOR_NAME + } + + fn invoke_method( + rt: &RT, + method: MethodNum, + params: Option, + ) -> Result, ActorError> + where + RT: Runtime, + RT::Blockstore: Blockstore + Clone, + { + if method == Method::Constructor as u64 { + fil_actors_runtime::dispatch(rt, method, Self::constructor, params) + } else if method == Method::SetBlockGasLimit as u64 { + fil_actors_runtime::dispatch(rt, method, Self::set_block_gas_limit, params) + } else if method == Method::BlockGasLimit as u64 { + fil_actors_runtime::dispatch(rt, method, Self::block_gas_limit, params) + } else { + Err(ActorError::not_found("method not found".into())) + } + } +} + +#[derive(Debug, Serialize_tuple, Deserialize_tuple)] +pub struct ConstructorParams { + block_gas_limit: Gas, +} + +#[cfg(test)] +mod tests { + use crate::{ConstructorParams, IPCGasActor, Method, State}; + use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; + use fil_actors_runtime::SYSTEM_ACTOR_ADDR; + use fvm_ipld_encoding::ipld_block::IpldBlock; + use fvm_shared::address::Address; + use fvm_shared::error::ExitCode; + + pub fn construct_and_verify() -> MockRuntime { + let rt = MockRuntime { + receiver: Address::new_id(10), + ..Default::default() + }; + + let result = rt + .call::( + Method::Constructor as u64, + IpldBlock::serialize_cbor(&ConstructorParams { + block_gas_limit: 100, + }) + .unwrap(), + ) + .unwrap(); + expect_empty(result); + rt.verify(); + rt.reset(); + + rt + } + + #[test] + fn test_set_ok() { + let rt = construct_and_verify(); + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + + let r = rt.call::( + Method::SetBlockGasLimit as u64, + IpldBlock::serialize_cbor(&20).unwrap(), + ); + assert!(r.is_ok()); + + let s = rt.get_state::(); + assert_eq!(s.block_gas_limit, 20); + } + + #[test] + fn test_not_allowed() { + let rt = construct_and_verify(); + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, Address::new_id(1000)); + + let code = rt + .call::( + Method::SetBlockGasLimit as u64, + IpldBlock::serialize_cbor(&20).unwrap(), + ) + .unwrap_err() + .exit_code(); + assert_eq!(code, ExitCode::USR_FORBIDDEN) + } +} diff --git a/fendermint/vm/core/src/gas.rs b/fendermint/vm/core/src/gas.rs new file mode 100644 index 000000000..ad791417a --- /dev/null +++ b/fendermint/vm/core/src/gas.rs @@ -0,0 +1,16 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +pub type Gas = u64; + +/// Handles the gas modeling in the current blockchain +pub trait GasLayer { + /// The state of the blockchain + type State; + + /// Update the block gas limit + fn set_block_gas_limit(&self, state: &mut Self::State); + + /// Obtain the current block gas limit + fn block_gas_limit(&self, state: &Self::State) -> Gas; +} \ No newline at end of file diff --git a/fendermint/vm/core/src/lib.rs b/fendermint/vm/core/src/lib.rs index b1ca80e3f..4ef87e8cf 100644 --- a/fendermint/vm/core/src/lib.rs +++ b/fendermint/vm/core/src/lib.rs @@ -2,5 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT pub mod chainid; mod timestamp; +pub mod gas; pub use timestamp::Timestamp; From dd2832d20a338b13ad6c0252941d492fa916b25a Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 6 Aug 2024 16:48:40 +0800 Subject: [PATCH 012/111] check gas limit in deliver --- Cargo.lock | 1 + fendermint/actors/gas/src/lib.rs | 20 ++-- fendermint/vm/actor_interface/src/gas.rs | 4 + fendermint/vm/actor_interface/src/lib.rs | 1 + fendermint/vm/core/src/gas.rs | 16 --- fendermint/vm/core/src/lib.rs | 1 - fendermint/vm/interpreter/Cargo.toml | 1 + fendermint/vm/interpreter/src/fvm/exec.rs | 7 +- .../vm/interpreter/src/fvm/gas/default.rs | 108 ++++++++++++++++++ fendermint/vm/interpreter/src/fvm/gas/mod.rs | 19 +++ fendermint/vm/interpreter/src/fvm/mod.rs | 4 + .../vm/interpreter/src/fvm/state/exec.rs | 20 +++- .../vm/interpreter/src/fvm/state/mod.rs | 1 + 13 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 fendermint/vm/actor_interface/src/gas.rs delete mode 100644 fendermint/vm/core/src/gas.rs create mode 100644 fendermint/vm/interpreter/src/fvm/gas/default.rs create mode 100644 fendermint/vm/interpreter/src/fvm/gas/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8c289c5cf..31c05f03f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3343,6 +3343,7 @@ dependencies = [ "ethers", "fendermint_actor_chainmetadata", "fendermint_actor_eam", + "fendermint_actor_gas", "fendermint_actors", "fendermint_crypto", "fendermint_eth_hardhat", diff --git a/fendermint/actors/gas/src/lib.rs b/fendermint/actors/gas/src/lib.rs index f88dd89c4..f65a710ff 100644 --- a/fendermint/actors/gas/src/lib.rs +++ b/fendermint/actors/gas/src/lib.rs @@ -14,24 +14,22 @@ use num_derive::FromPrimitive; fil_actors_runtime::wasm_trampoline!(IPCEamActor); pub const IPC_GAS_ACTOR_NAME: &str = "gas"; +pub type Gas = u64; + +pub struct IPCGasActor; #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct State { block_gas_limit: Gas, } -pub struct IPCGasActor; - #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), - BlockGasLimit = frc42_dispatch::method_hash!("BlockGasLimit"), } -type Gas = u64; - impl IPCGasActor { /// Creates the actor pub fn constructor(rt: &impl Runtime, args: ConstructorParams) -> Result<(), ActorError> { @@ -55,10 +53,6 @@ impl IPCGasActor { Ok(()) } - - fn block_gas_limit(rt: &impl Runtime) -> Result { - Ok(rt.state::()?.block_gas_limit) - } } impl ActorCode for IPCGasActor { @@ -81,8 +75,6 @@ impl ActorCode for IPCGasActor { fil_actors_runtime::dispatch(rt, method, Self::constructor, params) } else if method == Method::SetBlockGasLimit as u64 { fil_actors_runtime::dispatch(rt, method, Self::set_block_gas_limit, params) - } else if method == Method::BlockGasLimit as u64 { - fil_actors_runtime::dispatch(rt, method, Self::block_gas_limit, params) } else { Err(ActorError::not_found("method not found".into())) } @@ -94,6 +86,12 @@ pub struct ConstructorParams { block_gas_limit: Gas, } +impl State { + pub fn block_gas_limit(&self) -> Gas { + self.block_gas_limit + } +} + #[cfg(test)] mod tests { use crate::{ConstructorParams, IPCGasActor, Method, State}; diff --git a/fendermint/vm/actor_interface/src/gas.rs b/fendermint/vm/actor_interface/src/gas.rs new file mode 100644 index 000000000..75b430124 --- /dev/null +++ b/fendermint/vm/actor_interface/src/gas.rs @@ -0,0 +1,4 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +define_id!(GAS { id: 66 }); diff --git a/fendermint/vm/actor_interface/src/lib.rs b/fendermint/vm/actor_interface/src/lib.rs index 2e25c1885..eb9d0569d 100644 --- a/fendermint/vm/actor_interface/src/lib.rs +++ b/fendermint/vm/actor_interface/src/lib.rs @@ -50,6 +50,7 @@ pub mod diamond; pub mod eam; pub mod ethaccount; pub mod evm; +pub mod gas; pub mod init; pub mod ipc; pub mod multisig; diff --git a/fendermint/vm/core/src/gas.rs b/fendermint/vm/core/src/gas.rs deleted file mode 100644 index ad791417a..000000000 --- a/fendermint/vm/core/src/gas.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -pub type Gas = u64; - -/// Handles the gas modeling in the current blockchain -pub trait GasLayer { - /// The state of the blockchain - type State; - - /// Update the block gas limit - fn set_block_gas_limit(&self, state: &mut Self::State); - - /// Obtain the current block gas limit - fn block_gas_limit(&self, state: &Self::State) -> Gas; -} \ No newline at end of file diff --git a/fendermint/vm/core/src/lib.rs b/fendermint/vm/core/src/lib.rs index 4ef87e8cf..b1ca80e3f 100644 --- a/fendermint/vm/core/src/lib.rs +++ b/fendermint/vm/core/src/lib.rs @@ -2,6 +2,5 @@ // SPDX-License-Identifier: Apache-2.0, MIT pub mod chainid; mod timestamp; -pub mod gas; pub use timestamp::Timestamp; diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 63e591da8..73269b0f8 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -23,6 +23,7 @@ fendermint_rpc = { path = "../../rpc" } fendermint_tracing = { path = "../../tracing" } fendermint_actors = { path = "../../actors" } fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" } +fendermint_actor_gas = { path = "../../actors/gas" } fendermint_actor_eam = { workspace = true } fendermint_testing = { path = "../../testing", optional = true } ipc_actors_abis = { workspace = true } diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index f53e630a9..e677df587 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -57,6 +57,8 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); + self.gas.reset_block_gas_quota(&mut state)?; + // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); let block_height: u64 = state.block_height().try_into().unwrap(); @@ -149,7 +151,7 @@ where async fn deliver( &self, mut state: Self::State, - msg: Self::Message, + mut msg: Self::Message, ) -> anyhow::Result<(Self::State, Self::DeliverOutput)> { let (apply_ret, emitters, latency) = if msg.from == system::SYSTEM_ACTOR_ADDR { let (execution_result, latency) = measure_time(|| state.execute_implicit(msg.clone())); @@ -157,9 +159,12 @@ where (apply_ret, emitters, latency) } else { + msg.gas_limit = msg.gas_limit.min(self.gas.available_block_gas()); + let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; + self.gas.deduct_block_gas_quota(apply_ret.msg_receipt.gas_used)?; (apply_ret, emitters, latency) }; diff --git a/fendermint/vm/interpreter/src/fvm/gas/default.rs b/fendermint/vm/interpreter/src/fvm/gas/default.rs new file mode 100644 index 000000000..1964c64ea --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/gas/default.rs @@ -0,0 +1,108 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::gas::GasLayer; +use crate::fvm::state::{read_actor_state, FvmExecState}; +use crate::fvm::FvmMessage; +use anyhow::anyhow; +use fendermint_actor_gas::{Gas, State}; +use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; +use fendermint_vm_actor_interface::system; +use fvm_ipld_blockstore::Blockstore; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicU64, Ordering}; + +type AtomicGas = AtomicU64; + +pub struct DefaultGas { + /// The total gas available to be used by transactions in the current block + /// The executor requires Send + Sync, using an atomic variable instead of u64. + block_gas_quota: AtomicGas, + p: PhantomData, +} + +impl DefaultGas { + pub fn new() -> Self { + Self { + block_gas_quota: AtomicGas::new(0), + p: Default::default(), + } + } + + pub fn reset_block_gas_quota(&self, state: &mut ::State) -> anyhow::Result<()> { + let old_limit = self.block_gas_quota.load(Ordering::SeqCst); + self.atomic_set_block_gas_quota(old_limit, self.block_gas_limit(state)?)?; + Ok(()) + } + + pub fn available_block_gas(&self) -> Gas { + self.block_gas_quota.load(Ordering::SeqCst) + } + + pub fn deduct_block_gas_quota(&self, gas: Gas) -> anyhow::Result<()> { + let quota = self.block_gas_quota.load(Ordering::SeqCst); + match quota.checked_sub(gas) { + Some(v) => { + self.atomic_set_block_gas_quota(quota, v)?; + Ok(()) + } + None => Err(anyhow!("out of block gas")), + } + } + + fn atomic_set_block_gas_quota(&self, old: Gas, new: Gas) -> anyhow::Result<()> { + self.block_gas_quota.compare_exchange( + old, + new, + Ordering::SeqCst, + Ordering::SeqCst + ) + .map_err(|_| anyhow!("concurrent update to block gas available, should not happen"))?; + Ok(()) + } +} + +impl GasLayer for DefaultGas +where + DB: Blockstore + Clone + 'static, +{ + type State = FvmExecState; + + fn set_block_gas_limit(&self, state: &mut Self::State, limit: Gas) -> anyhow::Result<()> { + let params = fvm_ipld_encoding::RawBytes::serialize(limit)?; + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_ACTOR_ADDR, + sequence: state.block_height() as u64, + // exclude this from gas restriction + gas_limit: u64::MAX, + method_num: fendermint_actor_gas::Method::SetBlockGasLimit as u64, + params, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let (apply_ret, _) = state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to update block gas limit: {}", err) + } else { + Ok(()) + } + } + + fn block_gas_limit(&self, state: &Self::State) -> anyhow::Result { + let s = + read_actor_state::(state, fendermint_vm_actor_interface::gas::GAS_ACTOR_ID)?; + Ok(s.block_gas_limit()) + } +} + +impl Clone for DefaultGas { + fn clone(&self) -> Self { + Self { block_gas_quota: AtomicU64::new(self.block_gas_quota.load(Ordering::SeqCst)), p: Default::default() } + } +} \ No newline at end of file diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs new file mode 100644 index 000000000..890217825 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fendermint_actor_gas::Gas; + +pub(crate) mod default; + +/// Handles the gas modeling in the current blockchain +pub trait GasLayer { + /// The state of the blockchain + type State; + + /// Update the block gas limit. This will only take effect in the next block. + #[allow(dead_code)] + fn set_block_gas_limit(&self, state: &mut Self::State, limit: Gas) -> anyhow::Result<()>; + + /// Obtain the current block gas limit + fn block_gas_limit(&self, state: &Self::State) -> anyhow::Result; +} diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 0aefb4b2e..cd4eb57c3 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -16,8 +16,10 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; +pub(crate) mod gas; pub(crate) mod topdown; +use crate::fvm::gas::default::DefaultGas; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -82,6 +84,7 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, + gas: DefaultGas, } impl FvmMessageInterpreter @@ -107,6 +110,7 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, + gas: DefaultGas::new(), } } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 7d5f10dda..03794e221 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -15,11 +15,12 @@ use fvm::{ DefaultKernel, }; use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::RawBytes; +use fvm_ipld_encoding::{CborStore, RawBytes}; use fvm_shared::{ address::Address, chainid::ChainID, clock::ChainEpoch, econ::TokenAmount, error::ExitCode, message::Message, receipt::Receipt, version::NetworkVersion, ActorID, }; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -350,3 +351,20 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { }; (ret, Default::default()) } + +pub(crate) fn read_actor_state( + state: &FvmExecState, + actor_id: ActorID, +) -> anyhow::Result { + let state_tree = state.state_tree(); + + let state_cid = state_tree + .get_actor(actor_id)? + .ok_or_else(|| anyhow::anyhow!("actor state not found: {}", actor_id))? + .state; + + Ok(state_tree + .store() + .get_cbor::(&state_cid)? + .ok_or_else(|| anyhow::anyhow!("actor state should not be null"))?) +} diff --git a/fendermint/vm/interpreter/src/fvm/state/mod.rs b/fendermint/vm/interpreter/src/fvm/state/mod.rs index 22a1504e4..43098aa24 100644 --- a/fendermint/vm/interpreter/src/fvm/state/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/state/mod.rs @@ -12,6 +12,7 @@ pub mod snapshot; use std::sync::Arc; pub use check::FvmCheckState; +pub(crate) use exec::read_actor_state; pub use exec::{BlockHash, FvmExecState, FvmStateParams, FvmUpdatableParams}; pub use genesis::{empty_state_tree, FvmGenesisState}; pub use query::FvmQueryState; From 2ea920379315e54a3fbcb9a79ef8d6b183340541 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 7 Aug 2024 11:51:10 +0800 Subject: [PATCH 013/111] review feedback --- Cargo.lock | 4 +- Cargo.toml | 2 +- fendermint/actors/gas/src/lib.rs | 155 ------------------ .../actors/{gas => gas_market}/Cargo.toml | 2 +- fendermint/actors/gas_market/src/lib.rs | 140 ++++++++++++++++ fendermint/vm/actor_interface/src/gas.rs | 2 +- fendermint/vm/interpreter/Cargo.toml | 2 +- fendermint/vm/interpreter/src/fvm/exec.rs | 6 +- .../vm/interpreter/src/fvm/gas/default.rs | 108 ------------ fendermint/vm/interpreter/src/fvm/gas/mod.rs | 116 +++++++++++-- fendermint/vm/interpreter/src/fvm/mod.rs | 6 +- 11 files changed, 259 insertions(+), 284 deletions(-) delete mode 100644 fendermint/actors/gas/src/lib.rs rename fendermint/actors/{gas => gas_market}/Cargo.toml (96%) create mode 100644 fendermint/actors/gas_market/src/lib.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/gas/default.rs diff --git a/Cargo.lock b/Cargo.lock index 31c05f03f..7579620d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2836,7 +2836,7 @@ dependencies = [ ] [[package]] -name = "fendermint_actor_gas" +name = "fendermint_actor_gas_market" version = "0.1.0" dependencies = [ "anyhow", @@ -3343,7 +3343,7 @@ dependencies = [ "ethers", "fendermint_actor_chainmetadata", "fendermint_actor_eam", - "fendermint_actor_gas", + "fendermint_actor_gas_market", "fendermint_actors", "fendermint_crypto", "fendermint_eth_hardhat", diff --git a/Cargo.toml b/Cargo.toml index b01e0d35a..8de32ca16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ "fendermint/vm/*", "fendermint/actors", "fendermint/actors/chainmetadata", - "fendermint/actors/gas", + "fendermint/actors/gas_market", ] [workspace.package] diff --git a/fendermint/actors/gas/src/lib.rs b/fendermint/actors/gas/src/lib.rs deleted file mode 100644 index f65a710ff..000000000 --- a/fendermint/actors/gas/src/lib.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2021-2023 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use fil_actors_runtime::runtime::{ActorCode, Runtime}; -use fil_actors_runtime::ActorError; -use fil_actors_runtime::SYSTEM_ACTOR_ADDR; -use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::ipld_block::IpldBlock; -use fvm_ipld_encoding::tuple::*; -use fvm_shared::{MethodNum, METHOD_CONSTRUCTOR}; -use num_derive::FromPrimitive; - -#[cfg(feature = "fil-actor")] -fil_actors_runtime::wasm_trampoline!(IPCEamActor); - -pub const IPC_GAS_ACTOR_NAME: &str = "gas"; -pub type Gas = u64; - -pub struct IPCGasActor; - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct State { - block_gas_limit: Gas, -} - -#[derive(FromPrimitive)] -#[repr(u64)] -pub enum Method { - Constructor = METHOD_CONSTRUCTOR, - SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), -} - -impl IPCGasActor { - /// Creates the actor - pub fn constructor(rt: &impl Runtime, args: ConstructorParams) -> Result<(), ActorError> { - let st = State { - block_gas_limit: args.block_gas_limit, - }; - rt.create(&st)?; - - Ok(()) - } - - fn set_block_gas_limit(rt: &impl Runtime, limit: Gas) -> Result<(), ActorError> { - if rt.message().caller() != SYSTEM_ACTOR_ADDR { - return Err(ActorError::forbidden("not system actor".into())); - } - - rt.transaction(|st: &mut State, _rt| { - st.block_gas_limit = limit; - Ok(()) - })?; - - Ok(()) - } -} - -impl ActorCode for IPCGasActor { - type Methods = Method; - - fn name() -> &'static str { - IPC_GAS_ACTOR_NAME - } - - fn invoke_method( - rt: &RT, - method: MethodNum, - params: Option, - ) -> Result, ActorError> - where - RT: Runtime, - RT::Blockstore: Blockstore + Clone, - { - if method == Method::Constructor as u64 { - fil_actors_runtime::dispatch(rt, method, Self::constructor, params) - } else if method == Method::SetBlockGasLimit as u64 { - fil_actors_runtime::dispatch(rt, method, Self::set_block_gas_limit, params) - } else { - Err(ActorError::not_found("method not found".into())) - } - } -} - -#[derive(Debug, Serialize_tuple, Deserialize_tuple)] -pub struct ConstructorParams { - block_gas_limit: Gas, -} - -impl State { - pub fn block_gas_limit(&self) -> Gas { - self.block_gas_limit - } -} - -#[cfg(test)] -mod tests { - use crate::{ConstructorParams, IPCGasActor, Method, State}; - use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; - use fil_actors_runtime::SYSTEM_ACTOR_ADDR; - use fvm_ipld_encoding::ipld_block::IpldBlock; - use fvm_shared::address::Address; - use fvm_shared::error::ExitCode; - - pub fn construct_and_verify() -> MockRuntime { - let rt = MockRuntime { - receiver: Address::new_id(10), - ..Default::default() - }; - - let result = rt - .call::( - Method::Constructor as u64, - IpldBlock::serialize_cbor(&ConstructorParams { - block_gas_limit: 100, - }) - .unwrap(), - ) - .unwrap(); - expect_empty(result); - rt.verify(); - rt.reset(); - - rt - } - - #[test] - fn test_set_ok() { - let rt = construct_and_verify(); - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - - let r = rt.call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), - ); - assert!(r.is_ok()); - - let s = rt.get_state::(); - assert_eq!(s.block_gas_limit, 20); - } - - #[test] - fn test_not_allowed() { - let rt = construct_and_verify(); - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, Address::new_id(1000)); - - let code = rt - .call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), - ) - .unwrap_err() - .exit_code(); - assert_eq!(code, ExitCode::USR_FORBIDDEN) - } -} diff --git a/fendermint/actors/gas/Cargo.toml b/fendermint/actors/gas_market/Cargo.toml similarity index 96% rename from fendermint/actors/gas/Cargo.toml rename to fendermint/actors/gas_market/Cargo.toml index b6d551181..a2ff9f0eb 100644 --- a/fendermint/actors/gas/Cargo.toml +++ b/fendermint/actors/gas_market/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fendermint_actor_gas" +name = "fendermint_actor_gas_market" description = "Builtin transaction gas tracking actor for IPC" license.workspace = true edition.workspace = true diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs new file mode 100644 index 000000000..8b0e25118 --- /dev/null +++ b/fendermint/actors/gas_market/src/lib.rs @@ -0,0 +1,140 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fil_actors_runtime::actor_error; +use fil_actors_runtime::runtime::{ActorCode, Runtime}; +use fil_actors_runtime::SYSTEM_ACTOR_ADDR; +use fil_actors_runtime::{actor_dispatch, ActorError}; +use fvm_ipld_encoding::tuple::*; +use fvm_shared::METHOD_CONSTRUCTOR; +use num_derive::FromPrimitive; + +#[cfg(feature = "fil-actor")] +fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); + +pub const IPC_GAS_ACTOR_NAME: &str = "gas"; + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct EIP1559GasState { + pub block_gas_limit: u64, +} + +pub struct EIP1559GasMarketActor {} + +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum Method { + Constructor = METHOD_CONSTRUCTOR, + UpdateGasMarketState = frc42_dispatch::method_hash!("UpdateGasMarketState"), +} + +impl EIP1559GasMarketActor { + /// Creates the actor + pub fn constructor(rt: &impl Runtime, st: EIP1559GasState) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + rt.create(&st)?; + + Ok(()) + } + + fn update_gas_market_state( + rt: &impl Runtime, + new_state: EIP1559GasState, + ) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut EIP1559GasState, _rt| { + *st = new_state; + Ok(()) + })?; + + Ok(()) + } +} + +impl ActorCode for EIP1559GasMarketActor { + type Methods = Method; + + fn name() -> &'static str { + IPC_GAS_ACTOR_NAME + } + + actor_dispatch! { + Constructor => constructor, + UpdateGasMarketState => update_gas_market_state, + } +} + +#[cfg(test)] +mod tests { + use crate::{EIP1559GasMarketActor, EIP1559GasState, Method}; + use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; + use fil_actors_runtime::SYSTEM_ACTOR_ADDR; + use fvm_ipld_encoding::ipld_block::IpldBlock; + use fvm_shared::address::Address; + use fvm_shared::error::ExitCode; + + pub fn construct_and_verify() -> MockRuntime { + let rt = MockRuntime { + receiver: Address::new_id(10), + ..Default::default() + }; + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let result = rt + .call::( + Method::Constructor as u64, + IpldBlock::serialize_cbor(&EIP1559GasState { + block_gas_limit: 100, + }) + .unwrap(), + ) + .unwrap(); + expect_empty(result); + rt.verify(); + rt.reset(); + + rt + } + + #[test] + fn test_set_ok() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateGasMarketState as u64, + IpldBlock::serialize_cbor(&EIP1559GasState { + block_gas_limit: 20, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + let s = rt.get_state::(); + assert_eq!(s.block_gas_limit, 20); + } + + #[test] + fn test_not_allowed() { + let rt = construct_and_verify(); + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, Address::new_id(1000)); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let code = rt + .call::( + Method::UpdateGasMarketState as u64, + IpldBlock::serialize_cbor(&EIP1559GasState { + block_gas_limit: 20, + }) + .unwrap(), + ) + .unwrap_err() + .exit_code(); + assert_eq!(code, ExitCode::USR_FORBIDDEN) + } +} diff --git a/fendermint/vm/actor_interface/src/gas.rs b/fendermint/vm/actor_interface/src/gas.rs index 75b430124..3fae569a7 100644 --- a/fendermint/vm/actor_interface/src/gas.rs +++ b/fendermint/vm/actor_interface/src/gas.rs @@ -1,4 +1,4 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -define_id!(GAS { id: 66 }); +define_id!(GAS { id: 98 }); diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 73269b0f8..401d8141c 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -23,7 +23,7 @@ fendermint_rpc = { path = "../../rpc" } fendermint_tracing = { path = "../../tracing" } fendermint_actors = { path = "../../actors" } fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" } -fendermint_actor_gas = { path = "../../actors/gas" } +fendermint_actor_gas_market = { path = "../../actors/gas_market" } fendermint_actor_eam = { workspace = true } fendermint_testing = { path = "../../testing", optional = true } ipc_actors_abis = { workspace = true } diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index e677df587..249bceed8 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -57,7 +57,7 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); - self.gas.reset_block_gas_quota(&mut state)?; + self.gas.reset_from_chain_state(&state)?; // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); @@ -159,12 +159,14 @@ where (apply_ret, emitters, latency) } else { + // TODO: maybe compare the gas limits are better? msg.gas_limit = msg.gas_limit.min(self.gas.available_block_gas()); let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - self.gas.deduct_block_gas_quota(apply_ret.msg_receipt.gas_used)?; + self.gas + .deduct_available_block_gas(apply_ret.msg_receipt.gas_used)?; (apply_ret, emitters, latency) }; diff --git a/fendermint/vm/interpreter/src/fvm/gas/default.rs b/fendermint/vm/interpreter/src/fvm/gas/default.rs deleted file mode 100644 index 1964c64ea..000000000 --- a/fendermint/vm/interpreter/src/fvm/gas/default.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::gas::GasLayer; -use crate::fvm::state::{read_actor_state, FvmExecState}; -use crate::fvm::FvmMessage; -use anyhow::anyhow; -use fendermint_actor_gas::{Gas, State}; -use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; -use fendermint_vm_actor_interface::system; -use fvm_ipld_blockstore::Blockstore; -use std::marker::PhantomData; -use std::sync::atomic::{AtomicU64, Ordering}; - -type AtomicGas = AtomicU64; - -pub struct DefaultGas { - /// The total gas available to be used by transactions in the current block - /// The executor requires Send + Sync, using an atomic variable instead of u64. - block_gas_quota: AtomicGas, - p: PhantomData, -} - -impl DefaultGas { - pub fn new() -> Self { - Self { - block_gas_quota: AtomicGas::new(0), - p: Default::default(), - } - } - - pub fn reset_block_gas_quota(&self, state: &mut ::State) -> anyhow::Result<()> { - let old_limit = self.block_gas_quota.load(Ordering::SeqCst); - self.atomic_set_block_gas_quota(old_limit, self.block_gas_limit(state)?)?; - Ok(()) - } - - pub fn available_block_gas(&self) -> Gas { - self.block_gas_quota.load(Ordering::SeqCst) - } - - pub fn deduct_block_gas_quota(&self, gas: Gas) -> anyhow::Result<()> { - let quota = self.block_gas_quota.load(Ordering::SeqCst); - match quota.checked_sub(gas) { - Some(v) => { - self.atomic_set_block_gas_quota(quota, v)?; - Ok(()) - } - None => Err(anyhow!("out of block gas")), - } - } - - fn atomic_set_block_gas_quota(&self, old: Gas, new: Gas) -> anyhow::Result<()> { - self.block_gas_quota.compare_exchange( - old, - new, - Ordering::SeqCst, - Ordering::SeqCst - ) - .map_err(|_| anyhow!("concurrent update to block gas available, should not happen"))?; - Ok(()) - } -} - -impl GasLayer for DefaultGas -where - DB: Blockstore + Clone + 'static, -{ - type State = FvmExecState; - - fn set_block_gas_limit(&self, state: &mut Self::State, limit: Gas) -> anyhow::Result<()> { - let params = fvm_ipld_encoding::RawBytes::serialize(limit)?; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, - sequence: state.block_height() as u64, - // exclude this from gas restriction - gas_limit: u64::MAX, - method_num: fendermint_actor_gas::Method::SetBlockGasLimit as u64, - params, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let (apply_ret, _) = state.execute_implicit(msg)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update block gas limit: {}", err) - } else { - Ok(()) - } - } - - fn block_gas_limit(&self, state: &Self::State) -> anyhow::Result { - let s = - read_actor_state::(state, fendermint_vm_actor_interface::gas::GAS_ACTOR_ID)?; - Ok(s.block_gas_limit()) - } -} - -impl Clone for DefaultGas { - fn clone(&self) -> Self { - Self { block_gas_quota: AtomicU64::new(self.block_gas_quota.load(Ordering::SeqCst)), p: Default::default() } - } -} \ No newline at end of file diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index 890217825..6b7244a4c 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -1,19 +1,115 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use fendermint_actor_gas::Gas; +use crate::fvm::state::{read_actor_state, FvmExecState}; +use crate::fvm::FvmMessage; +use anyhow::anyhow; +use fendermint_actor_gas_market::EIP1559GasState; +use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; +use fendermint_vm_actor_interface::system; +use fvm_ipld_blockstore::Blockstore; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicU64, Ordering}; -pub(crate) mod default; +type AtomicGas = AtomicU64; +pub type Gas = u64; -/// Handles the gas modeling in the current blockchain -pub trait GasLayer { - /// The state of the blockchain - type State; +pub struct EIP1559GasMarket { + /// The total gas available to be used by transactions in the current block + /// The executor requires Send + Sync, using an atomic variable instead of u64. + available_block_gas: AtomicGas, + _p: PhantomData, +} + +impl EIP1559GasMarket { + pub fn new() -> Self { + Self { + available_block_gas: AtomicGas::new(0), + _p: Default::default(), + } + } + + pub fn reset_from_chain_state(&self, chain_state: &FvmExecState) -> anyhow::Result<()> { + let current = self.available_block_gas.load(Ordering::SeqCst); + let gas_state = get_gas_state(chain_state)?; + self.atomic_set_block_gas_quota(current, gas_state.block_gas_limit)?; + Ok(()) + } + + pub fn available_block_gas(&self) -> Gas { + self.available_block_gas.load(Ordering::SeqCst) + } + + pub fn deduct_available_block_gas(&self, gas: Gas) -> anyhow::Result<()> { + let available = self.available_block_gas.load(Ordering::SeqCst); + match available.checked_sub(gas) { + Some(v) => { + self.atomic_set_block_gas_quota(available, v)?; + Ok(()) + } + None => Err(anyhow!("out of block gas")), + } + } + + fn atomic_set_block_gas_quota(&self, old: Gas, new: Gas) -> anyhow::Result<()> { + self.available_block_gas + .compare_exchange(old, new, Ordering::SeqCst, Ordering::SeqCst) + .map_err(|_| anyhow!("concurrent update to block gas available, should not happen"))?; + Ok(()) + } +} - /// Update the block gas limit. This will only take effect in the next block. +impl EIP1559GasMarket +where + DB: Blockstore + Clone + 'static, +{ #[allow(dead_code)] - fn set_block_gas_limit(&self, state: &mut Self::State, limit: Gas) -> anyhow::Result<()>; + fn update_state( + &self, + blockchain_state: &mut FvmExecState, + gas_state: EIP1559GasState, + ) -> anyhow::Result<()> { + let params = fvm_ipld_encoding::RawBytes::serialize(gas_state)?; + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_ACTOR_ADDR, + sequence: blockchain_state.block_height() as u64, + // exclude this from gas restriction + gas_limit: u64::MAX, + method_num: fendermint_actor_gas_market::Method::UpdateGasMarketState as u64, + params, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let (apply_ret, _) = blockchain_state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to update EIP1559 gas state: {}", err) + } else { + Ok(()) + } + } +} + +fn get_gas_state( + state: &FvmExecState, +) -> anyhow::Result { + let s = read_actor_state::( + state, + fendermint_vm_actor_interface::gas::GAS_ACTOR_ID, + )?; + Ok(s) +} - /// Obtain the current block gas limit - fn block_gas_limit(&self, state: &Self::State) -> anyhow::Result; +impl Clone for EIP1559GasMarket { + fn clone(&self) -> Self { + Self { + available_block_gas: AtomicU64::new(self.available_block_gas.load(Ordering::SeqCst)), + _p: Default::default(), + } + } } diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index cd4eb57c3..3c5cee674 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -19,7 +19,7 @@ pub mod bundle; pub(crate) mod gas; pub(crate) mod topdown; -use crate::fvm::gas::default::DefaultGas; +use crate::fvm::gas::EIP1559GasMarket; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -84,7 +84,7 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, - gas: DefaultGas, + gas: EIP1559GasMarket, } impl FvmMessageInterpreter @@ -110,7 +110,7 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, - gas: DefaultGas::new(), + gas: EIP1559GasMarket::new(), } } From b686131a054ea6d1793551a24d36886fb686c225 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 7 Aug 2024 11:52:03 +0800 Subject: [PATCH 014/111] demote mod to file --- fendermint/vm/interpreter/src/fvm/{gas/mod.rs => gas.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fendermint/vm/interpreter/src/fvm/{gas/mod.rs => gas.rs} (100%) diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas.rs similarity index 100% rename from fendermint/vm/interpreter/src/fvm/gas/mod.rs rename to fendermint/vm/interpreter/src/fvm/gas.rs From 22219f1a60a340baa16cf078a56715835b555323 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:05:55 +0800 Subject: [PATCH 015/111] Update fendermint/vm/interpreter/src/genesis.rs Co-authored-by: Karel Moravec --- fendermint/vm/interpreter/src/genesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 951bec82a..6bca3cb4c 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -172,7 +172,7 @@ impl GenesisCreator { async fn init_state(&self) -> anyhow::Result> { let bundle = std::fs::read(&self.builtin_actors_path).with_context(|| { format!( - "failed to read bundle: {}", + "failed to read builtin actors bundle: {}", self.builtin_actors_path.to_string_lossy() ) })?; From 6a1b4dda9a9c12b6b35347c445c7065cd18e86b7 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:06:06 +0800 Subject: [PATCH 016/111] Update fendermint/vm/interpreter/src/genesis.rs Co-authored-by: Karel Moravec --- fendermint/vm/interpreter/src/genesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 6bca3cb4c..8729c7ac2 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -179,7 +179,7 @@ impl GenesisCreator { let custom_actors_bundle = std::fs::read(&self.custom_actors_path).with_context(|| { format!( - "failed to read custom actors_bundle: {}", + "failed to read custom actors bundle: {}", self.custom_actors_path.to_string_lossy() ) })?; From 7c6276b97cb33515a26506b3691bc1335dd11412 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 7 Aug 2024 22:35:24 +0800 Subject: [PATCH 017/111] review feedbacks --- Cargo.lock | 8 + Cargo.toml | 1 + fendermint/app/options/src/genesis.rs | 6 +- fendermint/app/src/app.rs | 5 +- fendermint/app/src/cmd/genesis.rs | 14 +- fendermint/vm/interpreter/Cargo.toml | 2 + fendermint/vm/interpreter/src/genesis.rs | 210 +++++------------------ 7 files changed, 68 insertions(+), 178 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecebaa242..21a0c7b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3319,6 +3319,7 @@ dependencies = [ "arbitrary", "async-stm", "async-trait", + "base64 0.21.7", "cid", "ethers", "fendermint_actor_chainmetadata", @@ -3360,6 +3361,7 @@ dependencies = [ "serde", "serde_json", "serde_with 2.3.3", + "snap", "strum 0.26.1", "tempfile", "tendermint 0.31.1", @@ -8741,6 +8743,12 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + [[package]] name = "snow" version = "0.9.6" diff --git a/Cargo.toml b/Cargo.toml index bb28ffb43..c40794e20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ serde_yaml = { version = "0.9" } serde_tuple = "0.5" serde_with = "2.3" serial_test = "3.0" +snap = "1.1.0" strum = { version = "0.26.1", features = ["derive"] } tempfile = "3.7" thiserror = "1" diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 7a115c197..364cc8c71 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -133,9 +133,9 @@ pub struct GenesisAddValidatorArgs { #[derive(Args, Debug)] pub struct GenesisIntoTendermintArgs { - /// Sealed genesis file that is the initial app bytes for cometbft + /// The initial app bytes path for cometbft #[arg(long, short)] - pub sealed: PathBuf, + pub app_state: Option, /// Output file name for the Tendermint genesis JSON file. #[arg(long, short)] pub out: PathBuf, @@ -151,7 +151,7 @@ pub enum GenesisIpcCommands { /// Fetch the genesis parameters of a subnet from the parent. FromParent(Box), /// Seal the genesis state from the genesis parameter file - SealState(SealGenesisArgs), + SealGenesis(SealGenesisArgs), } #[derive(Args, Debug, Clone)] diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index b2f237de5..78997c241 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -23,7 +23,7 @@ use fendermint_vm_interpreter::fvm::state::{ }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput, PowerUpdates}; -use fendermint_vm_interpreter::genesis::read_genesis_car; +use fendermint_vm_interpreter::genesis::{decode_and_decompress, read_genesis_car}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, @@ -378,8 +378,9 @@ where } fn parse_genesis_app_bytes(bytes: &[u8]) -> Result> { + // cometbft serves data in json format, convert from json string match serde_json::from_slice(bytes)? { - serde_json::Value::String(s) => Ok(hex::decode(s)?), + serde_json::Value::String(s) => Ok(decode_and_decompress(&s)?), _ => Err(anyhow!("invalid app state json")), } } diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index c5c55f014..ebd18f973 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -14,7 +14,7 @@ use fendermint_vm_genesis::{ ipc, Account, Actor, ActorMeta, Collateral, Genesis, Multisig, PermissionMode, SignerAddr, Validator, ValidatorKey, }; -use fendermint_vm_interpreter::genesis::GenesisCreator; +use fendermint_vm_interpreter::genesis::{compress_and_encode, GenesisCreator}; use crate::cmd; use crate::options::genesis::*; @@ -95,8 +95,8 @@ cmd! { set_ipc_gateway(&genesis_file, args), GenesisIpcCommands::FromParent(args) => new_genesis_from_parent(&genesis_file, args).await, - GenesisIpcCommands::SealState(args) => - seal_state(&genesis_file, args).await, + GenesisIpcCommands::SealGenesis(args) => + seal_genesis(&genesis_file, args).await, } } } @@ -216,7 +216,10 @@ fn set_eam_permissions( fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> anyhow::Result<()> { let genesis = read_genesis(genesis_file)?; - let app_state = hex::encode(std::fs::read(&args.sealed)?); + let app_state: String = match args.app_state { + None => String::from(""), + Some(ref path) => compress_and_encode(&std::fs::read(path)?)?, + }; let chain_id: u64 = chainid::from_str_hashed(&genesis.chain_name)?.into(); let chain_id = chain_id.to_string(); @@ -251,6 +254,7 @@ fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> // Hopefully leaving this empty will skip validation, // otherwise we have to run the genesis in memory here and now. app_hash: tendermint::AppHash::default(), + // cometbft serves data in json format, convert to string to be specific app_state: serde_json::Value::String(app_state), }; let tmg_json = serde_json::to_string_pretty(&tmg)?; @@ -283,7 +287,7 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh }) } -async fn seal_state(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::Result<()> { +async fn seal_genesis(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::Result<()> { let genesis = read_genesis(genesis_file)?; let genesis_creator = GenesisCreator::new( diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 63e591da8..20ca5b0a2 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -33,6 +33,7 @@ ipc-observability = { workspace = true } async-trait = { workspace = true } async-stm = { workspace = true } anyhow = { workspace = true } +base64 = { workspace = true } ethers = { workspace = true } hex = { workspace = true } num-traits = { workspace = true } @@ -58,6 +59,7 @@ futures-util = { workspace = true } libipld = { workspace = true } tokio = { workspace = true } pin-project = { workspace = true } +snap = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 951bec82a..8375cec73 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0, MIT use std::collections::{BTreeSet, HashMap}; +use std::io::{Read, Write}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Arc; use anyhow::{anyhow, Context}; +use base64::Engine; use cid::Cid; use ethers::abi::Tokenize; use ethers::core::types as et; @@ -68,6 +70,22 @@ impl GenesisMetadata { } } +pub fn compress_and_encode(bytes: &[u8]) -> anyhow::Result { + let mut wtr = snap::write::FrameEncoder::new(vec![]); + wtr.write_all(bytes)?; + let compressed = wtr.into_inner()?; + Ok(base64::engine::general_purpose::STANDARD.encode(compressed)) +} + +pub fn decode_and_decompress(raw: &str) -> anyhow::Result> { + let bytes = base64::engine::general_purpose::STANDARD.decode(raw)?; + + let mut buf = vec![]; + snap::read::FrameDecoder::new(bytes.as_slice()).read_to_end(&mut buf)?; + + Ok(buf) +} + pub async fn read_genesis_car( bytes: Vec, store: &DB, @@ -108,7 +126,7 @@ pub struct GenesisCreator { /// The custom actors bundle path custom_actors_path: PathBuf, /// The CAR path to flush the sealed genesis state - sealed_out_path: PathBuf, + out_path: PathBuf, } impl GenesisCreator { @@ -122,7 +140,7 @@ impl GenesisCreator { hardhat: Hardhat::new(artifacts_path), builtin_actors_path, custom_actors_path, - sealed_out_path, + out_path: sealed_out_path, } } @@ -140,7 +158,7 @@ impl GenesisCreator { out: GenesisOutput, store: MemoryBlockstore, ) -> anyhow::Result<()> { - let file = tokio::fs::File::create(&self.sealed_out_path).await?; + let file = tokio::fs::File::create(&self.out_path).await?; tracing::info!(state_root = state_root.to_string(), "state root"); @@ -156,13 +174,9 @@ impl GenesisCreator { // create the stream to stream all the data into the car file let mut streamer = tokio_stream::iter(vec![(metadata_cid, metadata_bytes)]).merge(streamer); - let write_task = tokio::spawn(async move { - let mut write = file.compat_write(); - car.write_stream_async(&mut Pin::new(&mut write), &mut streamer) - .await - }); - - write_task.await??; + let mut write = file.compat_write(); + car.write_stream_async(&mut Pin::new(&mut write), &mut streamer) + .await?; tracing::info!("written sealed genesis state to file"); @@ -515,7 +529,7 @@ where } /// Deploy a library contract with a dynamic ID and no constructor. - pub fn deploy_library( + fn deploy_library( &mut self, state: &mut FvmGenesisState, next_id: &mut u64, @@ -554,7 +568,7 @@ where } /// Construct the bytecode of a top-level contract and deploy it with some constructor parameters. - pub fn deploy_contract( + fn deploy_contract( &self, state: &mut FvmGenesisState, contract_name: &str, @@ -592,7 +606,7 @@ where } /// Collect Facet Cuts for the diamond pattern, where the facet address comes from already deployed library facets. - pub fn facets(&self, contract_name: &str) -> anyhow::Result> { + fn facets(&self, contract_name: &str) -> anyhow::Result> { let contract = self.top_contract(contract_name)?; let mut facet_cuts = Vec::new(); @@ -638,158 +652,18 @@ fn circ_supply(g: &Genesis) -> TokenAmount { .iter() .fold(TokenAmount::zero(), |s, a| s + a.balance.clone()) } -// -// #[cfg(test)] -// mod tests { -// use std::{str::FromStr, sync::Arc}; -// -// use cid::Cid; -// use fendermint_vm_genesis::{ipc::IpcParams, Genesis}; -// use fvm::engine::MultiEngine; -// use quickcheck::Arbitrary; -// use tendermint_rpc::{MockClient, MockRequestMethodMatcher}; -// -// use crate::{ -// fvm::{ -// bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, -// state::ipc::GatewayCaller, -// store::memory::MemoryBlockstore, -// upgrades::UpgradeScheduler, -// FvmMessageInterpreter, -// }, -// GenesisInterpreter, -// }; -// -// use super::FvmGenesisState; -// -// #[tokio::test] -// async fn load_genesis() { -// let genesis = make_genesis(); -// let bundle = read_bundle(); -// let custom_actors_bundle = read_custom_actors_bundle(); -// let interpreter = make_interpreter(); -// -// let multi_engine = Arc::new(MultiEngine::default()); -// let store = MemoryBlockstore::new(); -// -// let state = FvmGenesisState::new(store, multi_engine, &bundle, &custom_actors_bundle) -// .await -// .expect("failed to create state"); -// -// let (mut state, out) = interpreter -// .init(state, genesis.clone()) -// .await -// .expect("failed to create actors"); -// -// assert_eq!(out.validators.len(), genesis.validators.len()); -// -// // Try calling a method on the IPC Gateway. -// let exec_state = state.exec_state().expect("should be in exec stage"); -// let caller = GatewayCaller::default(); -// -// let period = caller -// .bottom_up_check_period(exec_state) -// .expect("error calling the gateway"); -// -// assert_eq!(period, genesis.ipc.unwrap().gateway.bottom_up_check_period); -// -// let _state_root = state.commit().expect("failed to commit"); -// } -// -// #[tokio::test] -// async fn load_genesis_deterministic() { -// let genesis = make_genesis(); -// let bundle = read_bundle(); -// let custom_actors_bundle = read_custom_actors_bundle(); -// let interpreter = make_interpreter(); -// let multi_engine = Arc::new(MultiEngine::default()); -// -// // Create a couple of states and load the same thing. -// let mut outputs = Vec::new(); -// for _ in 0..3 { -// let store = MemoryBlockstore::new(); -// let state = -// FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) -// .await -// .expect("failed to create state"); -// -// let (state, out) = interpreter -// .init(state, genesis.clone()) -// .await -// .expect("failed to create actors"); -// -// let state_root_hash = state.commit().expect("failed to commit"); -// outputs.push((state_root_hash, out)); -// } -// -// for out in &outputs[1..] { -// assert_eq!(out.0, outputs[0].0, "state root hash is different"); -// } -// } -// -// // This is a sort of canary test, if it fails means something changed in the way we do genesis, -// // which is probably fine, but it's better to know about it, and if anybody doesn't get the same -// // then we might have some non-determinism. -// #[ignore] // I see a different value on CI than locally. -// #[tokio::test] -// async fn load_genesis_known() { -// let genesis_json = "{\"chain_name\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"timestamp\":1073250,\"network_version\":18,\"base_fee\":\"1000\",\"power_scale\":3,\"validators\":[{\"public_key\":\"BLX9ojqB+8Z26aMmKoCRb3Te6AnSU6zY8hPcf1X5Q69XCNaHVcRxzYO2xx7o/2vgdS7nkDTMRRbkDGzy+FYdAFc=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BFcOveVieknZiscWsfXa06aGbBkKeucBycd/w0N1QHlaZfa/5dJcH7D0hvcdfv3B2Rv1OPuxo1PkgsEbWegWKcA=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BEP30ykovfrQp3zo+JVRvDVL2emC+Ju1Kpox3zMVYZyFKvYt64qyN/HOVjridDrkEsnQU8BVen4Aegja4fBZ+LU=\",\"power\":\"1000000000000000000\"}],\"accounts\":[{\"meta\":{\"Account\":{\"owner\":\"f410fggjevhgketpz6gw6ordusynlgcd5piyug4aomuq\"}},\"balance\":\"1000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410frbdnwklaitcjsqe7swjwp5naple6vthq4woyfry\"}},\"balance\":\"2000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410fxo4lih4n2acr3oadalidwqjgoqkzhp5dw3zwkvy\"}},\"balance\":\"1000000000000000000\"}],\"ipc\":{\"gateway\":{\"subnet_id\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"bottom_up_check_period\":30,\"msg_fee\":\"1000000000000\",\"majority_percentage\":60,\"active_validators_limit\":100}}}"; -// -// let genesis: Genesis = serde_json::from_str(genesis_json).expect("failed to parse genesis"); -// -// let bundle = read_bundle(); -// let custom_actors_bundle = read_custom_actors_bundle(); -// let interpreter = make_interpreter(); -// let multi_engine = Arc::new(MultiEngine::default()); -// -// let store = MemoryBlockstore::new(); -// let state = -// FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) -// .await -// .expect("failed to create state"); -// -// let (state, _) = interpreter -// .init(state, genesis.clone()) -// .await -// .expect("failed to create actors"); -// -// let state_root_hash = state.commit().expect("failed to commit"); -// -// let expected_root_hash = -// Cid::from_str("bafy2bzacedebgy4j7qnh2v2x4kkr2jqfkryql5ookbjrwge6dbrr24ytlqnj4") -// .unwrap(); -// -// assert_eq!(state_root_hash, expected_root_hash); -// } -// -// fn make_genesis() -> Genesis { -// let mut g = quickcheck::Gen::new(5); -// let mut genesis = Genesis::arbitrary(&mut g); -// -// // Make sure we have IPC enabled. -// genesis.ipc = Some(IpcParams::arbitrary(&mut g)); -// genesis -// } -// -// fn make_interpreter( -// ) -> FvmMessageInterpreter> { -// let (client, _) = MockClient::new(MockRequestMethodMatcher::default()); -// FvmMessageInterpreter::new( -// client, -// None, -// contracts_path(), -// 1.05, -// 1.05, -// false, -// UpgradeScheduler::new(), -// ) -// } -// -// fn read_bundle() -> Vec { -// std::fs::read(bundle_path()).expect("failed to read bundle") -// } -// -// fn read_custom_actors_bundle() -> Vec { -// std::fs::read(custom_actors_bundle_path()).expect("failed to read custom actor bundle") -// } -// } + +#[cfg(test)] +mod tests { + use crate::genesis::{compress_and_encode, decode_and_decompress}; + + #[test] + fn test_compression() { + let bytes = (0..10000).map(|_| rand::random::()).collect::>(); + + let s = compress_and_encode(&bytes).unwrap(); + let recovered = decode_and_decompress(&s).unwrap(); + + assert_eq!(recovered, bytes); + } +} \ No newline at end of file From fa0f3308a7cfefe747f6c3f05d05eeca909eb188 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 8 Aug 2024 00:26:21 +0800 Subject: [PATCH 018/111] add genesis app state schema --- fendermint/app/src/app.rs | 6 ++- fendermint/app/src/cmd/genesis.rs | 4 +- fendermint/vm/interpreter/src/genesis.rs | 68 ++++++++++++++++++------ 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 78997c241..877616b17 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -23,7 +23,9 @@ use fendermint_vm_interpreter::fvm::state::{ }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput, PowerUpdates}; -use fendermint_vm_interpreter::genesis::{decode_and_decompress, read_genesis_car}; +use fendermint_vm_interpreter::genesis::{ + decode_and_decompress, read_genesis_car, GenesisAppState, +}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, @@ -380,7 +382,7 @@ where fn parse_genesis_app_bytes(bytes: &[u8]) -> Result> { // cometbft serves data in json format, convert from json string match serde_json::from_slice(bytes)? { - serde_json::Value::String(s) => Ok(decode_and_decompress(&s)?), + serde_json::Value::String(s) => Ok(GenesisAppState::decode_and_decompress(&s)?), _ => Err(anyhow!("invalid app state json")), } } diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index ebd18f973..d525fc496 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -14,7 +14,7 @@ use fendermint_vm_genesis::{ ipc, Account, Actor, ActorMeta, Collateral, Genesis, Multisig, PermissionMode, SignerAddr, Validator, ValidatorKey, }; -use fendermint_vm_interpreter::genesis::{compress_and_encode, GenesisCreator}; +use fendermint_vm_interpreter::genesis::{compress_and_encode, GenesisAppState, GenesisCreator}; use crate::cmd; use crate::options::genesis::*; @@ -218,7 +218,7 @@ fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> let genesis = read_genesis(genesis_file)?; let app_state: String = match args.app_state { None => String::from(""), - Some(ref path) => compress_and_encode(&std::fs::read(path)?)?, + Some(ref path) => GenesisAppState::v1(std::fs::read(path)?).compress_and_encode()?, }; let chain_id: u64 = chainid::from_str_hashed(&genesis.chain_name)?.into(); diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 2f8e0ab95..494721c33 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -70,20 +70,54 @@ impl GenesisMetadata { } } -pub fn compress_and_encode(bytes: &[u8]) -> anyhow::Result { - let mut wtr = snap::write::FrameEncoder::new(vec![]); - wtr.write_all(bytes)?; - let compressed = wtr.into_inner()?; - Ok(base64::engine::general_purpose::STANDARD.encode(compressed)) +/// Genesis app state wrapper for cometbft +pub enum GenesisAppState { + V1(Vec), } -pub fn decode_and_decompress(raw: &str) -> anyhow::Result> { - let bytes = base64::engine::general_purpose::STANDARD.decode(raw)?; +impl GenesisAppState { + pub fn v1(bytes: Vec) -> Self { + Self::V1(bytes) + } + + pub fn into_bytes(self) -> Vec { + match self { + Self::V1(b) => b, + } + } + + pub fn compress_and_encode(&self) -> anyhow::Result { + let bytes = match self { + GenesisAppState::V1(ref bytes) => { + let mut wtr = snap::write::FrameEncoder::new(vec![]); + wtr.write_all(bytes)?; + let compressed = wtr.into_inner()?; + + let mut res = vec![0]; + res.extend(compressed); + + res + } + }; + + Ok(base64::engine::general_purpose::STANDARD.encode(bytes)) + } - let mut buf = vec![]; - snap::read::FrameDecoder::new(bytes.as_slice()).read_to_end(&mut buf)?; + pub fn decode_and_decompress(raw: &str) -> anyhow::Result> { + let bytes = base64::engine::general_purpose::STANDARD.decode(raw)?; + if bytes.is_empty() { + return Err(anyhow!("empty bytes for genesis app state")); + } - Ok(buf) + match bytes[0] { + 0 => { + let mut buf = vec![]; + snap::read::FrameDecoder::new(&bytes.as_slice()[1..]).read_to_end(&mut buf)?; + Ok(buf) + } + _ => Err(anyhow!("not supported schema versioin")), + } + } } pub async fn read_genesis_car( @@ -655,15 +689,19 @@ fn circ_supply(g: &Genesis) -> TokenAmount { #[cfg(test)] mod tests { - use crate::genesis::{compress_and_encode, decode_and_decompress}; + use crate::genesis::GenesisAppState; #[test] fn test_compression() { - let bytes = (0..10000).map(|_| rand::random::()).collect::>(); + let bytes = (0..10000) + .map(|_| rand::random::()) + .collect::>(); - let s = compress_and_encode(&bytes).unwrap(); - let recovered = decode_and_decompress(&s).unwrap(); + let s = GenesisAppState::v1(bytes.clone()) + .compress_and_encode() + .unwrap(); + let recovered = GenesisAppState::decode_and_decompress(&s).unwrap(); assert_eq!(recovered, bytes); } -} \ No newline at end of file +} From b0038cc5ed8ccab0b16148a4652bc6fbddc336b2 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 8 Aug 2024 12:01:52 +0800 Subject: [PATCH 019/111] update infra and simplify genesis --- fendermint/app/options/src/genesis.rs | 2 +- fendermint/app/src/app.rs | 4 +- fendermint/app/src/cmd/genesis.rs | 12 +- fendermint/testing/graph-test/scripts/init.sh | 4 +- .../testing/materializer/src/docker/node.rs | 4 +- fendermint/testing/smoke-test/scripts/init.sh | 4 +- .../testing/snapshot-test/scripts/init.sh | 4 +- fendermint/vm/interpreter/src/genesis.rs | 257 ++++++++++-------- infra/fendermint/Makefile.toml | 3 + infra/fendermint/scripts/genesis.toml | 6 +- infra/fendermint/scripts/subnet.toml | 1 + 11 files changed, 172 insertions(+), 129 deletions(-) diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 364cc8c71..740384758 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -167,7 +167,7 @@ pub struct SealGenesisArgs { /// The solidity artifacts output path. If you are using ipc-monorepo, it should be the `out` folder /// of `make build` #[arg(long, short)] - pub artifacts_path: PathBuf, + pub artifacts_path: Option, /// The sealed genesis state output path, i.e. finalized genesis state CAR file dump path #[arg(long, short)] diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 877616b17..af4e8da72 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -23,9 +23,7 @@ use fendermint_vm_interpreter::fvm::state::{ }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput, PowerUpdates}; -use fendermint_vm_interpreter::genesis::{ - decode_and_decompress, read_genesis_car, GenesisAppState, -}; +use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index d525fc496..598c4a3c7 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -14,7 +14,7 @@ use fendermint_vm_genesis::{ ipc, Account, Actor, ActorMeta, Collateral, Genesis, Multisig, PermissionMode, SignerAddr, Validator, ValidatorKey, }; -use fendermint_vm_interpreter::genesis::{compress_and_encode, GenesisAppState, GenesisCreator}; +use fendermint_vm_interpreter::genesis::{GenesisAppState, GenesisCreator}; use crate::cmd; use crate::options::genesis::*; @@ -216,9 +216,11 @@ fn set_eam_permissions( fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> anyhow::Result<()> { let genesis = read_genesis(genesis_file)?; - let app_state: String = match args.app_state { - None => String::from(""), - Some(ref path) => GenesisAppState::v1(std::fs::read(path)?).compress_and_encode()?, + let app_state: Option = match args.app_state { + Some(ref path) if path.exists() => { + Some(GenesisAppState::v1(std::fs::read(path)?).compress_and_encode()?) + } + _ => None, }; let chain_id: u64 = chainid::from_str_hashed(&genesis.chain_name)?.into(); @@ -255,7 +257,7 @@ fn into_tendermint(genesis_file: &PathBuf, args: &GenesisIntoTendermintArgs) -> // otherwise we have to run the genesis in memory here and now. app_hash: tendermint::AppHash::default(), // cometbft serves data in json format, convert to string to be specific - app_state: serde_json::Value::String(app_state), + app_state, }; let tmg_json = serde_json::to_string_pretty(&tmg)?; std::fs::write(&args.out, tmg_json)?; diff --git a/fendermint/testing/graph-test/scripts/init.sh b/fendermint/testing/graph-test/scripts/init.sh index 8fc376fb1..2e76741ee 100755 --- a/fendermint/testing/graph-test/scripts/init.sh +++ b/fendermint/testing/graph-test/scripts/init.sh @@ -52,7 +52,7 @@ fendermint \ fendermint \ genesis --genesis-file $GENESIS_FILE \ ipc \ - seal-state \ + seal-genesis \ --builtin-actors-path /fendermint/bundle.car \ --custom-actors-path /fendermint/custom_actors_bundle.car \ --artifacts-path /fendermint/contracts \ @@ -61,7 +61,7 @@ fendermint \ # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" + into-tendermint --out $CMT_DIR/config/genesis.json --app-state "${SEALED_GENESIS_FILE}" # Copy the default validator key cp $KEYS_DIR/$VALIDATOR_NAME.priv_validator_key.json \ diff --git a/fendermint/testing/materializer/src/docker/node.rs b/fendermint/testing/materializer/src/docker/node.rs index cfa9c79b9..128ab78ff 100644 --- a/fendermint/testing/materializer/src/docker/node.rs +++ b/fendermint/testing/materializer/src/docker/node.rs @@ -210,7 +210,7 @@ impl DockerNode { "genesis \ --genesis-file /fendermint/genesis.json \ ipc \ - seal-state \ + seal-genesis \ --builtin-actors-path /fendermint/bundle.car \ --custom-actors-path /fendermint/custom_actors_bundle.car \ --artifacts-path /fendermint/contracts \ @@ -227,7 +227,7 @@ impl DockerNode { --genesis-file /fendermint/genesis.json \ into-tendermint \ --out /cometbft/config/genesis.json \ - --sealed /cometbft/config/sealed.json \ + --app-state /cometbft/config/sealed.json \ ", ) .await diff --git a/fendermint/testing/smoke-test/scripts/init.sh b/fendermint/testing/smoke-test/scripts/init.sh index c3fa8d63a..0728e6eab 100755 --- a/fendermint/testing/smoke-test/scripts/init.sh +++ b/fendermint/testing/smoke-test/scripts/init.sh @@ -68,7 +68,7 @@ fendermint \ fendermint \ genesis --genesis-file $GENESIS_FILE \ ipc \ - seal-state \ + seal-genesis \ --builtin-actors-path /fendermint/bundle.car \ --custom-actors-path /fendermint/custom_actors_bundle.car \ --artifacts-path /fendermint/contracts \ @@ -77,7 +77,7 @@ fendermint \ # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" + into-tendermint --out $CMT_DIR/config/genesis.json --app-state "${SEALED_GENESIS_FILE}" # Convert FM validator key to CMT fendermint \ diff --git a/fendermint/testing/snapshot-test/scripts/init.sh b/fendermint/testing/snapshot-test/scripts/init.sh index 9e75058d9..1639e36f7 100755 --- a/fendermint/testing/snapshot-test/scripts/init.sh +++ b/fendermint/testing/snapshot-test/scripts/init.sh @@ -47,7 +47,7 @@ fendermint \ fendermint \ genesis --genesis-file $GENESIS_FILE \ ipc \ - seal-state \ + seal-genesis \ --builtin-actors-path /fendermint/bundle.car \ --custom-actors-path /fendermint/custom_actors_bundle.car \ --artifacts-path /fendermint/contracts \ @@ -56,7 +56,7 @@ fendermint \ # Convert FM genesis to CMT fendermint \ genesis --genesis-file $GENESIS_FILE \ - into-tendermint --out $CMT_DIR/config/genesis.json --sealed "${SEALED_GENESIS_FILE}" + into-tendermint --out $CMT_DIR/config/genesis.json --app-state "${SEALED_GENESIS_FILE}" # Copy the default validator key cp $KEYS_DIR/$VALIDATOR_NAME.priv_validator_key.json \ diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 494721c33..11df5c68f 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -14,7 +14,7 @@ use cid::Cid; use ethers::abi::Tokenize; use ethers::core::types as et; use fendermint_actor_eam::PermissionModeParams; -use fendermint_eth_hardhat::{Hardhat, FQN}; +use fendermint_eth_hardhat::{ContractSourceAndName, Hardhat, FQN}; use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; @@ -22,7 +22,7 @@ use fendermint_vm_actor_interface::{ account, burntfunds, chainmetadata, cron, eam, init, ipc, reward, system, EMPTY_ARR, }; use fendermint_vm_core::{chainid, Timestamp}; -use fendermint_vm_genesis::{ActorMeta, Genesis, Power, PowerScale, Validator}; +use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; use futures_util::io::Cursor; use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; @@ -37,6 +37,7 @@ use num_traits::Zero; use crate::fvm::state::snapshot::{derive_cid, StateTreeStreamer}; use crate::fvm::state::{FvmGenesisState, FvmStateParams}; use crate::fvm::store::memory::MemoryBlockstore; +use fendermint_vm_genesis::ipc::IpcParams; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use tokio_stream::StreamExt; @@ -80,23 +81,12 @@ impl GenesisAppState { Self::V1(bytes) } - pub fn into_bytes(self) -> Vec { - match self { - Self::V1(b) => b, - } - } - pub fn compress_and_encode(&self) -> anyhow::Result { let bytes = match self { GenesisAppState::V1(ref bytes) => { - let mut wtr = snap::write::FrameEncoder::new(vec![]); + let mut wtr = snap::write::FrameEncoder::new(vec![0]); wtr.write_all(bytes)?; - let compressed = wtr.into_inner()?; - - let mut res = vec![0]; - res.extend(compressed); - - res + wtr.into_inner()? } }; @@ -154,7 +144,7 @@ pub struct GenesisOutput { pub struct GenesisCreator { /// Hardhat like util to deploy ipc contracts - hardhat: Hardhat, + hardhat: Option, /// The built in actors bundle path builtin_actors_path: PathBuf, /// The custom actors bundle path @@ -167,11 +157,11 @@ impl GenesisCreator { pub fn new( builtin_actors_path: PathBuf, custom_actors_path: PathBuf, - artifacts_path: PathBuf, + maybe_artifacts_path: Option, sealed_out_path: PathBuf, ) -> Self { Self { - hardhat: Hardhat::new(artifacts_path), + hardhat: maybe_artifacts_path.map(Hardhat::new), builtin_actors_path, custom_actors_path, out_path: sealed_out_path, @@ -244,6 +234,19 @@ impl GenesisCreator { .context("failed to create genesis state") } + fn handle_ipc<'a, T, F: Fn(&'a Hardhat, &'a IpcParams) -> T>( + &'a self, + maybe_ipc: Option<&'a IpcParams>, + f: F, + ) -> anyhow::Result> { + // Only allocate IDs if the contracts are deployed. + match (maybe_ipc, &self.hardhat) { + (Some(ipc_params), Some(ref hardhat)) => Ok(Some(f(hardhat, ipc_params))), + (Some(_), None) => Err(anyhow!("ipc enabled but artifacts path not provided")), + _ => Ok(None), + } + } + fn populate_state( &self, state: &mut FvmGenesisState, @@ -277,38 +280,14 @@ impl GenesisCreator { }; // STAGE 0: Declare the built-in EVM contracts we'll have to deploy. - - // Pre-defined IDs for top-level Ethereum contracts. - let mut all_contracts = Vec::new(); - let mut top_level_contracts = EthContractMap::default(); - - // Only allocate IDs if the contracts are deployed. - if genesis.ipc.is_some() { - top_level_contracts.extend(IPC_CONTRACTS.clone()); - } - - all_contracts.extend(top_level_contracts.keys()); - all_contracts.extend( - top_level_contracts - .values() - .flat_map(|c| c.facets.iter().map(|f| f.name)), - ); - // Collect dependencies of the main IPC actors. - let mut eth_libs = self - .hardhat - .dependencies( - &all_contracts - .iter() - .map(|n| (contract_src(n), *n)) - .collect::>(), - ) - .context("failed to collect EVM contract dependencies")?; - - // Only keep library dependencies, not contracts with constructors. - eth_libs.retain(|(_, d)| !top_level_contracts.contains_key(d.as_str())); + // ipc_entrypoints contains the external user facing contracts + // all_ipc_contracts contains ipc_entrypoints + util contracts + let (all_ipc_contracts, ipc_entrypoints) = self + .handle_ipc(genesis.ipc.as_ref(), |h, _| collect_contracts(h))? + .transpose()? + .unwrap_or((Vec::new(), EthContractMap::new())); // STAGE 1: First we initialize native built-in actors. - // System actor state .create_builtin_actor( @@ -327,11 +306,11 @@ impl GenesisCreator { state.store(), genesis.chain_name.clone(), &genesis.accounts, - &top_level_contracts + &ipc_entrypoints .values() .map(|c| c.actor_id) .collect::>(), - eth_libs.len() as u64, + all_ipc_contracts.len() as u64, ) .context("failed to create init state")?; @@ -466,75 +445,131 @@ impl GenesisCreator { ) .context("failed to init exec state")?; - let mut deployer = - ContractDeployer::::new(&self.hardhat, &top_level_contracts); - - // Deploy Ethereum libraries. - for (lib_src, lib_name) in eth_libs { - deployer.deploy_library(state, &mut next_id, lib_src, &lib_name)?; + let validators = genesis.validators; + { + let (hardhat, ipc_params) = self + .handle_ipc(genesis.ipc.as_ref(), |hardhat, ipc_params| { + (hardhat, ipc_params) + })? + .unwrap(); + deploy_contracts( + all_ipc_contracts, + &ipc_entrypoints, + validators, + next_id, + state, + ipc_params, + hardhat, + )?; } - if let Some(ipc_params) = genesis.ipc { - // IPC Gateway actor. - let gateway_addr = { - use ipc::gateway::ConstructorParameters; + Ok(out) + } +} - let params = ConstructorParameters::new(ipc_params.gateway, genesis.validators) - .context("failed to create gateway constructor")?; +fn collect_contracts( + hardhat: &Hardhat, +) -> anyhow::Result<(Vec, EthContractMap)> { + let mut all_contracts = Vec::new(); + let mut top_level_contracts = EthContractMap::default(); + + top_level_contracts.extend(IPC_CONTRACTS.clone()); + + all_contracts.extend(top_level_contracts.keys()); + all_contracts.extend( + top_level_contracts + .values() + .flat_map(|c| c.facets.iter().map(|f| f.name)), + ); + // Collect dependencies of the main IPC actors. + let mut eth_libs = hardhat + .dependencies( + &all_contracts + .iter() + .map(|n| (contract_src(n), *n)) + .collect::>(), + ) + .context("failed to collect EVM contract dependencies")?; - let facets = deployer - .facets(ipc::gateway::CONTRACT_NAME) - .context("failed to collect gateway facets")?; + // Only keep library dependencies, not contracts with constructors. + eth_libs.retain(|(_, d)| !top_level_contracts.contains_key(d.as_str())); + Ok((eth_libs, top_level_contracts)) +} - deployer.deploy_contract(state, ipc::gateway::CONTRACT_NAME, (facets, params))? - }; +fn deploy_contracts( + ipc_contracts: Vec, + top_level_contracts: &EthContractMap, + validators: Vec>, + mut next_id: u64, + state: &mut FvmGenesisState, + ipc_params: &IpcParams, + hardhat: &Hardhat, +) -> anyhow::Result<()> { + let mut deployer = ContractDeployer::::new(hardhat, top_level_contracts); + + // Deploy Ethereum libraries. + for (lib_src, lib_name) in ipc_contracts { + deployer.deploy_library(state, &mut next_id, lib_src, &lib_name)?; + } - // IPC SubnetRegistry actor. - { - use ipc::registry::ConstructorParameters; - - let mut facets = deployer - .facets(ipc::registry::CONTRACT_NAME) - .context("failed to collect registry facets")?; - - let getter_facet = facets.remove(0); - let manager_facet = facets.remove(0); - let rewarder_facet = facets.remove(0); - let checkpointer_facet = facets.remove(0); - let pauser_facet = facets.remove(0); - let diamond_loupe_facet = facets.remove(0); - let diamond_cut_facet = facets.remove(0); - let ownership_facet = facets.remove(0); - - debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); - - let params = ConstructorParameters { - gateway: gateway_addr, - getter_facet: getter_facet.facet_address, - manager_facet: manager_facet.facet_address, - rewarder_facet: rewarder_facet.facet_address, - pauser_facet: pauser_facet.facet_address, - checkpointer_facet: checkpointer_facet.facet_address, - diamond_cut_facet: diamond_cut_facet.facet_address, - diamond_loupe_facet: diamond_loupe_facet.facet_address, - ownership_facet: ownership_facet.facet_address, - subnet_getter_selectors: getter_facet.function_selectors, - subnet_manager_selectors: manager_facet.function_selectors, - subnet_rewarder_selectors: rewarder_facet.function_selectors, - subnet_checkpointer_selectors: checkpointer_facet.function_selectors, - subnet_pauser_selectors: pauser_facet.function_selectors, - subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, - subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, - subnet_actor_ownership_selectors: ownership_facet.function_selectors, - creation_privileges: 0, - }; - - deployer.deploy_contract(state, ipc::registry::CONTRACT_NAME, (facets, params))?; - }; - } + // IPC Gateway actor. + let gateway_addr = { + use ipc::gateway::ConstructorParameters; - Ok(out) + let params = ConstructorParameters::new(ipc_params.gateway.clone(), validators) + .context("failed to create gateway constructor")?; + + let facets = deployer + .facets(ipc::gateway::CONTRACT_NAME) + .context("failed to collect gateway facets")?; + + deployer.deploy_contract(state, ipc::gateway::CONTRACT_NAME, (facets, params))? + }; + + // IPC SubnetRegistry actor. + { + use ipc::registry::ConstructorParameters; + + let mut facets = deployer + .facets(ipc::registry::CONTRACT_NAME) + .context("failed to collect registry facets")?; + + let getter_facet = facets.remove(0); + let manager_facet = facets.remove(0); + let rewarder_facet = facets.remove(0); + let checkpointer_facet = facets.remove(0); + let pauser_facet = facets.remove(0); + let diamond_loupe_facet = facets.remove(0); + let diamond_cut_facet = facets.remove(0); + let ownership_facet = facets.remove(0); + + debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); + + let params = ConstructorParameters { + gateway: gateway_addr, + getter_facet: getter_facet.facet_address, + manager_facet: manager_facet.facet_address, + rewarder_facet: rewarder_facet.facet_address, + pauser_facet: pauser_facet.facet_address, + checkpointer_facet: checkpointer_facet.facet_address, + diamond_cut_facet: diamond_cut_facet.facet_address, + diamond_loupe_facet: diamond_loupe_facet.facet_address, + ownership_facet: ownership_facet.facet_address, + subnet_getter_selectors: getter_facet.function_selectors, + subnet_manager_selectors: manager_facet.function_selectors, + subnet_rewarder_selectors: rewarder_facet.function_selectors, + subnet_checkpointer_selectors: checkpointer_facet.function_selectors, + subnet_pauser_selectors: pauser_facet.function_selectors, + subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, + subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, + subnet_actor_ownership_selectors: ownership_facet.function_selectors, + creation_privileges: 0, + }; + + deployer.deploy_contract(state, ipc::registry::CONTRACT_NAME, (facets, params))?; } + + Ok(()) } fn contract_src(name: &str) -> PathBuf { diff --git a/infra/fendermint/Makefile.toml b/infra/fendermint/Makefile.toml index 8e7549a86..799a6b7a4 100644 --- a/infra/fendermint/Makefile.toml +++ b/infra/fendermint/Makefile.toml @@ -73,6 +73,7 @@ CMT_DIR = "${BASE_DIR}/${NODE_NAME}/cometbft" ENV_FILE = "${BASE_DIR}/.env" GENESIS_FILE = "${BASE_DIR}/genesis.json" +SEALED_GENESIS = "${BASE_DIR}/sealed_genesis.car" KEYS_SUBDIR = "keys" VALIDATOR_KEY_NAME = "validator_key" @@ -122,6 +123,7 @@ echo - CometBFT directory: ${CMT_DIR} echo - Fendermint directory: ${FM_DIR} echo - Keys directory: ${KEYS_DIR} echo - Genesis file: ${GENESIS_FILE} +echo - Sealed Genesis: ${SEALED_GENESIS} echo - Validator Private key: ${VALIDATOR_PRIV_KEY_PATH} echo - Network: ${NETWORK_NAME} echo - CometBFT container: ${CMT_CONTAINER_NAME} @@ -131,6 +133,7 @@ echo echo 4 nodes testnet layout: echo - IPC directory: ${BASE_DIR} echo - Genesis file: ${GENESIS_FILE} +echo - Sealed Genesis: ${SEALED_GENESIS} echo - Network: ${NETWORK_NAME} echo """ diff --git a/infra/fendermint/scripts/genesis.toml b/infra/fendermint/scripts/genesis.toml index 28a424a2d..d7b23676a 100644 --- a/infra/fendermint/scripts/genesis.toml +++ b/infra/fendermint/scripts/genesis.toml @@ -35,7 +35,11 @@ env = { "CMD" = """genesis --genesis-file /data/genesis.json ipc gateway --subne --majority-percentage 67 \ """ } +[tasks.genesis-seal] +extend = "fendermint-tool" +env = { "CMD" = "genesis --genesis-file /data/genesis.json ipc seal-genesis --builtin-actors-path /fendermint/bundle.car --custom-actors-path /fendermint/custom_actors_bundle.car --artifacts-path /fendermint/contracts --output-path {SEALED_GENESIS}" } + [tasks.genesis-write] extend = "fendermint-tool" -env = { "CMD" = "genesis --genesis-file /data/genesis.json into-tendermint --out /data/genesis.committed.json" } +env = { "CMD" = "genesis --genesis-file /data/genesis.json into-tendermint --app-state {SEALED_GENESIS} --out /data/genesis.committed.json" } script.post = "cp ${BASE_DIR}/genesis.committed.json ${CMT_DIR}/config/genesis.json" diff --git a/infra/fendermint/scripts/subnet.toml b/infra/fendermint/scripts/subnet.toml index 9a30db494..583718ab0 100644 --- a/infra/fendermint/scripts/subnet.toml +++ b/infra/fendermint/scripts/subnet.toml @@ -103,6 +103,7 @@ dependencies = [ dependencies = [ "subnet-fetch-genesis", "subnet-genesis-set-eam-permissions", + "genesis-seal", "genesis-write", "fendermint-new-network-key", ] From 22e59581010842aa20e4dbf6f87467983a8bbee5 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 8 Aug 2024 12:05:27 +0800 Subject: [PATCH 020/111] fmt --- fendermint/vm/interpreter/src/genesis.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 11df5c68f..de9a258b4 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -445,17 +445,14 @@ impl GenesisCreator { ) .context("failed to init exec state")?; - let validators = genesis.validators; - { - let (hardhat, ipc_params) = self - .handle_ipc(genesis.ipc.as_ref(), |hardhat, ipc_params| { - (hardhat, ipc_params) - })? - .unwrap(); + let maybe_ipc = self.handle_ipc(genesis.ipc.as_ref(), |hardhat, ipc_params| { + (hardhat, ipc_params) + })?; + if let Some((hardhat, ipc_params)) = maybe_ipc { deploy_contracts( all_ipc_contracts, &ipc_entrypoints, - validators, + genesis.validators, next_id, state, ipc_params, From 03191418748742c2f29be184aa636bf724716f14 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 8 Aug 2024 13:43:00 +0800 Subject: [PATCH 021/111] fix infra --- infra/fendermint/Makefile.toml | 2 +- infra/fendermint/scripts/genesis.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/fendermint/Makefile.toml b/infra/fendermint/Makefile.toml index 799a6b7a4..42962658e 100644 --- a/infra/fendermint/Makefile.toml +++ b/infra/fendermint/Makefile.toml @@ -73,7 +73,7 @@ CMT_DIR = "${BASE_DIR}/${NODE_NAME}/cometbft" ENV_FILE = "${BASE_DIR}/.env" GENESIS_FILE = "${BASE_DIR}/genesis.json" -SEALED_GENESIS = "${BASE_DIR}/sealed_genesis.car" +SEALED_GENESIS = "/data/sealed_genesis.car" KEYS_SUBDIR = "keys" VALIDATOR_KEY_NAME = "validator_key" diff --git a/infra/fendermint/scripts/genesis.toml b/infra/fendermint/scripts/genesis.toml index d7b23676a..a182836be 100644 --- a/infra/fendermint/scripts/genesis.toml +++ b/infra/fendermint/scripts/genesis.toml @@ -37,9 +37,9 @@ env = { "CMD" = """genesis --genesis-file /data/genesis.json ipc gateway --subne [tasks.genesis-seal] extend = "fendermint-tool" -env = { "CMD" = "genesis --genesis-file /data/genesis.json ipc seal-genesis --builtin-actors-path /fendermint/bundle.car --custom-actors-path /fendermint/custom_actors_bundle.car --artifacts-path /fendermint/contracts --output-path {SEALED_GENESIS}" } +env = { "CMD" = "genesis --genesis-file /data/genesis.json ipc seal-genesis --builtin-actors-path /fendermint/bundle.car --custom-actors-path /fendermint/custom_actors_bundle.car --artifacts-path /fendermint/contracts --output-path ${SEALED_GENESIS}" } [tasks.genesis-write] extend = "fendermint-tool" -env = { "CMD" = "genesis --genesis-file /data/genesis.json into-tendermint --app-state {SEALED_GENESIS} --out /data/genesis.committed.json" } +env = { "CMD" = "genesis --genesis-file /data/genesis.json into-tendermint --app-state ${SEALED_GENESIS} --out /data/genesis.committed.json" } script.post = "cp ${BASE_DIR}/genesis.committed.json ${CMT_DIR}/config/genesis.json" From 672ed236b0e6496179c73d947c8a53a9f08f6c83 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 9 Aug 2024 13:18:22 +0800 Subject: [PATCH 022/111] update gas usage --- fendermint/actors/gas_market/src/lib.rs | 88 +++++++++++--- fendermint/vm/interpreter/src/fvm/exec.rs | 11 +- fendermint/vm/interpreter/src/fvm/gas.rs | 115 ------------------ .../vm/interpreter/src/fvm/gas/eip1559.rs | 108 ++++++++++++++++ fendermint/vm/interpreter/src/fvm/gas/mod.rs | 33 +++++ fendermint/vm/interpreter/src/fvm/mod.rs | 6 +- 6 files changed, 219 insertions(+), 142 deletions(-) delete mode 100644 fendermint/vm/interpreter/src/fvm/gas.rs create mode 100644 fendermint/vm/interpreter/src/fvm/gas/eip1559.rs create mode 100644 fendermint/vm/interpreter/src/fvm/gas/mod.rs diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 8b0e25118..a0161b9b3 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -6,17 +6,28 @@ use fil_actors_runtime::runtime::{ActorCode, Runtime}; use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fil_actors_runtime::{actor_dispatch, ActorError}; use fvm_ipld_encoding::tuple::*; +use fvm_shared::econ::TokenAmount; use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; +use num_traits::Zero; +use std::ops::Mul; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); -pub const IPC_GAS_ACTOR_NAME: &str = "gas"; +/// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +const EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; +/// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +const EIP1559_ELASTICITY_MULTIPLIER: u64 = 2; +/// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const EIP1559_INITIAL_BASE_FEE: u64 = 1_000_000_000; +pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; +pub type Gas = u64; #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559GasState { - pub block_gas_limit: u64, + pub block_gas_limit: Gas, + pub base_fee: TokenAmount, } pub struct EIP1559GasMarketActor {} @@ -25,7 +36,8 @@ pub struct EIP1559GasMarketActor {} #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - UpdateGasMarketState = frc42_dispatch::method_hash!("UpdateGasMarketState"), + SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), + UpdateBlockGasConsumption = frc42_dispatch::method_hash!("UpdateBlockGasConsumption"), } impl EIP1559GasMarketActor { @@ -37,31 +49,72 @@ impl EIP1559GasMarketActor { Ok(()) } - fn update_gas_market_state( - rt: &impl Runtime, - new_state: EIP1559GasState, - ) -> Result<(), ActorError> { + fn set_block_gas_limit(rt: &impl Runtime, block_gas_limit: Gas) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - *st = new_state; + st.block_gas_limit = block_gas_limit; Ok(()) })?; Ok(()) } + + fn update_block_gas_consumption( + rt: &impl Runtime, + block_gas_used: Gas, + ) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut EIP1559GasState, _rt| { + st.base_fee = update_base_fee(st.block_gas_limit, block_gas_used, st.base_fee.clone()); + Ok(()) + }) + } +} + +fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> TokenAmount { + let gas_target = gas_limit / EIP1559_ELASTICITY_MULTIPLIER; + + if gas_used == gas_target { + return base_fee; + } + + if gas_used > gas_target { + let gas_used_delta = gas_used - gas_target; + let base_fee_delta = base_fee + .clone() + .mul(gas_used_delta) + .div_floor(gas_target) + .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR) + .max(TokenAmount::from_atto(1)); + base_fee + base_fee_delta + } else { + let gas_used_delta = gas_target - gas_used; + let base_fee_per_gas_delta = base_fee + .clone() + .mul(gas_used_delta) + .div_floor(gas_target) + .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR); + if base_fee_per_gas_delta > base_fee { + TokenAmount::zero() + } else { + base_fee - base_fee_per_gas_delta + } + } } impl ActorCode for EIP1559GasMarketActor { type Methods = Method; fn name() -> &'static str { - IPC_GAS_ACTOR_NAME + IPC_GAS_MARKET_ACTOR_NAME } actor_dispatch! { Constructor => constructor, - UpdateGasMarketState => update_gas_market_state, + SetBlockGasLimit => set_block_gas_limit, + UpdateBlockGasConsumption => update_block_gas_consumption, } } @@ -88,6 +141,7 @@ mod tests { Method::Constructor as u64, IpldBlock::serialize_cbor(&EIP1559GasState { block_gas_limit: 100, + base_fee: Default::default(), }) .unwrap(), ) @@ -107,11 +161,8 @@ mod tests { rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); let r = rt.call::( - Method::UpdateGasMarketState as u64, - IpldBlock::serialize_cbor(&EIP1559GasState { - block_gas_limit: 20, - }) - .unwrap(), + Method::SetBlockGasLimit as u64, + IpldBlock::serialize_cbor(&20).unwrap(), ); assert!(r.is_ok()); @@ -127,11 +178,8 @@ mod tests { let code = rt .call::( - Method::UpdateGasMarketState as u64, - IpldBlock::serialize_cbor(&EIP1559GasState { - block_gas_limit: 20, - }) - .unwrap(), + Method::SetBlockGasLimit as u64, + IpldBlock::serialize_cbor(&20).unwrap(), ) .unwrap_err() .exit_code(); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 249bceed8..96cae811b 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -12,6 +12,7 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; +use crate::fvm::gas::GasMarket; use crate::ExecInterpreter; use super::{ @@ -57,7 +58,7 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); - self.gas.reset_from_chain_state(&state)?; + self.gas.reload_from_chain(&state)?; // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); @@ -159,14 +160,14 @@ where (apply_ret, emitters, latency) } else { - // TODO: maybe compare the gas limits are better? + // TODO: maybe compare the gas limits is better? msg.gas_limit = msg.gas_limit.min(self.gas.available_block_gas()); let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - self.gas - .deduct_available_block_gas(apply_ret.msg_receipt.gas_used)?; + self.gas.consume_gas(apply_ret.msg_receipt.gas_used)?; + (apply_ret, emitters, latency) }; @@ -193,6 +194,8 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { + self.gas.update_params(&mut state)?; + // TODO: Consider doing this async, since it's purely informational and not consensus-critical. let _ = checkpoint::emit_trace_if_check_checkpoint_finalized(&self.gateway, &mut state) .inspect_err(|e| { diff --git a/fendermint/vm/interpreter/src/fvm/gas.rs b/fendermint/vm/interpreter/src/fvm/gas.rs deleted file mode 100644 index 6b7244a4c..000000000 --- a/fendermint/vm/interpreter/src/fvm/gas.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::state::{read_actor_state, FvmExecState}; -use crate::fvm::FvmMessage; -use anyhow::anyhow; -use fendermint_actor_gas_market::EIP1559GasState; -use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; -use fendermint_vm_actor_interface::system; -use fvm_ipld_blockstore::Blockstore; -use std::marker::PhantomData; -use std::sync::atomic::{AtomicU64, Ordering}; - -type AtomicGas = AtomicU64; -pub type Gas = u64; - -pub struct EIP1559GasMarket { - /// The total gas available to be used by transactions in the current block - /// The executor requires Send + Sync, using an atomic variable instead of u64. - available_block_gas: AtomicGas, - _p: PhantomData, -} - -impl EIP1559GasMarket { - pub fn new() -> Self { - Self { - available_block_gas: AtomicGas::new(0), - _p: Default::default(), - } - } - - pub fn reset_from_chain_state(&self, chain_state: &FvmExecState) -> anyhow::Result<()> { - let current = self.available_block_gas.load(Ordering::SeqCst); - let gas_state = get_gas_state(chain_state)?; - self.atomic_set_block_gas_quota(current, gas_state.block_gas_limit)?; - Ok(()) - } - - pub fn available_block_gas(&self) -> Gas { - self.available_block_gas.load(Ordering::SeqCst) - } - - pub fn deduct_available_block_gas(&self, gas: Gas) -> anyhow::Result<()> { - let available = self.available_block_gas.load(Ordering::SeqCst); - match available.checked_sub(gas) { - Some(v) => { - self.atomic_set_block_gas_quota(available, v)?; - Ok(()) - } - None => Err(anyhow!("out of block gas")), - } - } - - fn atomic_set_block_gas_quota(&self, old: Gas, new: Gas) -> anyhow::Result<()> { - self.available_block_gas - .compare_exchange(old, new, Ordering::SeqCst, Ordering::SeqCst) - .map_err(|_| anyhow!("concurrent update to block gas available, should not happen"))?; - Ok(()) - } -} - -impl EIP1559GasMarket -where - DB: Blockstore + Clone + 'static, -{ - #[allow(dead_code)] - fn update_state( - &self, - blockchain_state: &mut FvmExecState, - gas_state: EIP1559GasState, - ) -> anyhow::Result<()> { - let params = fvm_ipld_encoding::RawBytes::serialize(gas_state)?; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, - sequence: blockchain_state.block_height() as u64, - // exclude this from gas restriction - gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::UpdateGasMarketState as u64, - params, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let (apply_ret, _) = blockchain_state.execute_implicit(msg)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update EIP1559 gas state: {}", err) - } else { - Ok(()) - } - } -} - -fn get_gas_state( - state: &FvmExecState, -) -> anyhow::Result { - let s = read_actor_state::( - state, - fendermint_vm_actor_interface::gas::GAS_ACTOR_ID, - )?; - Ok(s) -} - -impl Clone for EIP1559GasMarket { - fn clone(&self) -> Self { - Self { - available_block_gas: AtomicU64::new(self.available_block_gas.load(Ordering::SeqCst)), - _p: Default::default(), - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/gas/eip1559.rs b/fendermint/vm/interpreter/src/fvm/gas/eip1559.rs new file mode 100644 index 000000000..2ff0e5b9e --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/gas/eip1559.rs @@ -0,0 +1,108 @@ +use crate::fvm::gas::{Gas, GasMarket}; +use crate::fvm::state::{read_actor_state, FvmExecState}; +use crate::fvm::FvmMessage; +use anyhow::anyhow; +use fendermint_actor_gas_market::EIP1559GasState; +use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; +use fendermint_vm_actor_interface::system; +use fvm_ipld_blockstore::Blockstore; +use std::sync::atomic::{AtomicU64, Ordering}; + +type AtomicGas = AtomicU64; + +/// The gas market based on EIP1155 +#[derive(Default)] +pub struct EIP1559GasMarket { + /// The block gas limit + block_gas_limit: AtomicGas, + /// The accumulated gas usage so far + block_gas_used: AtomicGas, +} + +impl GasMarket for EIP1559GasMarket { + type State = EIP1559GasState; + + fn reload_from_chain( + &self, + chain_state: &FvmExecState, + ) -> anyhow::Result<()> { + let state = get_state(chain_state)?; + self.block_gas_used.store(0, Ordering::SeqCst); + self.block_gas_limit + .store(state.block_gas_limit, Ordering::SeqCst); + Ok(()) + } + + fn available_block_gas(&self) -> Gas { + self.block_gas_limit.load(Ordering::SeqCst) - self.block_gas_used.load(Ordering::SeqCst) + } + + fn consume_gas(&self, gas: Gas) -> anyhow::Result<()> { + let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); + + if block_gas_used + gas >= self.block_gas_limit.load(Ordering::SeqCst) { + return Err(anyhow!("out of block gas")); + } + + let new_gas_used = block_gas_used + gas; + self.update_block_gas_used(block_gas_used, new_gas_used) + } + + fn update_params( + &self, + chain_state: &mut FvmExecState, + ) -> anyhow::Result<()> { + let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); + let params = fvm_ipld_encoding::RawBytes::serialize(block_gas_used)?; + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_ACTOR_ADDR, + sequence: chain_state.block_height() as u64, + // exclude this from gas restriction + gas_limit: u64::MAX, + method_num: fendermint_actor_gas_market::Method::UpdateBlockGasConsumption as u64, + params, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let (apply_ret, _) = chain_state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to update EIP1559 gas state: {}", err) + } else { + Ok(()) + } + } +} + +impl EIP1559GasMarket { + fn update_block_gas_used(&self, old_used: Gas, new_used: Gas) -> anyhow::Result<()> { + self.block_gas_used + .compare_exchange(old_used, new_used, Ordering::SeqCst, Ordering::SeqCst) + .map_err(|_| anyhow!("concurrent update in block gas used, should not happen"))?; + Ok(()) + } +} + +impl Clone for EIP1559GasMarket { + fn clone(&self) -> Self { + Self { + block_gas_limit: AtomicGas::new(self.block_gas_limit.load(Ordering::SeqCst)), + block_gas_used: AtomicGas::new(self.block_gas_used.load(Ordering::SeqCst)), + } + } +} + +#[inline] +fn get_state( + chain_state: &FvmExecState, +) -> anyhow::Result { + read_actor_state::( + chain_state, + fendermint_vm_actor_interface::gas::GAS_ACTOR_ID, + ) +} diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs new file mode 100644 index 000000000..00710ce21 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::state::FvmExecState; +use fvm_ipld_blockstore::Blockstore; + +pub mod eip1559; + +pub type Gas = u64; + +/// The gas market for fendermint. This should be backed by an fvm actor. +pub trait GasMarket { + /// The gas market state + type State; + + /// Reset the gas market based on the current block chain state + fn reload_from_chain( + &self, + chain_state: &FvmExecState, + ) -> anyhow::Result<()>; + + /// Obtain the current block gas available for execution + fn available_block_gas(&self) -> Gas; + + /// Tracks the amount of gas consumed by a transaction + fn consume_gas(&self, gas: Gas) -> anyhow::Result<()>; + + /// Update the gas market params to blockchain state. This usually happens at the end of the block + fn update_params( + &self, + chain_state: &mut FvmExecState, + ) -> anyhow::Result<()>; +} diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 3c5cee674..35449396d 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -19,7 +19,7 @@ pub mod bundle; pub(crate) mod gas; pub(crate) mod topdown; -use crate::fvm::gas::EIP1559GasMarket; +use crate::fvm::gas::eip1559::EIP1559GasMarket; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -84,7 +84,7 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, - gas: EIP1559GasMarket, + gas: EIP1559GasMarket, } impl FvmMessageInterpreter @@ -110,7 +110,7 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, - gas: EIP1559GasMarket::new(), + gas: EIP1559GasMarket::default(), } } From 05d79e1428600a9fa8683e39fe02d48d2e8e2848 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 12 Aug 2024 12:18:19 +0800 Subject: [PATCH 023/111] review feedback --- fendermint/actors/gas_market/src/lib.rs | 12 +++ fendermint/vm/interpreter/src/fvm/exec.rs | 2 +- .../src/fvm/gas/{eip1559.rs => actor.rs} | 89 +++++++++++-------- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 11 +-- fendermint/vm/interpreter/src/fvm/mod.rs | 6 +- .../vm/interpreter/src/fvm/state/exec.rs | 20 +---- .../vm/interpreter/src/fvm/state/mod.rs | 1 - 7 files changed, 70 insertions(+), 71 deletions(-) rename fendermint/vm/interpreter/src/fvm/gas/{eip1559.rs => actor.rs} (56%) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index a0161b9b3..93c6af123 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -36,6 +36,7 @@ pub struct EIP1559GasMarketActor {} #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, + GetState = frc42_dispatch::method_hash!("GetState"), SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), UpdateBlockGasConsumption = frc42_dispatch::method_hash!("UpdateBlockGasConsumption"), } @@ -60,6 +61,10 @@ impl EIP1559GasMarketActor { Ok(()) } + fn get_state(rt: &impl Runtime) -> Result { + rt.state() + } + fn update_block_gas_consumption( rt: &impl Runtime, block_gas_used: Gas, @@ -114,6 +119,7 @@ impl ActorCode for EIP1559GasMarketActor { actor_dispatch! { Constructor => constructor, SetBlockGasLimit => set_block_gas_limit, + GetState => get_state, UpdateBlockGasConsumption => update_block_gas_consumption, } } @@ -166,6 +172,12 @@ mod tests { ); assert!(r.is_ok()); + let r = rt + .call::( + Method::GetState as u64, + IpldBlock::serialize_cbor(&()).unwrap(), + ) + .unwrap(); let s = rt.get_state::(); assert_eq!(s.block_gas_limit, 20); } diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 96cae811b..88a92de71 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -58,7 +58,7 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); - self.gas.reload_from_chain(&state)?; + self.gas.load(&mut state)?; // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); diff --git a/fendermint/vm/interpreter/src/fvm/gas/eip1559.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs similarity index 56% rename from fendermint/vm/interpreter/src/fvm/gas/eip1559.rs rename to fendermint/vm/interpreter/src/fvm/gas/actor.rs index 2ff0e5b9e..b00bfcbff 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/eip1559.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,38 +1,31 @@ use crate::fvm::gas::{Gas, GasMarket}; -use crate::fvm::state::{read_actor_state, FvmExecState}; +use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; -use anyhow::anyhow; -use fendermint_actor_gas_market::EIP1559GasState; +use anyhow::Context; + use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; use fendermint_vm_actor_interface::system; use fvm_ipld_blockstore::Blockstore; +use fvm_ipld_encoding::BytesDe; use std::sync::atomic::{AtomicU64, Ordering}; type AtomicGas = AtomicU64; +type GasMarketState = fendermint_actor_gas_market::EIP1559GasState; /// The gas market based on EIP1155 +/// Due to the reference trait bound limit (`&self` instead of `&mut self`) in Interpreter, `Atmomic` +/// is used. However, the calling pattern should be single threaded, so direct `store` could be used. +/// The usage of `Atomic` is purely to bypass the compilation issue without using unsafe. +/// TODO: remove this overhead when trait bound is updated. #[derive(Default)] -pub struct EIP1559GasMarket { +pub struct ActorGasMarket { /// The block gas limit block_gas_limit: AtomicGas, /// The accumulated gas usage so far block_gas_used: AtomicGas, } -impl GasMarket for EIP1559GasMarket { - type State = EIP1559GasState; - - fn reload_from_chain( - &self, - chain_state: &FvmExecState, - ) -> anyhow::Result<()> { - let state = get_state(chain_state)?; - self.block_gas_used.store(0, Ordering::SeqCst); - self.block_gas_limit - .store(state.block_gas_limit, Ordering::SeqCst); - Ok(()) - } - +impl GasMarket for ActorGasMarket { fn available_block_gas(&self) -> Gas { self.block_gas_limit.load(Ordering::SeqCst) - self.block_gas_used.load(Ordering::SeqCst) } @@ -41,11 +34,12 @@ impl GasMarket for EIP1559GasMarket { let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); if block_gas_used + gas >= self.block_gas_limit.load(Ordering::SeqCst) { - return Err(anyhow!("out of block gas")); + anyhow::bail!("out of block gas") } + self.block_gas_used + .store(block_gas_used + gas, Ordering::SeqCst); - let new_gas_used = block_gas_used + gas; - self.update_block_gas_used(block_gas_used, new_gas_used) + Ok(()) } fn update_params( @@ -79,16 +73,47 @@ impl GasMarket for EIP1559GasMarket { } } -impl EIP1559GasMarket { - fn update_block_gas_used(&self, old_used: Gas, new_used: Gas) -> anyhow::Result<()> { - self.block_gas_used - .compare_exchange(old_used, new_used, Ordering::SeqCst, Ordering::SeqCst) - .map_err(|_| anyhow!("concurrent update in block gas used, should not happen"))?; +impl ActorGasMarket { + pub fn load( + &self, + chain_state: &mut FvmExecState, + ) -> anyhow::Result<()> { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_ACTOR_ADDR, + sequence: chain_state.block_height() as u64, + // exclude this from gas restriction + gas_limit: u64::MAX, + method_num: fendermint_actor_gas_market::Method::GetState as u64, + params: fvm_ipld_encoding::RawBytes::serialize(())?, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let (apply_ret, _) = chain_state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to read gas market state: {}", err); + } + + let output = apply_ret + .msg_receipt + .return_data + .deserialize::() + .map(|bz| bz.0) + .context("failed to deserialize error data")?; + let state = fvm_ipld_encoding::from_slice::(&output)?; + + self.block_gas_used.store(0, Ordering::SeqCst); + self.block_gas_limit + .store(state.block_gas_limit, Ordering::SeqCst); Ok(()) } } -impl Clone for EIP1559GasMarket { +impl Clone for ActorGasMarket { fn clone(&self) -> Self { Self { block_gas_limit: AtomicGas::new(self.block_gas_limit.load(Ordering::SeqCst)), @@ -96,13 +121,3 @@ impl Clone for EIP1559GasMarket { } } } - -#[inline] -fn get_state( - chain_state: &FvmExecState, -) -> anyhow::Result { - read_actor_state::( - chain_state, - fendermint_vm_actor_interface::gas::GAS_ACTOR_ID, - ) -} diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index 00710ce21..f29b6cd5c 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -4,21 +4,12 @@ use crate::fvm::state::FvmExecState; use fvm_ipld_blockstore::Blockstore; -pub mod eip1559; +pub mod actor; pub type Gas = u64; /// The gas market for fendermint. This should be backed by an fvm actor. pub trait GasMarket { - /// The gas market state - type State; - - /// Reset the gas market based on the current block chain state - fn reload_from_chain( - &self, - chain_state: &FvmExecState, - ) -> anyhow::Result<()>; - /// Obtain the current block gas available for execution fn available_block_gas(&self) -> Gas; diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 35449396d..fa62398c6 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -19,7 +19,7 @@ pub mod bundle; pub(crate) mod gas; pub(crate) mod topdown; -use crate::fvm::gas::eip1559::EIP1559GasMarket; +use crate::fvm::gas::actor::ActorGasMarket; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -84,7 +84,7 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, - gas: EIP1559GasMarket, + gas: ActorGasMarket, } impl FvmMessageInterpreter @@ -110,7 +110,7 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, - gas: EIP1559GasMarket::default(), + gas: ActorGasMarket::default(), } } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 03794e221..7d5f10dda 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -15,12 +15,11 @@ use fvm::{ DefaultKernel, }; use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::{CborStore, RawBytes}; +use fvm_ipld_encoding::RawBytes; use fvm_shared::{ address::Address, chainid::ChainID, clock::ChainEpoch, econ::TokenAmount, error::ExitCode, message::Message, receipt::Receipt, version::NetworkVersion, ActorID, }; -use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -351,20 +350,3 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { }; (ret, Default::default()) } - -pub(crate) fn read_actor_state( - state: &FvmExecState, - actor_id: ActorID, -) -> anyhow::Result { - let state_tree = state.state_tree(); - - let state_cid = state_tree - .get_actor(actor_id)? - .ok_or_else(|| anyhow::anyhow!("actor state not found: {}", actor_id))? - .state; - - Ok(state_tree - .store() - .get_cbor::(&state_cid)? - .ok_or_else(|| anyhow::anyhow!("actor state should not be null"))?) -} diff --git a/fendermint/vm/interpreter/src/fvm/state/mod.rs b/fendermint/vm/interpreter/src/fvm/state/mod.rs index 43098aa24..22a1504e4 100644 --- a/fendermint/vm/interpreter/src/fvm/state/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/state/mod.rs @@ -12,7 +12,6 @@ pub mod snapshot; use std::sync::Arc; pub use check::FvmCheckState; -pub(crate) use exec::read_actor_state; pub use exec::{BlockHash, FvmExecState, FvmStateParams, FvmUpdatableParams}; pub use genesis::{empty_state_tree, FvmGenesisState}; pub use query::FvmQueryState; From 54381478f4e87c426a8ccaaee4a17227a07bfefb Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:52:29 +0800 Subject: [PATCH 024/111] Update fendermint/vm/interpreter/src/fvm/gas/actor.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index b00bfcbff..56730ae21 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -13,7 +13,7 @@ type AtomicGas = AtomicU64; type GasMarketState = fendermint_actor_gas_market::EIP1559GasState; /// The gas market based on EIP1155 -/// Due to the reference trait bound limit (`&self` instead of `&mut self`) in Interpreter, `Atmomic` +/// Due to the reference trait bound limit (`&self` instead of `&mut self`) in Interpreter, `Atomic` /// is used. However, the calling pattern should be single threaded, so direct `store` could be used. /// The usage of `Atomic` is purely to bypass the compilation issue without using unsafe. /// TODO: remove this overhead when trait bound is updated. From 99926b3e812466bb53ab6a59d518bef118e01edf Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 13 Aug 2024 19:14:06 +0800 Subject: [PATCH 025/111] review feedbacks --- fendermint/actors/gas_market/src/lib.rs | 21 ++-- fendermint/vm/interpreter/src/fvm/exec.rs | 21 ++-- .../vm/interpreter/src/fvm/gas/actor.rs | 110 ++++++++---------- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 11 +- fendermint/vm/interpreter/src/fvm/mod.rs | 3 - .../vm/interpreter/src/fvm/state/exec.rs | 24 ++-- 6 files changed, 89 insertions(+), 101 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 93c6af123..bb6b03b2f 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -16,13 +16,14 @@ use std::ops::Mul; fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; +const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const EIP1559_ELASTICITY_MULTIPLIER: u64 = 2; +const ELASTICITY_MULTIPLIER: u64 = 2; /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -pub const EIP1559_INITIAL_BASE_FEE: u64 = 1_000_000_000; +pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; +pub type GasMarketReading = EIP1559GasState; #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559GasState { @@ -36,7 +37,7 @@ pub struct EIP1559GasMarketActor {} #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - GetState = frc42_dispatch::method_hash!("GetState"), + CurrentGasReading = frc42_dispatch::method_hash!("CurrentGasReading"), SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), UpdateBlockGasConsumption = frc42_dispatch::method_hash!("UpdateBlockGasConsumption"), } @@ -61,7 +62,7 @@ impl EIP1559GasMarketActor { Ok(()) } - fn get_state(rt: &impl Runtime) -> Result { + fn current_gas_reading(rt: &impl Runtime) -> Result { rt.state() } @@ -79,7 +80,7 @@ impl EIP1559GasMarketActor { } fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> TokenAmount { - let gas_target = gas_limit / EIP1559_ELASTICITY_MULTIPLIER; + let gas_target = gas_limit / ELASTICITY_MULTIPLIER; if gas_used == gas_target { return base_fee; @@ -91,7 +92,7 @@ fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> Toke .clone() .mul(gas_used_delta) .div_floor(gas_target) - .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR) + .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR) .max(TokenAmount::from_atto(1)); base_fee + base_fee_delta } else { @@ -100,7 +101,7 @@ fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> Toke .clone() .mul(gas_used_delta) .div_floor(gas_target) - .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR); + .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR); if base_fee_per_gas_delta > base_fee { TokenAmount::zero() } else { @@ -119,7 +120,7 @@ impl ActorCode for EIP1559GasMarketActor { actor_dispatch! { Constructor => constructor, SetBlockGasLimit => set_block_gas_limit, - GetState => get_state, + CurrentGasReading => get_state, UpdateBlockGasConsumption => update_block_gas_consumption, } } @@ -174,7 +175,7 @@ mod tests { let r = rt .call::( - Method::GetState as u64, + Method::CurrentGasReading as u64, IpldBlock::serialize_cbor(&()).unwrap(), ) .unwrap(); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 88a92de71..bdcee5a60 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use anyhow::Context; +use anyhow::{bail, Context}; use async_trait::async_trait; use std::collections::HashMap; @@ -58,8 +58,6 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); - self.gas.load(&mut state)?; - // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); let block_height: u64 = state.block_height().try_into().unwrap(); @@ -152,7 +150,7 @@ where async fn deliver( &self, mut state: Self::State, - mut msg: Self::Message, + msg: Self::Message, ) -> anyhow::Result<(Self::State, Self::DeliverOutput)> { let (apply_ret, emitters, latency) = if msg.from == system::SYSTEM_ACTOR_ADDR { let (execution_result, latency) = measure_time(|| state.execute_implicit(msg.clone())); @@ -160,13 +158,20 @@ where (apply_ret, emitters, latency) } else { - // TODO: maybe compare the gas limits is better? - msg.gas_limit = msg.gas_limit.min(self.gas.available_block_gas()); + if msg.gas_limit > state.gas_market().available_block_gas() { + bail!("gas limit exceed available block gas limit") + } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - self.gas.consume_gas(apply_ret.msg_receipt.gas_used)?; + if state + .gas_market_mut() + .record_gas_used(apply_ret.msg_receipt.gas_used) + .is_err() + { + tracing::warn!("should not have exceeded block gas limit"); + } (apply_ret, emitters, latency) }; @@ -194,7 +199,7 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { - self.gas.update_params(&mut state)?; + state.update_gas_market()?; // TODO: Consider doing this async, since it's purely informational and not consensus-critical. let _ = checkpoint::emit_trace_if_check_checkpoint_finalized(&self.gateway, &mut state) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 56730ae21..00ae0f32b 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,123 +1,107 @@ use crate::fvm::gas::{Gas, GasMarket}; -use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; use anyhow::Context; use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; use fendermint_vm_actor_interface::system; -use fvm_ipld_blockstore::Blockstore; +use fvm::executor::{ApplyKind, Executor}; use fvm_ipld_encoding::BytesDe; -use std::sync::atomic::{AtomicU64, Ordering}; +use fvm_shared::clock::ChainEpoch; -type AtomicGas = AtomicU64; type GasMarketState = fendermint_actor_gas_market::EIP1559GasState; -/// The gas market based on EIP1155 -/// Due to the reference trait bound limit (`&self` instead of `&mut self`) in Interpreter, `Atomic` -/// is used. However, the calling pattern should be single threaded, so direct `store` could be used. -/// The usage of `Atomic` is purely to bypass the compilation issue without using unsafe. -/// TODO: remove this overhead when trait bound is updated. #[derive(Default)] pub struct ActorGasMarket { /// The block gas limit - block_gas_limit: AtomicGas, + block_gas_limit: Gas, /// The accumulated gas usage so far - block_gas_used: AtomicGas, + block_gas_used: Gas, } impl GasMarket for ActorGasMarket { fn available_block_gas(&self) -> Gas { - self.block_gas_limit.load(Ordering::SeqCst) - self.block_gas_used.load(Ordering::SeqCst) + self.block_gas_limit - self.block_gas_used } - fn consume_gas(&self, gas: Gas) -> anyhow::Result<()> { - let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); - - if block_gas_used + gas >= self.block_gas_limit.load(Ordering::SeqCst) { + fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()> { + if self.block_gas_used + gas >= self.block_gas_limit { anyhow::bail!("out of block gas") } - self.block_gas_used - .store(block_gas_used + gas, Ordering::SeqCst); + self.block_gas_used += gas; Ok(()) } +} - fn update_params( - &self, - chain_state: &mut FvmExecState, - ) -> anyhow::Result<()> { - let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); - let params = fvm_ipld_encoding::RawBytes::serialize(block_gas_used)?; - +impl ActorGasMarket { + pub fn new( + executor: &mut E, + block_height: ChainEpoch, + ) -> anyhow::Result { let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, to: GAS_ACTOR_ADDR, - sequence: chain_state.block_height() as u64, + sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::UpdateBlockGasConsumption as u64, - params, + method_num: fendermint_actor_gas_market::Method::GetState as u64, + params: fvm_ipld_encoding::RawBytes::serialize(())?, value: Default::default(), version: Default::default(), gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - let (apply_ret, _) = chain_state.execute_implicit(msg)?; + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update EIP1559 gas state: {}", err) - } else { - Ok(()) + anyhow::bail!("failed to read gas market state: {}", err); } + + let output = apply_ret + .msg_receipt + .return_data + .deserialize::() + .map(|bz| bz.0) + .context("failed to deserialize error data")?; + let state = fvm_ipld_encoding::from_slice::(&output)?; + + Ok(Self { + block_gas_limit: state.block_gas_limit, + block_gas_used: 0, + }) } -} -impl ActorGasMarket { - pub fn load( + pub fn commit( &self, - chain_state: &mut FvmExecState, + executor: &mut E, + block_height: ChainEpoch, ) -> anyhow::Result<()> { + let block_gas_used = self.block_gas_used; + let params = fvm_ipld_encoding::RawBytes::serialize(block_gas_used)?; + let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, to: GAS_ACTOR_ADDR, - sequence: chain_state.block_height() as u64, + sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::GetState as u64, - params: fvm_ipld_encoding::RawBytes::serialize(())?, + method_num: fendermint_actor_gas_market::Method::UpdateBlockGasConsumption as u64, + params, value: Default::default(), version: Default::default(), gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - let (apply_ret, _) = chain_state.execute_implicit(msg)?; + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to read gas market state: {}", err); - } - - let output = apply_ret - .msg_receipt - .return_data - .deserialize::() - .map(|bz| bz.0) - .context("failed to deserialize error data")?; - let state = fvm_ipld_encoding::from_slice::(&output)?; - - self.block_gas_used.store(0, Ordering::SeqCst); - self.block_gas_limit - .store(state.block_gas_limit, Ordering::SeqCst); - Ok(()) - } -} - -impl Clone for ActorGasMarket { - fn clone(&self) -> Self { - Self { - block_gas_limit: AtomicGas::new(self.block_gas_limit.load(Ordering::SeqCst)), - block_gas_used: AtomicGas::new(self.block_gas_used.load(Ordering::SeqCst)), + anyhow::bail!("failed to update EIP1559 gas state: {}", err) + } else { + Ok(()) } } } diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index f29b6cd5c..8c9dfb91b 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -1,9 +1,6 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::state::FvmExecState; -use fvm_ipld_blockstore::Blockstore; - pub mod actor; pub type Gas = u64; @@ -14,11 +11,5 @@ pub trait GasMarket { fn available_block_gas(&self) -> Gas; /// Tracks the amount of gas consumed by a transaction - fn consume_gas(&self, gas: Gas) -> anyhow::Result<()>; - - /// Update the gas market params to blockchain state. This usually happens at the end of the block - fn update_params( - &self, - chain_state: &mut FvmExecState, - ) -> anyhow::Result<()>; + fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()>; } diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index fa62398c6..3a301a43c 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -19,7 +19,6 @@ pub mod bundle; pub(crate) mod gas; pub(crate) mod topdown; -use crate::fvm::gas::actor::ActorGasMarket; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -84,7 +83,6 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, - gas: ActorGasMarket, } impl FvmMessageInterpreter @@ -110,7 +108,6 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, - gas: ActorGasMarket::default(), } } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 7d5f10dda..cbd6f6bc4 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use crate::fvm::externs::FendermintExterns; +use crate::fvm::gas::actor::ActorGasMarket; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; @@ -113,6 +114,8 @@ where /// Indicate whether the parameters have been updated. params_dirty: bool, + + gas_market: ActorGasMarket, } impl FvmExecState @@ -146,7 +149,8 @@ where let engine = multi_engine.get(&nc)?; let externs = FendermintExterns::new(blockstore.clone(), params.state_root); let machine = DefaultMachine::new(&mc, blockstore, externs)?; - let executor = DefaultExecutor::new(engine, machine)?; + let mut executor = DefaultExecutor::new(engine, machine)?; + let gas_market = ActorGasMarket::new(&mut executor, block_height)?; Ok(Self { executor, @@ -159,6 +163,7 @@ where power_scale: params.power_scale, }, params_dirty: false, + gas_market, }) } @@ -174,6 +179,14 @@ where self } + pub fn gas_market_mut(&mut self) -> &mut ActorGasMarket { + &mut self.gas_market + } + + pub fn gas_market(&self) -> &ActorGasMarket { + &self.gas_market + } + /// Execute message implicitly. pub fn execute_implicit(&mut self, msg: Message) -> ExecResult { self.execute_message(msg, ApplyKind::Implicit) @@ -286,12 +299,9 @@ where self.update_params(|p| f(&mut p.app_version)) } - /// Update the application version. - pub fn update_base_fee(&mut self, f: F) - where - F: FnOnce(&mut TokenAmount), - { - self.update_params(|p| f(&mut p.base_fee)) + pub fn update_gas_market(&mut self) -> anyhow::Result<()> { + let height = self.block_height(); + self.gas_market.commit(&mut self.executor, height) } /// Update the circulating supply, effective from the next block. From 79d1264b59d37a4d6113f04e9f16c053d555636a Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 13 Aug 2024 19:31:33 +0800 Subject: [PATCH 026/111] merge with main --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index b61ddd29d..f50cd9396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3342,6 +3342,7 @@ dependencies = [ "serde", "serde_json", "serde_with 2.3.3", + "snap", "strum", "tempfile", "tendermint 0.31.1", From 6d15ea6d2db018687c79cd029476a9219cf62cf8 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 13 Aug 2024 21:42:06 +0800 Subject: [PATCH 027/111] update based on review --- Cargo.lock | 1 + fendermint/actors/gas_market/Cargo.toml | 1 + fendermint/actors/gas_market/src/lib.rs | 78 +++++++++++-------- fendermint/vm/actor_interface/src/gas.rs | 2 +- .../vm/interpreter/src/fvm/gas/actor.rs | 14 ++-- 5 files changed, 57 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4949a7313..b60c2bc1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2848,6 +2848,7 @@ dependencies = [ "fvm_ipld_encoding", "fvm_shared", "hex-literal 0.4.1", + "lazy_static", "log", "multihash 0.18.1", "num-derive 0.3.3", diff --git a/fendermint/actors/gas_market/Cargo.toml b/fendermint/actors/gas_market/Cargo.toml index a2ff9f0eb..a4cc318da 100644 --- a/fendermint/actors/gas_market/Cargo.toml +++ b/fendermint/actors/gas_market/Cargo.toml @@ -22,6 +22,7 @@ log = { workspace = true } multihash = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } +lazy_static = { workspace = true } serde = { workspace = true } hex-literal = { workspace = true } frc42_dispatch = { workspace = true } diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index bb6b03b2f..8bc2b561e 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -8,8 +8,8 @@ use fil_actors_runtime::{actor_dispatch, ActorError}; use fvm_ipld_encoding::tuple::*; use fvm_shared::econ::TokenAmount; use fvm_shared::METHOD_CONSTRUCTOR; +use lazy_static::lazy_static; use num_derive::FromPrimitive; -use num_traits::Zero; use std::ops::Mul; #[cfg(feature = "fil-actor")] @@ -19,8 +19,11 @@ fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) const ELASTICITY_MULTIPLIER: u64 = 2; -/// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; +lazy_static! { + /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + static ref INITIAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(1_000_000_000); + static ref MIN_BASE_FEE: TokenAmount = TokenAmount::from_atto(100); +} pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; pub type GasMarketReading = EIP1559GasState; @@ -31,6 +34,16 @@ pub struct EIP1559GasState { pub base_fee: TokenAmount, } +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct BlockGasUtilization { + pub block_gas_used: Gas, +} + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct SetConstants { + pub block_gas_limit: Gas, +} + pub struct EIP1559GasMarketActor {} #[derive(FromPrimitive)] @@ -38,24 +51,22 @@ pub struct EIP1559GasMarketActor {} pub enum Method { Constructor = METHOD_CONSTRUCTOR, CurrentGasReading = frc42_dispatch::method_hash!("CurrentGasReading"), - SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), - UpdateBlockGasConsumption = frc42_dispatch::method_hash!("UpdateBlockGasConsumption"), + SetConstants = frc42_dispatch::method_hash!("SetConstants"), + UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), } impl EIP1559GasMarketActor { /// Creates the actor pub fn constructor(rt: &impl Runtime, st: EIP1559GasState) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - rt.create(&st)?; - - Ok(()) + rt.create(&st) } - fn set_block_gas_limit(rt: &impl Runtime, block_gas_limit: Gas) -> Result<(), ActorError> { + fn set_constants(rt: &impl Runtime, constants: SetConstants) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.block_gas_limit = block_gas_limit; + st.block_gas_limit = constants.block_gas_limit; Ok(()) })?; @@ -66,36 +77,39 @@ impl EIP1559GasMarketActor { rt.state() } - fn update_block_gas_consumption( + fn update_utilization( rt: &impl Runtime, - block_gas_used: Gas, + utilization: BlockGasUtilization, ) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.base_fee = update_base_fee(st.block_gas_limit, block_gas_used, st.base_fee.clone()); + st.base_fee = st.next_base_fee(utilization.block_gas_used); Ok(()) }) } } -fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> TokenAmount { - let gas_target = gas_limit / ELASTICITY_MULTIPLIER; +impl EIP1559GasState { + fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { + let base_fee = self.base_fee.clone(); + let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; - if gas_used == gas_target { - return base_fee; - } + if gas_used == gas_target { + return base_fee; + } + + if gas_used > gas_target { + let gas_used_delta = gas_used - gas_target; + let base_fee_delta = base_fee + .clone() + .mul(gas_used_delta) + .div_floor(gas_target) + .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR) + .max(TokenAmount::from_atto(1)); + return base_fee + base_fee_delta; + } - if gas_used > gas_target { - let gas_used_delta = gas_used - gas_target; - let base_fee_delta = base_fee - .clone() - .mul(gas_used_delta) - .div_floor(gas_target) - .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR) - .max(TokenAmount::from_atto(1)); - base_fee + base_fee_delta - } else { let gas_used_delta = gas_target - gas_used; let base_fee_per_gas_delta = base_fee .clone() @@ -103,7 +117,7 @@ fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> Toke .div_floor(gas_target) .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR); if base_fee_per_gas_delta > base_fee { - TokenAmount::zero() + MIN_BASE_FEE.clone() } else { base_fee - base_fee_per_gas_delta } @@ -119,9 +133,9 @@ impl ActorCode for EIP1559GasMarketActor { actor_dispatch! { Constructor => constructor, - SetBlockGasLimit => set_block_gas_limit, - CurrentGasReading => get_state, - UpdateBlockGasConsumption => update_block_gas_consumption, + SetConstants => set_constants, + CurrentGasReading => current_gas_reading, + UpdateUtilization => update_utilization, } } diff --git a/fendermint/vm/actor_interface/src/gas.rs b/fendermint/vm/actor_interface/src/gas.rs index 3fae569a7..ccc6c1f18 100644 --- a/fendermint/vm/actor_interface/src/gas.rs +++ b/fendermint/vm/actor_interface/src/gas.rs @@ -1,4 +1,4 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -define_id!(GAS { id: 98 }); +define_id!(GAS_MARKET { id: 98 }); diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 00ae0f32b..5ec0762f4 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -2,7 +2,7 @@ use crate::fvm::gas::{Gas, GasMarket}; use crate::fvm::FvmMessage; use anyhow::Context; -use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; +use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; use fendermint_vm_actor_interface::system; use fvm::executor::{ApplyKind, Executor}; use fvm_ipld_encoding::BytesDe; @@ -40,11 +40,11 @@ impl ActorGasMarket { ) -> anyhow::Result { let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::GetState as u64, + method_num: fendermint_actor_gas_market::Method::CurrentGasReading as u64, params: fvm_ipld_encoding::RawBytes::serialize(())?, value: Default::default(), version: Default::default(), @@ -79,15 +79,17 @@ impl ActorGasMarket { block_height: ChainEpoch, ) -> anyhow::Result<()> { let block_gas_used = self.block_gas_used; - let params = fvm_ipld_encoding::RawBytes::serialize(block_gas_used)?; + let params = fvm_ipld_encoding::RawBytes::serialize( + fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, + )?; let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::UpdateBlockGasConsumption as u64, + method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, params, value: Default::default(), version: Default::default(), From e7b3e0654d67f0c615f81f63b6ba0ed978d6cd08 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 13 Aug 2024 22:29:32 +0800 Subject: [PATCH 028/111] update base fee logic --- fendermint/actors/gas_market/src/lib.rs | 48 +++++++++++-------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 8bc2b561e..a90c3fa81 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -10,7 +10,7 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::METHOD_CONSTRUCTOR; use lazy_static::lazy_static; use num_derive::FromPrimitive; -use std::ops::Mul; +use std::cmp::Ordering; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); @@ -22,7 +22,7 @@ const ELASTICITY_MULTIPLIER: u64 = 2; lazy_static! { /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) static ref INITIAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(1_000_000_000); - static ref MIN_BASE_FEE: TokenAmount = TokenAmount::from_atto(100); + static ref MINIMAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(100); } pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; @@ -95,31 +95,25 @@ impl EIP1559GasState { let base_fee = self.base_fee.clone(); let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; - if gas_used == gas_target { - return base_fee; - } - - if gas_used > gas_target { - let gas_used_delta = gas_used - gas_target; - let base_fee_delta = base_fee - .clone() - .mul(gas_used_delta) - .div_floor(gas_target) - .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR) - .max(TokenAmount::from_atto(1)); - return base_fee + base_fee_delta; - } - - let gas_used_delta = gas_target - gas_used; - let base_fee_per_gas_delta = base_fee - .clone() - .mul(gas_used_delta) - .div_floor(gas_target) - .div_floor(BASE_FEE_MAX_CHANGE_DENOMINATOR); - if base_fee_per_gas_delta > base_fee { - MIN_BASE_FEE.clone() - } else { - base_fee - base_fee_per_gas_delta + match gas_used.cmp(&gas_target) { + Ordering::Equal => base_fee, + Ordering::Less => { + let base_fee_delta = base_fee.atto() * (gas_target - gas_used) + / gas_target + / BASE_FEE_MAX_CHANGE_DENOMINATOR; + let base_fee_delta = TokenAmount::from_atto(base_fee_delta); + if base_fee_delta >= base_fee { + MINIMAL_BASE_FEE.clone() + } else { + base_fee - base_fee_delta + } + } + Ordering::Greater => { + let gas_used_delta = gas_used - gas_target; + let delta = + base_fee.atto() * gas_used_delta / gas_target / BASE_FEE_MAX_CHANGE_DENOMINATOR; + base_fee + TokenAmount::from_atto(delta).max(TokenAmount::from_atto(1)) + } } } } From 0ae95c8eb681b0a2f85f01043a0419dcd0594428 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:06:43 +0800 Subject: [PATCH 029/111] Update fevm-contract-tests.yaml --- .github/workflows/fevm-contract-tests.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/fevm-contract-tests.yaml b/.github/workflows/fevm-contract-tests.yaml index f4b59d5e4..fcf8f6e88 100644 --- a/.github/workflows/fevm-contract-tests.yaml +++ b/.github/workflows/fevm-contract-tests.yaml @@ -71,8 +71,7 @@ jobs: id: docker-build working-directory: ipc/fendermint run: | - export PATH="$PATH:/home/runner/.config/.foundry/bin" - docker pull ghcr.io/consensus-shipyard/fendermint:latest + make docker-build - name: Run a testnode id: testnode working-directory: ipc/ From 169bc367c06f90b394cb578d143bd3a3c8e28638 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 14 Aug 2024 15:12:23 +0800 Subject: [PATCH 030/111] fix tests --- fendermint/actors/gas_market/src/lib.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index a90c3fa81..d3b5c18c7 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -135,7 +135,7 @@ impl ActorCode for EIP1559GasMarketActor { #[cfg(test)] mod tests { - use crate::{EIP1559GasMarketActor, EIP1559GasState, Method}; + use crate::{EIP1559GasMarketActor, EIP1559GasState, Method, SetConstants}; use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::ipld_block::IpldBlock; @@ -176,17 +176,11 @@ mod tests { rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); let r = rt.call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20 }).unwrap(), ); assert!(r.is_ok()); - let r = rt - .call::( - Method::CurrentGasReading as u64, - IpldBlock::serialize_cbor(&()).unwrap(), - ) - .unwrap(); let s = rt.get_state::(); assert_eq!(s.block_gas_limit, 20); } @@ -199,8 +193,8 @@ mod tests { let code = rt .call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20}).unwrap(), ) .unwrap_err() .exit_code(); From fa372d69ee8ab395c33eacd6f000267e5f043499 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:20:12 +0800 Subject: [PATCH 031/111] Update fevm-contract-tests.yaml --- .github/workflows/fevm-contract-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fevm-contract-tests.yaml b/.github/workflows/fevm-contract-tests.yaml index fcf8f6e88..b276b9e75 100644 --- a/.github/workflows/fevm-contract-tests.yaml +++ b/.github/workflows/fevm-contract-tests.yaml @@ -71,7 +71,7 @@ jobs: id: docker-build working-directory: ipc/fendermint run: | - make docker-build + export PATH="$PATH:/home/runner/.config/.foundry/bin" && make docker-build - name: Run a testnode id: testnode working-directory: ipc/ From 5332a31b9f49ad8a83b2a6424bf2929191d201d0 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:29:24 +0800 Subject: [PATCH 032/111] Update testnode.toml --- infra/fendermint/scripts/testnode.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/fendermint/scripts/testnode.toml b/infra/fendermint/scripts/testnode.toml index 3477c8039..7bf1de9bf 100644 --- a/infra/fendermint/scripts/testnode.toml +++ b/infra/fendermint/scripts/testnode.toml @@ -6,7 +6,6 @@ workspace = false dependencies = [ "create-log-volume", "testnode-down", - "fendermint-pull", "testnode-init", "docker-network-create", "cometbft-init", From ed66780cd6efc25b53367f4cdca177a7f4ea1d01 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 14 Aug 2024 17:02:45 +0800 Subject: [PATCH 033/111] skip fendermint build --- infra/fendermint/scripts/testnode.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infra/fendermint/scripts/testnode.toml b/infra/fendermint/scripts/testnode.toml index 7bf1de9bf..0fe82fc5d 100644 --- a/infra/fendermint/scripts/testnode.toml +++ b/infra/fendermint/scripts/testnode.toml @@ -6,6 +6,7 @@ workspace = false dependencies = [ "create-log-volume", "testnode-down", + "fendermint-pull", "testnode-init", "docker-network-create", "cometbft-init", @@ -18,6 +19,7 @@ dependencies = [ "ethapi-start", "testnode-report", ] +env = { FM_PULL_SKIP = true } [tasks.testnode-init] dependencies = [ @@ -70,6 +72,7 @@ dependencies = [ "genesis-new-accounts", "genesis-add-validator", "genesis-new-gateway", + "genesis-seal", "genesis-write", "testnode-export-keys", ] From 272c29c1133fde9cb57b55b33b259374abf7af0f Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 14 Aug 2024 17:33:41 +0800 Subject: [PATCH 034/111] remove pull --- infra/fendermint/scripts/testnode.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/fendermint/scripts/testnode.toml b/infra/fendermint/scripts/testnode.toml index 0fe82fc5d..503f116c6 100644 --- a/infra/fendermint/scripts/testnode.toml +++ b/infra/fendermint/scripts/testnode.toml @@ -6,7 +6,6 @@ workspace = false dependencies = [ "create-log-volume", "testnode-down", - "fendermint-pull", "testnode-init", "docker-network-create", "cometbft-init", @@ -19,7 +18,6 @@ dependencies = [ "ethapi-start", "testnode-report", ] -env = { FM_PULL_SKIP = true } [tasks.testnode-init] dependencies = [ From 29cda12c3144c5830cd4a70bd24a26538a099c6f Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 15 Aug 2024 20:26:29 +0800 Subject: [PATCH 035/111] prepare and process --- fendermint/actors/gas_market/src/lib.rs | 10 ++- fendermint/app/src/app.rs | 28 +++++++- fendermint/vm/interpreter/src/chain.rs | 91 ++++++++++++++++++++++--- 3 files changed, 113 insertions(+), 16 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index d3b5c18c7..37b1e4da1 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -177,7 +177,10 @@ mod tests { let r = rt.call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20 }).unwrap(), + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ); assert!(r.is_ok()); @@ -194,7 +197,10 @@ mod tests { let code = rt .call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20}).unwrap(), + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ) .unwrap_err() .exit_code(); diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index af4e8da72..09fefffb3 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -407,7 +407,7 @@ where Genesis = Vec, Output = FvmGenesisOutput, >, - I: ProposalInterpreter>, + I: ProposalInterpreter), Message = Vec>, I: ExecInterpreter< State = (ChainEnv, FvmExecState), Message = Vec, @@ -615,13 +615,25 @@ where ); let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect(); + let (state_params, block_height) = + self.state_params_at_height(request.height.value().into())?; + let state = FvmExecState::new( + self.state_store_clone(), + self.multi_engine.as_ref(), + block_height as ChainEpoch, + state_params, + ) + .context("error creating new state")?; + let txs = self .interpreter - .prepare(self.chain_env.clone(), txs) + .prepare((self.chain_env.clone(), state), txs) .await .context("failed to prepare proposal")?; let txs = txs.into_iter().map(bytes::Bytes::from).collect(); + // TODO: This seems leaky placed here, should be done in `interpreter`, that's where it's ipc + // TODO: aware, here might have filtered more important messages. let (txs, size) = take_until_max_size(txs, request.max_tx_bytes.try_into().unwrap()); emit(BlockProposalSent { @@ -648,9 +660,19 @@ where let size_txs = txs.iter().map(|tx| tx.len()).sum::(); let num_txs = txs.len(); + let (state_params, block_height) = + self.state_params_at_height(request.height.value().into())?; + let state = FvmExecState::new( + self.state_store_clone(), + self.multi_engine.as_ref(), + block_height as ChainEpoch, + state_params, + ) + .context("error creating new state")?; + let accept = self .interpreter - .process(self.chain_env.clone(), txs) + .process((self.chain_env.clone(), state), txs) .await .context("failed to process proposal")?; diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 79136138f..3d7513b3b 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,5 +1,6 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use crate::fvm::gas::{Gas, GasMarket}; use crate::fvm::state::ipc::GatewayCaller; use crate::fvm::{topdown, FvmApplyRet, PowerUpdates}; use crate::{ @@ -8,7 +9,7 @@ use crate::{ signed::{SignedMessageApplyRes, SignedMessageCheckRes, SyntheticMessage, VerifiableMessage}, CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, }; -use anyhow::{bail, Context}; +use anyhow::{anyhow, bail, Context}; use async_stm::atomically; use async_trait::async_trait; use fendermint_tracing::emit; @@ -94,6 +95,56 @@ impl ChainMessageInterpreter { gateway_caller: GatewayCaller::default(), } } + + fn signed_msgs_wtih_gas_limit( + &self, + msgs: Vec, + ) -> anyhow::Result> { + msgs.into_iter() + .map(|msg| match msg { + ChainMessage::Signed(inner) => { + let gas_limit = inner.message.gas_limit; + Ok((ChainMessage::Signed(inner), gas_limit)) + } + ChainMessage::Ipc(_) => { + Err(anyhow!("should not have ipc messages in user proposals")) + } + }) + .collect::>>() + } + + /// Performs message selection: + /// - Order by gas limit in descending order + /// - Make sure total gas limit does not exceed the `total_gas_limit` parameter + fn messages_selection( + &self, + msgs: Vec, + total_gas_limit: Gas, + ) -> anyhow::Result> { + let mut msgs_with_gas_limit = self.signed_msgs_wtih_gas_limit(msgs)?; + + // sort by gas limit descending + msgs_with_gas_limit.sort_by(|a, b| b.1.cmp(&a.1)); + + let mut total_gas_limit_consumed = 0; + let mut msgs = vec![]; + for (msg, gas_limit) in msgs_with_gas_limit { + if total_gas_limit_consumed + gas_limit <= total_gas_limit { + msgs.push(msg); + total_gas_limit_consumed += gas_limit; + } else { + break; + } + } + + tracing::info!( + num_msgs = msgs.len(), + total_gas_limit, + "selected message under total gas limit" + ); + + Ok(msgs) + } } #[async_trait] @@ -102,7 +153,7 @@ where DB: Blockstore + Clone + 'static + Send + Sync, I: Sync + Send, { - type State = ChainEnv; + type State = (ChainEnv, FvmExecState); type Message = ChainMessage; /// Check whether there are any "ready" messages in the IPLD resolution mempool which can be appended to the proposal. @@ -111,11 +162,13 @@ where /// account the transactions which are part of top-down or bottom-up checkpoints, to stay within gas limits. async fn prepare( &self, - state: Self::State, + (chain_env, state): Self::State, mut msgs: Vec, ) -> anyhow::Result> { + msgs = self.messages_selection(msgs, state.gas_market().available_block_gas())?; + // Collect resolved CIDs ready to be proposed from the pool. - let ckpts = atomically(|| state.checkpoint_pool.collect_resolved()).await; + let ckpts = atomically(|| chain_env.checkpoint_pool.collect_resolved()).await; // Create transactions ready to be included on the chain. let ckpts = ckpts.into_iter().map(|ckpt| match ckpt { @@ -124,14 +177,19 @@ where // Prepare top down proposals. // Before we try to find a quorum, pause incoming votes. This is optional but if there are lots of votes coming in it might hold up proposals. - atomically(|| state.parent_finality_votes.pause_votes_until_find_quorum()).await; + atomically(|| { + chain_env + .parent_finality_votes + .pause_votes_until_find_quorum() + }) + .await; // The pre-requisite for proposal is that there is a quorum of gossiped votes at that height. // The final proposal can be at most as high as the quorum, but can be less if we have already, // hit some limits such as how many blocks we can propose in a single step. let finalities = atomically(|| { - let parent = state.parent_finality_provider.next_proposal()?; - let quorum = state + let parent = chain_env.parent_finality_provider.next_proposal()?; + let quorum = chain_env .parent_finality_votes .find_quorum()? .map(|(height, block_hash)| IPCParentFinality { height, block_hash }); @@ -175,7 +233,13 @@ where } /// Perform finality checks on top-down transactions and availability checks on bottom-up transactions. - async fn process(&self, env: Self::State, msgs: Vec) -> anyhow::Result { + async fn process( + &self, + (chain_env, state): Self::State, + msgs: Vec, + ) -> anyhow::Result { + let mut block_gas_usage = 0; + for msg in msgs { match msg { ChainMessage::Ipc(IpcMessage::BottomUpExec(msg)) => { @@ -187,7 +251,7 @@ where // 1) we validated it when it was relayed, and // 2) if a validator proposes something invalid, we can make them pay during execution. let is_resolved = - atomically(|| match env.checkpoint_pool.get_status(&item)? { + atomically(|| match chain_env.checkpoint_pool.get_status(&item)? { None => Ok(false), Some(status) => status.is_resolved(), }) @@ -206,15 +270,20 @@ where block_hash, }; let is_final = - atomically(|| env.parent_finality_provider.check_proposal(&prop)).await; + atomically(|| chain_env.parent_finality_provider.check_proposal(&prop)) + .await; if !is_final { return Ok(false); } } + ChainMessage::Signed(signed) => { + block_gas_usage += signed.message.gas_limit; + } _ => {} }; } - Ok(true) + + Ok(block_gas_usage <= state.gas_market().available_block_gas()) } } From ef2740cf1cbb4e0a6854de2ebed637b61b5e758e Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 19 Aug 2024 13:23:31 +0800 Subject: [PATCH 036/111] rename to genesis builder --- fendermint/app/src/cmd/genesis.rs | 15 ++++++---- fendermint/vm/interpreter/src/genesis.rs | 38 ++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index 598c4a3c7..134c9ad4d 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -14,7 +14,7 @@ use fendermint_vm_genesis::{ ipc, Account, Actor, ActorMeta, Collateral, Genesis, Multisig, PermissionMode, SignerAddr, Validator, ValidatorKey, }; -use fendermint_vm_interpreter::genesis::{GenesisAppState, GenesisCreator}; +use fendermint_vm_interpreter::genesis::{GenesisAppState, GenesisBuilder}; use crate::cmd; use crate::options::genesis::*; @@ -290,16 +290,19 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh } async fn seal_genesis(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::Result<()> { - let genesis = read_genesis(genesis_file)?; + let genesis_params = read_genesis(genesis_file)?; - let genesis_creator = GenesisCreator::new( + let mut builder = GenesisBuilder::new( args.builtin_actors_path.clone(), args.custom_actors_path.clone(), - args.artifacts_path.clone(), - args.output_path.clone(), + genesis_params, ); - genesis_creator.create(genesis).await + if let Some(ref ipc_system_artifacts) = args.artifacts_path { + builder = builder.with_ipc_system_contracts(ipc_system_artifacts.clone()); + } + + builder.write_to(args.output_path.clone()).await } async fn new_genesis_from_parent( diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index de9a258b4..aefd7d242 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -142,51 +142,59 @@ pub struct GenesisOutput { pub validators: Vec>, } -pub struct GenesisCreator { +pub struct GenesisBuilder { /// Hardhat like util to deploy ipc contracts hardhat: Option, /// The built in actors bundle path builtin_actors_path: PathBuf, /// The custom actors bundle path custom_actors_path: PathBuf, - /// The CAR path to flush the sealed genesis state - out_path: PathBuf, + + /// Genesis params + genesis_params: Genesis, } -impl GenesisCreator { +impl GenesisBuilder { pub fn new( builtin_actors_path: PathBuf, custom_actors_path: PathBuf, - maybe_artifacts_path: Option, - sealed_out_path: PathBuf, + genesis_params: Genesis, ) -> Self { Self { - hardhat: maybe_artifacts_path.map(Hardhat::new), + hardhat: None, builtin_actors_path, custom_actors_path, - out_path: sealed_out_path, + genesis_params, } } - /// Initialize actor states from the Genesis parameters - pub async fn create(&self, genesis: Genesis) -> anyhow::Result<()> { + pub fn with_ipc_system_contracts(mut self, path: PathBuf) -> Self { + self.hardhat = Some(Hardhat::new(path)); + self + } + + /// Initialize actor states from the Genesis parameters and write the sealed genesis state to + /// a CAR file specified by `out_path` + pub async fn write_to(&self, out_path: PathBuf) -> anyhow::Result<()> { let mut state = self.init_state().await?; - let out = self.populate_state(&mut state, genesis)?; + let genesis_state = self.populate_state(&mut state, self.genesis_params.clone())?; let (state_root, store) = state.finalize()?; - self.write_car(state_root, out, store).await + self.write_car(state_root, genesis_state, out_path, store) + .await } async fn write_car( &self, state_root: Cid, - out: GenesisOutput, + genesis_state: GenesisOutput, + out_path: PathBuf, store: MemoryBlockstore, ) -> anyhow::Result<()> { - let file = tokio::fs::File::create(&self.out_path).await?; + let file = tokio::fs::File::create(&out_path).await?; tracing::info!(state_root = state_root.to_string(), "state root"); - let metadata = GenesisMetadata::new(state_root, out); + let metadata = GenesisMetadata::new(state_root, genesis_state); let streamer = StateTreeStreamer::new(state_root, store); let (metadata_cid, metadata_bytes) = derive_cid(&metadata)?; From b07453024b695e228f600aa667b4739061372029 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:17:47 +0800 Subject: [PATCH 037/111] Update fendermint/vm/interpreter/src/fvm/state/exec.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/state/exec.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index cbd6f6bc4..b93201afb 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -114,7 +114,8 @@ where /// Indicate whether the parameters have been updated. params_dirty: bool, - +/// Keeps track of block gas usage during execution, and takes care of updating +/// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). gas_market: ActorGasMarket, } From 5b4d061988bea91f68702d111a3781cf28051a4a Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 16:57:38 +0800 Subject: [PATCH 038/111] update bail to logging --- fendermint/actors/gas_market/src/lib.rs | 10 ++++++++-- fendermint/vm/interpreter/src/fvm/exec.rs | 4 ++-- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 4 ++-- fendermint/vm/interpreter/src/fvm/state/exec.rs | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index d3b5c18c7..37b1e4da1 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -177,7 +177,10 @@ mod tests { let r = rt.call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20 }).unwrap(), + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ); assert!(r.is_ok()); @@ -194,7 +197,10 @@ mod tests { let code = rt .call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { block_gas_limit: 20}).unwrap(), + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ) .unwrap_err() .exit_code(); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index bdcee5a60..95a7e974d 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use anyhow::{bail, Context}; +use anyhow::Context; use async_trait::async_trait; use std::collections::HashMap; @@ -159,7 +159,7 @@ where (apply_ret, emitters, latency) } else { if msg.gas_limit > state.gas_market().available_block_gas() { - bail!("gas limit exceed available block gas limit") + tracing::warn!("gas limit exceed available block gas limit"); } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 5ec0762f4..196adb5e8 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -25,9 +25,9 @@ impl GasMarket for ActorGasMarket { fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()> { if self.block_gas_used + gas >= self.block_gas_limit { - anyhow::bail!("out of block gas") + tracing::warn!("out of block gas, should not have happened") } - self.block_gas_used += gas; + self.block_gas_used = self.block_gas_used.saturating_add(gas); Ok(()) } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index b93201afb..782d3c3fe 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -114,8 +114,8 @@ where /// Indicate whether the parameters have been updated. params_dirty: bool, -/// Keeps track of block gas usage during execution, and takes care of updating -/// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). + /// Keeps track of block gas usage during execution, and takes care of updating + /// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). gas_market: ActorGasMarket, } From ca9ba85fcc97d5f65d0e0fa798d0109e74aa06c8 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 16:58:50 +0800 Subject: [PATCH 039/111] add header --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 196adb5e8..fd2d79ca6 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,3 +1,5 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT use crate::fvm::gas::{Gas, GasMarket}; use crate::fvm::FvmMessage; use anyhow::Context; From f04e987b8b22a183414e9c81da90bf0fc6308a72 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 17:28:06 +0800 Subject: [PATCH 040/111] init gas market in genesis --- fendermint/actors/gas_market/src/lib.rs | 4 ++++ fendermint/vm/interpreter/src/genesis.rs | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 37b1e4da1..138f89847 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -91,6 +91,10 @@ impl EIP1559GasMarketActor { } impl EIP1559GasState { + pub fn new(block_gas_limit: Gas) -> Self { + Self { block_gas_limit, base_fee: INITIAL_BASE_FEE.clone() } + } + fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { let base_fee = self.base_fee.clone(); let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index aefd7d242..bc4a906ec 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -19,7 +19,7 @@ use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, cron, eam, init, ipc, reward, system, EMPTY_ARR, + account, burntfunds, chainmetadata, gas, cron, eam, init, ipc, reward, system, EMPTY_ARR, }; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; @@ -28,6 +28,7 @@ use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_car::{load_car, CarHeader}; use fvm_ipld_encoding::CborStore; +use fvm_shared::BLOCK_GAS_LIMIT; use fvm_shared::chainid::ChainID; use fvm_shared::econ::TokenAmount; use fvm_shared::version::NetworkVersion; @@ -417,6 +418,19 @@ impl GenesisBuilder { ) .context("failed to replace built in eam actor")?; + let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new( + BLOCK_GAS_LIMIT + ); + state + .create_custom_actor( + fendermint_actor_gas_market::IPC_GAS_MARKET_ACTOR_NAME, + gas::GAS_MARKET_ACTOR_ID, + &gas_market_state, + TokenAmount::zero(), + None, + ) + .context("failed to create gas market actor")?; + // STAGE 2: Create non-builtin accounts which do not have a fixed ID. // The next ID is going to be _after_ the accounts, which have already been assigned an ID by the `Init` actor. From a33c4b924516eaf969f74b38091c8ec95ada57e9 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 17:35:21 +0800 Subject: [PATCH 041/111] fmt --- fendermint/actors/gas_market/src/lib.rs | 5 ++++- fendermint/vm/interpreter/src/genesis.rs | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 138f89847..045fb2cba 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -92,7 +92,10 @@ impl EIP1559GasMarketActor { impl EIP1559GasState { pub fn new(block_gas_limit: Gas) -> Self { - Self { block_gas_limit, base_fee: INITIAL_BASE_FEE.clone() } + Self { + block_gas_limit, + base_fee: INITIAL_BASE_FEE.clone(), + } } fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index bc4a906ec..9ac78235f 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -19,7 +19,7 @@ use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, gas, cron, eam, init, ipc, reward, system, EMPTY_ARR, + account, burntfunds, chainmetadata, cron, eam, gas, init, ipc, reward, system, EMPTY_ARR, }; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; @@ -28,10 +28,10 @@ use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_car::{load_car, CarHeader}; use fvm_ipld_encoding::CborStore; -use fvm_shared::BLOCK_GAS_LIMIT; use fvm_shared::chainid::ChainID; use fvm_shared::econ::TokenAmount; use fvm_shared::version::NetworkVersion; +use fvm_shared::BLOCK_GAS_LIMIT; use ipc_actors_abis::i_diamond::FacetCut; use num_traits::Zero; @@ -418,9 +418,7 @@ impl GenesisBuilder { ) .context("failed to replace built in eam actor")?; - let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new( - BLOCK_GAS_LIMIT - ); + let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new(BLOCK_GAS_LIMIT); state .create_custom_actor( fendermint_actor_gas_market::IPC_GAS_MARKET_ACTOR_NAME, From 9f968961bb8ea23e2e4fe77037087b2c89f822f3 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 18:09:08 +0800 Subject: [PATCH 042/111] add gas_market to custom actor build --- fendermint/actors/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/actors/build.rs b/fendermint/actors/build.rs index 887950ad0..fc1a180e9 100644 --- a/fendermint/actors/build.rs +++ b/fendermint/actors/build.rs @@ -8,7 +8,7 @@ use std::path::Path; use std::process::{Command, Stdio}; use std::thread; -const ACTORS: &[&str] = &["chainmetadata", "eam"]; +const ACTORS: &[&str] = &["chainmetadata", "eam", "gas_market"]; const FILES_TO_WATCH: &[&str] = &["Cargo.toml", "src"]; From 5b72dc5b96a527dc8ed44508212b382116df06cc Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 18:37:00 +0800 Subject: [PATCH 043/111] format code --- fendermint/actors/gas_market/src/lib.rs | 1 + .../vm/interpreter/src/fvm/gas/actor.rs | 22 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 045fb2cba..967c0ea7c 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -74,6 +74,7 @@ impl EIP1559GasMarketActor { } fn current_gas_reading(rt: &impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; rt.state() } diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index fd2d79ca6..4b9b0d0f7 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -4,14 +4,12 @@ use crate::fvm::gas::{Gas, GasMarket}; use crate::fvm::FvmMessage; use anyhow::Context; +use fendermint_actor_gas_market::GasMarketReading; use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; use fendermint_vm_actor_interface::system; use fvm::executor::{ApplyKind, Executor}; -use fvm_ipld_encoding::BytesDe; use fvm_shared::clock::ChainEpoch; -type GasMarketState = fendermint_actor_gas_market::EIP1559GasState; - #[derive(Default)] pub struct ActorGasMarket { /// The block gas limit @@ -45,9 +43,9 @@ impl ActorGasMarket { to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction - gas_limit: u64::MAX, + gas_limit: fvm_shared::BLOCK_GAS_LIMIT, method_num: fendermint_actor_gas_market::Method::CurrentGasReading as u64, - params: fvm_ipld_encoding::RawBytes::serialize(())?, + params: fvm_ipld_encoding::RawBytes::default(), value: Default::default(), version: Default::default(), gas_fee_cap: Default::default(), @@ -61,16 +59,12 @@ impl ActorGasMarket { anyhow::bail!("failed to read gas market state: {}", err); } - let output = apply_ret - .msg_receipt - .return_data - .deserialize::() - .map(|bz| bz.0) - .context("failed to deserialize error data")?; - let state = fvm_ipld_encoding::from_slice::(&output)?; + let reading = + fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse gas market readying")?; Ok(Self { - block_gas_limit: state.block_gas_limit, + block_gas_limit: reading.block_gas_limit, block_gas_used: 0, }) } @@ -90,7 +84,7 @@ impl ActorGasMarket { to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction - gas_limit: u64::MAX, + gas_limit: fvm_shared::BLOCK_GAS_LIMIT, method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, params, value: Default::default(), From 4de6eac171f46d719f918b57143990b95c85a4a8 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:05:42 +0800 Subject: [PATCH 044/111] feat(node): block gas limit PR review changes (#1107) Co-authored-by: cryptoAtwill Co-authored-by: raulk --- Cargo.lock | 1 + fendermint/actors/gas_market/Cargo.toml | 1 + fendermint/actors/gas_market/src/lib.rs | 127 ++++++++++-------- fendermint/vm/actor_interface/src/gas.rs | 2 +- fendermint/vm/interpreter/src/fvm/exec.rs | 19 ++- .../vm/interpreter/src/fvm/gas/actor.rs | 122 ++++++++--------- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 11 +- fendermint/vm/interpreter/src/fvm/mod.rs | 3 - .../vm/interpreter/src/fvm/state/exec.rs | 25 +++- 9 files changed, 157 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6203003e..792df29d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2829,6 +2829,7 @@ dependencies = [ "fvm_ipld_encoding", "fvm_shared", "hex-literal 0.4.1", + "lazy_static", "log", "multihash 0.18.1", "num-derive 0.3.3", diff --git a/fendermint/actors/gas_market/Cargo.toml b/fendermint/actors/gas_market/Cargo.toml index a2ff9f0eb..a4cc318da 100644 --- a/fendermint/actors/gas_market/Cargo.toml +++ b/fendermint/actors/gas_market/Cargo.toml @@ -22,6 +22,7 @@ log = { workspace = true } multihash = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } +lazy_static = { workspace = true } serde = { workspace = true } hex-literal = { workspace = true } frc42_dispatch = { workspace = true } diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 93c6af123..37b1e4da1 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -8,21 +8,25 @@ use fil_actors_runtime::{actor_dispatch, ActorError}; use fvm_ipld_encoding::tuple::*; use fvm_shared::econ::TokenAmount; use fvm_shared::METHOD_CONSTRUCTOR; +use lazy_static::lazy_static; use num_derive::FromPrimitive; -use num_traits::Zero; -use std::ops::Mul; +use std::cmp::Ordering; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; +const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const EIP1559_ELASTICITY_MULTIPLIER: u64 = 2; -/// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -pub const EIP1559_INITIAL_BASE_FEE: u64 = 1_000_000_000; +const ELASTICITY_MULTIPLIER: u64 = 2; +lazy_static! { + /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + static ref INITIAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(1_000_000_000); + static ref MINIMAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(100); +} pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; +pub type GasMarketReading = EIP1559GasState; #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559GasState { @@ -30,81 +34,86 @@ pub struct EIP1559GasState { pub base_fee: TokenAmount, } +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct BlockGasUtilization { + pub block_gas_used: Gas, +} + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct SetConstants { + pub block_gas_limit: Gas, +} + pub struct EIP1559GasMarketActor {} #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - GetState = frc42_dispatch::method_hash!("GetState"), - SetBlockGasLimit = frc42_dispatch::method_hash!("SetBlockGasLimit"), - UpdateBlockGasConsumption = frc42_dispatch::method_hash!("UpdateBlockGasConsumption"), + CurrentGasReading = frc42_dispatch::method_hash!("CurrentGasReading"), + SetConstants = frc42_dispatch::method_hash!("SetConstants"), + UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), } impl EIP1559GasMarketActor { /// Creates the actor pub fn constructor(rt: &impl Runtime, st: EIP1559GasState) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - rt.create(&st)?; - - Ok(()) + rt.create(&st) } - fn set_block_gas_limit(rt: &impl Runtime, block_gas_limit: Gas) -> Result<(), ActorError> { + fn set_constants(rt: &impl Runtime, constants: SetConstants) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.block_gas_limit = block_gas_limit; + st.block_gas_limit = constants.block_gas_limit; Ok(()) })?; Ok(()) } - fn get_state(rt: &impl Runtime) -> Result { + fn current_gas_reading(rt: &impl Runtime) -> Result { rt.state() } - fn update_block_gas_consumption( + fn update_utilization( rt: &impl Runtime, - block_gas_used: Gas, + utilization: BlockGasUtilization, ) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.base_fee = update_base_fee(st.block_gas_limit, block_gas_used, st.base_fee.clone()); + st.base_fee = st.next_base_fee(utilization.block_gas_used); Ok(()) }) } } -fn update_base_fee(gas_limit: Gas, gas_used: Gas, base_fee: TokenAmount) -> TokenAmount { - let gas_target = gas_limit / EIP1559_ELASTICITY_MULTIPLIER; - - if gas_used == gas_target { - return base_fee; - } - - if gas_used > gas_target { - let gas_used_delta = gas_used - gas_target; - let base_fee_delta = base_fee - .clone() - .mul(gas_used_delta) - .div_floor(gas_target) - .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR) - .max(TokenAmount::from_atto(1)); - base_fee + base_fee_delta - } else { - let gas_used_delta = gas_target - gas_used; - let base_fee_per_gas_delta = base_fee - .clone() - .mul(gas_used_delta) - .div_floor(gas_target) - .div_floor(EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR); - if base_fee_per_gas_delta > base_fee { - TokenAmount::zero() - } else { - base_fee - base_fee_per_gas_delta +impl EIP1559GasState { + fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { + let base_fee = self.base_fee.clone(); + let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; + + match gas_used.cmp(&gas_target) { + Ordering::Equal => base_fee, + Ordering::Less => { + let base_fee_delta = base_fee.atto() * (gas_target - gas_used) + / gas_target + / BASE_FEE_MAX_CHANGE_DENOMINATOR; + let base_fee_delta = TokenAmount::from_atto(base_fee_delta); + if base_fee_delta >= base_fee { + MINIMAL_BASE_FEE.clone() + } else { + base_fee - base_fee_delta + } + } + Ordering::Greater => { + let gas_used_delta = gas_used - gas_target; + let delta = + base_fee.atto() * gas_used_delta / gas_target / BASE_FEE_MAX_CHANGE_DENOMINATOR; + base_fee + TokenAmount::from_atto(delta).max(TokenAmount::from_atto(1)) + } } } } @@ -118,15 +127,15 @@ impl ActorCode for EIP1559GasMarketActor { actor_dispatch! { Constructor => constructor, - SetBlockGasLimit => set_block_gas_limit, - GetState => get_state, - UpdateBlockGasConsumption => update_block_gas_consumption, + SetConstants => set_constants, + CurrentGasReading => current_gas_reading, + UpdateUtilization => update_utilization, } } #[cfg(test)] mod tests { - use crate::{EIP1559GasMarketActor, EIP1559GasState, Method}; + use crate::{EIP1559GasMarketActor, EIP1559GasState, Method, SetConstants}; use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::ipld_block::IpldBlock; @@ -167,17 +176,14 @@ mod tests { rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); let r = rt.call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ); assert!(r.is_ok()); - let r = rt - .call::( - Method::GetState as u64, - IpldBlock::serialize_cbor(&()).unwrap(), - ) - .unwrap(); let s = rt.get_state::(); assert_eq!(s.block_gas_limit, 20); } @@ -190,8 +196,11 @@ mod tests { let code = rt .call::( - Method::SetBlockGasLimit as u64, - IpldBlock::serialize_cbor(&20).unwrap(), + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&SetConstants { + block_gas_limit: 20, + }) + .unwrap(), ) .unwrap_err() .exit_code(); diff --git a/fendermint/vm/actor_interface/src/gas.rs b/fendermint/vm/actor_interface/src/gas.rs index 3fae569a7..ccc6c1f18 100644 --- a/fendermint/vm/actor_interface/src/gas.rs +++ b/fendermint/vm/actor_interface/src/gas.rs @@ -1,4 +1,4 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -define_id!(GAS { id: 98 }); +define_id!(GAS_MARKET { id: 98 }); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 88a92de71..95a7e974d 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -58,8 +58,6 @@ where // Block height (FVM epoch) as sequence is intentional let height = state.block_height(); - self.gas.load(&mut state)?; - // check for upgrades in the upgrade_scheduler let chain_id = state.chain_id(); let block_height: u64 = state.block_height().try_into().unwrap(); @@ -152,7 +150,7 @@ where async fn deliver( &self, mut state: Self::State, - mut msg: Self::Message, + msg: Self::Message, ) -> anyhow::Result<(Self::State, Self::DeliverOutput)> { let (apply_ret, emitters, latency) = if msg.from == system::SYSTEM_ACTOR_ADDR { let (execution_result, latency) = measure_time(|| state.execute_implicit(msg.clone())); @@ -160,13 +158,20 @@ where (apply_ret, emitters, latency) } else { - // TODO: maybe compare the gas limits is better? - msg.gas_limit = msg.gas_limit.min(self.gas.available_block_gas()); + if msg.gas_limit > state.gas_market().available_block_gas() { + tracing::warn!("gas limit exceed available block gas limit"); + } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - self.gas.consume_gas(apply_ret.msg_receipt.gas_used)?; + if state + .gas_market_mut() + .record_gas_used(apply_ret.msg_receipt.gas_used) + .is_err() + { + tracing::warn!("should not have exceeded block gas limit"); + } (apply_ret, emitters, latency) }; @@ -194,7 +199,7 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { - self.gas.update_params(&mut state)?; + state.update_gas_market()?; // TODO: Consider doing this async, since it's purely informational and not consensus-critical. let _ = checkpoint::emit_trace_if_check_checkpoint_finalized(&self.gateway, &mut state) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 56730ae21..fd2d79ca6 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,123 +1,111 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT use crate::fvm::gas::{Gas, GasMarket}; -use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; use anyhow::Context; -use fendermint_vm_actor_interface::gas::GAS_ACTOR_ADDR; +use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; use fendermint_vm_actor_interface::system; -use fvm_ipld_blockstore::Blockstore; +use fvm::executor::{ApplyKind, Executor}; use fvm_ipld_encoding::BytesDe; -use std::sync::atomic::{AtomicU64, Ordering}; +use fvm_shared::clock::ChainEpoch; -type AtomicGas = AtomicU64; type GasMarketState = fendermint_actor_gas_market::EIP1559GasState; -/// The gas market based on EIP1155 -/// Due to the reference trait bound limit (`&self` instead of `&mut self`) in Interpreter, `Atomic` -/// is used. However, the calling pattern should be single threaded, so direct `store` could be used. -/// The usage of `Atomic` is purely to bypass the compilation issue without using unsafe. -/// TODO: remove this overhead when trait bound is updated. #[derive(Default)] pub struct ActorGasMarket { /// The block gas limit - block_gas_limit: AtomicGas, + block_gas_limit: Gas, /// The accumulated gas usage so far - block_gas_used: AtomicGas, + block_gas_used: Gas, } impl GasMarket for ActorGasMarket { fn available_block_gas(&self) -> Gas { - self.block_gas_limit.load(Ordering::SeqCst) - self.block_gas_used.load(Ordering::SeqCst) + self.block_gas_limit - self.block_gas_used } - fn consume_gas(&self, gas: Gas) -> anyhow::Result<()> { - let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); - - if block_gas_used + gas >= self.block_gas_limit.load(Ordering::SeqCst) { - anyhow::bail!("out of block gas") + fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()> { + if self.block_gas_used + gas >= self.block_gas_limit { + tracing::warn!("out of block gas, should not have happened") } - self.block_gas_used - .store(block_gas_used + gas, Ordering::SeqCst); + self.block_gas_used = self.block_gas_used.saturating_add(gas); Ok(()) } +} - fn update_params( - &self, - chain_state: &mut FvmExecState, - ) -> anyhow::Result<()> { - let block_gas_used = self.block_gas_used.load(Ordering::SeqCst); - let params = fvm_ipld_encoding::RawBytes::serialize(block_gas_used)?; - +impl ActorGasMarket { + pub fn new( + executor: &mut E, + block_height: ChainEpoch, + ) -> anyhow::Result { let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, - sequence: chain_state.block_height() as u64, + to: GAS_MARKET_ACTOR_ADDR, + sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::UpdateBlockGasConsumption as u64, - params, + method_num: fendermint_actor_gas_market::Method::CurrentGasReading as u64, + params: fvm_ipld_encoding::RawBytes::serialize(())?, value: Default::default(), version: Default::default(), gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - let (apply_ret, _) = chain_state.execute_implicit(msg)?; + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update EIP1559 gas state: {}", err) - } else { - Ok(()) + anyhow::bail!("failed to read gas market state: {}", err); } + + let output = apply_ret + .msg_receipt + .return_data + .deserialize::() + .map(|bz| bz.0) + .context("failed to deserialize error data")?; + let state = fvm_ipld_encoding::from_slice::(&output)?; + + Ok(Self { + block_gas_limit: state.block_gas_limit, + block_gas_used: 0, + }) } -} -impl ActorGasMarket { - pub fn load( + pub fn commit( &self, - chain_state: &mut FvmExecState, + executor: &mut E, + block_height: ChainEpoch, ) -> anyhow::Result<()> { + let block_gas_used = self.block_gas_used; + let params = fvm_ipld_encoding::RawBytes::serialize( + fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, + )?; + let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, - to: GAS_ACTOR_ADDR, - sequence: chain_state.block_height() as u64, + to: GAS_MARKET_ACTOR_ADDR, + sequence: block_height as u64, // exclude this from gas restriction gas_limit: u64::MAX, - method_num: fendermint_actor_gas_market::Method::GetState as u64, - params: fvm_ipld_encoding::RawBytes::serialize(())?, + method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, + params, value: Default::default(), version: Default::default(), gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - let (apply_ret, _) = chain_state.execute_implicit(msg)?; + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to read gas market state: {}", err); - } - - let output = apply_ret - .msg_receipt - .return_data - .deserialize::() - .map(|bz| bz.0) - .context("failed to deserialize error data")?; - let state = fvm_ipld_encoding::from_slice::(&output)?; - - self.block_gas_used.store(0, Ordering::SeqCst); - self.block_gas_limit - .store(state.block_gas_limit, Ordering::SeqCst); - Ok(()) - } -} - -impl Clone for ActorGasMarket { - fn clone(&self) -> Self { - Self { - block_gas_limit: AtomicGas::new(self.block_gas_limit.load(Ordering::SeqCst)), - block_gas_used: AtomicGas::new(self.block_gas_used.load(Ordering::SeqCst)), + anyhow::bail!("failed to update EIP1559 gas state: {}", err) + } else { + Ok(()) } } } diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index f29b6cd5c..8c9dfb91b 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -1,9 +1,6 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::state::FvmExecState; -use fvm_ipld_blockstore::Blockstore; - pub mod actor; pub type Gas = u64; @@ -14,11 +11,5 @@ pub trait GasMarket { fn available_block_gas(&self) -> Gas; /// Tracks the amount of gas consumed by a transaction - fn consume_gas(&self, gas: Gas) -> anyhow::Result<()>; - - /// Update the gas market params to blockchain state. This usually happens at the end of the block - fn update_params( - &self, - chain_state: &mut FvmExecState, - ) -> anyhow::Result<()>; + fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()>; } diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index fa62398c6..3a301a43c 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -19,7 +19,6 @@ pub mod bundle; pub(crate) mod gas; pub(crate) mod topdown; -use crate::fvm::gas::actor::ActorGasMarket; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; pub use exec::FvmApplyRet; @@ -84,7 +83,6 @@ where gateway: GatewayCaller, /// Upgrade scheduler stores all the upgrades to be executed at given heights. upgrade_scheduler: UpgradeScheduler, - gas: ActorGasMarket, } impl FvmMessageInterpreter @@ -110,7 +108,6 @@ where push_chain_meta: true, gateway: GatewayCaller::default(), upgrade_scheduler, - gas: ActorGasMarket::default(), } } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 7d5f10dda..782d3c3fe 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use crate::fvm::externs::FendermintExterns; +use crate::fvm::gas::actor::ActorGasMarket; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; @@ -113,6 +114,9 @@ where /// Indicate whether the parameters have been updated. params_dirty: bool, + /// Keeps track of block gas usage during execution, and takes care of updating + /// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). + gas_market: ActorGasMarket, } impl FvmExecState @@ -146,7 +150,8 @@ where let engine = multi_engine.get(&nc)?; let externs = FendermintExterns::new(blockstore.clone(), params.state_root); let machine = DefaultMachine::new(&mc, blockstore, externs)?; - let executor = DefaultExecutor::new(engine, machine)?; + let mut executor = DefaultExecutor::new(engine, machine)?; + let gas_market = ActorGasMarket::new(&mut executor, block_height)?; Ok(Self { executor, @@ -159,6 +164,7 @@ where power_scale: params.power_scale, }, params_dirty: false, + gas_market, }) } @@ -174,6 +180,14 @@ where self } + pub fn gas_market_mut(&mut self) -> &mut ActorGasMarket { + &mut self.gas_market + } + + pub fn gas_market(&self) -> &ActorGasMarket { + &self.gas_market + } + /// Execute message implicitly. pub fn execute_implicit(&mut self, msg: Message) -> ExecResult { self.execute_message(msg, ApplyKind::Implicit) @@ -286,12 +300,9 @@ where self.update_params(|p| f(&mut p.app_version)) } - /// Update the application version. - pub fn update_base_fee(&mut self, f: F) - where - F: FnOnce(&mut TokenAmount), - { - self.update_params(|p| f(&mut p.base_fee)) + pub fn update_gas_market(&mut self) -> anyhow::Result<()> { + let height = self.block_height(); + self.gas_market.commit(&mut self.executor, height) } /// Update the circulating supply, effective from the next block. From a73287220f415f9bcd00859877d3e94ee0a3a96b Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:43:20 +0800 Subject: [PATCH 045/111] Update fendermint/vm/interpreter/src/fvm/exec.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 95a7e974d..2d46cb37f 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -170,7 +170,7 @@ where .record_gas_used(apply_ret.msg_receipt.gas_used) .is_err() { - tracing::warn!("should not have exceeded block gas limit"); + tracing::warn!("[ASSERTION FAILED] gas market failed while recording utilization: {err}"); } (apply_ret, emitters, latency) From 7e6e2ca6b32c5b145359aa2c1383c2db1dbb4615 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:43:32 +0800 Subject: [PATCH 046/111] Update fendermint/vm/interpreter/src/fvm/exec.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/exec.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 2d46cb37f..7f1b1cc4c 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -159,7 +159,8 @@ where (apply_ret, emitters, latency) } else { if msg.gas_limit > state.gas_market().available_block_gas() { - tracing::warn!("gas limit exceed available block gas limit"); + // This is panic-worthy, but we suppress it to avoid liveness issues. + tracing::warn!("[ASSERTION FAILED] message gas limit exceed available block gas limit; consensus engine is misbehaving"); } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); From cf0228a2bb8acb1951363d1db850a79b8c4bacd9 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:53:36 +0800 Subject: [PATCH 047/111] Update fendermint/actors/gas_market/src/lib.rs Co-authored-by: raulk --- fendermint/actors/gas_market/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 967c0ea7c..793f820d9 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -136,7 +136,7 @@ impl ActorCode for EIP1559GasMarketActor { actor_dispatch! { Constructor => constructor, SetConstants => set_constants, - CurrentGasReading => current_gas_reading, + CurrentReading => current_reading, UpdateUtilization => update_utilization, } } From b00bf392d60448f1a8b894fbe28d473f2dd92c14 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:53:43 +0800 Subject: [PATCH 048/111] Update fendermint/vm/interpreter/src/fvm/gas/actor.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 4b9b0d0f7..48d753bcf 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -23,7 +23,7 @@ impl GasMarket for ActorGasMarket { self.block_gas_limit - self.block_gas_used } - fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()> { + fn record_utilization(&mut self, gas: Gas) -> anyhow::Result<()> { if self.block_gas_used + gas >= self.block_gas_limit { tracing::warn!("out of block gas, should not have happened") } From 88090ac8802e0811650b775b38002ebe3a6e6f55 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:53:57 +0800 Subject: [PATCH 049/111] Update fendermint/vm/interpreter/src/fvm/gas/actor.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 48d753bcf..41e424e3a 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -19,7 +19,11 @@ pub struct ActorGasMarket { } impl GasMarket for ActorGasMarket { - fn available_block_gas(&self) -> Gas { + struct Available { + block_gas: Gas + } + + fn available(&self) -> Available { self.block_gas_limit - self.block_gas_used } From ca9b0af8050de981f9429ff0cab3711c7cd63a4f Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:54:07 +0800 Subject: [PATCH 050/111] Update fendermint/vm/interpreter/src/fvm/gas/actor.rs Co-authored-by: raulk --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 41e424e3a..d7267b988 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -38,7 +38,7 @@ impl GasMarket for ActorGasMarket { } impl ActorGasMarket { - pub fn new( + pub fn create( executor: &mut E, block_height: ChainEpoch, ) -> anyhow::Result { From 7ffaa0c02505a5df50712c9936b60000f77f6ca5 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 22:25:21 +0800 Subject: [PATCH 051/111] feedbacks --- fendermint/actors/gas_market/src/lib.rs | 39 ++++++++++++------- fendermint/vm/interpreter/src/fvm/exec.rs | 20 +++++----- .../vm/interpreter/src/fvm/gas/actor.rs | 22 +++++------ fendermint/vm/interpreter/src/fvm/gas/mod.rs | 8 +++- .../vm/interpreter/src/fvm/state/exec.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 14 ++++++- 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 793f820d9..d127ec7f7 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -8,7 +8,6 @@ use fil_actors_runtime::{actor_dispatch, ActorError}; use fvm_ipld_encoding::tuple::*; use fvm_shared::econ::TokenAmount; use fvm_shared::METHOD_CONSTRUCTOR; -use lazy_static::lazy_static; use num_derive::FromPrimitive; use std::cmp::Ordering; @@ -19,11 +18,7 @@ fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) const ELASTICITY_MULTIPLIER: u64 = 2; -lazy_static! { - /// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - static ref INITIAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(1_000_000_000); - static ref MINIMAL_BASE_FEE: TokenAmount = TokenAmount::from_atto(100); -} + pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; pub type GasMarketReading = EIP1559GasState; @@ -32,6 +27,7 @@ pub type GasMarketReading = EIP1559GasState; pub struct EIP1559GasState { pub block_gas_limit: Gas, pub base_fee: TokenAmount, + pub minimal_base_fee: TokenAmount, } #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] @@ -41,7 +37,9 @@ pub struct BlockGasUtilization { #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct SetConstants { - pub block_gas_limit: Gas, + pub block_gas_limit: Option, + pub base_fee: Option, + pub minimal_base_fee: Option, } pub struct EIP1559GasMarketActor {} @@ -50,7 +48,7 @@ pub struct EIP1559GasMarketActor {} #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - CurrentGasReading = frc42_dispatch::method_hash!("CurrentGasReading"), + CurrentReading = frc42_dispatch::method_hash!("CurrentReading"), SetConstants = frc42_dispatch::method_hash!("SetConstants"), UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), } @@ -66,14 +64,14 @@ impl EIP1559GasMarketActor { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.block_gas_limit = constants.block_gas_limit; + st.set_constants(constants); Ok(()) })?; Ok(()) } - fn current_gas_reading(rt: &impl Runtime) -> Result { + fn current_reading(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; rt.state() } @@ -92,13 +90,27 @@ impl EIP1559GasMarketActor { } impl EIP1559GasState { - pub fn new(block_gas_limit: Gas) -> Self { + pub fn new(block_gas_limit: Gas, base_fee: TokenAmount, minimal_base_fee: TokenAmount) -> Self { Self { block_gas_limit, - base_fee: INITIAL_BASE_FEE.clone(), + base_fee, + minimal_base_fee, + } + } + + #[inline] + fn update_if_some(maybe_some: Option, to_change: &mut T) { + if let Some(v) = maybe_some { + *to_change = v; } } + fn set_constants(&mut self, constants: SetConstants) { + Self::update_if_some(constants.minimal_base_fee, &mut self.minimal_base_fee); + Self::update_if_some(constants.base_fee, &mut self.base_fee); + Self::update_if_some(constants.block_gas_limit, &mut self.block_gas_limit); + } + fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { let base_fee = self.base_fee.clone(); let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; @@ -111,7 +123,7 @@ impl EIP1559GasState { / BASE_FEE_MAX_CHANGE_DENOMINATOR; let base_fee_delta = TokenAmount::from_atto(base_fee_delta); if base_fee_delta >= base_fee { - MINIMAL_BASE_FEE.clone() + self.minimal_base_fee.clone() } else { base_fee - base_fee_delta } @@ -165,6 +177,7 @@ mod tests { IpldBlock::serialize_cbor(&EIP1559GasState { block_gas_limit: 100, base_fee: Default::default(), + minimal_base_fee: Default::default(), }) .unwrap(), ) diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 7f1b1cc4c..1f8912edf 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -158,21 +158,23 @@ where (apply_ret, emitters, latency) } else { - if msg.gas_limit > state.gas_market().available_block_gas() { - // This is panic-worthy, but we suppress it to avoid liveness issues. - tracing::warn!("[ASSERTION FAILED] message gas limit exceed available block gas limit; consensus engine is misbehaving"); + let available_gas = state.gas_market().available().block_gas; + if msg.gas_limit > available_gas { + // This is panic-worthy, but we suppress it to avoid liveness issues. + // Consider maybe record as evidence for the validator slashing? + tracing::warn!( + txn_gas_limit = msg.gas_limit, + block_gas_available = available_gas, + "[ASSERTION FAILED] message gas limit exceed available block gas limit; consensus engine is misbehaving" + ); } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - if state + state .gas_market_mut() - .record_gas_used(apply_ret.msg_receipt.gas_used) - .is_err() - { - tracing::warn!("[ASSERTION FAILED] gas market failed while recording utilization: {err}"); - } + .record_utilization(apply_ret.msg_receipt.gas_used); (apply_ret, emitters, latency) }; diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index d7267b988..e7c188d5e 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,6 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::gas::{Gas, GasMarket}; + +use crate::fvm::gas::{Available, Gas, GasMarket}; use crate::fvm::FvmMessage; use anyhow::Context; @@ -19,21 +20,18 @@ pub struct ActorGasMarket { } impl GasMarket for ActorGasMarket { - struct Available { - block_gas: Gas - } - fn available(&self) -> Available { - self.block_gas_limit - self.block_gas_used + Available { + block_gas: self.block_gas_limit - self.block_gas_used, + } } - fn record_utilization(&mut self, gas: Gas) -> anyhow::Result<()> { + fn record_utilization(&mut self, gas: Gas) { + // sanity check if self.block_gas_used + gas >= self.block_gas_limit { - tracing::warn!("out of block gas, should not have happened") + tracing::warn!("out of block gas, should not have happened, vm execution more than available gas limit"); } - self.block_gas_used = self.block_gas_used.saturating_add(gas); - - Ok(()) + self.block_gas_used += gas; } } @@ -48,7 +46,7 @@ impl ActorGasMarket { sequence: block_height as u64, // exclude this from gas restriction gas_limit: fvm_shared::BLOCK_GAS_LIMIT, - method_num: fendermint_actor_gas_market::Method::CurrentGasReading as u64, + method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, params: fvm_ipld_encoding::RawBytes::default(), value: Default::default(), version: Default::default(), diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index 8c9dfb91b..768c47bb6 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -5,11 +5,15 @@ pub mod actor; pub type Gas = u64; +pub struct Available { + pub block_gas: Gas, +} + /// The gas market for fendermint. This should be backed by an fvm actor. pub trait GasMarket { /// Obtain the current block gas available for execution - fn available_block_gas(&self) -> Gas; + fn available(&self) -> Available; /// Tracks the amount of gas consumed by a transaction - fn record_gas_used(&mut self, gas: Gas) -> anyhow::Result<()>; + fn record_utilization(&mut self, gas: Gas); } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 782d3c3fe..577317dc1 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -151,7 +151,7 @@ where let externs = FendermintExterns::new(blockstore.clone(), params.state_root); let machine = DefaultMachine::new(&mc, blockstore, externs)?; let mut executor = DefaultExecutor::new(engine, machine)?; - let gas_market = ActorGasMarket::new(&mut executor, block_height)?; + let gas_market = ActorGasMarket::create(&mut executor, block_height)?; Ok(Self { executor, diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 9ac78235f..7562bc26e 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -418,7 +418,19 @@ impl GenesisBuilder { ) .context("failed to replace built in eam actor")?; - let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new(BLOCK_GAS_LIMIT); + // currently hard code them for now, once genesis V2 is implemented, should be taken + // from genesis. + // initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + // minimal base fee taken from filecoin. + let (initial_base_fee, minimal_base_fee) = ( + TokenAmount::from_atto(1_000_000_000), + TokenAmount::from_atto(100), + ); + let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new( + BLOCK_GAS_LIMIT, + initial_base_fee, + minimal_base_fee, + ); state .create_custom_actor( fendermint_actor_gas_market::IPC_GAS_MARKET_ACTOR_NAME, From 1a6fb2102b7f2902620dbdcc4ad2f83df54deb7d Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 20 Aug 2024 22:59:57 +0800 Subject: [PATCH 052/111] more customization params --- fendermint/actors/gas_market/src/lib.rs | 113 ++++++++++++++---- .../vm/interpreter/src/fvm/gas/actor.rs | 11 +- fendermint/vm/interpreter/src/genesis.rs | 15 +-- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index d127ec7f7..260f4d541 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -14,20 +14,38 @@ use std::cmp::Ordering; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); -/// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; -/// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) -const ELASTICITY_MULTIPLIER: u64 = 2; - pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; -pub type GasMarketReading = EIP1559GasState; + +/// Constant params used by EIP1559 +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct EIP1559Constants { + /// The minimal base fee when gas utilization is low + minimal_base_fee: TokenAmount, + /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + elasticity_multiplier: u64, + /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + base_fee_max_change_denominator: u64, +} #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559GasState { + block_gas_limit: Gas, + base_fee: TokenAmount, + constants: EIP1559Constants, +} + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct GasActorConstructorParams { + block_gas_limit: Gas, + base_fee: TokenAmount, + constants: Option, +} + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct GasMarketReading { pub block_gas_limit: Gas, pub base_fee: TokenAmount, - pub minimal_base_fee: TokenAmount, } #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] @@ -39,7 +57,7 @@ pub struct BlockGasUtilization { pub struct SetConstants { pub block_gas_limit: Option, pub base_fee: Option, - pub minimal_base_fee: Option, + pub constants: Option, } pub struct EIP1559GasMarketActor {} @@ -55,8 +73,13 @@ pub enum Method { impl EIP1559GasMarketActor { /// Creates the actor - pub fn constructor(rt: &impl Runtime, st: EIP1559GasState) -> Result<(), ActorError> { + pub fn constructor( + rt: &impl Runtime, + params: GasActorConstructorParams, + ) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + let st = EIP1559GasState::from(params); rt.create(&st) } @@ -73,7 +96,12 @@ impl EIP1559GasMarketActor { fn current_reading(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; - rt.state() + + let st = rt.state::()?; + Ok(GasMarketReading { + block_gas_limit: st.block_gas_limit, + base_fee: st.base_fee, + }) } fn update_utilization( @@ -89,15 +117,45 @@ impl EIP1559GasMarketActor { } } -impl EIP1559GasState { - pub fn new(block_gas_limit: Gas, base_fee: TokenAmount, minimal_base_fee: TokenAmount) -> Self { +impl Default for EIP1559Constants { + fn default() -> Self { + Self { + // Take from filecoin setting + minimal_base_fee: TokenAmount::from_atto(100), + // Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + elasticity_multiplier: 2, + // Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + base_fee_max_change_denominator: 8, + } + } +} + +impl From for EIP1559GasState { + fn from(params: GasActorConstructorParams) -> Self { + Self { + block_gas_limit: params.block_gas_limit, + base_fee: params.base_fee, + constants: params.constants.unwrap_or_default(), + } + } +} + +impl GasActorConstructorParams { + pub fn new(block_gas_limit: Gas, base_fee: TokenAmount) -> Self { Self { block_gas_limit, base_fee, - minimal_base_fee, + constants: None, } } + pub fn with_constants(mut self, constants: EIP1559Constants) -> Self { + self.constants = Some(constants); + self + } +} + +impl EIP1559GasState { #[inline] fn update_if_some(maybe_some: Option, to_change: &mut T) { if let Some(v) = maybe_some { @@ -106,32 +164,33 @@ impl EIP1559GasState { } fn set_constants(&mut self, constants: SetConstants) { - Self::update_if_some(constants.minimal_base_fee, &mut self.minimal_base_fee); + Self::update_if_some(constants.constants, &mut self.constants); Self::update_if_some(constants.base_fee, &mut self.base_fee); Self::update_if_some(constants.block_gas_limit, &mut self.block_gas_limit); } fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { let base_fee = self.base_fee.clone(); - let gas_target = self.block_gas_limit / ELASTICITY_MULTIPLIER; + let gas_target = self.block_gas_limit / self.constants.elasticity_multiplier; match gas_used.cmp(&gas_target) { Ordering::Equal => base_fee, Ordering::Less => { let base_fee_delta = base_fee.atto() * (gas_target - gas_used) / gas_target - / BASE_FEE_MAX_CHANGE_DENOMINATOR; + / self.constants.base_fee_max_change_denominator; let base_fee_delta = TokenAmount::from_atto(base_fee_delta); if base_fee_delta >= base_fee { - self.minimal_base_fee.clone() + self.constants.minimal_base_fee.clone() } else { base_fee - base_fee_delta } } Ordering::Greater => { let gas_used_delta = gas_used - gas_target; - let delta = - base_fee.atto() * gas_used_delta / gas_target / BASE_FEE_MAX_CHANGE_DENOMINATOR; + let delta = base_fee.atto() * gas_used_delta + / gas_target + / self.constants.base_fee_max_change_denominator; base_fee + TokenAmount::from_atto(delta).max(TokenAmount::from_atto(1)) } } @@ -155,7 +214,9 @@ impl ActorCode for EIP1559GasMarketActor { #[cfg(test)] mod tests { - use crate::{EIP1559GasMarketActor, EIP1559GasState, Method, SetConstants}; + use crate::{ + EIP1559GasMarketActor, EIP1559GasState, GasActorConstructorParams, Method, SetConstants, + }; use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::ipld_block::IpldBlock; @@ -174,10 +235,10 @@ mod tests { let result = rt .call::( Method::Constructor as u64, - IpldBlock::serialize_cbor(&EIP1559GasState { + IpldBlock::serialize_cbor(&GasActorConstructorParams { block_gas_limit: 100, base_fee: Default::default(), - minimal_base_fee: Default::default(), + constants: None, }) .unwrap(), ) @@ -199,7 +260,9 @@ mod tests { let r = rt.call::( Method::SetConstants as u64, IpldBlock::serialize_cbor(&SetConstants { - block_gas_limit: 20, + block_gas_limit: Some(20), + base_fee: None, + constants: None, }) .unwrap(), ); @@ -219,7 +282,9 @@ mod tests { .call::( Method::SetConstants as u64, IpldBlock::serialize_cbor(&SetConstants { - block_gas_limit: 20, + block_gas_limit: Some(20), + base_fee: None, + constants: None, }) .unwrap(), ) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index e7c188d5e..ed69dcc9d 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -22,16 +22,17 @@ pub struct ActorGasMarket { impl GasMarket for ActorGasMarket { fn available(&self) -> Available { Available { - block_gas: self.block_gas_limit - self.block_gas_used, + block_gas: self.block_gas_limit - self.block_gas_used.min(self.block_gas_limit), } } fn record_utilization(&mut self, gas: Gas) { + self.block_gas_used += gas; + // sanity check - if self.block_gas_used + gas >= self.block_gas_limit { - tracing::warn!("out of block gas, should not have happened, vm execution more than available gas limit"); + if self.block_gas_used >= self.block_gas_limit { + tracing::warn!("out of block gas, vm execution more than available gas limit"); } - self.block_gas_used += gas; } } @@ -76,7 +77,7 @@ impl ActorGasMarket { executor: &mut E, block_height: ChainEpoch, ) -> anyhow::Result<()> { - let block_gas_used = self.block_gas_used; + let block_gas_used = self.block_gas_used.min(self.block_gas_limit); let params = fvm_ipld_encoding::RawBytes::serialize( fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, )?; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 7562bc26e..9b0cfb949 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -421,15 +421,12 @@ impl GenesisBuilder { // currently hard code them for now, once genesis V2 is implemented, should be taken // from genesis. // initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - // minimal base fee taken from filecoin. - let (initial_base_fee, minimal_base_fee) = ( - TokenAmount::from_atto(1_000_000_000), - TokenAmount::from_atto(100), - ); - let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::new( - BLOCK_GAS_LIMIT, - initial_base_fee, - minimal_base_fee, + let initial_base_fee = TokenAmount::from_atto(1_000_000_000); + let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::from( + fendermint_actor_gas_market::GasActorConstructorParams::new( + BLOCK_GAS_LIMIT, + initial_base_fee, + ), ); state .create_custom_actor( From cd90e98c5f5d73ea9d1541f90fc17b97e16b8471 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 21 Aug 2024 10:30:00 +0800 Subject: [PATCH 053/111] up gas limit --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index ed69dcc9d..a3e28033b 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -46,7 +46,7 @@ impl ActorGasMarket { to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction - gas_limit: fvm_shared::BLOCK_GAS_LIMIT, + gas_limit: i64::MAX as u64, method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, params: fvm_ipld_encoding::RawBytes::default(), value: Default::default(), @@ -87,7 +87,7 @@ impl ActorGasMarket { to: GAS_MARKET_ACTOR_ADDR, sequence: block_height as u64, // exclude this from gas restriction - gas_limit: fvm_shared::BLOCK_GAS_LIMIT, + gas_limit: i64::MAX as u64, method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, params, value: Default::default(), From 47556e93be8fed7cfaea6e6a0b24874751fc0e74 Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Thu, 22 Aug 2024 02:28:51 +0800 Subject: [PATCH 054/111] fix(node): remove genesis interpreter (#1118) Co-authored-by: cryptoAtwill --- fendermint/app/src/app.rs | 11 +- fendermint/app/src/cmd/run.rs | 1 - fendermint/testing/contract-test/Cargo.toml | 2 +- fendermint/testing/contract-test/src/lib.rs | 149 ++-- .../contract-test/tests/run_upgrades.rs | 17 +- .../contract-test/tests/staking/machine.rs | 12 +- fendermint/vm/interpreter/Cargo.toml | 1 + fendermint/vm/interpreter/src/bytes.rs | 44 +- fendermint/vm/interpreter/src/chain.rs | 21 +- fendermint/vm/interpreter/src/fvm/genesis.rs | 682 ------------------ fendermint/vm/interpreter/src/fvm/mod.rs | 7 - .../vm/interpreter/src/fvm/state/genesis.rs | 11 - fendermint/vm/interpreter/src/genesis.rs | 17 + fendermint/vm/interpreter/src/lib.rs | 17 - fendermint/vm/interpreter/src/signed.rs | 20 +- fendermint/vm/snapshot/Cargo.toml | 2 +- fendermint/vm/snapshot/src/manager.rs | 60 +- 17 files changed, 98 insertions(+), 976 deletions(-) delete mode 100644 fendermint/vm/interpreter/src/fvm/genesis.rs diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index af4e8da72..c44b9addd 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -18,15 +18,15 @@ use fendermint_vm_interpreter::bytes::{ }; use fendermint_vm_interpreter::chain::{ChainEnv, ChainMessageApplyRet, IllegalMessage}; use fendermint_vm_interpreter::fvm::state::{ - empty_state_tree, CheckStateRef, FvmExecState, FvmGenesisState, FvmQueryState, FvmStateParams, + empty_state_tree, CheckStateRef, FvmExecState, FvmQueryState, FvmStateParams, FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; -use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput, PowerUpdates}; +use fendermint_vm_interpreter::fvm::{FvmApplyRet, PowerUpdates}; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ - CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, + CheckInterpreter, ExecInterpreter, ProposalInterpreter, QueryInterpreter, }; use fendermint_vm_message::query::FvmQueryHeight; use fendermint_vm_snapshot::{SnapshotClient, SnapshotError}; @@ -402,11 +402,6 @@ where S::Namespace: Sync + Send, DB: KVWritable + KVReadable + Clone + Send + Sync + 'static, SS: Blockstore + Clone + Send + Sync + 'static, - I: GenesisInterpreter< - State = FvmGenesisState, - Genesis = Vec, - Output = FvmGenesisOutput, - >, I: ProposalInterpreter>, I: ExecInterpreter< State = (ChainEnv, FvmExecState), diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index 50556135c..dca099b1f 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -137,7 +137,6 @@ async fn run(settings: Settings) -> anyhow::Result<()> { let interpreter = FvmMessageInterpreter::::new( tendermint_client.clone(), validator_ctx, - settings.contracts_dir(), settings.fvm.gas_overestimation_rate, settings.fvm.gas_search_step, settings.fvm.exec_in_check, diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 0c186f8b6..109e5fae1 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -31,7 +31,7 @@ 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", + "bundle", "test-util" ] } [dev-dependencies] diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index a6290d665..65a4ce8ad 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -3,72 +3,52 @@ 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 fvm_shared::clock::ChainEpoch; use std::{future::Future, sync::Arc}; use fendermint_vm_genesis::Genesis; +use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; use fendermint_vm_interpreter::{ fvm::{ bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, - state::{FvmExecState, FvmGenesisState, FvmStateParams, FvmUpdatableParams}, + state::{FvmExecState, FvmStateParams, FvmUpdatableParams}, store::memory::MemoryBlockstore, - upgrades::UpgradeScheduler, - FvmApplyRet, FvmGenesisOutput, FvmMessage, FvmMessageInterpreter, + FvmApplyRet, FvmMessage, }, - ExecInterpreter, GenesisInterpreter, + ExecInterpreter, }; use fvm::engine::MultiEngine; pub mod ipc; -pub async fn init_exec_state( - multi_engine: Arc, +pub async fn create_test_exec_state( genesis: Genesis, -) -> anyhow::Result<(FvmExecState, FvmGenesisOutput)> { +) -> Result<( + FvmExecState, + GenesisOutput, + MemoryBlockstore, +)> { 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 store = MemoryBlockstore::new(); - - let state = FvmGenesisState::new(store, multi_engine, &bundle, &custom_actors_bundle) - .await - .context("failed to create state")?; - - let (client, _) = - tendermint_rpc::MockClient::new(tendermint_rpc::MockRequestMethodMatcher::default()); - - let interpreter = FvmMessageInterpreter::new( - client, - None, - contracts_path(), - 1.05, - 1.05, - false, - UpgradeScheduler::new(), - ); - - let (state, out) = interpreter - .init(state, genesis) - .await - .context("failed to create actors")?; - - let state = state - .into_exec_state() - .map_err(|_| anyhow!("should be in exec stage"))?; - - Ok((state, out)) + let maybe_contract_path = genesis.ipc.as_ref().map(|_| contracts_path()); + + let (state, out) = create_test_genesis_state( + bundle_path, + custom_actors_bundle_path, + genesis, + maybe_contract_path, + ) + .await?; + let store = state.store().clone(); + Ok(( + state + .into_exec_state() + .map_err(|_| anyhow!("cannot parse state"))?, + out, + store, + )) } pub struct Tester { @@ -81,11 +61,6 @@ pub struct Tester { impl Tester where - I: GenesisInterpreter< - State = FvmGenesisState, - Genesis = Genesis, - Output = FvmGenesisOutput, - >, I: ExecInterpreter< State = FvmExecState, Message = FvmMessage, @@ -94,61 +69,13 @@ where EndOutput = PowerUpdates, >, { - fn state_store_clone(&self) -> MemoryBlockstore { - self.state_store.as_ref().clone() - } + pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result { + let (exec_state, out, store) = create_test_exec_state(genesis).await?; + let (state_root, _, _) = exec_state + .commit() + .context("failed to commit genesis state")?; - 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 { + let state_params = FvmStateParams { state_root, timestamp: out.timestamp, network_version: out.network_version, @@ -159,7 +86,13 @@ where app_version: 0, }; - Ok(()) + Ok(Self { + interpreter: Arc::new(interpreter), + state_store: Arc::new(store), + multi_engine: Arc::new(MultiEngine::new(1)), + exec_state: Arc::new(tokio::sync::Mutex::new(None)), + state_params, + }) } /// Take the execution state, update it, put it back, return the output. diff --git a/fendermint/testing/contract-test/tests/run_upgrades.rs b/fendermint/testing/contract-test/tests/run_upgrades.rs index 00b3f4a4d..532c2f71f 100644 --- a/fendermint/testing/contract-test/tests/run_upgrades.rs +++ b/fendermint/testing/contract-test/tests/run_upgrades.rs @@ -26,7 +26,7 @@ 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}; +use fendermint_vm_interpreter::fvm::FvmMessageInterpreter; // returns a seeded secret key which is guaranteed to be the same every time fn my_secret_key() -> SecretKey { @@ -194,17 +194,8 @@ async fn test_applying_upgrades() { ) .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 interpreter: FvmMessageInterpreter = + FvmMessageInterpreter::new(NeverCallClient, None, 1.05, 1.05, false, upgrade_scheduler); let genesis = Genesis { chain_name: CHAIN_NAME.to_string(), @@ -223,7 +214,7 @@ async fn test_applying_upgrades() { ipc: None, }; - tester.init(genesis).await.unwrap(); + let mut tester = Tester::new(interpreter, genesis).await.unwrap(); // check that the app version is 0 assert_eq!(tester.state_params().app_version, 0); diff --git a/fendermint/testing/contract-test/tests/staking/machine.rs b/fendermint/testing/contract-test/tests/staking/machine.rs index 76adc405f..99ecb95fa 100644 --- a/fendermint/testing/contract-test/tests/staking/machine.rs +++ b/fendermint/testing/contract-test/tests/staking/machine.rs @@ -1,6 +1,6 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::{cell::RefCell, collections::HashSet, sync::Arc}; +use std::{cell::RefCell, collections::HashSet}; use arbitrary::{Arbitrary, Unstructured}; use fendermint_contract_test::ipc::{registry::RegistryCaller, subnet::SubnetCaller}; @@ -19,7 +19,6 @@ use fendermint_vm_message::{ conv::from_fvm::{self, to_eth_tokens}, signed::sign_secp256k1, }; -use fvm::engine::MultiEngine; use fvm_ipld_blockstore::Blockstore; use fvm_shared::bigint::Integer; use fvm_shared::econ::TokenAmount; @@ -65,9 +64,7 @@ pub enum StakingCommand { } #[derive(Default)] -pub struct StakingMachine { - multi_engine: Arc, -} +pub struct StakingMachine {} impl StateMachine for StakingMachine { type System = StakingSystem; @@ -86,9 +83,8 @@ impl StateMachine for StakingMachine { fn new_system(&self, state: &Self::State) -> Self::System { let rt = tokio::runtime::Runtime::new().expect("create tokio runtime for init"); - let (mut exec_state, _) = rt - .block_on(fendermint_contract_test::init_exec_state( - self.multi_engine.clone(), + let (mut exec_state, _, _) = rt + .block_on(fendermint_contract_test::create_test_exec_state( state.parent_genesis.clone(), )) .expect("failed to init parent"); diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 20ca5b0a2..8e40ffed5 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -88,3 +88,4 @@ arb = [ "fendermint_testing/arb", "rand", ] +test-util = [] diff --git a/fendermint/vm/interpreter/src/bytes.rs b/fendermint/vm/interpreter/src/bytes.rs index 1af84dd85..ca5b54909 100644 --- a/fendermint/vm/interpreter/src/bytes.rs +++ b/fendermint/vm/interpreter/src/bytes.rs @@ -1,16 +1,15 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use anyhow::{anyhow, Context}; +use anyhow::Context; use async_trait::async_trait; use cid::Cid; -use fendermint_vm_genesis::Genesis; use fendermint_vm_message::chain::ChainMessage; use fvm_ipld_encoding::Error as IpldError; use crate::{ chain::{ChainMessageApplyRet, ChainMessageCheckRes}, fvm::{FvmQuery, FvmQueryRet}, - CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, + CheckInterpreter, ExecInterpreter, ProposalInterpreter, QueryInterpreter, }; pub type BytesMessageApplyRes = Result; @@ -276,42 +275,3 @@ where Ok((state, Ok(ret))) } } - -#[async_trait] -impl GenesisInterpreter for BytesMessageInterpreter -where - I: GenesisInterpreter, -{ - type State = I::State; - type Genesis = Vec; - type Output = I::Output; - - async fn init( - &self, - state: Self::State, - genesis: Self::Genesis, - ) -> anyhow::Result<(Self::State, Self::Output)> { - // TODO (IPC-44): Handle the serialized application state as well as `Genesis`. - let genesis: Genesis = parse_genesis(&genesis)?; - self.inner.init(state, genesis).await - } -} - -/// Parse the initial genesis either as JSON or CBOR. -fn parse_genesis(bytes: &[u8]) -> anyhow::Result { - try_parse_genesis_json(bytes).or_else(|e1| { - try_parse_genesis_cbor(bytes) - .map_err(|e2| anyhow!("failed to deserialize genesis as JSON or CBOR: {e1}; {e2}")) - }) -} - -fn try_parse_genesis_json(bytes: &[u8]) -> anyhow::Result { - let json = String::from_utf8(bytes.to_vec())?; - let genesis = serde_json::from_str(&json)?; - Ok(genesis) -} - -fn try_parse_genesis_cbor(bytes: &[u8]) -> anyhow::Result { - let genesis = fvm_ipld_encoding::from_slice(bytes)?; - Ok(genesis) -} diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 79136138f..ae4d6af24 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -6,7 +6,7 @@ use crate::{ fvm::state::FvmExecState, fvm::FvmMessage, signed::{SignedMessageApplyRes, SignedMessageCheckRes, SyntheticMessage, VerifiableMessage}, - CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, + CheckInterpreter, ExecInterpreter, ProposalInterpreter, QueryInterpreter, }; use anyhow::{bail, Context}; use async_stm::atomically; @@ -497,25 +497,6 @@ where } } -#[async_trait] -impl GenesisInterpreter for ChainMessageInterpreter -where - DB: Blockstore + Clone + 'static + Send + Sync, - I: GenesisInterpreter, -{ - type State = I::State; - type Genesis = I::Genesis; - type Output = I::Output; - - async fn init( - &self, - state: Self::State, - genesis: Self::Genesis, - ) -> anyhow::Result<(Self::State, Self::Output)> { - self.inner.init(state, genesis).await - } -} - /// Convert a signed relayed bottom-up checkpoint to a syntetic message we can send to the FVM. /// /// By mapping to an FVM message we invoke the right contract to validate the checkpoint, diff --git a/fendermint/vm/interpreter/src/fvm/genesis.rs b/fendermint/vm/interpreter/src/fvm/genesis.rs deleted file mode 100644 index 23de68361..000000000 --- a/fendermint/vm/interpreter/src/fvm/genesis.rs +++ /dev/null @@ -1,682 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use std::collections::{BTreeSet, HashMap}; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, Context}; -use async_trait::async_trait; -use ethers::abi::Tokenize; -use ethers::core::types as et; -use fendermint_actor_eam::PermissionModeParams; -use fendermint_eth_hardhat::{Hardhat, FQN}; -use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; -use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; -use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, cron, eam, init, ipc, reward, system, EMPTY_ARR, -}; -use fendermint_vm_core::{chainid, Timestamp}; -use fendermint_vm_genesis::{ActorMeta, Genesis, Power, PowerScale, Validator}; -use fvm_ipld_blockstore::Blockstore; -use fvm_shared::chainid::ChainID; -use fvm_shared::econ::TokenAmount; -use fvm_shared::version::NetworkVersion; -use ipc_actors_abis::i_diamond::FacetCut; -use num_traits::Zero; - -use crate::GenesisInterpreter; - -use super::state::FvmGenesisState; -use super::FvmMessageInterpreter; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FvmGenesisOutput { - pub chain_id: ChainID, - pub timestamp: Timestamp, - pub network_version: NetworkVersion, - pub base_fee: TokenAmount, - pub power_scale: PowerScale, - pub circ_supply: TokenAmount, - pub validators: Vec>, -} - -#[async_trait] -impl GenesisInterpreter for FvmMessageInterpreter -where - DB: Blockstore + 'static + Send + Sync + Clone, - TC: Send + Sync + 'static, -{ - type State = FvmGenesisState; - type Genesis = Genesis; - type Output = FvmGenesisOutput; - - /// Initialize actor states from the Genesis spec. - /// - /// This method doesn't create all builtin Filecoin actors, - /// it leaves out the ones specific to file storage. - /// - /// The ones included are: - /// * system - /// * init - /// * cron - /// * EAM - /// * burnt funds - /// * rewards (placeholder) - /// * accounts - /// * IPC - /// - /// TODO: - /// * faucet? - /// - /// See genesis initialization in: - /// * [Lotus](https://github.com/filecoin-project/lotus/blob/v1.20.4/chain/gen/genesis/genesis.go) - /// * [ref-fvm tester](https://github.com/filecoin-project/ref-fvm/blob/fvm%40v3.1.0/testing/integration/src/tester.rs#L99-L103) - /// * [fvm-workbench](https://github.com/anorth/fvm-workbench/blob/67219b3fd0b5654d54f722ab5acea6ec0abb2edc/builtin/src/genesis.rs) - async fn init( - &self, - mut state: Self::State, - genesis: Self::Genesis, - ) -> anyhow::Result<(Self::State, Self::Output)> { - // Log the genesis in JSON format, hopefully it's not enormous. - tracing::debug!(genesis = serde_json::to_string(&genesis)?, "init"); - - // NOTE: We could consider adding the chain ID to the interpreter - // and rejecting genesis if it doesn't match the expectation, - // but the Tendermint genesis file also has this field, and - // presumably Tendermint checks that its peers have the same. - let chain_id = chainid::from_str_hashed(&genesis.chain_name)?; - - // Convert validators to CometBFT power scale. - let validators = genesis - .validators - .iter() - .cloned() - .map(|vc| vc.map_power(|c| c.into_power(genesis.power_scale))) - .collect(); - - // Currently we just pass them back as they are, but later we should - // store them in the IPC actors; or in case of a snapshot restore them - // from the state. - let out = FvmGenesisOutput { - chain_id, - timestamp: genesis.timestamp, - network_version: genesis.network_version, - circ_supply: circ_supply(&genesis), - base_fee: genesis.base_fee, - power_scale: genesis.power_scale, - validators, - }; - - // STAGE 0: Declare the built-in EVM contracts we'll have to deploy. - - // Pre-defined IDs for top-level Ethereum contracts. - let mut eth_builtin_ids = BTreeSet::new(); - let mut eth_root_contracts = Vec::new(); - let mut eth_contracts = EthContractMap::default(); - - // Only allocate IDs if the contracts are deployed. - if genesis.ipc.is_some() { - eth_contracts.extend(IPC_CONTRACTS.clone()); - } - - eth_builtin_ids.extend(eth_contracts.values().map(|c| c.actor_id)); - eth_root_contracts.extend(eth_contracts.keys()); - eth_root_contracts.extend( - eth_contracts - .values() - .flat_map(|c| c.facets.iter().map(|f| f.name)), - ); - // Collect dependencies of the main IPC actors. - let mut eth_libs = self - .contracts - .dependencies( - ð_root_contracts - .iter() - .map(|n| (contract_src(n), *n)) - .collect::>(), - ) - .context("failed to collect EVM contract dependencies")?; - - // Only keep library dependencies, not contracts with constructors. - eth_libs.retain(|(_, d)| !eth_contracts.contains_key(d.as_str())); - - // STAGE 1: First we initialize native built-in actors. - - // System actor - state - .create_builtin_actor( - system::SYSTEM_ACTOR_CODE_ID, - system::SYSTEM_ACTOR_ID, - &system::State { - builtin_actors: state.manifest_data_cid, - }, - TokenAmount::zero(), - None, - ) - .context("failed to create system actor")?; - - // Init actor - let (init_state, addr_to_id) = init::State::new( - state.store(), - genesis.chain_name.clone(), - &genesis.accounts, - ð_builtin_ids, - eth_libs.len() as u64, - ) - .context("failed to create init state")?; - - state - .create_builtin_actor( - init::INIT_ACTOR_CODE_ID, - init::INIT_ACTOR_ID, - &init_state, - TokenAmount::zero(), - None, - ) - .context("failed to create init actor")?; - - // Cron actor - state - .create_builtin_actor( - cron::CRON_ACTOR_CODE_ID, - cron::CRON_ACTOR_ID, - &cron::State { - entries: vec![], // TODO: Maybe with the IPC. - }, - TokenAmount::zero(), - None, - ) - .context("failed to create cron actor")?; - - // Ethereum Account Manager (EAM) actor - state - .create_builtin_actor( - eam::EAM_ACTOR_CODE_ID, - eam::EAM_ACTOR_ID, - &EMPTY_ARR, - TokenAmount::zero(), - None, - ) - .context("failed to create EAM actor")?; - - // Burnt funds actor (it's just an account). - state - .create_builtin_actor( - account::ACCOUNT_ACTOR_CODE_ID, - burntfunds::BURNT_FUNDS_ACTOR_ID, - &account::State { - address: burntfunds::BURNT_FUNDS_ACTOR_ADDR, - }, - TokenAmount::zero(), - None, - ) - .context("failed to create burnt funds actor")?; - - // A placeholder for the reward actor, beause I don't think - // using the one in the builtin actors library would be appropriate. - // This effectively burns the miner rewards. Better than panicking. - state - .create_builtin_actor( - account::ACCOUNT_ACTOR_CODE_ID, - reward::REWARD_ACTOR_ID, - &account::State { - address: reward::REWARD_ACTOR_ADDR, - }, - TokenAmount::zero(), - None, - ) - .context("failed to create reward actor")?; - - // STAGE 1b: Then we initialize the in-repo custom actors. - - // Initialize the chain metadata actor which handles saving metadata about the chain - // (e.g. block hashes) which we can query. - let chainmetadata_state = fendermint_actor_chainmetadata::State::new( - &state.store(), - fendermint_actor_chainmetadata::DEFAULT_LOOKBACK_LEN, - )?; - state - .create_custom_actor( - fendermint_actor_chainmetadata::CHAINMETADATA_ACTOR_NAME, - chainmetadata::CHAINMETADATA_ACTOR_ID, - &chainmetadata_state, - TokenAmount::zero(), - None, - ) - .context("failed to create chainmetadata actor")?; - - let eam_state = fendermint_actor_eam::State::new( - state.store(), - PermissionModeParams::from(genesis.eam_permission_mode), - )?; - state - .replace_builtin_actor( - eam::EAM_ACTOR_NAME, - eam::EAM_ACTOR_ID, - fendermint_actor_eam::IPC_EAM_ACTOR_NAME, - &eam_state, - TokenAmount::zero(), - None, - ) - .context("failed to replace built in eam actor")?; - - // STAGE 2: Create non-builtin accounts which do not have a fixed ID. - - // The next ID is going to be _after_ the accounts, which have already been assigned an ID by the `Init` actor. - // The reason we aren't using the `init_state.next_id` is because that already accounted for the multisig accounts. - let mut next_id = init::FIRST_NON_SINGLETON_ADDR + addr_to_id.len() as u64; - - for a in genesis.accounts { - let balance = a.balance; - match a.meta { - ActorMeta::Account(acct) => { - state - .create_account_actor(acct, balance, &addr_to_id) - .context("failed to create account actor")?; - } - ActorMeta::Multisig(ms) => { - state - .create_multisig_actor(ms, balance, &addr_to_id, next_id) - .context("failed to create multisig actor")?; - next_id += 1; - } - } - } - - // STAGE 3: Initialize the FVM and create built-in FEVM actors. - - state - .init_exec_state( - out.timestamp, - out.network_version, - out.base_fee.clone(), - out.circ_supply.clone(), - out.chain_id.into(), - out.power_scale, - ) - .context("failed to init exec state")?; - - let mut deployer = ContractDeployer::::new(&self.contracts, ð_contracts); - - // Deploy Ethereum libraries. - for (lib_src, lib_name) in eth_libs { - deployer.deploy_library(&mut state, &mut next_id, lib_src, &lib_name)?; - } - - if let Some(ipc_params) = genesis.ipc { - // IPC Gateway actor. - let gateway_addr = { - use ipc::gateway::ConstructorParameters; - - let params = ConstructorParameters::new(ipc_params.gateway, genesis.validators) - .context("failed to create gateway constructor")?; - - let facets = deployer - .facets(ipc::gateway::CONTRACT_NAME) - .context("failed to collect gateway facets")?; - - deployer.deploy_contract( - &mut state, - ipc::gateway::CONTRACT_NAME, - (facets, params), - )? - }; - - // IPC SubnetRegistry actor. - { - use ipc::registry::ConstructorParameters; - - let mut facets = deployer - .facets(ipc::registry::CONTRACT_NAME) - .context("failed to collect registry facets")?; - - let getter_facet = facets.remove(0); - let manager_facet = facets.remove(0); - let rewarder_facet = facets.remove(0); - let checkpointer_facet = facets.remove(0); - let pauser_facet = facets.remove(0); - let diamond_loupe_facet = facets.remove(0); - let diamond_cut_facet = facets.remove(0); - let ownership_facet = facets.remove(0); - - debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); - - let params = ConstructorParameters { - gateway: gateway_addr, - getter_facet: getter_facet.facet_address, - manager_facet: manager_facet.facet_address, - rewarder_facet: rewarder_facet.facet_address, - pauser_facet: pauser_facet.facet_address, - checkpointer_facet: checkpointer_facet.facet_address, - diamond_cut_facet: diamond_cut_facet.facet_address, - diamond_loupe_facet: diamond_loupe_facet.facet_address, - ownership_facet: ownership_facet.facet_address, - subnet_getter_selectors: getter_facet.function_selectors, - subnet_manager_selectors: manager_facet.function_selectors, - subnet_rewarder_selectors: rewarder_facet.function_selectors, - subnet_checkpointer_selectors: checkpointer_facet.function_selectors, - subnet_pauser_selectors: pauser_facet.function_selectors, - subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, - subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, - subnet_actor_ownership_selectors: ownership_facet.function_selectors, - creation_privileges: 0, - }; - - deployer.deploy_contract( - &mut state, - ipc::registry::CONTRACT_NAME, - (facets, params), - )?; - }; - } - - Ok((state, out)) - } -} - -fn contract_src(name: &str) -> PathBuf { - PathBuf::from(format!("{name}.sol")) -} - -struct ContractDeployer<'a, DB> { - hardhat: &'a Hardhat, - top_contracts: &'a EthContractMap, - // Assign dynamic ID addresses to libraries, but use fixed addresses for the top level contracts. - lib_addrs: HashMap, - phantom_db: PhantomData, -} - -impl<'a, DB> ContractDeployer<'a, DB> -where - DB: Blockstore + 'static + Send + Sync + Clone, -{ - pub fn new(hardhat: &'a Hardhat, top_contracts: &'a EthContractMap) -> Self { - Self { - hardhat, - top_contracts, - lib_addrs: Default::default(), - phantom_db: PhantomData, - } - } - - /// Deploy a library contract with a dynamic ID and no constructor. - pub fn deploy_library( - &mut self, - state: &mut FvmGenesisState, - next_id: &mut u64, - lib_src: impl AsRef, - lib_name: &str, - ) -> anyhow::Result<()> { - let fqn = self.hardhat.fqn(lib_src.as_ref(), lib_name); - - let bytecode = self - .hardhat - .bytecode(&lib_src, lib_name, &self.lib_addrs) - .with_context(|| format!("failed to load library bytecode {fqn}"))?; - - let eth_addr = state - .create_evm_actor(*next_id, bytecode) - .with_context(|| format!("failed to create library actor {fqn}"))?; - - let id_addr = et::Address::from(EthAddress::from_id(*next_id).0); - let eth_addr = et::Address::from(eth_addr.0); - - tracing::info!( - actor_id = next_id, - ?eth_addr, - ?id_addr, - fqn, - "deployed Ethereum library" - ); - - // We can use the masked ID here or the delegated address. - // Maybe the masked ID is quicker because it doesn't need to be resolved. - self.lib_addrs.insert(fqn, id_addr); - - *next_id += 1; - - Ok(()) - } - - /// Construct the bytecode of a top-level contract and deploy it with some constructor parameters. - pub fn deploy_contract( - &self, - state: &mut FvmGenesisState, - contract_name: &str, - constructor_params: T, - ) -> anyhow::Result - where - T: Tokenize, - { - let contract = self.top_contract(contract_name)?; - let contract_id = contract.actor_id; - let contract_src = contract_src(contract_name); - - let bytecode = self - .hardhat - .bytecode(contract_src, contract_name, &self.lib_addrs) - .with_context(|| format!("failed to load {contract_name} bytecode"))?; - - let eth_addr = state - .create_evm_actor_with_cons(contract_id, &contract.abi, bytecode, constructor_params) - .with_context(|| format!("failed to create {contract_name} actor"))?; - - let id_addr = et::Address::from(EthAddress::from_id(contract_id).0); - let eth_addr = et::Address::from(eth_addr.0); - - tracing::info!( - actor_id = contract_id, - ?eth_addr, - ?id_addr, - contract_name, - "deployed Ethereum contract" - ); - - // The Ethereum address is more usable inside the EVM than the ID address. - Ok(eth_addr) - } - - /// Collect Facet Cuts for the diamond pattern, where the facet address comes from already deployed library facets. - pub fn facets(&self, contract_name: &str) -> anyhow::Result> { - let contract = self.top_contract(contract_name)?; - let mut facet_cuts = Vec::new(); - - for facet in contract.facets.iter() { - let facet_name = facet.name; - let facet_src = contract_src(facet_name); - let facet_fqn = self.hardhat.fqn(&facet_src, facet_name); - - let facet_addr = self - .lib_addrs - .get(&facet_fqn) - .ok_or_else(|| anyhow!("facet {facet_name} has not been deployed"))?; - - let method_sigs = facet - .abi - .functions() - .filter(|f| f.signature() != "init(bytes)") - .map(|f| f.short_signature()) - .collect(); - - let facet_cut = FacetCut { - facet_address: *facet_addr, - action: 0, // Add - function_selectors: method_sigs, - }; - - facet_cuts.push(facet_cut); - } - - Ok(facet_cuts) - } - - fn top_contract(&self, contract_name: &str) -> anyhow::Result<&EthContract> { - self.top_contracts - .get(contract_name) - .ok_or_else(|| anyhow!("unknown top contract name: {contract_name}")) - } -} - -/// Sum of balances in the genesis accounts. -fn circ_supply(g: &Genesis) -> TokenAmount { - g.accounts - .iter() - .fold(TokenAmount::zero(), |s, a| s + a.balance.clone()) -} - -#[cfg(test)] -mod tests { - use std::{str::FromStr, sync::Arc}; - - use cid::Cid; - use fendermint_vm_genesis::{ipc::IpcParams, Genesis}; - use fvm::engine::MultiEngine; - use quickcheck::Arbitrary; - use tendermint_rpc::{MockClient, MockRequestMethodMatcher}; - - use crate::{ - fvm::{ - bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, - state::ipc::GatewayCaller, - store::memory::MemoryBlockstore, - upgrades::UpgradeScheduler, - FvmMessageInterpreter, - }, - GenesisInterpreter, - }; - - use super::FvmGenesisState; - - #[tokio::test] - async fn load_genesis() { - let genesis = make_genesis(); - let bundle = read_bundle(); - let custom_actors_bundle = read_custom_actors_bundle(); - let interpreter = make_interpreter(); - - let multi_engine = Arc::new(MultiEngine::default()); - let store = MemoryBlockstore::new(); - - let state = FvmGenesisState::new(store, multi_engine, &bundle, &custom_actors_bundle) - .await - .expect("failed to create state"); - - let (mut state, out) = interpreter - .init(state, genesis.clone()) - .await - .expect("failed to create actors"); - - assert_eq!(out.validators.len(), genesis.validators.len()); - - // Try calling a method on the IPC Gateway. - let exec_state = state.exec_state().expect("should be in exec stage"); - let caller = GatewayCaller::default(); - - let period = caller - .bottom_up_check_period(exec_state) - .expect("error calling the gateway"); - - assert_eq!(period, genesis.ipc.unwrap().gateway.bottom_up_check_period); - - let _state_root = state.commit().expect("failed to commit"); - } - - #[tokio::test] - async fn load_genesis_deterministic() { - let genesis = make_genesis(); - let bundle = read_bundle(); - let custom_actors_bundle = read_custom_actors_bundle(); - let interpreter = make_interpreter(); - let multi_engine = Arc::new(MultiEngine::default()); - - // Create a couple of states and load the same thing. - let mut outputs = Vec::new(); - for _ in 0..3 { - let store = MemoryBlockstore::new(); - let state = - FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) - .await - .expect("failed to create state"); - - let (state, out) = interpreter - .init(state, genesis.clone()) - .await - .expect("failed to create actors"); - - let state_root_hash = state.commit().expect("failed to commit"); - outputs.push((state_root_hash, out)); - } - - for out in &outputs[1..] { - assert_eq!(out.0, outputs[0].0, "state root hash is different"); - } - } - - // This is a sort of canary test, if it fails means something changed in the way we do genesis, - // which is probably fine, but it's better to know about it, and if anybody doesn't get the same - // then we might have some non-determinism. - #[ignore] // I see a different value on CI than locally. - #[tokio::test] - async fn load_genesis_known() { - let genesis_json = "{\"chain_name\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"timestamp\":1073250,\"network_version\":18,\"base_fee\":\"1000\",\"power_scale\":3,\"validators\":[{\"public_key\":\"BLX9ojqB+8Z26aMmKoCRb3Te6AnSU6zY8hPcf1X5Q69XCNaHVcRxzYO2xx7o/2vgdS7nkDTMRRbkDGzy+FYdAFc=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BFcOveVieknZiscWsfXa06aGbBkKeucBycd/w0N1QHlaZfa/5dJcH7D0hvcdfv3B2Rv1OPuxo1PkgsEbWegWKcA=\",\"power\":\"1000000000000000000\"},{\"public_key\":\"BEP30ykovfrQp3zo+JVRvDVL2emC+Ju1Kpox3zMVYZyFKvYt64qyN/HOVjridDrkEsnQU8BVen4Aegja4fBZ+LU=\",\"power\":\"1000000000000000000\"}],\"accounts\":[{\"meta\":{\"Account\":{\"owner\":\"f410fggjevhgketpz6gw6ordusynlgcd5piyug4aomuq\"}},\"balance\":\"1000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410frbdnwklaitcjsqe7swjwp5naple6vthq4woyfry\"}},\"balance\":\"2000000000000000000\"},{\"meta\":{\"Account\":{\"owner\":\"f410fxo4lih4n2acr3oadalidwqjgoqkzhp5dw3zwkvy\"}},\"balance\":\"1000000000000000000\"}],\"ipc\":{\"gateway\":{\"subnet_id\":\"/r314159/f410fnfmitm2ww7oehhtbokf6wulhrr62sgq3sgqmenq\",\"bottom_up_check_period\":30,\"msg_fee\":\"1000000000000\",\"majority_percentage\":60,\"active_validators_limit\":100}}}"; - - let genesis: Genesis = serde_json::from_str(genesis_json).expect("failed to parse genesis"); - - let bundle = read_bundle(); - let custom_actors_bundle = read_custom_actors_bundle(); - let interpreter = make_interpreter(); - let multi_engine = Arc::new(MultiEngine::default()); - - let store = MemoryBlockstore::new(); - let state = - FvmGenesisState::new(store, multi_engine.clone(), &bundle, &custom_actors_bundle) - .await - .expect("failed to create state"); - - let (state, _) = interpreter - .init(state, genesis.clone()) - .await - .expect("failed to create actors"); - - let state_root_hash = state.commit().expect("failed to commit"); - - let expected_root_hash = - Cid::from_str("bafy2bzacedebgy4j7qnh2v2x4kkr2jqfkryql5ookbjrwge6dbrr24ytlqnj4") - .unwrap(); - - assert_eq!(state_root_hash, expected_root_hash); - } - - fn make_genesis() -> Genesis { - let mut g = quickcheck::Gen::new(5); - let mut genesis = Genesis::arbitrary(&mut g); - - // Make sure we have IPC enabled. - genesis.ipc = Some(IpcParams::arbitrary(&mut g)); - genesis - } - - fn make_interpreter( - ) -> FvmMessageInterpreter> { - let (client, _) = MockClient::new(MockRequestMethodMatcher::default()); - FvmMessageInterpreter::new( - client, - None, - contracts_path(), - 1.05, - 1.05, - false, - UpgradeScheduler::new(), - ) - } - - fn read_bundle() -> Vec { - std::fs::read(bundle_path()).expect("failed to read bundle") - } - - fn read_custom_actors_bundle() -> Vec { - std::fs::read(custom_actors_bundle_path()).expect("failed to read custom actor bundle") - } -} diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 0aefb4b2e..3e098765b 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -1,13 +1,11 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::path::PathBuf; mod broadcast; mod check; mod checkpoint; mod exec; mod externs; -mod genesis; pub mod observe; mod query; pub mod state; @@ -22,10 +20,8 @@ pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; 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; @@ -63,7 +59,6 @@ pub struct FvmMessageInterpreter where DB: Blockstore + 'static + Clone, { - contracts: Hardhat, /// Tendermint client for querying the RPC. client: C, /// If this is a validator node, this should be the key we can use to sign transactions. @@ -91,7 +86,6 @@ where pub fn new( client: C, validator_ctx: Option>, - contracts_dir: PathBuf, gas_overestimation_rate: f64, gas_search_step: f64, exec_in_check: bool, @@ -100,7 +94,6 @@ where Self { client, validator_ctx, - contracts: Hardhat::new(contracts_dir), gas_overestimation_rate, gas_search_step, exec_in_check, diff --git a/fendermint/vm/interpreter/src/fvm/state/genesis.rs b/fendermint/vm/interpreter/src/fvm/state/genesis.rs index da90cbe0a..bdbed9947 100644 --- a/fendermint/vm/interpreter/src/fvm/state/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/state/genesis.rs @@ -166,17 +166,6 @@ where Ok(()) } - /// Flush the data to the block store. - pub fn commit(self) -> anyhow::Result { - match self.stage { - Stage::Tree(mut state_tree) => Ok(state_tree.flush()?), - Stage::Exec(exec_state) => match exec_state.commit()? { - (_, _, true) => bail!("FVM parameters are not expected to be updated in genesis"), - (cid, _, _) => Ok(cid), - }, - } - } - /// Flush the data to the block store. Returns the state root cid and the underlying state store. pub fn finalize(self) -> anyhow::Result<(Cid, DB)> { match self.stage { diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index aefd7d242..988482a05 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -727,6 +727,23 @@ fn circ_supply(g: &Genesis) -> TokenAmount { .fold(TokenAmount::zero(), |s, a| s + a.balance.clone()) } +#[cfg(any(feature = "test-util", test))] +pub async fn create_test_genesis_state( + bundle_path: PathBuf, + custom_actors_bundle_path: PathBuf, + genesis_params: Genesis, + maybe_ipc_path: Option, +) -> anyhow::Result<(FvmGenesisState, GenesisOutput)> { + let mut builder = GenesisBuilder::new(bundle_path, custom_actors_bundle_path, genesis_params); + if let Some(p) = maybe_ipc_path { + builder = builder.with_ipc_system_contracts(p); + } + + let mut state = builder.init_state().await?; + let out = builder.populate_state(&mut state, builder.genesis_params.clone())?; + Ok((state, out)) +} + #[cfg(test)] mod tests { use crate::genesis::GenesisAppState; diff --git a/fendermint/vm/interpreter/src/lib.rs b/fendermint/vm/interpreter/src/lib.rs index afa1b43db..40868b6cd 100644 --- a/fendermint/vm/interpreter/src/lib.rs +++ b/fendermint/vm/interpreter/src/lib.rs @@ -11,23 +11,6 @@ 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. -#[async_trait] -pub trait GenesisInterpreter: Sync + Send { - type State: Send; - type Genesis: Send; - type Output; - - /// Initialize the chain. - async fn init( - &self, - state: Self::State, - genesis: Self::Genesis, - ) -> anyhow::Result<(Self::State, Self::Output)>; -} - /// Prepare and process transaction proposals. #[async_trait] pub trait ProposalInterpreter: Sync + Send { diff --git a/fendermint/vm/interpreter/src/signed.rs b/fendermint/vm/interpreter/src/signed.rs index 847d790a5..0998fb27e 100644 --- a/fendermint/vm/interpreter/src/signed.rs +++ b/fendermint/vm/interpreter/src/signed.rs @@ -14,7 +14,7 @@ use serde::Serialize; use crate::{ fvm::{FvmApplyRet, FvmCheckRet, FvmMessage}, - CheckInterpreter, ExecInterpreter, GenesisInterpreter, QueryInterpreter, + CheckInterpreter, ExecInterpreter, QueryInterpreter, }; /// Message validation failed due to an invalid signature. @@ -230,21 +230,3 @@ where self.inner.query(state, qry).await } } - -#[async_trait] -impl GenesisInterpreter for SignedMessageInterpreter -where - I: GenesisInterpreter, -{ - type State = I::State; - type Genesis = I::Genesis; - type Output = I::Output; - - async fn init( - &self, - state: Self::State, - genesis: Self::Genesis, - ) -> anyhow::Result<(Self::State, Self::Output)> { - self.inner.init(state, genesis).await - } -} diff --git a/fendermint/vm/snapshot/Cargo.toml b/fendermint/vm/snapshot/Cargo.toml index 4183249fd..25b5c1e1b 100644 --- a/fendermint/vm/snapshot/Cargo.toml +++ b/fendermint/vm/snapshot/Cargo.toml @@ -43,7 +43,7 @@ fendermint_testing = { path = "../../testing", features = ["arb"], optional = tr [dev-dependencies] fvm = { workspace = true } fendermint_testing = { path = "../../testing", features = ["golden"] } -fendermint_vm_interpreter = { path = "../interpreter", features = ["bundle"] } +fendermint_vm_interpreter = { path = "../interpreter", features = ["bundle", "test-util"] } fendermint_vm_genesis = { path = "../genesis", features = ["arb"] } fendermint_vm_snapshot = { path = ".", features = ["arb"] } diff --git a/fendermint/vm/snapshot/src/manager.rs b/fendermint/vm/snapshot/src/manager.rs index 495219067..4a6f490f8 100644 --- a/fendermint/vm/snapshot/src/manager.rs +++ b/fendermint/vm/snapshot/src/manager.rs @@ -309,21 +309,16 @@ fn move_or_copy(from: &Path, to: &Path) -> anyhow::Result<()> { #[cfg(test)] mod tests { - use std::{sync::Arc, time::Duration}; + use std::time::Duration; use async_stm::{atomically, retry}; use fendermint_vm_genesis::Genesis; - use fendermint_vm_interpreter::{ - fvm::{ - bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, - state::{snapshot::Snapshot, FvmGenesisState, FvmStateParams}, - store::memory::MemoryBlockstore, - upgrades::UpgradeScheduler, - FvmMessageInterpreter, - }, - GenesisInterpreter, + use fendermint_vm_interpreter::fvm::{ + bundle::{bundle_path, contracts_path, custom_actors_bundle_path}, + state::{snapshot::Snapshot, FvmStateParams}, + store::memory::MemoryBlockstore, }; - use fvm::engine::MultiEngine; + use fendermint_vm_interpreter::genesis::create_test_genesis_state; use quickcheck::Arbitrary; use crate::{manager::SnapshotParams, manifest, PARTS_DIR_NAME}; @@ -446,33 +441,22 @@ mod tests { let mut g = quickcheck::Gen::new(5); let genesis = Genesis::arbitrary(&mut g); - let bundle = std::fs::read(bundle_path()).expect("failed to read bundle"); - let custom_actors_bundle = std::fs::read(custom_actors_bundle_path()) - .expect("failed to read custom actors bundle"); - let multi_engine = Arc::new(MultiEngine::default()); - - let store = MemoryBlockstore::new(); - let state = - FvmGenesisState::new(store.clone(), multi_engine, &bundle, &custom_actors_bundle) - .await - .expect("failed to create state"); - - let interpreter = FvmMessageInterpreter::new( - mock_client(), - None, - contracts_path(), - 1.05, - 1.05, - false, - UpgradeScheduler::new(), - ); - - let (state, out) = interpreter - .init(state, genesis) - .await - .expect("failed to init genesis"); - - let state_root = state.commit().expect("failed to commit"); + let maybe_contract_path = genesis.ipc.as_ref().map(|_| contracts_path()); + let (state, out) = create_test_genesis_state( + bundle_path(), + custom_actors_bundle_path(), + genesis, + maybe_contract_path, + ) + .await + .expect("cannot create genesis state"); + let store = state.store().clone(); + // unwrap_or_else + panic is used because the return type is not Result, also the exec state + // does not implement debug, which expect cannot be used + let state = state + .into_exec_state() + .unwrap_or_else(|_| panic!("cannot create exec state")); + let (state_root, _, _) = state.commit().expect("failed to commit"); let state_params = FvmStateParams { state_root, From e5fb277deec96d7ac332224b66f2a789e6125a99 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 22 Aug 2024 11:59:39 +0800 Subject: [PATCH 055/111] update gas market actor --- fendermint/actors/gas_market/src/lib.rs | 151 +++++++++++++----- .../vm/interpreter/src/fvm/state/genesis.rs | 17 ++ fendermint/vm/interpreter/src/genesis.rs | 6 +- 3 files changed, 128 insertions(+), 46 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 260f4d541..998370e77 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -16,10 +16,12 @@ fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; pub type Gas = u64; +pub type SetConstants = EIP1559Constants; /// Constant params used by EIP1559 #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559Constants { + block_gas_limit: Gas, /// The minimal base fee when gas utilization is low minimal_base_fee: TokenAmount, /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) @@ -30,14 +32,12 @@ pub struct EIP1559Constants { #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559GasState { - block_gas_limit: Gas, base_fee: TokenAmount, constants: EIP1559Constants, } #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct GasActorConstructorParams { - block_gas_limit: Gas, base_fee: TokenAmount, constants: Option, } @@ -53,13 +53,6 @@ pub struct BlockGasUtilization { pub block_gas_used: Gas, } -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct SetConstants { - pub block_gas_limit: Option, - pub base_fee: Option, - pub constants: Option, -} - pub struct EIP1559GasMarketActor {} #[derive(FromPrimitive)] @@ -67,6 +60,7 @@ pub struct EIP1559GasMarketActor {} pub enum Method { Constructor = METHOD_CONSTRUCTOR, CurrentReading = frc42_dispatch::method_hash!("CurrentReading"), + GetConstants = frc42_dispatch::method_hash!("GetConstants"), SetConstants = frc42_dispatch::method_hash!("SetConstants"), UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), } @@ -87,7 +81,7 @@ impl EIP1559GasMarketActor { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.set_constants(constants); + st.constants = constants; Ok(()) })?; @@ -99,11 +93,18 @@ impl EIP1559GasMarketActor { let st = rt.state::()?; Ok(GasMarketReading { - block_gas_limit: st.block_gas_limit, + block_gas_limit: st.constants.block_gas_limit, base_fee: st.base_fee, }) } + fn get_constants(rt: &impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + + let st = rt.state::()?; + Ok(st.constants) + } + fn update_utilization( rt: &impl Runtime, utilization: BlockGasUtilization, @@ -120,7 +121,8 @@ impl EIP1559GasMarketActor { impl Default for EIP1559Constants { fn default() -> Self { Self { - // Take from filecoin setting + // Take from filecoin setting, fvm_shared::BLOCK_GAS_LIMIT + block_gas_limit: 10_000_000_000, minimal_base_fee: TokenAmount::from_atto(100), // Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) elasticity_multiplier: 2, @@ -133,7 +135,6 @@ impl Default for EIP1559Constants { impl From for EIP1559GasState { fn from(params: GasActorConstructorParams) -> Self { Self { - block_gas_limit: params.block_gas_limit, base_fee: params.base_fee, constants: params.constants.unwrap_or_default(), } @@ -141,9 +142,8 @@ impl From for EIP1559GasState { } impl GasActorConstructorParams { - pub fn new(block_gas_limit: Gas, base_fee: TokenAmount) -> Self { + pub fn new(base_fee: TokenAmount) -> Self { Self { - block_gas_limit, base_fee, constants: None, } @@ -156,22 +156,9 @@ impl GasActorConstructorParams { } impl EIP1559GasState { - #[inline] - fn update_if_some(maybe_some: Option, to_change: &mut T) { - if let Some(v) = maybe_some { - *to_change = v; - } - } - - fn set_constants(&mut self, constants: SetConstants) { - Self::update_if_some(constants.constants, &mut self.constants); - Self::update_if_some(constants.base_fee, &mut self.base_fee); - Self::update_if_some(constants.block_gas_limit, &mut self.block_gas_limit); - } - fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { let base_fee = self.base_fee.clone(); - let gas_target = self.block_gas_limit / self.constants.elasticity_multiplier; + let gas_target = self.constants.block_gas_limit / self.constants.elasticity_multiplier; match gas_used.cmp(&gas_target) { Ordering::Equal => base_fee, @@ -208,6 +195,7 @@ impl ActorCode for EIP1559GasMarketActor { Constructor => constructor, SetConstants => set_constants, CurrentReading => current_reading, + GetConstants => get_constants, UpdateUtilization => update_utilization, } } @@ -215,12 +203,14 @@ impl ActorCode for EIP1559GasMarketActor { #[cfg(test)] mod tests { use crate::{ - EIP1559GasMarketActor, EIP1559GasState, GasActorConstructorParams, Method, SetConstants, + BlockGasUtilization, EIP1559Constants, EIP1559GasMarketActor, EIP1559GasState, + GasActorConstructorParams, GasMarketReading, Method, }; use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::ipld_block::IpldBlock; use fvm_shared::address::Address; + use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; pub fn construct_and_verify() -> MockRuntime { @@ -236,8 +226,7 @@ mod tests { .call::( Method::Constructor as u64, IpldBlock::serialize_cbor(&GasActorConstructorParams { - block_gas_limit: 100, - base_fee: Default::default(), + base_fee: TokenAmount::from_atto(100), constants: None, }) .unwrap(), @@ -259,17 +248,96 @@ mod tests { let r = rt.call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { - block_gas_limit: Some(20), - base_fee: None, - constants: None, + IpldBlock::serialize_cbor(&EIP1559Constants { + minimal_base_fee: Default::default(), + elasticity_multiplier: 0, + base_fee_max_change_denominator: 0, + block_gas_limit: 20, }) .unwrap(), ); assert!(r.is_ok()); let s = rt.get_state::(); - assert_eq!(s.block_gas_limit, 20); + assert_eq!(s.constants.block_gas_limit, 20); + } + + #[test] + fn test_update_utilization_full_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&BlockGasUtilization { + // full block usage + block_gas_used: 10_000_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(112)); + } + + #[test] + fn test_update_utilization_equal_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&BlockGasUtilization { + // full block usage + block_gas_used: 5_000_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(100)); + } + + #[test] + fn test_update_utilization_under_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&BlockGasUtilization { + // full block usage + block_gas_used: 100_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(88)); } #[test] @@ -281,10 +349,11 @@ mod tests { let code = rt .call::( Method::SetConstants as u64, - IpldBlock::serialize_cbor(&SetConstants { - block_gas_limit: Some(20), - base_fee: None, - constants: None, + IpldBlock::serialize_cbor(&EIP1559Constants { + minimal_base_fee: TokenAmount::from_atto(10000), + elasticity_multiplier: 0, + base_fee_max_change_denominator: 0, + block_gas_limit: 20, }) .unwrap(), ) diff --git a/fendermint/vm/interpreter/src/fvm/state/genesis.rs b/fendermint/vm/interpreter/src/fvm/state/genesis.rs index da90cbe0a..f6a25784a 100644 --- a/fendermint/vm/interpreter/src/fvm/state/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/state/genesis.rs @@ -290,6 +290,23 @@ where self.create_actor_internal(code_cid, id, state, balance, delegated_address) } + pub fn construct_custom_actor( + &mut self, + name: &str, + id: ActorID, + state: &impl Serialize, + balance: TokenAmount, + delegated_address: Option
, + ) -> anyhow::Result<()> { + // Retrieve the CID of the actor code by the numeric ID. + let code_cid = *self + .custom_actor_manifest + .code_by_name(name) + .ok_or_else(|| anyhow!("can't find actor: {name} in the custom actor manifest"))?; + + self.create_actor_internal(code_cid, id, state, balance, delegated_address) + } + /// Creates an actor using code specified in the manifest. fn create_actor_internal( &mut self, diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 9b0cfb949..e252057d9 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -31,7 +31,6 @@ use fvm_ipld_encoding::CborStore; use fvm_shared::chainid::ChainID; use fvm_shared::econ::TokenAmount; use fvm_shared::version::NetworkVersion; -use fvm_shared::BLOCK_GAS_LIMIT; use ipc_actors_abis::i_diamond::FacetCut; use num_traits::Zero; @@ -423,10 +422,7 @@ impl GenesisBuilder { // initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) let initial_base_fee = TokenAmount::from_atto(1_000_000_000); let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::from( - fendermint_actor_gas_market::GasActorConstructorParams::new( - BLOCK_GAS_LIMIT, - initial_base_fee, - ), + fendermint_actor_gas_market::GasActorConstructorParams::new(initial_base_fee), ); state .create_custom_actor( From d3bbb3566eba03bc9741618978075370a62058a8 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 22 Aug 2024 19:59:26 +0800 Subject: [PATCH 056/111] end block --- fendermint/actors/gas_market/src/lib.rs | 8 +- fendermint/app/src/app.rs | 9 +- fendermint/app/src/tmconv.rs | 29 +---- fendermint/testing/contract-test/src/lib.rs | 4 +- fendermint/vm/interpreter/src/chain.rs | 8 +- fendermint/vm/interpreter/src/fvm/cometbft.rs | 115 ++++++++++++++++++ fendermint/vm/interpreter/src/fvm/exec.rs | 10 +- .../vm/interpreter/src/fvm/gas/actor.rs | 67 +++++++++- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 11 ++ fendermint/vm/interpreter/src/fvm/mod.rs | 1 + 10 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 fendermint/vm/interpreter/src/fvm/cometbft.rs diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index 998370e77..cbabdc202 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -21,13 +21,13 @@ pub type SetConstants = EIP1559Constants; /// Constant params used by EIP1559 #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct EIP1559Constants { - block_gas_limit: Gas, + pub block_gas_limit: Gas, /// The minimal base fee when gas utilization is low - minimal_base_fee: TokenAmount, + pub minimal_base_fee: TokenAmount, /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - elasticity_multiplier: u64, + pub elasticity_multiplier: u64, /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - base_fee_max_change_denominator: u64, + pub base_fee_max_change_denominator: u64, } #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 1ddd69486..e34f790f4 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -17,12 +17,13 @@ use fendermint_vm_interpreter::bytes::{ BytesMessageApplyRes, BytesMessageCheckRes, BytesMessageQuery, BytesMessageQueryRes, }; use fendermint_vm_interpreter::chain::{ChainEnv, ChainMessageApplyRet, IllegalMessage}; +use fendermint_vm_interpreter::fvm::cometbft::{to_validator_updates, EndBlockUpdate}; use fendermint_vm_interpreter::fvm::state::{ empty_state_tree, CheckStateRef, FvmExecState, FvmQueryState, FvmStateParams, FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; -use fendermint_vm_interpreter::fvm::{FvmApplyRet, PowerUpdates}; +use fendermint_vm_interpreter::fvm::FvmApplyRet; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ @@ -408,7 +409,7 @@ where Message = Vec, BeginOutput = FvmApplyRet, DeliverOutput = BytesMessageApplyRes, - EndOutput = PowerUpdates, + EndOutput = EndBlockUpdate, >, I: CheckInterpreter< State = FvmExecState>, @@ -789,9 +790,7 @@ where .await .context("end failed")?; - let r = to_end_block(ret)?; - - Ok(r) + Ok(response::EndBlock::try_from(ret)?) } /// Commit the current state at the current height. diff --git a/fendermint/app/src/tmconv.rs b/fendermint/app/src/tmconv.rs index 984abc4fa..21746c02b 100644 --- a/fendermint/app/src/tmconv.rs +++ b/fendermint/app/src/tmconv.rs @@ -3,10 +3,9 @@ //! Conversions to Tendermint data types. use anyhow::{anyhow, bail, Context}; use fendermint_vm_core::Timestamp; -use fendermint_vm_genesis::{Power, Validator}; use fendermint_vm_interpreter::fvm::{ state::{BlockHash, FvmStateParams}, - FvmApplyRet, FvmCheckRet, FvmQueryRet, PowerUpdates, + FvmApplyRet, FvmCheckRet, FvmQueryRet, }; use fendermint_vm_message::signed::DomainHash; use fendermint_vm_snapshot::{SnapshotItem, SnapshotManifest}; @@ -148,18 +147,6 @@ pub fn to_check_tx(ret: FvmCheckRet) -> response::CheckTx { } } -/// Map the return values from epoch boundary operations to validator updates. -pub fn to_end_block(power_table: PowerUpdates) -> anyhow::Result { - let validator_updates = - to_validator_updates(power_table.0).context("failed to convert validator updates")?; - - Ok(response::EndBlock { - validator_updates, - consensus_param_updates: None, - events: Vec::new(), // TODO: Events from epoch transitions? - }) -} - /// Map the return values from cron operations. pub fn to_begin_block(ret: FvmApplyRet) -> response::BeginBlock { let events = to_events("event", ret.apply_ret.events, ret.emitters); @@ -319,20 +306,6 @@ pub fn to_query(ret: FvmQueryRet, block_height: BlockHeight) -> anyhow::Result>, -) -> anyhow::Result> { - let mut updates = vec![]; - for v in validators { - updates.push(tendermint::validator::Update { - pub_key: tendermint::PublicKey::try_from(v.public_key)?, - power: tendermint::vote::Power::try_from(v.power.0)?, - }); - } - Ok(updates) -} - pub fn to_timestamp(time: tendermint::time::Time) -> Timestamp { Timestamp( time.unix_timestamp() diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index 65a4ce8ad..060c532af 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -4,11 +4,11 @@ use anyhow::{anyhow, Context, Result}; use byteorder::{BigEndian, WriteBytesExt}; use fendermint_vm_core::Timestamp; -use fendermint_vm_interpreter::fvm::PowerUpdates; use fvm_shared::clock::ChainEpoch; use std::{future::Future, sync::Arc}; use fendermint_vm_genesis::Genesis; +use fendermint_vm_interpreter::fvm::cometbft::EndBlockUpdate; use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; use fendermint_vm_interpreter::{ fvm::{ @@ -66,7 +66,7 @@ where Message = FvmMessage, BeginOutput = FvmApplyRet, DeliverOutput = FvmApplyRet, - EndOutput = PowerUpdates, + EndOutput = EndBlockUpdate, >, { pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result { diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 1eb7b079a..a54bb57e7 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,8 +1,9 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use crate::fvm::cometbft::EndBlockUpdate; use crate::fvm::gas::{Gas, GasMarket}; use crate::fvm::state::ipc::GatewayCaller; -use crate::fvm::{topdown, FvmApplyRet, PowerUpdates}; +use crate::fvm::{topdown, FvmApplyRet}; use crate::{ fvm::state::FvmExecState, fvm::FvmMessage, @@ -245,7 +246,7 @@ where Message = VerifiableMessage, DeliverOutput = SignedMessageApplyRes, State = FvmExecState, - EndOutput = PowerUpdates, + EndOutput = EndBlockUpdate, >, { // The state consists of the resolver pool, which this interpreter needs, and the rest of the @@ -426,8 +427,9 @@ where let (state, out) = self.inner.end(state).await?; // Update any component that needs to know about changes in the power table. - if !out.0.is_empty() { + if !out.validators.0.is_empty() { let power_updates = out + .validators .0 .iter() .map(|v| { diff --git a/fendermint/vm/interpreter/src/fvm/cometbft.rs b/fendermint/vm/interpreter/src/fvm/cometbft.rs new file mode 100644 index 000000000..daa733307 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/cometbft.rs @@ -0,0 +1,115 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::state::FvmExecState; +use crate::fvm::{FvmMessageInterpreter, PowerUpdates}; +use anyhow::Context; +use fendermint_vm_genesis::{Power, Validator}; +use fvm_ipld_blockstore::Blockstore; +use tendermint_rpc::Client; + +/// The end block update for cometbft +pub struct EndBlockUpdate { + pub consensus: Option, + pub validators: PowerUpdates, +} + +/// Potential updates to cometbft consensus parameters. Currently only block `max_gas` needs to be +/// updated, but in the future, more parameter could be updated. +pub struct ConsensusBlockUpdate { + max_gas: Option, +} + +/// Convert validator power to tendermint validator update. +/// TODO: the import is quite strange, `Validator` and `Power` are imported from `genesis` crate, +/// TODO: which should be from a `type` or `validator` crate. +pub fn to_validator_updates( + validators: Vec>, +) -> anyhow::Result> { + let mut updates = vec![]; + for v in validators { + updates.push(tendermint::validator::Update { + pub_key: tendermint::PublicKey::try_from(v.public_key)?, + power: tendermint::vote::Power::try_from(v.power.0)?, + }); + } + Ok(updates) +} + +impl TryFrom for tendermint::abci::response::EndBlock { + type Error = anyhow::Error; + + fn try_from(value: EndBlockUpdate) -> Result { + let validator_updates = to_validator_updates(value.validators.0) + .context("failed to convert validator updates")?; + + Ok(tendermint::abci::response::EndBlock { + validator_updates, + consensus_param_updates: value.consensus, + events: Vec::new(), // TODO: Events from epoch transitions? + }) + } +} + +impl FvmMessageInterpreter +where + DB: Blockstore + Clone + 'static + Send + Sync, + TC: Client + Clone + Send + Sync + 'static, +{ + pub(crate) async fn update_cometbft_consensus_params( + &self, + state: &mut FvmExecState, + end_block: &mut EndBlockUpdate, + ) -> anyhow::Result<()> { + let mut updates = ConsensusBlockUpdate::empty(); + + state.gas_market().process_consensus_update(&mut updates); + + if !updates.is_some() { + return Ok(()); + } + + let params = self + .client + .consensus_params(tendermint::block::Height::try_from(state.block_height())?) + .await? + .consensus_params; + end_block.with_consensus(updates.apply(params)); + + Ok(()) + } +} + +impl ConsensusBlockUpdate { + pub fn empty() -> Self { + Self { max_gas: None } + } + + pub fn is_some(&self) -> bool { + self.max_gas.is_some() + } + + pub fn apply(self, mut params: tendermint::consensus::Params) -> tendermint::consensus::Params { + if let Some(ref max_gas) = self.max_gas { + params.block.max_gas = *max_gas as i64; + } + params + } + + pub fn process_block_size(&mut self, block_gas_limit: u64) { + self.max_gas = Some(block_gas_limit); + } +} + +impl EndBlockUpdate { + pub fn new(power: PowerUpdates) -> Self { + Self { + validators: power, + consensus: None, + } + } + + pub fn with_consensus(&mut self, params: tendermint::consensus::Params) { + self.consensus = Some(params) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 1f8912edf..9fcd1a15e 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -12,6 +12,7 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; +use crate::fvm::cometbft::EndBlockUpdate; use crate::fvm::gas::GasMarket; use crate::ExecInterpreter; @@ -49,7 +50,7 @@ where /// Return validator power updates. /// Currently ignoring events as there aren't any emitted by the smart contract, /// but keep in mind that if there were, those would have to be propagated. - type EndOutput = PowerUpdates; + type EndOutput = EndBlockUpdate; async fn begin( &self, @@ -262,6 +263,11 @@ where PowerUpdates::default() }; - Ok((state, updates)) + // process cometbft end block updates + let mut end_block = EndBlockUpdate::new(updates); + self.update_cometbft_consensus_params(&mut state, &mut end_block) + .await?; + + Ok((state, end_block)) } } diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index a3e28033b..b832f3437 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -5,10 +5,11 @@ use crate::fvm::gas::{Available, Gas, GasMarket}; use crate::fvm::FvmMessage; use anyhow::Context; -use fendermint_actor_gas_market::GasMarketReading; +use crate::fvm::cometbft::ConsensusBlockUpdate; +use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; use fendermint_vm_actor_interface::system; -use fvm::executor::{ApplyKind, Executor}; +use fvm::executor::{ApplyKind, ApplyRet, Executor}; use fvm_shared::clock::ChainEpoch; #[derive(Default)] @@ -17,9 +18,21 @@ pub struct ActorGasMarket { block_gas_limit: Gas, /// The accumulated gas usage so far block_gas_used: Gas, + /// Pending update to the underlying gas actor + constant_update: Option, } impl GasMarket for ActorGasMarket { + type Constant = SetConstants; + + fn get_constants(&self) -> anyhow::Result { + todo!() + } + + fn set_constants(&mut self, constants: Self::Constant) { + self.constant_update = Some(constants); + } + fn available(&self) -> Available { Available { block_gas: self.block_gas_limit - self.block_gas_used.min(self.block_gas_limit), @@ -69,13 +82,56 @@ impl ActorGasMarket { Ok(Self { block_gas_limit: reading.block_gas_limit, block_gas_used: 0, + constant_update: None, }) } + pub fn process_consensus_update(&self, update: &mut ConsensusBlockUpdate) { + if let Some(ref set_constant) = self.constant_update { + update.process_block_size(set_constant.block_gas_limit); + } + } + pub fn commit( &self, executor: &mut E, block_height: ChainEpoch, + ) -> anyhow::Result<()> { + self.commit_constants(executor, block_height)?; + self.commit_utilization(executor, block_height) + } + + fn commit_constants( + &self, + executor: &mut E, + block_height: ChainEpoch, + ) -> anyhow::Result<()> { + let Some(ref constants) = self.constant_update else { + return Ok(()); + }; + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, + sequence: block_height as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: fendermint_actor_gas_market::Method::SetConstants as u64, + params: fvm_ipld_encoding::RawBytes::serialize(constants)?, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + self.call_fvm(msg, executor)?; + + Ok(()) + } + + fn commit_utilization( + &self, + executor: &mut E, + block_height: ChainEpoch, ) -> anyhow::Result<()> { let block_gas_used = self.block_gas_used.min(self.block_gas_limit); let params = fvm_ipld_encoding::RawBytes::serialize( @@ -96,13 +152,18 @@ impl ActorGasMarket { gas_premium: Default::default(), }; + self.call_fvm(msg, executor)?; + Ok(()) + } + + fn call_fvm(&self, msg: FvmMessage, executor: &mut E) -> anyhow::Result { let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; if let Some(err) = apply_ret.failure_info { anyhow::bail!("failed to update EIP1559 gas state: {}", err) } else { - Ok(()) + Ok(apply_ret) } } } diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index 768c47bb6..f6e26ef93 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -11,6 +11,17 @@ pub struct Available { /// The gas market for fendermint. This should be backed by an fvm actor. pub trait GasMarket { + /// The constant parameters that determines the readings of gas market, such as block gas limit. + type Constant; + + #[allow(dead_code)] + fn get_constants(&self) -> anyhow::Result; + + /// Update the constants of the gas market. If the gas market is actor based, then it's recommended + /// to flush at EndBlock. + #[allow(dead_code)] + fn set_constants(&mut self, constants: Self::Constant); + /// Obtain the current block gas available for execution fn available(&self) -> Available; diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 73296813e..1c14ab2eb 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -14,6 +14,7 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; +pub mod cometbft; pub(crate) mod gas; pub(crate) mod topdown; From 65a6802d37039241fa5181f39d5c09a788786c1e Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 26 Aug 2024 21:53:58 +0800 Subject: [PATCH 057/111] add validator tracking and gas premium distribution --- Cargo.lock | 2 + fendermint/app/src/app.rs | 24 +++++-- fendermint/app/src/cmd/run.rs | 3 +- fendermint/app/src/ipc.rs | 14 ++-- fendermint/app/src/lib.rs | 1 + fendermint/app/src/validators.rs | 69 +++++++++++++++++++ fendermint/vm/interpreter/src/chain.rs | 4 +- fendermint/vm/interpreter/src/fvm/exec.rs | 4 +- .../vm/interpreter/src/fvm/gas/actor.rs | 63 ++++++++++++++--- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 19 ++++- .../vm/interpreter/src/fvm/state/exec.rs | 21 +++--- 11 files changed, 190 insertions(+), 34 deletions(-) create mode 100644 fendermint/app/src/validators.rs diff --git a/Cargo.lock b/Cargo.lock index c6203003e..bf262d147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2828,7 +2828,9 @@ dependencies = [ "fvm_ipld_blockstore", "fvm_ipld_encoding", "fvm_shared", + "hex", "hex-literal 0.4.1", + "k256 0.13.3", "log", "multihash 0.18.1", "num-derive 0.3.3", diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index e34f790f4..f9b6259a1 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -42,12 +42,14 @@ use num_traits::Zero; use serde::{Deserialize, Serialize}; use tendermint::abci::request::CheckTxKind; use tendermint::abci::{request, response}; +use tendermint_rpc::Client; use tracing::instrument; use crate::observe::{ BlockCommitted, BlockProposalEvaluated, BlockProposalReceived, BlockProposalSent, Message, MpoolReceived, }; +use crate::validators::ValidatorTracker; use crate::AppExitCode; use crate::BlockHeight; use crate::{tmconv::*, VERSION}; @@ -116,10 +118,11 @@ pub struct AppConfig { /// Handle ABCI requests. #[derive(Clone)] -pub struct App +pub struct App where SS: Blockstore + Clone + 'static, S: KVStore, + C: Client, { /// Database backing all key-value operations. db: Arc, @@ -160,9 +163,11 @@ where /// /// Zero means unlimited. state_hist_size: u64, + /// Tracks the validator + validators: ValidatorTracker, } -impl App +impl App where S: KVStore + Codec @@ -171,6 +176,7 @@ where + Codec, DB: KVWritable + KVReadable + Clone + 'static, SS: Blockstore + Clone + 'static, + C: Client, { pub fn new( config: AppConfig, @@ -179,6 +185,7 @@ where interpreter: I, chain_env: ChainEnv, snapshots: Option, + client: C, ) -> Result { let app = Self { db: Arc::new(db), @@ -193,13 +200,14 @@ where snapshots, exec_state: Arc::new(tokio::sync::Mutex::new(None)), check_state: Arc::new(tokio::sync::Mutex::new(None)), + validators: ValidatorTracker::new(client), }; app.init_committed_state()?; Ok(app) } } -impl App +impl App where S: KVStore + Codec @@ -208,6 +216,7 @@ where + Codec, DB: KVWritable + KVReadable + 'static + Clone, SS: Blockstore + 'static + Clone, + C: Client, { /// Get an owned clone of the state store. fn state_store_clone(&self) -> SS { @@ -393,7 +402,7 @@ where // the `tower-abci` library would throw an exception when it tried to convert a // `Response::Exception` into a `ConsensusResponse` for example. #[async_trait] -impl Application for App +impl Application for App where S: KVStore + Codec @@ -421,6 +430,7 @@ where Query = BytesMessageQuery, Output = BytesMessageQueryRes, >, + C: Client + Sync, { /// Provide information about the ABCI application. async fn info(&self, _request: request::Info) -> AbciResult { @@ -727,10 +737,14 @@ where state_params.timestamp = to_timestamp(request.header.time); + let validator = self + .validators + .get_validator(&request.header.proposer_address, block_height) + .await?; let state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) .context("error creating new state")? .with_block_hash(block_hash) - .with_validator_id(request.header.proposer_address); + .with_validator(validator); tracing::debug!("initialized exec state"); diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index dca099b1f..9d8e68516 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -294,7 +294,7 @@ async fn run(settings: Settings) -> anyhow::Result<()> { None }; - let app: App<_, _, AppStore, _> = App::new( + let app: App<_, _, AppStore, _, _> = App::new( AppConfig { app_namespace: ns.app, state_hist_namespace: ns.state_hist, @@ -310,6 +310,7 @@ async fn run(settings: Settings) -> anyhow::Result<()> { parent_finality_votes: parent_finality_votes.clone(), }, snapshots, + tendermint_client.clone(), )?; if let Some((agent_proxy, config)) = ipc_tuple { diff --git a/fendermint/app/src/ipc.rs b/fendermint/app/src/ipc.rs index 6a03bc4b2..5415a0efe 100644 --- a/fendermint/app/src/ipc.rs +++ b/fendermint/app/src/ipc.rs @@ -15,6 +15,7 @@ use fvm_ipld_blockstore::Blockstore; use std::sync::Arc; use serde::{Deserialize, Serialize}; +use tendermint_rpc::Client; /// All the things that can be voted on in a subnet. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -24,17 +25,18 @@ pub enum AppVote { } /// Queries the LATEST COMMITTED parent finality from the storage -pub struct AppParentFinalityQuery +pub struct AppParentFinalityQuery where SS: Blockstore + Clone + 'static, S: KVStore, + C: Client, { /// The app to get state - app: App, + app: App, gateway_caller: GatewayCaller>>, } -impl AppParentFinalityQuery +impl AppParentFinalityQuery where S: KVStore + Codec @@ -43,8 +45,9 @@ where + Codec, DB: KVWritable + KVReadable + 'static + Clone, SS: Blockstore + 'static + Clone, + C: Client, { - pub fn new(app: App) -> Self { + pub fn new(app: App) -> Self { Self { app, gateway_caller: GatewayCaller::default(), @@ -62,7 +65,7 @@ where } } -impl ParentFinalityStateQuery for AppParentFinalityQuery +impl ParentFinalityStateQuery for AppParentFinalityQuery where S: KVStore + Codec @@ -71,6 +74,7 @@ where + Codec, DB: KVWritable + KVReadable + 'static + Clone, SS: Blockstore + 'static + Clone, + C: Client, { fn get_latest_committed_finality(&self) -> anyhow::Result> { self.with_exec_state(|mut exec_state| { diff --git a/fendermint/app/src/lib.rs b/fendermint/app/src/lib.rs index f9a45b9a5..a43f8d93f 100644 --- a/fendermint/app/src/lib.rs +++ b/fendermint/app/src/lib.rs @@ -8,6 +8,7 @@ pub mod metrics; pub mod observe; mod store; mod tmconv; +mod validators; pub use app::{App, AppConfig}; pub use store::{AppStore, BitswapBlockstore}; diff --git a/fendermint/app/src/validators.rs b/fendermint/app/src/validators.rs new file mode 100644 index 000000000..5870b7160 --- /dev/null +++ b/fendermint/app/src/validators.rs @@ -0,0 +1,69 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Tracks the validator id from tendermint to their corresponding public key. + +use anyhow::anyhow; +use fendermint_crypto::PublicKey; +use fvm_shared::clock::ChainEpoch; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use tendermint::block::Height; +use tendermint_rpc::{Client, Paging}; + +#[derive(Clone)] +pub(crate) struct ValidatorTracker { + client: C, + public_keys: Arc>>, +} + +impl ValidatorTracker { + pub fn new(client: C) -> Self { + Self { + client, + public_keys: Arc::new(RwLock::new(HashMap::new())), + } + } +} + +impl ValidatorTracker { + /// Get the public key of the validator by id. Note that the id is expected to be a validator. + pub async fn get_validator( + &self, + id: &tendermint::account::Id, + height: ChainEpoch, + ) -> anyhow::Result { + if let Some(key) = self.get_from_cache(id) { + return Ok(key); + } + + // this means validators have changed, re-pull all validators + let height = Height::try_from(height)?; + let response = self.client.validators(height, Paging::All).await?; + + let mut new_validators = HashMap::new(); + let mut pubkey = None; + for validator in response.validators { + let p = validator.pub_key.secp256k1().unwrap(); + let compressed = p.to_encoded_point(true); + let b = compressed.as_bytes(); + let key = PublicKey::parse_slice(b, None)?; + + if *id == validator.address { + pubkey = Some(key); + } + + new_validators.insert(validator.address, key); + } + + *self.public_keys.write().unwrap() = new_validators; + + // cannot find the validator, this should not have happened usually + pubkey.ok_or_else(|| anyhow!("{} not validator", id)) + } + + fn get_from_cache(&self, id: &tendermint::account::Id) -> Option { + let keys = self.public_keys.read().unwrap(); + keys.get(id).copied() + } +} diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index a54bb57e7..dc9a9b553 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -383,7 +383,9 @@ where tracing::debug!("chain interpreter applied topdown msgs"); let local_block_height = state.block_height() as u64; - let proposer = state.validator_id().map(|id| id.to_string()); + let proposer = state + .validator_pubkey() + .map(|id| hex::encode(id.serialize_compressed())); let proposer_ref = proposer.as_deref(); atomically(|| { diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 9fcd1a15e..619b278aa 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -13,7 +13,7 @@ use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; use crate::fvm::cometbft::EndBlockUpdate; -use crate::fvm::gas::GasMarket; +use crate::fvm::gas::{GasMarket, GasUtilization}; use crate::ExecInterpreter; use super::{ @@ -175,7 +175,7 @@ where state .gas_market_mut() - .record_utilization(apply_ret.msg_receipt.gas_used); + .record_utilization(GasUtilization::from(&apply_ret)); (apply_ret, emitters, latency) }; diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index b832f3437..2fdd78d36 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,19 +1,26 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::gas::{Available, Gas, GasMarket}; +use crate::fvm::gas::{Available, Gas, GasMarket, GasUtilization}; use crate::fvm::FvmMessage; use anyhow::Context; use crate::fvm::cometbft::ConsensusBlockUpdate; use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; +use fendermint_crypto::PublicKey; +use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; -use fendermint_vm_actor_interface::system; +use fendermint_vm_actor_interface::{reward, system}; use fvm::executor::{ApplyKind, ApplyRet, Executor}; +use fvm_shared::address::Address; use fvm_shared::clock::ChainEpoch; +use fvm_shared::econ::TokenAmount; +use fvm_shared::METHOD_SEND; #[derive(Default)] pub struct ActorGasMarket { + /// The total gas premium for the miner + gas_premium: TokenAmount, /// The block gas limit block_gas_limit: Gas, /// The accumulated gas usage so far @@ -39,8 +46,9 @@ impl GasMarket for ActorGasMarket { } } - fn record_utilization(&mut self, gas: Gas) { - self.block_gas_used += gas; + fn record_utilization(&mut self, utilization: GasUtilization) { + self.gas_premium += utilization.gas_premium; + self.block_gas_used += utilization.gas_used; // sanity check if self.block_gas_used >= self.block_gas_limit { @@ -80,6 +88,7 @@ impl ActorGasMarket { .context("failed to parse gas market readying")?; Ok(Self { + gas_premium: TokenAmount::from_atto(0), block_gas_limit: reading.block_gas_limit, block_gas_used: 0, constant_update: None, @@ -96,9 +105,43 @@ impl ActorGasMarket { &self, executor: &mut E, block_height: ChainEpoch, + validator: Option, ) -> anyhow::Result<()> { self.commit_constants(executor, block_height)?; - self.commit_utilization(executor, block_height) + self.commit_utilization(executor, block_height)?; + self.distribute_reward(executor, block_height, validator) + } + + fn distribute_reward( + &self, + executor: &mut E, + block_height: ChainEpoch, + validator: Option, + ) -> anyhow::Result<()> { + if validator.is_none() || self.gas_premium.is_zero() { + return Ok(()); + } + + let validator = validator.unwrap(); + let validator = Address::from(EthAddress::new_secp256k1(&validator.serialize())?); + + let msg = FvmMessage { + from: reward::REWARD_ACTOR_ADDR, + to: validator, + sequence: block_height as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: METHOD_SEND, + params: fvm_ipld_encoding::RawBytes::default(), + value: self.gas_premium.clone(), + + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + self.exec_msg_implicitly(msg, executor)?; + + Ok(()) } fn commit_constants( @@ -123,7 +166,7 @@ impl ActorGasMarket { gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - self.call_fvm(msg, executor)?; + self.exec_msg_implicitly(msg, executor)?; Ok(()) } @@ -152,11 +195,15 @@ impl ActorGasMarket { gas_premium: Default::default(), }; - self.call_fvm(msg, executor)?; + self.exec_msg_implicitly(msg, executor)?; Ok(()) } - fn call_fvm(&self, msg: FvmMessage, executor: &mut E) -> anyhow::Result { + fn exec_msg_implicitly( + &self, + msg: FvmMessage, + executor: &mut E, + ) -> anyhow::Result { let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index f6e26ef93..346f57c94 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -1,6 +1,9 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use fvm::executor::ApplyRet; +use fvm_shared::econ::TokenAmount; + pub mod actor; pub type Gas = u64; @@ -9,6 +12,11 @@ pub struct Available { pub block_gas: Gas, } +pub struct GasUtilization { + gas_used: Gas, + gas_premium: TokenAmount, +} + /// The gas market for fendermint. This should be backed by an fvm actor. pub trait GasMarket { /// The constant parameters that determines the readings of gas market, such as block gas limit. @@ -26,5 +34,14 @@ pub trait GasMarket { fn available(&self) -> Available; /// Tracks the amount of gas consumed by a transaction - fn record_utilization(&mut self, gas: Gas); + fn record_utilization(&mut self, gas: GasUtilization); +} + +impl From<&ApplyRet> for GasUtilization { + fn from(ret: &ApplyRet) -> Self { + Self { + gas_used: ret.msg_receipt.gas_used, + gas_premium: ret.miner_tip.clone(), + } + } } diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 577317dc1..10497ce20 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -5,6 +5,7 @@ use std::collections::{HashMap, HashSet}; use anyhow::Ok; use cid::Cid; +use fendermint_crypto::PublicKey; use fendermint_vm_genesis::PowerScale; use fvm::{ call_manager::DefaultCallManager, @@ -30,9 +31,6 @@ use fendermint_vm_encoding::IsHumanReadable; pub type BlockHash = [u8; 32]; -/// First 20 bytes of SHA256(PublicKey) -pub type ValidatorId = tendermint::account::Id; - pub type ActorAddressMap = HashMap; /// The result of the message application bundled with any delegated addresses of event emitters. @@ -107,8 +105,8 @@ where /// execution interpreter without having to add yet another piece to track at the app level. block_hash: Option, - /// ID of the validator who created this block. For queries and checks this is empty. - validator_id: Option, + /// Public key of the validator who created this block. For queries and checks this is empty. + validator_pubkey: Option, /// State of parameters that are outside the control of the FVM but can change and need to be persisted. params: FvmUpdatableParams, @@ -156,7 +154,7 @@ where Ok(Self { executor, block_hash: None, - validator_id: None, + validator_pubkey: None, params: FvmUpdatableParams { app_version: params.app_version, base_fee: params.base_fee, @@ -175,8 +173,8 @@ where } /// Set the validator during execution. - pub fn with_validator_id(mut self, validator_id: ValidatorId) -> Self { - self.validator_id = Some(validator_id); + pub fn with_validator(mut self, key: PublicKey) -> Self { + self.validator_pubkey = Some(key); self } @@ -233,8 +231,8 @@ where } /// Identity of the block creator, if we are indeed executing any blocks. - pub fn validator_id(&self) -> Option { - self.validator_id + pub fn validator_pubkey(&self) -> Option { + self.validator_pubkey } /// The timestamp of the currently executing block. @@ -302,7 +300,8 @@ where pub fn update_gas_market(&mut self) -> anyhow::Result<()> { let height = self.block_height(); - self.gas_market.commit(&mut self.executor, height) + self.gas_market + .commit(&mut self.executor, height, self.validator_pubkey) } /// Update the circulating supply, effective from the next block. From 05ec86b8837d67a79a8b7d16266af9a47111e4e3 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 27 Aug 2024 17:27:34 +0800 Subject: [PATCH 058/111] distribute validator reward --- Cargo.lock | 3 +- fendermint/app/Cargo.toml | 1 + fendermint/app/src/app.rs | 27 +++- fendermint/app/src/tmconv.rs | 66 +++++++- fendermint/testing/contract-test/src/lib.rs | 4 +- fendermint/vm/interpreter/src/chain.rs | 151 +++--------------- fendermint/vm/interpreter/src/fvm/cometbft.rs | 115 ------------- fendermint/vm/interpreter/src/fvm/exec.rs | 10 +- .../vm/interpreter/src/fvm/gas/actor.rs | 11 +- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 3 - fendermint/vm/interpreter/src/fvm/mod.rs | 1 - fendermint/vm/interpreter/src/lib.rs | 1 + fendermint/vm/interpreter/src/selector.rs | 46 ++++++ 13 files changed, 158 insertions(+), 281 deletions(-) delete mode 100644 fendermint/vm/interpreter/src/fvm/cometbft.rs create mode 100644 fendermint/vm/interpreter/src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index bf262d147..16aab6317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2828,9 +2828,7 @@ dependencies = [ "fvm_ipld_blockstore", "fvm_ipld_encoding", "fvm_shared", - "hex", "hex-literal 0.4.1", - "k256 0.13.3", "log", "multihash 0.18.1", "num-derive 0.3.3", @@ -2863,6 +2861,7 @@ dependencies = [ "bytes", "cid", "fendermint_abci", + "fendermint_actor_gas_market", "fendermint_app_options", "fendermint_app_settings", "fendermint_crypto", diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index 8e884e69f..d2b275e1b 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -51,6 +51,7 @@ fendermint_rocksdb = { path = "../rocksdb" } fendermint_rpc = { path = "../rpc" } fendermint_storage = { path = "../storage" } fendermint_tracing = { path = "../tracing" } +fendermint_actor_gas_market = { path = "../actors/gas_market" } fendermint_vm_actor_interface = { path = "../vm/actor_interface" } fendermint_vm_core = { path = "../vm/core" } fendermint_vm_encoding = { path = "../vm/encoding" } diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index f9b6259a1..c1a193ebd 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -17,13 +17,12 @@ use fendermint_vm_interpreter::bytes::{ BytesMessageApplyRes, BytesMessageCheckRes, BytesMessageQuery, BytesMessageQueryRes, }; use fendermint_vm_interpreter::chain::{ChainEnv, ChainMessageApplyRet, IllegalMessage}; -use fendermint_vm_interpreter::fvm::cometbft::{to_validator_updates, EndBlockUpdate}; use fendermint_vm_interpreter::fvm::state::{ empty_state_tree, CheckStateRef, FvmExecState, FvmQueryState, FvmStateParams, FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; -use fendermint_vm_interpreter::fvm::FvmApplyRet; +use fendermint_vm_interpreter::fvm::{FvmApplyRet, PowerUpdates}; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ @@ -165,6 +164,8 @@ where state_hist_size: u64, /// Tracks the validator validators: ValidatorTracker, + /// The cometbft client + client: C, } impl App @@ -176,7 +177,7 @@ where + Codec, DB: KVWritable + KVReadable + Clone + 'static, SS: Blockstore + Clone + 'static, - C: Client, + C: Client + Clone, { pub fn new( config: AppConfig, @@ -200,7 +201,8 @@ where snapshots, exec_state: Arc::new(tokio::sync::Mutex::new(None)), check_state: Arc::new(tokio::sync::Mutex::new(None)), - validators: ValidatorTracker::new(client), + validators: ValidatorTracker::new(client.clone()), + client, }; app.init_committed_state()?; Ok(app) @@ -418,7 +420,7 @@ where Message = Vec, BeginOutput = FvmApplyRet, DeliverOutput = BytesMessageApplyRes, - EndOutput = EndBlockUpdate, + EndOutput = PowerUpdates, >, I: CheckInterpreter< State = FvmExecState>, @@ -430,7 +432,7 @@ where Query = BytesMessageQuery, Output = BytesMessageQueryRes, >, - C: Client + Sync, + C: Client + Sync + Clone, { /// Provide information about the ABCI application. async fn info(&self, _request: request::Info) -> AbciResult { @@ -800,11 +802,20 @@ where // TODO: Return events from epoch transitions. let ret = self - .modify_exec_state(|s| self.interpreter.end(s)) + .modify_exec_state(|s| async { + let ((chain_env, mut state), update) = self.interpreter.end(s).await?; + + let mut end_block = EndBlockUpdate::new(update); + if let Some(gas) = state.gas_market_mut().take_constant_update() { + end_block.update_gas(gas) + } + + Ok(((chain_env, state), end_block)) + }) .await .context("end failed")?; - Ok(response::EndBlock::try_from(ret)?) + Ok(to_end_block(&self.client, request.height, ret).await?) } /// Commit the current state at the current height. diff --git a/fendermint/app/src/tmconv.rs b/fendermint/app/src/tmconv.rs index 21746c02b..6c031072e 100644 --- a/fendermint/app/src/tmconv.rs +++ b/fendermint/app/src/tmconv.rs @@ -2,18 +2,22 @@ // SPDX-License-Identifier: Apache-2.0, MIT //! Conversions to Tendermint data types. use anyhow::{anyhow, bail, Context}; +use fendermint_actor_gas_market::SetConstants; use fendermint_vm_core::Timestamp; +use fendermint_vm_genesis::{Power, Validator}; use fendermint_vm_interpreter::fvm::{ state::{BlockHash, FvmStateParams}, - FvmApplyRet, FvmCheckRet, FvmQueryRet, + FvmApplyRet, FvmCheckRet, FvmQueryRet, PowerUpdates, }; use fendermint_vm_message::signed::DomainHash; use fendermint_vm_snapshot::{SnapshotItem, SnapshotManifest}; +use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, error::ExitCode, event::StampedEvent, ActorID}; use prost::Message; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, num::NonZeroU32}; use tendermint::abci::{response, Code, Event, EventAttribute}; +use tendermint_rpc::Client; use crate::{app::AppError, BlockHeight}; @@ -23,6 +27,25 @@ struct SnapshotMetadata { state_params: FvmStateParams, } +/// The end block update for cometbft +pub struct EndBlockUpdate { + pub max_gas: Option, + pub validators: PowerUpdates, +} + +impl EndBlockUpdate { + pub fn new(power: PowerUpdates) -> Self { + Self { + max_gas: None, + validators: power, + } + } + + pub fn update_gas(&mut self, constants: SetConstants) { + self.max_gas = Some(constants.block_gas_limit); + } +} + /// IPLD encoding of data types we know we must be able to encode. macro_rules! ipld_encode { ($var:ident) => { @@ -31,6 +54,31 @@ macro_rules! ipld_encode { }; } +pub(crate) async fn to_end_block( + client: &C, + height: ChainEpoch, + value: EndBlockUpdate, +) -> anyhow::Result { + let validator_updates = + to_validator_updates(value.validators.0).context("failed to convert validator updates")?; + + let mut consensus_param_updates = None; + if let Some(max_gas) = value.max_gas { + let mut consensus_params = client + .consensus_params(tendermint::block::Height::try_from(height)?) + .await? + .consensus_params; + consensus_params.block.max_gas = max_gas as i64; + consensus_param_updates = Some(consensus_params); + } + + Ok(response::EndBlock { + validator_updates, + consensus_param_updates, + events: Vec::new(), // TODO: Events from epoch transitions? + }) +} + /// Response to delivery where the input was blatantly invalid. /// This indicates that the validator who made the block was Byzantine. pub fn invalid_deliver_tx(err: AppError, description: String) -> response::DeliverTx { @@ -63,6 +111,22 @@ pub fn invalid_query(err: AppError, description: String) -> response::Query { } } +/// Convert validator power to tendermint validator update. +/// TODO: the import is quite strange, `Validator` and `Power` are imported from `genesis` crate, +/// TODO: which should be from a `type` or `validator` crate. +pub fn to_validator_updates( + validators: Vec>, +) -> anyhow::Result> { + let mut updates = vec![]; + for v in validators { + updates.push(tendermint::validator::Update { + pub_key: tendermint::PublicKey::try_from(v.public_key)?, + power: tendermint::vote::Power::try_from(v.power.0)?, + }); + } + Ok(updates) +} + pub fn to_deliver_tx( ret: FvmApplyRet, domain_hash: Option, diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index 060c532af..4ed529025 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -8,7 +8,7 @@ use fvm_shared::clock::ChainEpoch; use std::{future::Future, sync::Arc}; use fendermint_vm_genesis::Genesis; -use fendermint_vm_interpreter::fvm::cometbft::EndBlockUpdate; +use fendermint_vm_interpreter::fvm::PowerUpdates; use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; use fendermint_vm_interpreter::{ fvm::{ @@ -66,7 +66,7 @@ where Message = FvmMessage, BeginOutput = FvmApplyRet, DeliverOutput = FvmApplyRet, - EndOutput = EndBlockUpdate, + EndOutput = PowerUpdates, >, { pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result { diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index dc9a9b553..67b84bb76 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,9 +1,9 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::cometbft::EndBlockUpdate; -use crate::fvm::gas::{Gas, GasMarket}; +use crate::fvm::gas::GasMarket; use crate::fvm::state::ipc::GatewayCaller; -use crate::fvm::{topdown, FvmApplyRet}; +use crate::fvm::{topdown, FvmApplyRet, PowerUpdates}; +use crate::selector::{GasLimitSelector, MessageSelector}; use crate::{ fvm::state::FvmExecState, fvm::FvmMessage, @@ -116,7 +116,7 @@ where (chain_env, state): Self::State, mut msgs: Vec, ) -> anyhow::Result> { - msgs = messages_selection(msgs, state.gas_market().available().block_gas)?; + msgs = messages_selection(msgs, &state)?; // Collect resolved CIDs ready to be proposed from the pool. let ckpts = atomically(|| chain_env.checkpoint_pool.collect_resolved()).await; @@ -246,7 +246,7 @@ where Message = VerifiableMessage, DeliverOutput = SignedMessageApplyRes, State = FvmExecState, - EndOutput = EndBlockUpdate, + EndOutput = PowerUpdates, >, { // The state consists of the resolver pool, which this interpreter needs, and the rest of the @@ -429,9 +429,8 @@ where let (state, out) = self.inner.end(state).await?; // Update any component that needs to know about changes in the power table. - if !out.validators.0.is_empty() { + if !out.0.is_empty() { let power_updates = out - .validators .0 .iter() .map(|v| { @@ -550,135 +549,23 @@ fn relayed_bottom_up_ckpt_to_fvm( Ok(msg) } -fn signed_msgs_with_gas_limit(msgs: Vec) -> anyhow::Result> { - msgs.into_iter() +fn messages_selection( + msgs: Vec, + state: &FvmExecState, +) -> anyhow::Result> { + let mut signed = msgs + .into_iter() .map(|msg| match msg { - ChainMessage::Signed(inner) => { - let gas_limit = inner.message.gas_limit; - Ok((ChainMessage::Signed(inner), gas_limit)) - } + ChainMessage::Signed(inner) => Ok(inner), ChainMessage::Ipc(_) => Err(anyhow!("should not have ipc messages in user proposals")), }) - .collect::>>() -} - -/// Performs message selection: -/// - Order by gas limit in descending order -/// - Make sure total gas limit does not exceed the `total_gas_limit` parameter -fn messages_selection( - msgs: Vec, - total_gas_limit: Gas, -) -> anyhow::Result> { - let mut msgs_with_gas_limit = signed_msgs_with_gas_limit(msgs)?; - - // sort by gas limit descending - msgs_with_gas_limit.sort_by(|a, b| b.1.cmp(&a.1)); - - let mut total_gas_limit_consumed = 0; - let mut msgs = vec![]; - for (msg, gas_limit) in msgs_with_gas_limit { - if total_gas_limit_consumed + gas_limit <= total_gas_limit { - msgs.push(msg); - total_gas_limit_consumed += gas_limit; - } else { - break; - } - } - - tracing::info!( - num_msgs = msgs.len(), - total_gas_limit, - "selected message under total gas limit" - ); - - Ok(msgs) -} - -#[cfg(test)] -mod tests { - use crate::chain::messages_selection; - use fendermint_vm_message::chain::ChainMessage; - use fendermint_vm_message::signed::SignedMessage; - use quickcheck::Arbitrary; - use rand::random; - - #[test] - fn test_message_selection_partial_selected() { - let mut gen = quickcheck::Gen::new(100); - - let mut total = 0; - let msgs_len = 100; - - let messages = (0..msgs_len) - .map(|_| { - let mut msg = SignedMessage::arbitrary(&mut gen); - - msg.message.gas_limit = random::() % fvm_shared::BLOCK_GAS_LIMIT; - total += msg.message.gas_limit; + .collect::>>()?; - ChainMessage::Signed(msg) - }) - .collect::>(); - - let selected = messages_selection(messages, total / 2).unwrap(); - let selected_len = selected.len(); - let mut selected_total = 0; - let mut max = u64::MAX; - - for s in selected { - if let ChainMessage::Signed(signed) = s { - selected_total += signed.message.gas_limit; - assert!( - max >= signed.message.gas_limit, - "gas limit should be sorted descending" - ); - max = signed.message.gas_limit; - } else { - unreachable!("should be all signed messages") - } - } - - assert!(selected_total <= total / 2, "should not exceed gas limit"); - assert!(selected_len <= msgs_len, "not full selection"); + // currently only one selector, we can potentially extend to more selectors + let selectors = vec![GasLimitSelector {}]; + for s in selectors { + signed = s.select_messages(state, signed) } - #[test] - fn test_message_selection_all_selected() { - let mut gen = quickcheck::Gen::new(100); - - let mut total = 0; - let msgs_len = 100; - - let messages = (0..msgs_len) - .map(|_| { - let mut msg = SignedMessage::arbitrary(&mut gen); - - msg.message.gas_limit = random::() % fvm_shared::BLOCK_GAS_LIMIT; - total += msg.message.gas_limit; - - ChainMessage::Signed(msg) - }) - .collect::>(); - - let selected = messages_selection(messages, total).unwrap(); - let selected_len = selected.len(); - let mut selected_total = 0; - let mut max = u64::MAX; - - for s in selected { - if let ChainMessage::Signed(signed) = s { - selected_total += signed.message.gas_limit; - assert!( - max >= signed.message.gas_limit, - "gas limit should be sorted descending" - ); - max = signed.message.gas_limit; - } else { - unreachable!("should be all signed messages") - } - } - - assert_eq!(selected_total, total, "should not exceed gas limit"); - assert_eq!(selected_len, msgs_len, "not full selection"); - } + Ok(signed.into_iter().map(ChainMessage::Signed).collect()) } diff --git a/fendermint/vm/interpreter/src/fvm/cometbft.rs b/fendermint/vm/interpreter/src/fvm/cometbft.rs deleted file mode 100644 index daa733307..000000000 --- a/fendermint/vm/interpreter/src/fvm/cometbft.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::state::FvmExecState; -use crate::fvm::{FvmMessageInterpreter, PowerUpdates}; -use anyhow::Context; -use fendermint_vm_genesis::{Power, Validator}; -use fvm_ipld_blockstore::Blockstore; -use tendermint_rpc::Client; - -/// The end block update for cometbft -pub struct EndBlockUpdate { - pub consensus: Option, - pub validators: PowerUpdates, -} - -/// Potential updates to cometbft consensus parameters. Currently only block `max_gas` needs to be -/// updated, but in the future, more parameter could be updated. -pub struct ConsensusBlockUpdate { - max_gas: Option, -} - -/// Convert validator power to tendermint validator update. -/// TODO: the import is quite strange, `Validator` and `Power` are imported from `genesis` crate, -/// TODO: which should be from a `type` or `validator` crate. -pub fn to_validator_updates( - validators: Vec>, -) -> anyhow::Result> { - let mut updates = vec![]; - for v in validators { - updates.push(tendermint::validator::Update { - pub_key: tendermint::PublicKey::try_from(v.public_key)?, - power: tendermint::vote::Power::try_from(v.power.0)?, - }); - } - Ok(updates) -} - -impl TryFrom for tendermint::abci::response::EndBlock { - type Error = anyhow::Error; - - fn try_from(value: EndBlockUpdate) -> Result { - let validator_updates = to_validator_updates(value.validators.0) - .context("failed to convert validator updates")?; - - Ok(tendermint::abci::response::EndBlock { - validator_updates, - consensus_param_updates: value.consensus, - events: Vec::new(), // TODO: Events from epoch transitions? - }) - } -} - -impl FvmMessageInterpreter -where - DB: Blockstore + Clone + 'static + Send + Sync, - TC: Client + Clone + Send + Sync + 'static, -{ - pub(crate) async fn update_cometbft_consensus_params( - &self, - state: &mut FvmExecState, - end_block: &mut EndBlockUpdate, - ) -> anyhow::Result<()> { - let mut updates = ConsensusBlockUpdate::empty(); - - state.gas_market().process_consensus_update(&mut updates); - - if !updates.is_some() { - return Ok(()); - } - - let params = self - .client - .consensus_params(tendermint::block::Height::try_from(state.block_height())?) - .await? - .consensus_params; - end_block.with_consensus(updates.apply(params)); - - Ok(()) - } -} - -impl ConsensusBlockUpdate { - pub fn empty() -> Self { - Self { max_gas: None } - } - - pub fn is_some(&self) -> bool { - self.max_gas.is_some() - } - - pub fn apply(self, mut params: tendermint::consensus::Params) -> tendermint::consensus::Params { - if let Some(ref max_gas) = self.max_gas { - params.block.max_gas = *max_gas as i64; - } - params - } - - pub fn process_block_size(&mut self, block_gas_limit: u64) { - self.max_gas = Some(block_gas_limit); - } -} - -impl EndBlockUpdate { - pub fn new(power: PowerUpdates) -> Self { - Self { - validators: power, - consensus: None, - } - } - - pub fn with_consensus(&mut self, params: tendermint::consensus::Params) { - self.consensus = Some(params) - } -} diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 619b278aa..cce99378c 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -12,7 +12,6 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; -use crate::fvm::cometbft::EndBlockUpdate; use crate::fvm::gas::{GasMarket, GasUtilization}; use crate::ExecInterpreter; @@ -50,7 +49,7 @@ where /// Return validator power updates. /// Currently ignoring events as there aren't any emitted by the smart contract, /// but keep in mind that if there were, those would have to be propagated. - type EndOutput = EndBlockUpdate; + type EndOutput = PowerUpdates; async fn begin( &self, @@ -263,11 +262,6 @@ where PowerUpdates::default() }; - // process cometbft end block updates - let mut end_block = EndBlockUpdate::new(updates); - self.update_cometbft_consensus_params(&mut state, &mut end_block) - .await?; - - Ok((state, end_block)) + Ok((state, updates)) } } diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 2fdd78d36..4e576ba36 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -5,7 +5,6 @@ use crate::fvm::gas::{Available, Gas, GasMarket, GasUtilization}; use crate::fvm::FvmMessage; use anyhow::Context; -use crate::fvm::cometbft::ConsensusBlockUpdate; use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; use fendermint_crypto::PublicKey; use fendermint_vm_actor_interface::eam::EthAddress; @@ -32,10 +31,6 @@ pub struct ActorGasMarket { impl GasMarket for ActorGasMarket { type Constant = SetConstants; - fn get_constants(&self) -> anyhow::Result { - todo!() - } - fn set_constants(&mut self, constants: Self::Constant) { self.constant_update = Some(constants); } @@ -95,10 +90,8 @@ impl ActorGasMarket { }) } - pub fn process_consensus_update(&self, update: &mut ConsensusBlockUpdate) { - if let Some(ref set_constant) = self.constant_update { - update.process_block_size(set_constant.block_gas_limit); - } + pub fn take_constant_update(&mut self) -> Option { + self.constant_update.take() } pub fn commit( diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index 346f57c94..e372deb39 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -22,9 +22,6 @@ pub trait GasMarket { /// The constant parameters that determines the readings of gas market, such as block gas limit. type Constant; - #[allow(dead_code)] - fn get_constants(&self) -> anyhow::Result; - /// Update the constants of the gas market. If the gas market is actor based, then it's recommended /// to flush at EndBlock. #[allow(dead_code)] diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 1c14ab2eb..73296813e 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -14,7 +14,6 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; -pub mod cometbft; pub(crate) mod gas; pub(crate) mod topdown; diff --git a/fendermint/vm/interpreter/src/lib.rs b/fendermint/vm/interpreter/src/lib.rs index 40868b6cd..34b3e61b0 100644 --- a/fendermint/vm/interpreter/src/lib.rs +++ b/fendermint/vm/interpreter/src/lib.rs @@ -10,6 +10,7 @@ pub mod signed; #[cfg(feature = "arb")] mod arb; +mod selector; /// Prepare and process transaction proposals. #[async_trait] diff --git a/fendermint/vm/interpreter/src/selector.rs b/fendermint/vm/interpreter/src/selector.rs new file mode 100644 index 000000000..e3ab7197c --- /dev/null +++ b/fendermint/vm/interpreter/src/selector.rs @@ -0,0 +1,46 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Gas related message selection + +use crate::fvm::gas::GasMarket; +use crate::fvm::state::FvmExecState; +use fendermint_vm_message::signed::SignedMessage; +use fvm_ipld_blockstore::Blockstore; + +/// Implement this trait to perform message selection +pub trait MessageSelector { + fn select_messages( + &self, + state: &FvmExecState, + msgs: Vec, + ) -> Vec; +} + +pub(crate) struct GasLimitSelector; + +impl MessageSelector for GasLimitSelector { + fn select_messages( + &self, + state: &FvmExecState, + mut msgs: Vec, + ) -> Vec { + let total_gas_limit = state.gas_market().available().block_gas; + + // sort by gas limit descending + msgs.sort_by(|a, b| b.message.gas_limit.cmp(&a.message.gas_limit)); + + let mut total_gas_limit_consumed = 0; + let mut selected = vec![]; + for msg in msgs { + if total_gas_limit_consumed + msg.message.gas_limit <= total_gas_limit { + total_gas_limit_consumed += msg.message.gas_limit; + selected.push(msg); + } else { + break; + } + } + + selected + } +} From 7fc7df0366e758a2ec89462f7b96a8c31279dc1f Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 27 Aug 2024 17:48:02 +0800 Subject: [PATCH 059/111] rename methods --- fendermint/vm/interpreter/src/fvm/gas/actor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index 4e576ba36..cc5a988c7 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -132,7 +132,7 @@ impl ActorGasMarket { gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - self.exec_msg_implicitly(msg, executor)?; + self.apply_implicit_message(msg, executor)?; Ok(()) } @@ -159,7 +159,7 @@ impl ActorGasMarket { gas_fee_cap: Default::default(), gas_premium: Default::default(), }; - self.exec_msg_implicitly(msg, executor)?; + self.apply_implicit_message(msg, executor)?; Ok(()) } @@ -188,11 +188,11 @@ impl ActorGasMarket { gas_premium: Default::default(), }; - self.exec_msg_implicitly(msg, executor)?; + self.apply_implicit_message(msg, executor)?; Ok(()) } - fn exec_msg_implicitly( + fn apply_implicit_message( &self, msg: FvmMessage, executor: &mut E, From ae6277b4efa05af6b5670de424a41b398ddb0bcf Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 27 Aug 2024 20:47:05 +0800 Subject: [PATCH 060/111] use read only block store --- fendermint/app/src/app.rs | 29 +++++++++----------------- fendermint/vm/interpreter/src/chain.rs | 3 ++- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index c1a193ebd..6d2095267 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -414,7 +414,10 @@ where S::Namespace: Sync + Send, DB: KVWritable + KVReadable + Clone + Send + Sync + 'static, SS: Blockstore + Clone + Send + Sync + 'static, - I: ProposalInterpreter), Message = Vec>, + I: ProposalInterpreter< + State = (ChainEnv, FvmExecState>>), + Message = Vec, + >, I: ExecInterpreter< State = (ChainEnv, FvmExecState), Message = Vec, @@ -623,15 +626,9 @@ where ); let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect(); - let (state_params, block_height) = - self.state_params_at_height(request.height.value().into())?; - let state = FvmExecState::new( - self.state_store_clone(), - self.multi_engine.as_ref(), - block_height as ChainEpoch, - state_params, - ) - .context("error creating new state")?; + let state = self + .new_read_only_exec_state()? + .ok_or_else(|| anyhow!("exec state should be present"))?; let txs = self .interpreter @@ -668,15 +665,9 @@ where let size_txs = txs.iter().map(|tx| tx.len()).sum::(); let num_txs = txs.len(); - let (state_params, block_height) = - self.state_params_at_height(request.height.value().into())?; - let state = FvmExecState::new( - self.state_store_clone(), - self.multi_engine.as_ref(), - block_height as ChainEpoch, - state_params, - ) - .context("error creating new state")?; + let state = self + .new_read_only_exec_state()? + .ok_or_else(|| anyhow!("exec state should be present"))?; let accept = self .interpreter diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 67b84bb76..1d63bef78 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::fvm::gas::GasMarket; use crate::fvm::state::ipc::GatewayCaller; +use crate::fvm::store::ReadOnlyBlockstore; use crate::fvm::{topdown, FvmApplyRet, PowerUpdates}; use crate::selector::{GasLimitSelector, MessageSelector}; use crate::{ @@ -104,7 +105,7 @@ where DB: Blockstore + Clone + 'static + Send + Sync, I: Sync + Send, { - type State = (ChainEnv, FvmExecState); + type State = (ChainEnv, FvmExecState>>); type Message = ChainMessage; /// Check whether there are any "ready" messages in the IPLD resolution mempool which can be appended to the proposal. From 46f8e2b640a5d299286370990753ec8ac9250743 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 11 Sep 2024 17:46:00 +0800 Subject: [PATCH 061/111] add more tests --- Cargo.lock | 1 + fendermint/testing/contract-test/Cargo.toml | 2 + fendermint/testing/contract-test/src/lib.rs | 32 ++- .../testing/contract-test/tests/gas_market.rs | 258 ++++++++++++++++++ .../vm/interpreter/src/fvm/gas/actor.rs | 13 +- fendermint/vm/interpreter/src/fvm/mod.rs | 2 +- 6 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 fendermint/testing/contract-test/tests/gas_market.rs diff --git a/Cargo.lock b/Cargo.lock index 16aab6317..1388824c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2986,6 +2986,7 @@ dependencies = [ "bytes", "cid", "ethers", + "fendermint_actor_gas_market", "fendermint_crypto", "fendermint_rpc", "fendermint_testing", diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 109e5fae1..643bc2653 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -44,3 +44,5 @@ lazy_static = { workspace = true } bytes = { workspace = true } fvm_ipld_encoding = { workspace = true } multihash = { workspace = true } +fvm = { workspace = true, features = ["testing"] } +fendermint_actor_gas_market = { path = "../../actors/gas_market" } \ No newline at end of file diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index 4ed529025..e5a0eeac2 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -20,6 +20,7 @@ use fendermint_vm_interpreter::{ ExecInterpreter, }; use fvm::engine::MultiEngine; +use fendermint_crypto::PublicKey; pub mod ipc; @@ -96,7 +97,7 @@ where } /// Take the execution state, update it, put it back, return the output. - async fn modify_exec_state(&self, f: F) -> anyhow::Result + pub async fn modify_exec_state(&self, f: F) -> anyhow::Result where F: FnOnce(FvmExecState) -> R, R: Future, T)>>, @@ -125,6 +126,10 @@ where } pub async fn begin_block(&self, block_height: ChainEpoch) -> Result<()> { + self.begin_block_with_validator(block_height, None).await + } + + pub async fn begin_block_with_validator(&self, block_height: ChainEpoch, maybe_validator: Option) -> Result<()> { let mut block_hash: [u8; 32] = [0; 32]; let _ = block_hash.as_mut().write_i64::(block_height); @@ -132,9 +137,12 @@ where 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) + let mut state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) .context("error creating new state")? .with_block_hash(block_hash); + if let Some(validator) = maybe_validator { + state = state.with_validator(validator); + } self.put_exec_state(state).await; @@ -146,6 +154,26 @@ where Ok(()) } + + pub async fn execute_msgs(&self, msgs: Vec) -> Result<()> { + let _ret = self + .modify_exec_state(|mut s| async { + for msg in msgs { + let (a, out) = self.interpreter.deliver(s, msg).await?; + if let Some(e) = out.apply_ret.failure_info { + println!("failed: {}", e); + return Err(anyhow!("err in msg deliver")); + } + s = a; + } + Ok((s, ())) + }) + .await + .context("execute msgs failed")?; + + Ok(()) + } + pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> { let _ret = self .modify_exec_state(|s| self.interpreter.end(s)) diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs new file mode 100644 index 000000000..0e0be5260 --- /dev/null +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -0,0 +1,258 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +mod staking; + +use anyhow::Context; +use async_trait::async_trait; +use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; +use fendermint_contract_test::Tester; +use fendermint_crypto::{PublicKey, SecretKey}; +use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; +use fendermint_vm_actor_interface::system; +use fendermint_vm_core::Timestamp; +use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; +use fendermint_vm_interpreter::fvm::gas::GasMarket; +use fendermint_vm_interpreter::fvm::state::FvmExecState; +use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; +use fendermint_vm_interpreter::fvm::upgrades::UpgradeScheduler; +use fendermint_vm_interpreter::fvm::FvmMessageInterpreter; +use fvm_shared::address::Address; +use fvm_shared::bigint::Zero; +use fvm_shared::clock::ChainEpoch; +use fvm_shared::econ::TokenAmount; +use fvm_shared::message::Message; +use fvm_shared::version::NetworkVersion; +use lazy_static::lazy_static; +use rand::rngs::StdRng; +use rand::SeedableRng; +use tendermint_rpc::Client; +use fendermint_vm_actor_interface::eam::EthAddress; + +lazy_static! { + static ref ADDR: Address = + Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); + static ref ADDR2: Address = + Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); +} +const CHAIN_NAME: &str = "mychain"; +type I = 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)) +} + +/// Creates a default tester with validator public key +async fn default_tester() -> (Tester, PublicKey) { + let validator = my_secret_key().public_key(); + let upgrade_scheduler = UpgradeScheduler::new(); + + let interpreter: FvmMessageInterpreter = + FvmMessageInterpreter::new(NeverCallClient, None, 1.05, 1.05, false, upgrade_scheduler); + + 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_whole(100), + }, + Actor { + meta: ActorMeta::Account(Account { + owner: SignerAddr(*ADDR2), + }), + balance: TokenAmount::from_whole(10), + }, + ], + eam_permission_mode: PermissionMode::Unrestricted, + ipc: None, + }; + (Tester::new(interpreter, genesis).await.unwrap(), validator) +} + +#[tokio::test] +async fn test_gas_market_base_fee_oscillation() { + let (mut tester, _) = default_tester().await; + + let num_msgs = 10; + let total_gas_limit = 6178630; + let base_gas_limit = total_gas_limit / num_msgs; + + let mut gas_constants = SetConstants::default(); + gas_constants.block_gas_limit = total_gas_limit; + + let messages = (0..num_msgs) + .map(|i| Message { + version: 0, + from: ADDR.clone(), + to: Address::new_id(10), + sequence: i, + value: TokenAmount::from_atto(1), + method_num: 0, + params: Default::default(), + gas_limit: base_gas_limit, + gas_fee_cap: Default::default(), + gas_premium: TokenAmount::from_atto(1), + }) + .collect::>(); + + // iterate over all the upgrades + let height = 1; + tester.begin_block(height).await.unwrap(); + tester + .modify_exec_state(|mut state| async { + state.gas_market_mut().set_constants(gas_constants); + Ok((state, ())) + }) + .await + .unwrap(); + tester.end_block(height).await.unwrap(); + tester.commit().await.unwrap(); + + let height = 2; + tester.begin_block(height).await.unwrap(); + let before_reading = tester + .modify_exec_state(|mut state| async { + let reading = current_reading(&mut state, height)?; + Ok((state, reading)) + }) + .await + .unwrap(); + tester.execute_msgs(messages).await.unwrap(); + tester.end_block(height).await.unwrap(); + tester.commit().await.unwrap(); + + let height = 3; + tester.begin_block(height).await.unwrap(); + let post_full_block_reading = tester + .modify_exec_state(|mut state| async { + let reading = current_reading(&mut state, height)?; + Ok((state, reading)) + }) + .await + .unwrap(); + tester.end_block(height).await.unwrap(); + tester.commit().await.unwrap(); + assert!( + before_reading.base_fee < post_full_block_reading.base_fee, + "base fee should have increased" + ); + + let height = 4; + tester.begin_block(height).await.unwrap(); + let post_empty_block_reading = tester + .modify_exec_state(|mut state| async { + let reading = current_reading(&mut state, height)?; + Ok((state, reading)) + }) + .await + .unwrap(); + tester.end_block(height).await.unwrap(); + tester.commit().await.unwrap(); + assert!( + post_empty_block_reading.base_fee < post_full_block_reading.base_fee, + "base fee should have decreased" + ); +} + +#[tokio::test] +async fn test_gas_market_premium_distribution() { + let (mut tester, validator) = default_tester().await; + let evm_address = Address::from(EthAddress::new_secp256k1(&validator.serialize()).unwrap()); + + let num_msgs = 10; + let total_gas_limit = 62306300; + let premium = 1; + let base_gas_limit = total_gas_limit / num_msgs; + + let messages = (0..num_msgs) + .map(|i| Message { + version: 0, + from: ADDR.clone(), + to: ADDR2.clone(), + sequence: i, + value: TokenAmount::from_atto(1), + method_num: 0, + params: Default::default(), + gas_limit: base_gas_limit, + gas_fee_cap: TokenAmount::from_atto(base_gas_limit), + gas_premium: TokenAmount::from_atto(premium), + }) + .collect::>(); + + // iterate over all the upgrades + let height = 1; + tester.begin_block_with_validator(height, Some(validator)).await.unwrap(); + let initial_balance = tester + .modify_exec_state(|state| async { + let tree = state.state_tree(); + let balance = tree.get_actor_by_address(&evm_address)?.map(|v| v.balance).unwrap_or(TokenAmount::zero()); + Ok((state, balance)) + }) + .await + .unwrap(); + assert_eq!(initial_balance, TokenAmount::zero()); + + tester.execute_msgs(messages).await.unwrap(); + tester.end_block(height).await.unwrap(); + let final_balance = tester + .modify_exec_state(|state| async { + let tree = state.state_tree(); + let balance = tree.get_actor_by_address(&evm_address)?.map(|v| v.balance).unwrap_or(TokenAmount::zero()); + Ok((state, balance)) + }) + .await + .unwrap(); + tester.commit().await.unwrap(); + + assert!(final_balance > initial_balance, "validator balance should have increased") +} + +pub fn current_reading( + state: &mut FvmExecState, + block_height: ChainEpoch, +) -> anyhow::Result { + let msg = Message { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, + sequence: block_height as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, + params: fvm_ipld_encoding::RawBytes::default(), + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + let (apply_ret, _) = state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to read gas market state: {}", err); + } + + let r = fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse gas market readying")?; + Ok(r) +} + +#[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/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index cc5a988c7..e0da5b38c 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -53,10 +53,10 @@ impl GasMarket for ActorGasMarket { } impl ActorGasMarket { - pub fn create( + pub fn current_reading( executor: &mut E, block_height: ChainEpoch, - ) -> anyhow::Result { + ) -> anyhow::Result { let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, to: GAS_MARKET_ACTOR_ADDR, @@ -78,10 +78,17 @@ impl ActorGasMarket { anyhow::bail!("failed to read gas market state: {}", err); } - let reading = + let r = fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) .context("failed to parse gas market readying")?; + Ok(r) + } + pub fn create( + executor: &mut E, + block_height: ChainEpoch, + ) -> anyhow::Result { + let reading = Self::current_reading(executor, block_height)?; Ok(Self { gas_premium: TokenAmount::from_atto(0), block_gas_limit: reading.block_gas_limit, diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 73296813e..c45d8039c 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -14,7 +14,7 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; -pub(crate) mod gas; +pub mod gas; pub(crate) mod topdown; pub use check::FvmCheckRet; From f681494e589e799324cbbf8dca1c09023ff9ab07 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 11 Sep 2024 18:06:18 +0800 Subject: [PATCH 062/111] fmt --- fendermint/testing/contract-test/src/lib.rs | 22 ++++++++++--------- .../testing/contract-test/tests/gas_market.rs | 22 ++++++++++++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index e5a0eeac2..cd8e1ded9 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -7,6 +7,7 @@ use fendermint_vm_core::Timestamp; use fvm_shared::clock::ChainEpoch; use std::{future::Future, sync::Arc}; +use fendermint_crypto::PublicKey; use fendermint_vm_genesis::Genesis; use fendermint_vm_interpreter::fvm::PowerUpdates; use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; @@ -20,7 +21,6 @@ use fendermint_vm_interpreter::{ ExecInterpreter, }; use fvm::engine::MultiEngine; -use fendermint_crypto::PublicKey; pub mod ipc; @@ -129,7 +129,11 @@ where self.begin_block_with_validator(block_height, None).await } - pub async fn begin_block_with_validator(&self, block_height: ChainEpoch, maybe_validator: Option) -> Result<()> { + pub async fn begin_block_with_validator( + &self, + block_height: ChainEpoch, + maybe_validator: Option, + ) -> Result<()> { let mut block_hash: [u8; 32] = [0; 32]; let _ = block_hash.as_mut().write_i64::(block_height); @@ -137,9 +141,10 @@ where let mut state_params = self.state_params.clone(); state_params.timestamp = Timestamp(block_height as u64); - let mut state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) - .context("error creating new state")? - .with_block_hash(block_hash); + let mut state = + FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) + .context("error creating new state")? + .with_block_hash(block_hash); if let Some(validator) = maybe_validator { state = state.with_validator(validator); } @@ -154,9 +159,8 @@ where Ok(()) } - pub async fn execute_msgs(&self, msgs: Vec) -> Result<()> { - let _ret = self + self .modify_exec_state(|mut s| async { for msg in msgs { let (a, out) = self.interpreter.deliver(s, msg).await?; @@ -169,9 +173,7 @@ where Ok((s, ())) }) .await - .context("execute msgs failed")?; - - Ok(()) + .context("execute msgs failed") } pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> { diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs index 0e0be5260..8070a8f52 100644 --- a/fendermint/testing/contract-test/tests/gas_market.rs +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -8,6 +8,7 @@ use async_trait::async_trait; use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; use fendermint_contract_test::Tester; use fendermint_crypto::{PublicKey, SecretKey}; +use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; use fendermint_vm_actor_interface::system; use fendermint_vm_core::Timestamp; @@ -27,7 +28,6 @@ use lazy_static::lazy_static; use rand::rngs::StdRng; use rand::SeedableRng; use tendermint_rpc::Client; -use fendermint_vm_actor_interface::eam::EthAddress; lazy_static! { static ref ADDR: Address = @@ -190,11 +190,17 @@ async fn test_gas_market_premium_distribution() { // iterate over all the upgrades let height = 1; - tester.begin_block_with_validator(height, Some(validator)).await.unwrap(); + tester + .begin_block_with_validator(height, Some(validator)) + .await + .unwrap(); let initial_balance = tester .modify_exec_state(|state| async { let tree = state.state_tree(); - let balance = tree.get_actor_by_address(&evm_address)?.map(|v| v.balance).unwrap_or(TokenAmount::zero()); + let balance = tree + .get_actor_by_address(&evm_address)? + .map(|v| v.balance) + .unwrap_or(TokenAmount::zero()); Ok((state, balance)) }) .await @@ -206,14 +212,20 @@ async fn test_gas_market_premium_distribution() { let final_balance = tester .modify_exec_state(|state| async { let tree = state.state_tree(); - let balance = tree.get_actor_by_address(&evm_address)?.map(|v| v.balance).unwrap_or(TokenAmount::zero()); + let balance = tree + .get_actor_by_address(&evm_address)? + .map(|v| v.balance) + .unwrap_or(TokenAmount::zero()); Ok((state, balance)) }) .await .unwrap(); tester.commit().await.unwrap(); - assert!(final_balance > initial_balance, "validator balance should have increased") + assert!( + final_balance > initial_balance, + "validator balance should have increased" + ) } pub fn current_reading( From 706e44e0c7e484b32ccab2c97870aeebe7a4cde2 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 12 Sep 2024 10:37:26 +0800 Subject: [PATCH 063/111] base fee rotation --- fendermint/actors/gas_market/src/lib.rs | 11 ++++++-- fendermint/testing/contract-test/src/lib.rs | 25 +++++++++-------- .../vm/interpreter/src/fvm/gas/actor.rs | 27 ++++++++++++------- fendermint/vm/interpreter/src/fvm/gas/mod.rs | 4 +++ .../vm/interpreter/src/fvm/state/exec.rs | 7 +++-- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs index cbabdc202..d9b316d58 100644 --- a/fendermint/actors/gas_market/src/lib.rs +++ b/fendermint/actors/gas_market/src/lib.rs @@ -53,6 +53,11 @@ pub struct BlockGasUtilization { pub block_gas_used: Gas, } +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct BlockGasUtilizationRet { + pub base_fee: TokenAmount, +} + pub struct EIP1559GasMarketActor {} #[derive(FromPrimitive)] @@ -108,12 +113,14 @@ impl EIP1559GasMarketActor { fn update_utilization( rt: &impl Runtime, utilization: BlockGasUtilization, - ) -> Result<(), ActorError> { + ) -> Result { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; rt.transaction(|st: &mut EIP1559GasState, _rt| { st.base_fee = st.next_base_fee(utilization.block_gas_used); - Ok(()) + Ok(BlockGasUtilizationRet { + base_fee: st.base_fee.clone(), + }) }) } } diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index cd8e1ded9..fb9d5236c 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -160,20 +160,19 @@ where } pub async fn execute_msgs(&self, msgs: Vec) -> Result<()> { - self - .modify_exec_state(|mut s| async { - for msg in msgs { - let (a, out) = self.interpreter.deliver(s, msg).await?; - if let Some(e) = out.apply_ret.failure_info { - println!("failed: {}", e); - return Err(anyhow!("err in msg deliver")); - } - s = a; + self.modify_exec_state(|mut s| async { + for msg in msgs { + let (a, out) = self.interpreter.deliver(s, msg).await?; + if let Some(e) = out.apply_ret.failure_info { + println!("failed: {}", e); + return Err(anyhow!("err in msg deliver")); } - Ok((s, ())) - }) - .await - .context("execute msgs failed") + s = a; + } + Ok((s, ())) + }) + .await + .context("execute msgs failed") } pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> { diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs index e0da5b38c..6efe35588 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/actor.rs @@ -1,11 +1,11 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::gas::{Available, Gas, GasMarket, GasUtilization}; +use crate::fvm::gas::{Available, CommitRet, Gas, GasMarket, GasUtilization}; use crate::fvm::FvmMessage; -use anyhow::Context; +use anyhow::{anyhow, Context}; -use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; +use fendermint_actor_gas_market::{BlockGasUtilizationRet, GasMarketReading, SetConstants}; use fendermint_crypto::PublicKey; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; @@ -18,6 +18,8 @@ use fvm_shared::METHOD_SEND; #[derive(Default)] pub struct ActorGasMarket { + /// The base fee for fvm + base_fee: TokenAmount, /// The total gas premium for the miner gas_premium: TokenAmount, /// The block gas limit @@ -90,6 +92,7 @@ impl ActorGasMarket { ) -> anyhow::Result { let reading = Self::current_reading(executor, block_height)?; Ok(Self { + base_fee: reading.base_fee, gas_premium: TokenAmount::from_atto(0), block_gas_limit: reading.block_gas_limit, block_gas_used: 0, @@ -106,10 +109,10 @@ impl ActorGasMarket { executor: &mut E, block_height: ChainEpoch, validator: Option, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { + self.distribute_reward(executor, block_height, validator)?; self.commit_constants(executor, block_height)?; - self.commit_utilization(executor, block_height)?; - self.distribute_reward(executor, block_height, validator) + self.commit_utilization(executor, block_height) } fn distribute_reward( @@ -175,7 +178,7 @@ impl ActorGasMarket { &self, executor: &mut E, block_height: ChainEpoch, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { let block_gas_used = self.block_gas_used.min(self.block_gas_limit); let params = fvm_ipld_encoding::RawBytes::serialize( fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, @@ -195,8 +198,14 @@ impl ActorGasMarket { gas_premium: Default::default(), }; - self.apply_implicit_message(msg, executor)?; - Ok(()) + let apply_ret = self.apply_implicit_message(msg, executor)?; + let r = fvm_ipld_encoding::from_slice::( + &apply_ret.msg_receipt.return_data, + ) + .context("failed to parse gas utilization result")?; + Ok(CommitRet { + base_fee: r.base_fee, + }) } fn apply_implicit_message( diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs index e372deb39..3d189276f 100644 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/gas/mod.rs @@ -12,6 +12,10 @@ pub struct Available { pub block_gas: Gas, } +pub struct CommitRet { + pub base_fee: TokenAmount, +} + pub struct GasUtilization { gas_used: Gas, gas_premium: TokenAmount, diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 10497ce20..ea439486c 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -300,8 +300,11 @@ where pub fn update_gas_market(&mut self) -> anyhow::Result<()> { let height = self.block_height(); - self.gas_market - .commit(&mut self.executor, height, self.validator_pubkey) + let ret = self + .gas_market + .commit(&mut self.executor, height, self.validator_pubkey)?; + self.params.base_fee = ret.base_fee; + Ok(()) } /// Update the circulating supply, effective from the next block. From 4ff965b9e55ae7f2fd2b88492bbecadc5abb71ad Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 12 Sep 2024 14:21:31 +0800 Subject: [PATCH 064/111] more tests --- fendermint/testing/contract-test/src/lib.rs | 25 ++++--- .../testing/contract-test/tests/gas_market.rs | 67 ++++++++++++++++++- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index cd8e1ded9..fb9d5236c 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -160,20 +160,19 @@ where } pub async fn execute_msgs(&self, msgs: Vec) -> Result<()> { - self - .modify_exec_state(|mut s| async { - for msg in msgs { - let (a, out) = self.interpreter.deliver(s, msg).await?; - if let Some(e) = out.apply_ret.failure_info { - println!("failed: {}", e); - return Err(anyhow!("err in msg deliver")); - } - s = a; + self.modify_exec_state(|mut s| async { + for msg in msgs { + let (a, out) = self.interpreter.deliver(s, msg).await?; + if let Some(e) = out.apply_ret.failure_info { + println!("failed: {}", e); + return Err(anyhow!("err in msg deliver")); } - Ok((s, ())) - }) - .await - .context("execute msgs failed") + s = a; + } + Ok((s, ())) + }) + .await + .context("execute msgs failed") } pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> { diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs index 8070a8f52..5695d08eb 100644 --- a/fendermint/testing/contract-test/tests/gas_market.rs +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -16,7 +16,7 @@ use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, use fendermint_vm_interpreter::fvm::gas::GasMarket; use fendermint_vm_interpreter::fvm::state::FvmExecState; use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; -use fendermint_vm_interpreter::fvm::upgrades::UpgradeScheduler; +use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; use fendermint_vm_interpreter::fvm::FvmMessageInterpreter; use fvm_shared::address::Address; use fvm_shared::bigint::Zero; @@ -45,8 +45,14 @@ fn my_secret_key() -> SecretKey { /// Creates a default tester with validator public key async fn default_tester() -> (Tester, PublicKey) { + tester_with_upgrader(UpgradeScheduler::new()).await +} + +/// Creates a default tester with validator public key +async fn tester_with_upgrader( + upgrade_scheduler: UpgradeScheduler, +) -> (Tester, PublicKey) { let validator = my_secret_key().public_key(); - let upgrade_scheduler = UpgradeScheduler::new(); let interpreter: FvmMessageInterpreter = FvmMessageInterpreter::new(NeverCallClient, None, 1.05, 1.05, false, upgrade_scheduler); @@ -228,6 +234,63 @@ async fn test_gas_market_premium_distribution() { ) } +#[tokio::test] +async fn test_gas_market_upgrade() { + let mut upgrader = UpgradeScheduler::new(); + + let total_gas_limit = 100; + upgrader + .add( + Upgrade::new(CHAIN_NAME, 1, Some(1), |state| { + println!( + "[Upgrade at height {}] Update gas market params", + state.block_height() + ); + + let mut gas_constants = SetConstants::default(); + gas_constants.block_gas_limit = 100; + + state.gas_market_mut().set_constants(gas_constants); + + Ok(()) + }) + .unwrap(), + ) + .unwrap(); + + let (mut tester, _) = tester_with_upgrader(upgrader).await; + + let height = 1; + tester.begin_block(height).await.unwrap(); + let reading = tester + .modify_exec_state(|mut state| async { + let reading = current_reading(&mut state, height)?; + Ok((state, reading)) + }) + .await + .unwrap(); + assert_ne!( + reading.block_gas_limit, total_gas_limit, + "gas limit should not equal at start" + ); + tester.end_block(height).await.unwrap(); + tester.commit().await.unwrap(); + + let height = 2; + tester.begin_block(height).await.unwrap(); + let reading = tester + .modify_exec_state(|mut state| async { + let reading = current_reading(&mut state, height)?; + Ok((state, reading)) + }) + .await + .unwrap(); + assert_eq!( + reading.block_gas_limit, total_gas_limit, + "gas limit should equal after upgrade" + ); +} + pub fn current_reading( state: &mut FvmExecState, block_height: ChainEpoch, From 22f8b515dbd6984689d815ddc847b44156f36f31 Mon Sep 17 00:00:00 2001 From: raulk Date: Wed, 2 Oct 2024 17:14:23 +0100 Subject: [PATCH 065/111] prototype the validator rewards feature. --- .../gateway/router/CheckpointingFacet.sol | 9 ++++++- .../interfaces/IValidatorRewarder.sol | 18 +++++++++++++ .../contracts/lib/LibSubnetActorStorage.sol | 19 ++++++++++++++ contracts/contracts/structs/CrossNet.sol | 25 +++++++++++++++++++ .../subnet/SubnetActorCheckpointingFacet.sol | 4 +++ .../subnet/SubnetActorRewardFacet.sol | 12 +++++++++ .../vm/interpreter/src/fvm/checkpoint.rs | 7 ++++++ ipc/api/src/checkpoint.rs | 1 + ipc/cli/src/commands/checkpoint/relayer.rs | 7 ++++++ 9 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/interfaces/IValidatorRewarder.sol diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index a49780cc8..1b55a015a 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol"; -import {BottomUpCheckpoint} from "../../structs/CrossNet.sol"; +import {BottomUpCheckpoint, ActivitySummary, ActivitySummaryCommitted} from "../../structs/CrossNet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; import {LibQuorum} from "../../lib/LibQuorum.sol"; import {Subnet} from "../../structs/Subnet.sol"; @@ -49,6 +49,7 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @param membershipWeight - the total weight of the membership function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, + // TODO(rewarder) ActivitySummary calldata summary, bytes32 membershipRootHash, uint256 membershipWeight ) external systemActorOnly { @@ -56,6 +57,9 @@ contract CheckpointingFacet is GatewayActorModifiers { revert CheckpointAlreadyExists(); } + // TODO(rewarder): compute the commitment to the summary and set it in the checkpoint. + // Collect summaries to relay and put them in the checkpoint. Reset the pending summaries map. + LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, objHeight: checkpoint.blockHeight, @@ -64,6 +68,9 @@ contract CheckpointingFacet is GatewayActorModifiers { membershipWeight: membershipWeight, majorityPercentage: s.majorityPercentage }); + + // TODO(rewarder): emit an ActivitySummaryCommittedevent so relayers can pick it up. + LibGateway.storeBottomUpCheckpoint(checkpoint); } diff --git a/contracts/contracts/interfaces/IValidatorRewarder.sol b/contracts/contracts/interfaces/IValidatorRewarder.sol new file mode 100644 index 000000000..6001dfb22 --- /dev/null +++ b/contracts/contracts/interfaces/IValidatorRewarder.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; + +/// @title ValidatorRewarder interface. +/// +/// @dev Implement this interface and supply the address of the implementation contract at subnet creation to process +/// subnet summaries at this level, and disburse rewards to validators based on their block production activity. +/// +/// This interface will be called by the subnet actor when a relayer presents a +interface IValidatorRewarder { + /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and + /// disburse any relevant rewards. + /// The + /// @dev This method should revert if the summary is invalid; this will cause the + function disburseRewards(SubnetID memory id, ActivitySummary memory summary) external; +} diff --git a/contracts/contracts/lib/LibSubnetActorStorage.sol b/contracts/contracts/lib/LibSubnetActorStorage.sol index eaf800ca5..474f84c0f 100644 --- a/contracts/contracts/lib/LibSubnetActorStorage.sol +++ b/contracts/contracts/lib/LibSubnetActorStorage.sol @@ -65,6 +65,25 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet address[] genesisBalanceKeys; /// @notice The validator gater, if address(0), no validator gating is performed address validatorGater; + + /// BEGIN Validator Rewards. + + /// @notice The validator rewarder. + /// If address(0), this subnet does not process activity summaries, but instead forwards them to the parent + /// network via bottom-up checkpoints. + /// If address(0), and this is the root network, summaries are discarded (> /dev/null). + /// TODO(rewarder): set this address correctly from the constructor. + address validatorRewarder; + /// @notice Summaries pending to be processed. + /// If the validator rewarder is non-zero, these denote summaries presentable at this level. + /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. + /// Partitioned by subnet ID, in the sequence they must be presented. + /// TODO(rewarder): optimize this pair of data structures. + mapping(SubnetID => bytes32[]) pendingSummaries; + /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. + /// Only used if the validator rewarder is non-zero. + /// TODO(rewarder): optimize this pair of data structures. + mapping(bytes32 => SubnetID) presentableSummaries; } library LibSubnetActorStorage { diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 368554b60..8c96fb1b9 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -29,6 +29,31 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; + /// @dev A commitment to the summary of our chain activity since the previous checkpoint and this one. + bytes32 summary; + /// @dev Summaries relayed upwards from descendants of this subnet. + /// NOTE: Not merkelized to keep it simple, but we will merkelize later to scale better. + RelayedSummary[] relayedSummaries; +} + +struct ActivitySummary { + /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. + uint256[2] blockRange; + /// @dev The validators whose activity we're reporting about. + address[] validators; + /// @dev The number of blocks committed by each validator in the position they appear in the validators array. + /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. + uint64[] blocksCommitted; +} + +event ActivitySummaryCommitted(bytes32 indexed commitment, ActivitySummary summary); + +struct RelayedSummary { + /// @dev The subnet IDs whose activity is being relayed. + SubnetID subnet; + /// @dev The commitment to the summary, so it can be presented later by the relayer. + /// A blake2b hash of the summary generated by abi.encode'ing the ActivitySummary and hashing it via the Eth precompile. + bytes32 commitment; } /// @notice A batch of bottom-up messages for execution. diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index 667433d8b..84aefa146 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -42,6 +42,10 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, s.lastBottomUpCheckpointHeight = checkpoint.blockHeight; + // TODO(rewarder): if we have a non-zero validator rewarder at this level, queue the commitment for processing in storage (add to pending and presentable summaries). + // If we have a zero validator rewarder at this level, and we are the L1, discard the incoming commitments. + // If we have a zero validator rewarder at this level, and we are not the L1, relay the commitments upwards (add to pending summaries). + // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); diff --git a/contracts/contracts/subnet/SubnetActorRewardFacet.sol b/contracts/contracts/subnet/SubnetActorRewardFacet.sol index 1b8b84b7d..4862e352b 100644 --- a/contracts/contracts/subnet/SubnetActorRewardFacet.sol +++ b/contracts/contracts/subnet/SubnetActorRewardFacet.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; +import {ActivitySummary} from "../structs/CrossNet.sol"; import {QuorumObjKind} from "../structs/Quorum.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; @@ -13,6 +14,17 @@ import {Asset} from "../structs/Subnet.sol"; contract SubnetActorRewardFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using AssetHelper for Asset; + // TODO(rewards): add this function so that relayers can submit summaries to process reward payouts in the root network. + function submitSummary(SubnetID subnetId, ActivitySummary memory summary) external nonReentrant whenNotPaused { + // TODO(rewards): + // 1. Check that the subnet is active. + // 2. Check that the subnet has a non-zero ValidatorRewarder. + // 3. Hash the activity summary to get the commitment. + // 4. Validate that the commitment is pending and presentable, and validate that it matches the expected subnet. + // 5. Send the summary to the ValidatorRewarder#disburseRewards. + // 6. If OK (not reverted), drop the summary from the pending and presentable commitments. + } + /// @notice Validator claims their released collateral. function claim() external nonReentrant whenNotPaused { uint256 amount = LibStaking.claimCollateral(msg.sender); diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index ab9b2c0d5..19890678d 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -96,6 +96,13 @@ where let num_msgs = msgs.len(); + // TODO(rewards): query block producers for the blocks from the last checkpointed epoch to the current one. + // Ideally keep a live cache of block producers, append to it when new blocks are committed, and prune it when generating a checkpoint. + // But for now, we can try to keep it simple and query CometBFT, although that adds latency. + // If we do this, this method seems to be the quickest way: https://docs.cometbft.com/main/rpc/#/Info/block_search + + // TODO(rewards): populate the ActivitySummary struct with the information above, and pass it to the create_bottom_up_checkpoint call. + // Construct checkpoint. let checkpoint = BottomUpCheckpoint { subnet_id, diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 88f41f891..0d07d701a 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -94,6 +94,7 @@ pub struct BottomUpCheckpoint { pub next_configuration_number: u64, /// The list of messages for execution pub msgs: Vec, + // TODO(rewards): add new fields and data types for summaries and commitments. } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/cli/src/commands/checkpoint/relayer.rs b/ipc/cli/src/commands/checkpoint/relayer.rs index 12a121880..2db1122a8 100644 --- a/ipc/cli/src/commands/checkpoint/relayer.rs +++ b/ipc/cli/src/commands/checkpoint/relayer.rs @@ -32,6 +32,13 @@ impl CommandLineHandler for BottomUpRelayer { async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { log::debug!("start bottom up relayer with args: {:?}", arguments); + // TODO(rewards): enable the relayer to watch multiple subnets at once. + + // TODO(rewards): add a new flag --process-summaries to activate processing summaries on all subnets. + // Enabling this mode makes the relayer watch for ActivitySummaryCommitted events, and stores the summaries in a database. + // It then tracks which summaries have been committed to the root (right now we only support submitting to the L1), to chase + // after those and present them via SubnetActor#submitSummary in order to trigger reward payout. + // Prometheus metrics match &arguments.metrics_address { Some(addr) => { From 5b8c2da4c7256b060c958a2bb8271addab011145 Mon Sep 17 00:00:00 2001 From: raulk Date: Fri, 4 Oct 2024 16:58:25 +0100 Subject: [PATCH 066/111] fix Cargo.lock. --- Cargo.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5b4988e2..1388824c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8807,12 +8807,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - [[package]] name = "snow" version = "0.9.6" From 37b6cd3287a76163ed196e674368e82070bed03e Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 7 Oct 2024 18:07:10 +0800 Subject: [PATCH 067/111] skeleton implementation --- Cargo.lock | 20 ++ .../GatewayActorModifiers.json | 340 +++++++++--------- .../.storage-layouts/GatewayDiamond.json | 338 ++++++++--------- .../.storage-layouts/SubnetActorDiamond.json | 278 +++++++------- .../SubnetActorModifiers.json | 280 +++++++-------- contracts/binding/src/lib.rs | 61 ---- contracts/contracts/errors/IPCErrors.sol | 4 + .../gateway/router/CheckpointingFacet.sol | 6 + .../contracts/lib/LibGatewayActorStorage.sol | 17 + .../contracts/lib/LibSubnetActorStorage.sol | 19 - .../IValidatorRewarder.sol | 4 +- .../reward/ValidatorActivityTracker.sol | 75 ++++ .../contracts/reward/ValidatorReward.sol | 54 +++ .../reward/ValidatorRewardParentFacet.sol | 145 ++++++++ contracts/contracts/structs/CrossNet.sol | 8 +- .../subnet/SubnetActorRewardFacet.sol | 11 - contracts/test/IntegrationTestBase.sol | 6 +- contracts/test/helpers/SelectorLibrary.sol | 4 +- .../test/integration/GatewayDiamond.t.sol | 47 ++- .../integration/GatewayDiamondToken.t.sol | 9 +- contracts/test/integration/MultiSubnet.t.sol | 8 +- .../test/integration/SubnetActorDiamond.t.sol | 51 ++- .../vm/interpreter/src/fvm/checkpoint.rs | 3 + fendermint/vm/interpreter/src/fvm/exec.rs | 2 + fendermint/vm/reward/Cargo.toml | 21 ++ fendermint/vm/reward/src/lib.rs | 32 ++ 26 files changed, 1089 insertions(+), 754 deletions(-) rename contracts/contracts/{interfaces => reward}/IValidatorRewarder.sol (81%) create mode 100644 contracts/contracts/reward/ValidatorActivityTracker.sol create mode 100644 contracts/contracts/reward/ValidatorReward.sol create mode 100644 contracts/contracts/reward/ValidatorRewardParentFacet.sol create mode 100644 fendermint/vm/reward/Cargo.toml create mode 100644 fendermint/vm/reward/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f50cd9396..1c4886caf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3472,6 +3472,26 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "fendermint_vm_validater_reward" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "cid", + "ethers", + "fendermint_crypto", + "fendermint_tracing", + "fendermint_vm_event", + "fvm_ipld_encoding", + "fvm_shared", + "hex", + "ipc-observability", + "serde", + "thiserror", + "tracing", +] + [[package]] name = "ff" version = "0.12.1" diff --git a/contracts/.storage-layouts/GatewayActorModifiers.json b/contracts/.storage-layouts/GatewayActorModifiers.json index c1987575a..b2f59a894 100644 --- a/contracts/.storage-layouts/GatewayActorModifiers.json +++ b/contracts/.storage-layouts/GatewayActorModifiers.json @@ -1,12 +1,12 @@ { "storage": [ { - "astId": 11979, + "astId": 11959, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "s", "offset": 0, "slot": "0", - "type": "t_struct(GatewayActorStorage)11965_storage" + "type": "t_struct(GatewayActorStorage)11945_storage" } ], "types": { @@ -27,14 +27,14 @@ "label": "bytes32[]", "numberOfBytes": "32" }, - "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage": { - "base": "t_struct(IpcEnvelope)18702_storage", + "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage": { + "base": "t_struct(IpcEnvelope)19408_storage", "encoding": "dynamic_array", "label": "struct IpcEnvelope[]", "numberOfBytes": "32" }, - "t_array(t_struct(Validator)18966_storage)dyn_storage": { - "base": "t_struct(Validator)18966_storage", + "t_array(t_struct(Validator)19672_storage)dyn_storage": { + "base": "t_struct(Validator)19672_storage", "encoding": "dynamic_array", "label": "struct Validator[]", "numberOfBytes": "32" @@ -54,22 +54,22 @@ "label": "bytes", "numberOfBytes": "32" }, - "t_enum(IpcMsgKind)18679": { + "t_enum(IpcMsgKind)19385": { "encoding": "inplace", "label": "enum IpcMsgKind", "numberOfBytes": "1" }, - "t_enum(PermissionMode)18912": { + "t_enum(PermissionMode)19618": { "encoding": "inplace", "label": "enum PermissionMode", "numberOfBytes": "1" }, - "t_enum(QuorumObjKind)18748": { + "t_enum(QuorumObjKind)19454": { "encoding": "inplace", "label": "enum QuorumObjKind", "numberOfBytes": "1" }, - "t_enum(StakingOperation)18835": { + "t_enum(StakingOperation)19541": { "encoding": "inplace", "label": "enum StakingOperation", "numberOfBytes": "1" @@ -81,12 +81,12 @@ "numberOfBytes": "32", "value": "t_bytes_storage" }, - "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)": { + "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct ValidatorInfo)", "numberOfBytes": "32", - "value": "t_struct(ValidatorInfo)18907_storage" + "value": "t_struct(ValidatorInfo)19613_storage" }, "t_mapping(t_address,t_uint16)": { "encoding": "mapping", @@ -95,19 +95,19 @@ "numberOfBytes": "32", "value": "t_uint16" }, - "t_mapping(t_bytes32,t_struct(IpcEnvelope)18702_storage)": { + "t_mapping(t_bytes32,t_struct(IpcEnvelope)19408_storage)": { "encoding": "mapping", "key": "t_bytes32", "label": "mapping(bytes32 => struct IpcEnvelope)", "numberOfBytes": "32", - "value": "t_struct(IpcEnvelope)18702_storage" + "value": "t_struct(IpcEnvelope)19408_storage" }, - "t_mapping(t_bytes32,t_struct(Subnet)18829_storage)": { + "t_mapping(t_bytes32,t_struct(Subnet)19535_storage)": { "encoding": "mapping", "key": "t_bytes32", "label": "mapping(bytes32 => struct Subnet)", "numberOfBytes": "32", - "value": "t_struct(Subnet)18829_storage" + "value": "t_struct(Subnet)19535_storage" }, "t_mapping(t_bytes32,t_uint256)": { "encoding": "mapping", @@ -137,40 +137,40 @@ "numberOfBytes": "32", "value": "t_struct(AddressSet)3459_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpCheckpoint)", "numberOfBytes": "32", - "value": "t_struct(BottomUpCheckpoint)18654_storage" + "value": "t_struct(BottomUpCheckpoint)19331_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)18668_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)19374_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpMsgBatch)", "numberOfBytes": "32", - "value": "t_struct(BottomUpMsgBatch)18668_storage" + "value": "t_struct(BottomUpMsgBatch)19374_storage" }, - "t_mapping(t_uint256,t_struct(ParentFinality)18634_storage)": { + "t_mapping(t_uint256,t_struct(ParentFinality)19311_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct ParentFinality)", "numberOfBytes": "32", - "value": "t_struct(ParentFinality)18634_storage" + "value": "t_struct(ParentFinality)19311_storage" }, - "t_mapping(t_uint256,t_struct(QuorumInfo)18765_storage)": { + "t_mapping(t_uint256,t_struct(QuorumInfo)19471_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct QuorumInfo)", "numberOfBytes": "32", - "value": "t_struct(QuorumInfo)18765_storage" + "value": "t_struct(QuorumInfo)19471_storage" }, - "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)": { + "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)": { "encoding": "mapping", "key": "t_uint64", "label": "mapping(uint64 => struct StakingChange)", "numberOfBytes": "32", - "value": "t_struct(StakingChange)18844_storage" + "value": "t_struct(StakingChange)19550_storage" }, "t_struct(AddressSet)3459_storage": { "encoding": "inplace", @@ -187,20 +187,20 @@ ], "numberOfBytes": "64" }, - "t_struct(BottomUpCheckpoint)18654_storage": { + "t_struct(BottomUpCheckpoint)19331_storage": { "encoding": "inplace", "label": "struct BottomUpCheckpoint", "members": [ { - "astId": 18639, + "astId": 19316, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18642, + "astId": 19319, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "blockHeight", "offset": 0, @@ -208,7 +208,7 @@ "type": "t_uint256" }, { - "astId": 18645, + "astId": 19322, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "blockHash", "offset": 0, @@ -216,7 +216,7 @@ "type": "t_bytes32" }, { - "astId": 18648, + "astId": 19325, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "nextConfigurationNumber", "offset": 0, @@ -224,30 +224,30 @@ "type": "t_uint64" }, { - "astId": 18653, + "astId": 19330, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "msgs", "offset": 0, "slot": "5", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "192" }, - "t_struct(BottomUpMsgBatch)18668_storage": { + "t_struct(BottomUpMsgBatch)19374_storage": { "encoding": "inplace", "label": "struct BottomUpMsgBatch", "members": [ { - "astId": 18659, + "astId": 19365, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18662, + "astId": 19368, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "blockHeight", "offset": 0, @@ -255,12 +255,12 @@ "type": "t_uint256" }, { - "astId": 18667, + "astId": 19373, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "msgs", "offset": 0, "slot": "3", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "128" @@ -280,12 +280,12 @@ ], "numberOfBytes": "64" }, - "t_struct(FvmAddress)18733_storage": { + "t_struct(FvmAddress)19439_storage": { "encoding": "inplace", "label": "struct FvmAddress", "members": [ { - "astId": 18730, + "astId": 19436, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "addrType", "offset": 0, @@ -293,7 +293,7 @@ "type": "t_uint8" }, { - "astId": 18732, + "astId": 19438, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "payload", "offset": 0, @@ -303,12 +303,12 @@ ], "numberOfBytes": "64" }, - "t_struct(GatewayActorStorage)11965_storage": { + "t_struct(GatewayActorStorage)11945_storage": { "encoding": "inplace", "label": "struct GatewayActorStorage", "members": [ { - "astId": 11877, + "astId": 11857, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "latestParentHeight", "offset": 0, @@ -316,7 +316,7 @@ "type": "t_uint256" }, { - "astId": 11880, + "astId": 11860, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "bottomUpCheckPeriod", "offset": 0, @@ -324,7 +324,7 @@ "type": "t_uint256" }, { - "astId": 11883, + "astId": 11863, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "bottomUpMsgBatchPeriod", "offset": 0, @@ -332,7 +332,7 @@ "type": "t_uint256" }, { - "astId": 11886, + "astId": 11866, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "bottomUpNonce", "offset": 0, @@ -340,7 +340,7 @@ "type": "t_uint64" }, { - "astId": 11889, + "astId": 11869, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "appliedTopDownNonce", "offset": 8, @@ -348,7 +348,7 @@ "type": "t_uint64" }, { - "astId": 11892, + "astId": 11872, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "totalSubnets", "offset": 16, @@ -356,7 +356,7 @@ "type": "t_uint64" }, { - "astId": 11895, + "astId": 11875, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "maxMsgsPerBottomUpBatch", "offset": 24, @@ -364,7 +364,7 @@ "type": "t_uint64" }, { - "astId": 11898, + "astId": 11878, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "majorityPercentage", "offset": 0, @@ -372,7 +372,7 @@ "type": "t_uint8" }, { - "astId": 11901, + "astId": 11881, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "commitSha", "offset": 0, @@ -380,7 +380,7 @@ "type": "t_bytes32" }, { - "astId": 11904, + "astId": 11884, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "maxTreeDepth", "offset": 0, @@ -388,7 +388,7 @@ "type": "t_uint8" }, { - "astId": 11907, + "astId": 11887, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "generalPurposeCrossMsg", "offset": 1, @@ -396,7 +396,7 @@ "type": "t_bool" }, { - "astId": 11910, + "astId": 11890, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "multiLevelCrossMsg", "offset": 2, @@ -404,87 +404,87 @@ "type": "t_bool" }, { - "astId": 11914, + "astId": 11894, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "currentMembership", "offset": 0, "slot": "7", - "type": "t_struct(Membership)18974_storage" + "type": "t_struct(Membership)19680_storage" }, { - "astId": 11918, + "astId": 11898, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "lastMembership", "offset": 0, "slot": "9", - "type": "t_struct(Membership)18974_storage" + "type": "t_struct(Membership)19680_storage" }, { - "astId": 11922, + "astId": 11902, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "checkpointQuorumMap", "offset": 0, "slot": "11", - "type": "t_struct(QuorumMap)18797_storage" + "type": "t_struct(QuorumMap)19503_storage" }, { - "astId": 11926, + "astId": 11906, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "networkName", "offset": 0, "slot": "18", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 11930, + "astId": 11910, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "validatorsTracker", "offset": 0, "slot": "20", - "type": "t_struct(ParentValidatorsTracker)18950_storage" + "type": "t_struct(ParentValidatorsTracker)19656_storage" }, { - "astId": 11936, + "astId": 11916, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "subnets", "offset": 0, "slot": "31", - "type": "t_mapping(t_bytes32,t_struct(Subnet)18829_storage)" + "type": "t_mapping(t_bytes32,t_struct(Subnet)19535_storage)" }, { - "astId": 11942, + "astId": 11922, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "finalitiesMap", "offset": 0, "slot": "32", - "type": "t_mapping(t_uint256,t_struct(ParentFinality)18634_storage)" + "type": "t_mapping(t_uint256,t_struct(ParentFinality)19311_storage)" }, { - "astId": 11948, + "astId": 11928, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "postbox", "offset": 0, "slot": "33", - "type": "t_mapping(t_bytes32,t_struct(IpcEnvelope)18702_storage)" + "type": "t_mapping(t_bytes32,t_struct(IpcEnvelope)19408_storage)" }, { - "astId": 11954, + "astId": 11934, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "bottomUpCheckpoints", "offset": 0, "slot": "34", - "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)" }, { - "astId": 11960, + "astId": 11940, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "bottomUpMsgBatches", "offset": 0, "slot": "35", - "type": "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)18668_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)19374_storage)" }, { - "astId": 11964, + "astId": 11944, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "subnetKeys", "offset": 0, @@ -494,59 +494,59 @@ ], "numberOfBytes": "1216" }, - "t_struct(IPCAddress)18958_storage": { + "t_struct(IPCAddress)19664_storage": { "encoding": "inplace", "label": "struct IPCAddress", "members": [ { - "astId": 18954, + "astId": 19660, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "subnetId", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18957, + "astId": 19663, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "rawAddress", "offset": 0, "slot": "2", - "type": "t_struct(FvmAddress)18733_storage" + "type": "t_struct(FvmAddress)19439_storage" } ], "numberOfBytes": "128" }, - "t_struct(IpcEnvelope)18702_storage": { + "t_struct(IpcEnvelope)19408_storage": { "encoding": "inplace", "label": "struct IpcEnvelope", "members": [ { - "astId": 18684, + "astId": 19390, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(IpcMsgKind)18679" + "type": "t_enum(IpcMsgKind)19385" }, { - "astId": 18688, + "astId": 19394, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "to", "offset": 0, "slot": "1", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18692, + "astId": 19398, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "from", "offset": 0, "slot": "5", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18695, + "astId": 19401, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "nonce", "offset": 0, @@ -554,7 +554,7 @@ "type": "t_uint64" }, { - "astId": 18698, + "astId": 19404, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "value", "offset": 0, @@ -562,7 +562,7 @@ "type": "t_uint256" }, { - "astId": 18701, + "astId": 19407, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "message", "offset": 0, @@ -572,35 +572,35 @@ ], "numberOfBytes": "384" }, - "t_struct(MaxPQ)17125_storage": { + "t_struct(MaxPQ)17052_storage": { "encoding": "inplace", "label": "struct MaxPQ", "members": [ { - "astId": 17124, + "astId": 17051, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(Membership)18974_storage": { + "t_struct(Membership)19680_storage": { "encoding": "inplace", "label": "struct Membership", "members": [ { - "astId": 18971, + "astId": 19677, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "validators", "offset": 0, "slot": "0", - "type": "t_array(t_struct(Validator)18966_storage)dyn_storage" + "type": "t_array(t_struct(Validator)19672_storage)dyn_storage" }, { - "astId": 18973, + "astId": 19679, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "configurationNumber", "offset": 0, @@ -610,27 +610,27 @@ ], "numberOfBytes": "64" }, - "t_struct(MinPQ)17743_storage": { + "t_struct(MinPQ)17670_storage": { "encoding": "inplace", "label": "struct MinPQ", "members": [ { - "astId": 17742, + "astId": 17669, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(PQ)18373_storage": { + "t_struct(PQ)18300_storage": { "encoding": "inplace", "label": "struct PQ", "members": [ { - "astId": 18362, + "astId": 18289, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "size", "offset": 0, @@ -638,7 +638,7 @@ "type": "t_uint16" }, { - "astId": 18367, + "astId": 18294, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "addressToPos", "offset": 0, @@ -646,7 +646,7 @@ "type": "t_mapping(t_address,t_uint16)" }, { - "astId": 18372, + "astId": 18299, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "posToAddress", "offset": 0, @@ -656,12 +656,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ParentFinality)18634_storage": { + "t_struct(ParentFinality)19311_storage": { "encoding": "inplace", "label": "struct ParentFinality", "members": [ { - "astId": 18631, + "astId": 19308, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "height", "offset": 0, @@ -669,7 +669,7 @@ "type": "t_uint256" }, { - "astId": 18633, + "astId": 19310, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "blockHash", "offset": 0, @@ -679,35 +679,35 @@ ], "numberOfBytes": "64" }, - "t_struct(ParentValidatorsTracker)18950_storage": { + "t_struct(ParentValidatorsTracker)19656_storage": { "encoding": "inplace", "label": "struct ParentValidatorsTracker", "members": [ { - "astId": 18946, + "astId": 19652, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "validators", "offset": 0, "slot": "0", - "type": "t_struct(ValidatorSet)18942_storage" + "type": "t_struct(ValidatorSet)19648_storage" }, { - "astId": 18949, + "astId": 19655, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "changes", "offset": 0, "slot": "9", - "type": "t_struct(StakingChangeLog)18865_storage" + "type": "t_struct(StakingChangeLog)19571_storage" } ], "numberOfBytes": "352" }, - "t_struct(QuorumInfo)18765_storage": { + "t_struct(QuorumInfo)19471_storage": { "encoding": "inplace", "label": "struct QuorumInfo", "members": [ { - "astId": 18752, + "astId": 19458, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "hash", "offset": 0, @@ -715,7 +715,7 @@ "type": "t_bytes32" }, { - "astId": 18755, + "astId": 19461, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "rootHash", "offset": 0, @@ -723,7 +723,7 @@ "type": "t_bytes32" }, { - "astId": 18758, + "astId": 19464, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "threshold", "offset": 0, @@ -731,7 +731,7 @@ "type": "t_uint256" }, { - "astId": 18761, + "astId": 19467, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "currentWeight", "offset": 0, @@ -739,7 +739,7 @@ "type": "t_uint256" }, { - "astId": 18764, + "astId": 19470, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "reached", "offset": 0, @@ -749,20 +749,20 @@ ], "numberOfBytes": "160" }, - "t_struct(QuorumMap)18797_storage": { + "t_struct(QuorumMap)19503_storage": { "encoding": "inplace", "label": "struct QuorumMap", "members": [ { - "astId": 18770, + "astId": 19476, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "quorumObjKind", "offset": 0, "slot": "0", - "type": "t_enum(QuorumObjKind)18748" + "type": "t_enum(QuorumObjKind)19454" }, { - "astId": 18773, + "astId": 19479, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "retentionHeight", "offset": 0, @@ -770,15 +770,15 @@ "type": "t_uint256" }, { - "astId": 18779, + "astId": 19485, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "quorumInfo", "offset": 0, "slot": "2", - "type": "t_mapping(t_uint256,t_struct(QuorumInfo)18765_storage)" + "type": "t_mapping(t_uint256,t_struct(QuorumInfo)19471_storage)" }, { - "astId": 18783, + "astId": 19489, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "incompleteQuorums", "offset": 0, @@ -786,7 +786,7 @@ "type": "t_struct(UintSet)3616_storage" }, { - "astId": 18789, + "astId": 19495, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "quorumSignatureSenders", "offset": 0, @@ -794,7 +794,7 @@ "type": "t_mapping(t_uint256,t_struct(AddressSet)3459_storage)" }, { - "astId": 18796, + "astId": 19502, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "quorumSignatures", "offset": 0, @@ -827,20 +827,20 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingChange)18844_storage": { + "t_struct(StakingChange)19550_storage": { "encoding": "inplace", "label": "struct StakingChange", "members": [ { - "astId": 18839, + "astId": 19545, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "op", "offset": 0, "slot": "0", - "type": "t_enum(StakingOperation)18835" + "type": "t_enum(StakingOperation)19541" }, { - "astId": 18841, + "astId": 19547, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "payload", "offset": 0, @@ -848,7 +848,7 @@ "type": "t_bytes_storage" }, { - "astId": 18843, + "astId": 19549, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "validator", "offset": 0, @@ -858,12 +858,12 @@ ], "numberOfBytes": "96" }, - "t_struct(StakingChangeLog)18865_storage": { + "t_struct(StakingChangeLog)19571_storage": { "encoding": "inplace", "label": "struct StakingChangeLog", "members": [ { - "astId": 18855, + "astId": 19561, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "nextConfigurationNumber", "offset": 0, @@ -871,7 +871,7 @@ "type": "t_uint64" }, { - "astId": 18858, + "astId": 19564, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "startConfigurationNumber", "offset": 8, @@ -879,22 +879,22 @@ "type": "t_uint64" }, { - "astId": 18864, + "astId": 19570, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "changes", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)" + "type": "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)" } ], "numberOfBytes": "64" }, - "t_struct(Subnet)18829_storage": { + "t_struct(Subnet)19535_storage": { "encoding": "inplace", "label": "struct Subnet", "members": [ { - "astId": 18817, + "astId": 19523, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "stake", "offset": 0, @@ -902,7 +902,7 @@ "type": "t_uint256" }, { - "astId": 18819, + "astId": 19525, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "genesisEpoch", "offset": 0, @@ -910,7 +910,7 @@ "type": "t_uint256" }, { - "astId": 18821, + "astId": 19527, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "circSupply", "offset": 0, @@ -918,7 +918,7 @@ "type": "t_uint256" }, { - "astId": 18823, + "astId": 19529, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "topDownNonce", "offset": 0, @@ -926,7 +926,7 @@ "type": "t_uint64" }, { - "astId": 18825, + "astId": 19531, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "appliedBottomUpNonce", "offset": 8, @@ -934,22 +934,22 @@ "type": "t_uint64" }, { - "astId": 18828, + "astId": 19534, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "id", "offset": 0, "slot": "4", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" } ], "numberOfBytes": "192" }, - "t_struct(SubnetID)18814_storage": { + "t_struct(SubnetID)19520_storage": { "encoding": "inplace", "label": "struct SubnetID", "members": [ { - "astId": 18809, + "astId": 19515, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "root", "offset": 0, @@ -957,7 +957,7 @@ "type": "t_uint64" }, { - "astId": 18813, + "astId": 19519, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "route", "offset": 0, @@ -982,12 +982,12 @@ ], "numberOfBytes": "64" }, - "t_struct(Validator)18966_storage": { + "t_struct(Validator)19672_storage": { "encoding": "inplace", "label": "struct Validator", "members": [ { - "astId": 18961, + "astId": 19667, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "weight", "offset": 0, @@ -995,7 +995,7 @@ "type": "t_uint256" }, { - "astId": 18963, + "astId": 19669, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "addr", "offset": 0, @@ -1003,7 +1003,7 @@ "type": "t_address" }, { - "astId": 18965, + "astId": 19671, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "metadata", "offset": 0, @@ -1013,12 +1013,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ValidatorInfo)18907_storage": { + "t_struct(ValidatorInfo)19613_storage": { "encoding": "inplace", "label": "struct ValidatorInfo", "members": [ { - "astId": 18899, + "astId": 19605, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "federatedPower", "offset": 0, @@ -1026,7 +1026,7 @@ "type": "t_uint256" }, { - "astId": 18901, + "astId": 19607, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "confirmedCollateral", "offset": 0, @@ -1034,7 +1034,7 @@ "type": "t_uint256" }, { - "astId": 18903, + "astId": 19609, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "totalCollateral", "offset": 0, @@ -1042,7 +1042,7 @@ "type": "t_uint256" }, { - "astId": 18906, + "astId": 19612, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "metadata", "offset": 0, @@ -1052,20 +1052,20 @@ ], "numberOfBytes": "128" }, - "t_struct(ValidatorSet)18942_storage": { + "t_struct(ValidatorSet)19648_storage": { "encoding": "inplace", "label": "struct ValidatorSet", "members": [ { - "astId": 18921, + "astId": 19627, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "permissionMode", "offset": 0, "slot": "0", - "type": "t_enum(PermissionMode)18912" + "type": "t_enum(PermissionMode)19618" }, { - "astId": 18924, + "astId": 19630, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "activeLimit", "offset": 1, @@ -1073,7 +1073,7 @@ "type": "t_uint16" }, { - "astId": 18927, + "astId": 19633, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "totalConfirmedCollateral", "offset": 0, @@ -1081,28 +1081,28 @@ "type": "t_uint256" }, { - "astId": 18933, + "astId": 19639, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "validators", "offset": 0, "slot": "2", - "type": "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)" + "type": "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)" }, { - "astId": 18937, + "astId": 19643, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "activeValidators", "offset": 0, "slot": "3", - "type": "t_struct(MinPQ)17743_storage" + "type": "t_struct(MinPQ)17670_storage" }, { - "astId": 18941, + "astId": 19647, "contract": "contracts/lib/LibGatewayActorStorage.sol:GatewayActorModifiers", "label": "waitingValidators", "offset": 0, "slot": "6", - "type": "t_struct(MaxPQ)17125_storage" + "type": "t_struct(MaxPQ)17052_storage" } ], "numberOfBytes": "288" diff --git a/contracts/.storage-layouts/GatewayDiamond.json b/contracts/.storage-layouts/GatewayDiamond.json index 8d68e6165..a40c2ec87 100644 --- a/contracts/.storage-layouts/GatewayDiamond.json +++ b/contracts/.storage-layouts/GatewayDiamond.json @@ -6,7 +6,7 @@ "label": "s", "offset": 0, "slot": "0", - "type": "t_struct(GatewayActorStorage)11965_storage" + "type": "t_struct(GatewayActorStorage)11945_storage" } ], "types": { @@ -27,14 +27,14 @@ "label": "bytes32[]", "numberOfBytes": "32" }, - "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage": { - "base": "t_struct(IpcEnvelope)18702_storage", + "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage": { + "base": "t_struct(IpcEnvelope)19408_storage", "encoding": "dynamic_array", "label": "struct IpcEnvelope[]", "numberOfBytes": "32" }, - "t_array(t_struct(Validator)18966_storage)dyn_storage": { - "base": "t_struct(Validator)18966_storage", + "t_array(t_struct(Validator)19672_storage)dyn_storage": { + "base": "t_struct(Validator)19672_storage", "encoding": "dynamic_array", "label": "struct Validator[]", "numberOfBytes": "32" @@ -54,22 +54,22 @@ "label": "bytes", "numberOfBytes": "32" }, - "t_enum(IpcMsgKind)18679": { + "t_enum(IpcMsgKind)19385": { "encoding": "inplace", "label": "enum IpcMsgKind", "numberOfBytes": "1" }, - "t_enum(PermissionMode)18912": { + "t_enum(PermissionMode)19618": { "encoding": "inplace", "label": "enum PermissionMode", "numberOfBytes": "1" }, - "t_enum(QuorumObjKind)18748": { + "t_enum(QuorumObjKind)19454": { "encoding": "inplace", "label": "enum QuorumObjKind", "numberOfBytes": "1" }, - "t_enum(StakingOperation)18835": { + "t_enum(StakingOperation)19541": { "encoding": "inplace", "label": "enum StakingOperation", "numberOfBytes": "1" @@ -81,12 +81,12 @@ "numberOfBytes": "32", "value": "t_bytes_storage" }, - "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)": { + "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct ValidatorInfo)", "numberOfBytes": "32", - "value": "t_struct(ValidatorInfo)18907_storage" + "value": "t_struct(ValidatorInfo)19613_storage" }, "t_mapping(t_address,t_uint16)": { "encoding": "mapping", @@ -95,19 +95,19 @@ "numberOfBytes": "32", "value": "t_uint16" }, - "t_mapping(t_bytes32,t_struct(IpcEnvelope)18702_storage)": { + "t_mapping(t_bytes32,t_struct(IpcEnvelope)19408_storage)": { "encoding": "mapping", "key": "t_bytes32", "label": "mapping(bytes32 => struct IpcEnvelope)", "numberOfBytes": "32", - "value": "t_struct(IpcEnvelope)18702_storage" + "value": "t_struct(IpcEnvelope)19408_storage" }, - "t_mapping(t_bytes32,t_struct(Subnet)18829_storage)": { + "t_mapping(t_bytes32,t_struct(Subnet)19535_storage)": { "encoding": "mapping", "key": "t_bytes32", "label": "mapping(bytes32 => struct Subnet)", "numberOfBytes": "32", - "value": "t_struct(Subnet)18829_storage" + "value": "t_struct(Subnet)19535_storage" }, "t_mapping(t_bytes32,t_uint256)": { "encoding": "mapping", @@ -137,40 +137,40 @@ "numberOfBytes": "32", "value": "t_struct(AddressSet)3459_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpCheckpoint)", "numberOfBytes": "32", - "value": "t_struct(BottomUpCheckpoint)18654_storage" + "value": "t_struct(BottomUpCheckpoint)19331_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)18668_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)19374_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpMsgBatch)", "numberOfBytes": "32", - "value": "t_struct(BottomUpMsgBatch)18668_storage" + "value": "t_struct(BottomUpMsgBatch)19374_storage" }, - "t_mapping(t_uint256,t_struct(ParentFinality)18634_storage)": { + "t_mapping(t_uint256,t_struct(ParentFinality)19311_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct ParentFinality)", "numberOfBytes": "32", - "value": "t_struct(ParentFinality)18634_storage" + "value": "t_struct(ParentFinality)19311_storage" }, - "t_mapping(t_uint256,t_struct(QuorumInfo)18765_storage)": { + "t_mapping(t_uint256,t_struct(QuorumInfo)19471_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct QuorumInfo)", "numberOfBytes": "32", - "value": "t_struct(QuorumInfo)18765_storage" + "value": "t_struct(QuorumInfo)19471_storage" }, - "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)": { + "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)": { "encoding": "mapping", "key": "t_uint64", "label": "mapping(uint64 => struct StakingChange)", "numberOfBytes": "32", - "value": "t_struct(StakingChange)18844_storage" + "value": "t_struct(StakingChange)19550_storage" }, "t_struct(AddressSet)3459_storage": { "encoding": "inplace", @@ -187,20 +187,20 @@ ], "numberOfBytes": "64" }, - "t_struct(BottomUpCheckpoint)18654_storage": { + "t_struct(BottomUpCheckpoint)19331_storage": { "encoding": "inplace", "label": "struct BottomUpCheckpoint", "members": [ { - "astId": 18639, + "astId": 19316, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18642, + "astId": 19319, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "blockHeight", "offset": 0, @@ -208,7 +208,7 @@ "type": "t_uint256" }, { - "astId": 18645, + "astId": 19322, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "blockHash", "offset": 0, @@ -216,7 +216,7 @@ "type": "t_bytes32" }, { - "astId": 18648, + "astId": 19325, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "nextConfigurationNumber", "offset": 0, @@ -224,30 +224,30 @@ "type": "t_uint64" }, { - "astId": 18653, + "astId": 19330, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "msgs", "offset": 0, "slot": "5", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "192" }, - "t_struct(BottomUpMsgBatch)18668_storage": { + "t_struct(BottomUpMsgBatch)19374_storage": { "encoding": "inplace", "label": "struct BottomUpMsgBatch", "members": [ { - "astId": 18659, + "astId": 19365, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18662, + "astId": 19368, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "blockHeight", "offset": 0, @@ -255,12 +255,12 @@ "type": "t_uint256" }, { - "astId": 18667, + "astId": 19373, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "msgs", "offset": 0, "slot": "3", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "128" @@ -280,12 +280,12 @@ ], "numberOfBytes": "64" }, - "t_struct(FvmAddress)18733_storage": { + "t_struct(FvmAddress)19439_storage": { "encoding": "inplace", "label": "struct FvmAddress", "members": [ { - "astId": 18730, + "astId": 19436, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "addrType", "offset": 0, @@ -293,7 +293,7 @@ "type": "t_uint8" }, { - "astId": 18732, + "astId": 19438, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "payload", "offset": 0, @@ -303,12 +303,12 @@ ], "numberOfBytes": "64" }, - "t_struct(GatewayActorStorage)11965_storage": { + "t_struct(GatewayActorStorage)11945_storage": { "encoding": "inplace", "label": "struct GatewayActorStorage", "members": [ { - "astId": 11877, + "astId": 11857, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "latestParentHeight", "offset": 0, @@ -316,7 +316,7 @@ "type": "t_uint256" }, { - "astId": 11880, + "astId": 11860, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "bottomUpCheckPeriod", "offset": 0, @@ -324,7 +324,7 @@ "type": "t_uint256" }, { - "astId": 11883, + "astId": 11863, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "bottomUpMsgBatchPeriod", "offset": 0, @@ -332,7 +332,7 @@ "type": "t_uint256" }, { - "astId": 11886, + "astId": 11866, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "bottomUpNonce", "offset": 0, @@ -340,7 +340,7 @@ "type": "t_uint64" }, { - "astId": 11889, + "astId": 11869, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "appliedTopDownNonce", "offset": 8, @@ -348,7 +348,7 @@ "type": "t_uint64" }, { - "astId": 11892, + "astId": 11872, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "totalSubnets", "offset": 16, @@ -356,7 +356,7 @@ "type": "t_uint64" }, { - "astId": 11895, + "astId": 11875, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "maxMsgsPerBottomUpBatch", "offset": 24, @@ -364,7 +364,7 @@ "type": "t_uint64" }, { - "astId": 11898, + "astId": 11878, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "majorityPercentage", "offset": 0, @@ -372,7 +372,7 @@ "type": "t_uint8" }, { - "astId": 11901, + "astId": 11881, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "commitSha", "offset": 0, @@ -380,7 +380,7 @@ "type": "t_bytes32" }, { - "astId": 11904, + "astId": 11884, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "maxTreeDepth", "offset": 0, @@ -388,7 +388,7 @@ "type": "t_uint8" }, { - "astId": 11907, + "astId": 11887, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "generalPurposeCrossMsg", "offset": 1, @@ -396,7 +396,7 @@ "type": "t_bool" }, { - "astId": 11910, + "astId": 11890, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "multiLevelCrossMsg", "offset": 2, @@ -404,87 +404,87 @@ "type": "t_bool" }, { - "astId": 11914, + "astId": 11894, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "currentMembership", "offset": 0, "slot": "7", - "type": "t_struct(Membership)18974_storage" + "type": "t_struct(Membership)19680_storage" }, { - "astId": 11918, + "astId": 11898, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "lastMembership", "offset": 0, "slot": "9", - "type": "t_struct(Membership)18974_storage" + "type": "t_struct(Membership)19680_storage" }, { - "astId": 11922, + "astId": 11902, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "checkpointQuorumMap", "offset": 0, "slot": "11", - "type": "t_struct(QuorumMap)18797_storage" + "type": "t_struct(QuorumMap)19503_storage" }, { - "astId": 11926, + "astId": 11906, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "networkName", "offset": 0, "slot": "18", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 11930, + "astId": 11910, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "validatorsTracker", "offset": 0, "slot": "20", - "type": "t_struct(ParentValidatorsTracker)18950_storage" + "type": "t_struct(ParentValidatorsTracker)19656_storage" }, { - "astId": 11936, + "astId": 11916, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "subnets", "offset": 0, "slot": "31", - "type": "t_mapping(t_bytes32,t_struct(Subnet)18829_storage)" + "type": "t_mapping(t_bytes32,t_struct(Subnet)19535_storage)" }, { - "astId": 11942, + "astId": 11922, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "finalitiesMap", "offset": 0, "slot": "32", - "type": "t_mapping(t_uint256,t_struct(ParentFinality)18634_storage)" + "type": "t_mapping(t_uint256,t_struct(ParentFinality)19311_storage)" }, { - "astId": 11948, + "astId": 11928, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "postbox", "offset": 0, "slot": "33", - "type": "t_mapping(t_bytes32,t_struct(IpcEnvelope)18702_storage)" + "type": "t_mapping(t_bytes32,t_struct(IpcEnvelope)19408_storage)" }, { - "astId": 11954, + "astId": 11934, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "bottomUpCheckpoints", "offset": 0, "slot": "34", - "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)" }, { - "astId": 11960, + "astId": 11940, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "bottomUpMsgBatches", "offset": 0, "slot": "35", - "type": "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)18668_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpMsgBatch)19374_storage)" }, { - "astId": 11964, + "astId": 11944, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "subnetKeys", "offset": 0, @@ -494,59 +494,59 @@ ], "numberOfBytes": "1216" }, - "t_struct(IPCAddress)18958_storage": { + "t_struct(IPCAddress)19664_storage": { "encoding": "inplace", "label": "struct IPCAddress", "members": [ { - "astId": 18954, + "astId": 19660, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "subnetId", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18957, + "astId": 19663, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "rawAddress", "offset": 0, "slot": "2", - "type": "t_struct(FvmAddress)18733_storage" + "type": "t_struct(FvmAddress)19439_storage" } ], "numberOfBytes": "128" }, - "t_struct(IpcEnvelope)18702_storage": { + "t_struct(IpcEnvelope)19408_storage": { "encoding": "inplace", "label": "struct IpcEnvelope", "members": [ { - "astId": 18684, + "astId": 19390, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(IpcMsgKind)18679" + "type": "t_enum(IpcMsgKind)19385" }, { - "astId": 18688, + "astId": 19394, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "to", "offset": 0, "slot": "1", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18692, + "astId": 19398, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "from", "offset": 0, "slot": "5", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18695, + "astId": 19401, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "nonce", "offset": 0, @@ -554,7 +554,7 @@ "type": "t_uint64" }, { - "astId": 18698, + "astId": 19404, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "value", "offset": 0, @@ -562,7 +562,7 @@ "type": "t_uint256" }, { - "astId": 18701, + "astId": 19407, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "message", "offset": 0, @@ -572,35 +572,35 @@ ], "numberOfBytes": "384" }, - "t_struct(MaxPQ)17125_storage": { + "t_struct(MaxPQ)17052_storage": { "encoding": "inplace", "label": "struct MaxPQ", "members": [ { - "astId": 17124, + "astId": 17051, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(Membership)18974_storage": { + "t_struct(Membership)19680_storage": { "encoding": "inplace", "label": "struct Membership", "members": [ { - "astId": 18971, + "astId": 19677, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "validators", "offset": 0, "slot": "0", - "type": "t_array(t_struct(Validator)18966_storage)dyn_storage" + "type": "t_array(t_struct(Validator)19672_storage)dyn_storage" }, { - "astId": 18973, + "astId": 19679, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "configurationNumber", "offset": 0, @@ -610,27 +610,27 @@ ], "numberOfBytes": "64" }, - "t_struct(MinPQ)17743_storage": { + "t_struct(MinPQ)17670_storage": { "encoding": "inplace", "label": "struct MinPQ", "members": [ { - "astId": 17742, + "astId": 17669, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(PQ)18373_storage": { + "t_struct(PQ)18300_storage": { "encoding": "inplace", "label": "struct PQ", "members": [ { - "astId": 18362, + "astId": 18289, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "size", "offset": 0, @@ -638,7 +638,7 @@ "type": "t_uint16" }, { - "astId": 18367, + "astId": 18294, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "addressToPos", "offset": 0, @@ -646,7 +646,7 @@ "type": "t_mapping(t_address,t_uint16)" }, { - "astId": 18372, + "astId": 18299, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "posToAddress", "offset": 0, @@ -656,12 +656,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ParentFinality)18634_storage": { + "t_struct(ParentFinality)19311_storage": { "encoding": "inplace", "label": "struct ParentFinality", "members": [ { - "astId": 18631, + "astId": 19308, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "height", "offset": 0, @@ -669,7 +669,7 @@ "type": "t_uint256" }, { - "astId": 18633, + "astId": 19310, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "blockHash", "offset": 0, @@ -679,35 +679,35 @@ ], "numberOfBytes": "64" }, - "t_struct(ParentValidatorsTracker)18950_storage": { + "t_struct(ParentValidatorsTracker)19656_storage": { "encoding": "inplace", "label": "struct ParentValidatorsTracker", "members": [ { - "astId": 18946, + "astId": 19652, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "validators", "offset": 0, "slot": "0", - "type": "t_struct(ValidatorSet)18942_storage" + "type": "t_struct(ValidatorSet)19648_storage" }, { - "astId": 18949, + "astId": 19655, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "changes", "offset": 0, "slot": "9", - "type": "t_struct(StakingChangeLog)18865_storage" + "type": "t_struct(StakingChangeLog)19571_storage" } ], "numberOfBytes": "352" }, - "t_struct(QuorumInfo)18765_storage": { + "t_struct(QuorumInfo)19471_storage": { "encoding": "inplace", "label": "struct QuorumInfo", "members": [ { - "astId": 18752, + "astId": 19458, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "hash", "offset": 0, @@ -715,7 +715,7 @@ "type": "t_bytes32" }, { - "astId": 18755, + "astId": 19461, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "rootHash", "offset": 0, @@ -723,7 +723,7 @@ "type": "t_bytes32" }, { - "astId": 18758, + "astId": 19464, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "threshold", "offset": 0, @@ -731,7 +731,7 @@ "type": "t_uint256" }, { - "astId": 18761, + "astId": 19467, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "currentWeight", "offset": 0, @@ -739,7 +739,7 @@ "type": "t_uint256" }, { - "astId": 18764, + "astId": 19470, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "reached", "offset": 0, @@ -749,20 +749,20 @@ ], "numberOfBytes": "160" }, - "t_struct(QuorumMap)18797_storage": { + "t_struct(QuorumMap)19503_storage": { "encoding": "inplace", "label": "struct QuorumMap", "members": [ { - "astId": 18770, + "astId": 19476, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "quorumObjKind", "offset": 0, "slot": "0", - "type": "t_enum(QuorumObjKind)18748" + "type": "t_enum(QuorumObjKind)19454" }, { - "astId": 18773, + "astId": 19479, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "retentionHeight", "offset": 0, @@ -770,15 +770,15 @@ "type": "t_uint256" }, { - "astId": 18779, + "astId": 19485, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "quorumInfo", "offset": 0, "slot": "2", - "type": "t_mapping(t_uint256,t_struct(QuorumInfo)18765_storage)" + "type": "t_mapping(t_uint256,t_struct(QuorumInfo)19471_storage)" }, { - "astId": 18783, + "astId": 19489, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "incompleteQuorums", "offset": 0, @@ -786,7 +786,7 @@ "type": "t_struct(UintSet)3616_storage" }, { - "astId": 18789, + "astId": 19495, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "quorumSignatureSenders", "offset": 0, @@ -794,7 +794,7 @@ "type": "t_mapping(t_uint256,t_struct(AddressSet)3459_storage)" }, { - "astId": 18796, + "astId": 19502, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "quorumSignatures", "offset": 0, @@ -827,20 +827,20 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingChange)18844_storage": { + "t_struct(StakingChange)19550_storage": { "encoding": "inplace", "label": "struct StakingChange", "members": [ { - "astId": 18839, + "astId": 19545, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "op", "offset": 0, "slot": "0", - "type": "t_enum(StakingOperation)18835" + "type": "t_enum(StakingOperation)19541" }, { - "astId": 18841, + "astId": 19547, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "payload", "offset": 0, @@ -848,7 +848,7 @@ "type": "t_bytes_storage" }, { - "astId": 18843, + "astId": 19549, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "validator", "offset": 0, @@ -858,12 +858,12 @@ ], "numberOfBytes": "96" }, - "t_struct(StakingChangeLog)18865_storage": { + "t_struct(StakingChangeLog)19571_storage": { "encoding": "inplace", "label": "struct StakingChangeLog", "members": [ { - "astId": 18855, + "astId": 19561, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "nextConfigurationNumber", "offset": 0, @@ -871,7 +871,7 @@ "type": "t_uint64" }, { - "astId": 18858, + "astId": 19564, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "startConfigurationNumber", "offset": 8, @@ -879,22 +879,22 @@ "type": "t_uint64" }, { - "astId": 18864, + "astId": 19570, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "changes", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)" + "type": "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)" } ], "numberOfBytes": "64" }, - "t_struct(Subnet)18829_storage": { + "t_struct(Subnet)19535_storage": { "encoding": "inplace", "label": "struct Subnet", "members": [ { - "astId": 18817, + "astId": 19523, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "stake", "offset": 0, @@ -902,7 +902,7 @@ "type": "t_uint256" }, { - "astId": 18819, + "astId": 19525, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "genesisEpoch", "offset": 0, @@ -910,7 +910,7 @@ "type": "t_uint256" }, { - "astId": 18821, + "astId": 19527, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "circSupply", "offset": 0, @@ -918,7 +918,7 @@ "type": "t_uint256" }, { - "astId": 18823, + "astId": 19529, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "topDownNonce", "offset": 0, @@ -926,7 +926,7 @@ "type": "t_uint64" }, { - "astId": 18825, + "astId": 19531, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "appliedBottomUpNonce", "offset": 8, @@ -934,22 +934,22 @@ "type": "t_uint64" }, { - "astId": 18828, + "astId": 19534, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "id", "offset": 0, "slot": "4", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" } ], "numberOfBytes": "192" }, - "t_struct(SubnetID)18814_storage": { + "t_struct(SubnetID)19520_storage": { "encoding": "inplace", "label": "struct SubnetID", "members": [ { - "astId": 18809, + "astId": 19515, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "root", "offset": 0, @@ -957,7 +957,7 @@ "type": "t_uint64" }, { - "astId": 18813, + "astId": 19519, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "route", "offset": 0, @@ -982,12 +982,12 @@ ], "numberOfBytes": "64" }, - "t_struct(Validator)18966_storage": { + "t_struct(Validator)19672_storage": { "encoding": "inplace", "label": "struct Validator", "members": [ { - "astId": 18961, + "astId": 19667, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "weight", "offset": 0, @@ -995,7 +995,7 @@ "type": "t_uint256" }, { - "astId": 18963, + "astId": 19669, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "addr", "offset": 0, @@ -1003,7 +1003,7 @@ "type": "t_address" }, { - "astId": 18965, + "astId": 19671, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "metadata", "offset": 0, @@ -1013,12 +1013,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ValidatorInfo)18907_storage": { + "t_struct(ValidatorInfo)19613_storage": { "encoding": "inplace", "label": "struct ValidatorInfo", "members": [ { - "astId": 18899, + "astId": 19605, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "federatedPower", "offset": 0, @@ -1026,7 +1026,7 @@ "type": "t_uint256" }, { - "astId": 18901, + "astId": 19607, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "confirmedCollateral", "offset": 0, @@ -1034,7 +1034,7 @@ "type": "t_uint256" }, { - "astId": 18903, + "astId": 19609, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "totalCollateral", "offset": 0, @@ -1042,7 +1042,7 @@ "type": "t_uint256" }, { - "astId": 18906, + "astId": 19612, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "metadata", "offset": 0, @@ -1052,20 +1052,20 @@ ], "numberOfBytes": "128" }, - "t_struct(ValidatorSet)18942_storage": { + "t_struct(ValidatorSet)19648_storage": { "encoding": "inplace", "label": "struct ValidatorSet", "members": [ { - "astId": 18921, + "astId": 19627, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "permissionMode", "offset": 0, "slot": "0", - "type": "t_enum(PermissionMode)18912" + "type": "t_enum(PermissionMode)19618" }, { - "astId": 18924, + "astId": 19630, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "activeLimit", "offset": 1, @@ -1073,7 +1073,7 @@ "type": "t_uint16" }, { - "astId": 18927, + "astId": 19633, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "totalConfirmedCollateral", "offset": 0, @@ -1081,28 +1081,28 @@ "type": "t_uint256" }, { - "astId": 18933, + "astId": 19639, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "validators", "offset": 0, "slot": "2", - "type": "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)" + "type": "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)" }, { - "astId": 18937, + "astId": 19643, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "activeValidators", "offset": 0, "slot": "3", - "type": "t_struct(MinPQ)17743_storage" + "type": "t_struct(MinPQ)17670_storage" }, { - "astId": 18941, + "astId": 19647, "contract": "contracts/GatewayDiamond.sol:GatewayDiamond", "label": "waitingValidators", "offset": 0, "slot": "6", - "type": "t_struct(MaxPQ)17125_storage" + "type": "t_struct(MaxPQ)17052_storage" } ], "numberOfBytes": "288" diff --git a/contracts/.storage-layouts/SubnetActorDiamond.json b/contracts/.storage-layouts/SubnetActorDiamond.json index 1a7511cd6..0d89b7492 100644 --- a/contracts/.storage-layouts/SubnetActorDiamond.json +++ b/contracts/.storage-layouts/SubnetActorDiamond.json @@ -6,7 +6,7 @@ "label": "s", "offset": 0, "slot": "0", - "type": "t_struct(SubnetActorStorage)16379_storage" + "type": "t_struct(SubnetActorStorage)16306_storage" } ], "types": { @@ -27,14 +27,14 @@ "label": "bytes32[]", "numberOfBytes": "32" }, - "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage": { - "base": "t_struct(IpcEnvelope)18702_storage", + "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage": { + "base": "t_struct(IpcEnvelope)19408_storage", "encoding": "dynamic_array", "label": "struct IpcEnvelope[]", "numberOfBytes": "32" }, - "t_array(t_struct(Validator)18966_storage)dyn_storage": { - "base": "t_struct(Validator)18966_storage", + "t_array(t_struct(Validator)19672_storage)dyn_storage": { + "base": "t_struct(Validator)19672_storage", "encoding": "dynamic_array", "label": "struct Validator[]", "numberOfBytes": "32" @@ -54,7 +54,7 @@ "label": "bytes", "numberOfBytes": "32" }, - "t_enum(AssetKind)18987": { + "t_enum(AssetKind)19693": { "encoding": "inplace", "label": "enum AssetKind", "numberOfBytes": "1" @@ -64,17 +64,17 @@ "label": "enum ConsensusType", "numberOfBytes": "1" }, - "t_enum(IpcMsgKind)18679": { + "t_enum(IpcMsgKind)19385": { "encoding": "inplace", "label": "enum IpcMsgKind", "numberOfBytes": "1" }, - "t_enum(PermissionMode)18912": { + "t_enum(PermissionMode)19618": { "encoding": "inplace", "label": "enum PermissionMode", "numberOfBytes": "1" }, - "t_enum(StakingOperation)18835": { + "t_enum(StakingOperation)19541": { "encoding": "inplace", "label": "enum StakingOperation", "numberOfBytes": "1" @@ -91,19 +91,19 @@ "numberOfBytes": "32", "value": "t_string_storage" }, - "t_mapping(t_address,t_struct(AddressStakingReleases)18884_storage)": { + "t_mapping(t_address,t_struct(AddressStakingReleases)19590_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct AddressStakingReleases)", "numberOfBytes": "32", - "value": "t_struct(AddressStakingReleases)18884_storage" + "value": "t_struct(AddressStakingReleases)19590_storage" }, - "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)": { + "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct ValidatorInfo)", "numberOfBytes": "32", - "value": "t_struct(ValidatorInfo)18907_storage" + "value": "t_struct(ValidatorInfo)19613_storage" }, "t_mapping(t_address,t_uint16)": { "encoding": "mapping", @@ -133,26 +133,26 @@ "numberOfBytes": "32", "value": "t_address" }, - "t_mapping(t_uint16,t_struct(StakingRelease)18873_storage)": { + "t_mapping(t_uint16,t_struct(StakingRelease)19579_storage)": { "encoding": "mapping", "key": "t_uint16", "label": "mapping(uint16 => struct StakingRelease)", "numberOfBytes": "32", - "value": "t_struct(StakingRelease)18873_storage" + "value": "t_struct(StakingRelease)19579_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpCheckpoint)", "numberOfBytes": "32", - "value": "t_struct(BottomUpCheckpoint)18654_storage" + "value": "t_struct(BottomUpCheckpoint)19331_storage" }, - "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)": { + "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)": { "encoding": "mapping", "key": "t_uint64", "label": "mapping(uint64 => struct StakingChange)", "numberOfBytes": "32", - "value": "t_struct(StakingChange)18844_storage" + "value": "t_struct(StakingChange)19550_storage" }, "t_string_storage": { "encoding": "bytes", @@ -174,12 +174,12 @@ ], "numberOfBytes": "64" }, - "t_struct(AddressStakingReleases)18884_storage": { + "t_struct(AddressStakingReleases)19590_storage": { "encoding": "inplace", "label": "struct AddressStakingReleases", "members": [ { - "astId": 18876, + "astId": 19582, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "length", "offset": 0, @@ -187,7 +187,7 @@ "type": "t_uint16" }, { - "astId": 18878, + "astId": 19584, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "startIdx", "offset": 2, @@ -195,30 +195,30 @@ "type": "t_uint16" }, { - "astId": 18883, + "astId": 19589, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "releases", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint16,t_struct(StakingRelease)18873_storage)" + "type": "t_mapping(t_uint16,t_struct(StakingRelease)19579_storage)" } ], "numberOfBytes": "64" }, - "t_struct(Asset)18983_storage": { + "t_struct(Asset)19689_storage": { "encoding": "inplace", "label": "struct Asset", "members": [ { - "astId": 18979, + "astId": 19685, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(AssetKind)18987" + "type": "t_enum(AssetKind)19693" }, { - "astId": 18982, + "astId": 19688, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "tokenAddress", "offset": 1, @@ -228,20 +228,20 @@ ], "numberOfBytes": "32" }, - "t_struct(BottomUpCheckpoint)18654_storage": { + "t_struct(BottomUpCheckpoint)19331_storage": { "encoding": "inplace", "label": "struct BottomUpCheckpoint", "members": [ { - "astId": 18639, + "astId": 19316, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18642, + "astId": 19319, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "blockHeight", "offset": 0, @@ -249,7 +249,7 @@ "type": "t_uint256" }, { - "astId": 18645, + "astId": 19322, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "blockHash", "offset": 0, @@ -257,7 +257,7 @@ "type": "t_bytes32" }, { - "astId": 18648, + "astId": 19325, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "nextConfigurationNumber", "offset": 0, @@ -265,22 +265,22 @@ "type": "t_uint64" }, { - "astId": 18653, + "astId": 19330, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "msgs", "offset": 0, "slot": "5", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "192" }, - "t_struct(FvmAddress)18733_storage": { + "t_struct(FvmAddress)19439_storage": { "encoding": "inplace", "label": "struct FvmAddress", "members": [ { - "astId": 18730, + "astId": 19436, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "addrType", "offset": 0, @@ -288,7 +288,7 @@ "type": "t_uint8" }, { - "astId": 18732, + "astId": 19438, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "payload", "offset": 0, @@ -298,59 +298,59 @@ ], "numberOfBytes": "64" }, - "t_struct(IPCAddress)18958_storage": { + "t_struct(IPCAddress)19664_storage": { "encoding": "inplace", "label": "struct IPCAddress", "members": [ { - "astId": 18954, + "astId": 19660, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "subnetId", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18957, + "astId": 19663, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "rawAddress", "offset": 0, "slot": "2", - "type": "t_struct(FvmAddress)18733_storage" + "type": "t_struct(FvmAddress)19439_storage" } ], "numberOfBytes": "128" }, - "t_struct(IpcEnvelope)18702_storage": { + "t_struct(IpcEnvelope)19408_storage": { "encoding": "inplace", "label": "struct IpcEnvelope", "members": [ { - "astId": 18684, + "astId": 19390, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(IpcMsgKind)18679" + "type": "t_enum(IpcMsgKind)19385" }, { - "astId": 18688, + "astId": 19394, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "to", "offset": 0, "slot": "1", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18692, + "astId": 19398, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "from", "offset": 0, "slot": "5", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18695, + "astId": 19401, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "nonce", "offset": 0, @@ -358,7 +358,7 @@ "type": "t_uint64" }, { - "astId": 18698, + "astId": 19404, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "value", "offset": 0, @@ -366,7 +366,7 @@ "type": "t_uint256" }, { - "astId": 18701, + "astId": 19407, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "message", "offset": 0, @@ -376,42 +376,42 @@ ], "numberOfBytes": "384" }, - "t_struct(MaxPQ)17125_storage": { + "t_struct(MaxPQ)17052_storage": { "encoding": "inplace", "label": "struct MaxPQ", "members": [ { - "astId": 17124, + "astId": 17051, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(MinPQ)17743_storage": { + "t_struct(MinPQ)17670_storage": { "encoding": "inplace", "label": "struct MinPQ", "members": [ { - "astId": 17742, + "astId": 17669, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(PQ)18373_storage": { + "t_struct(PQ)18300_storage": { "encoding": "inplace", "label": "struct PQ", "members": [ { - "astId": 18362, + "astId": 18289, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "size", "offset": 0, @@ -419,7 +419,7 @@ "type": "t_uint16" }, { - "astId": 18367, + "astId": 18294, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "addressToPos", "offset": 0, @@ -427,7 +427,7 @@ "type": "t_mapping(t_address,t_uint16)" }, { - "astId": 18372, + "astId": 18299, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "posToAddress", "offset": 0, @@ -460,20 +460,20 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingChange)18844_storage": { + "t_struct(StakingChange)19550_storage": { "encoding": "inplace", "label": "struct StakingChange", "members": [ { - "astId": 18839, + "astId": 19545, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "op", "offset": 0, "slot": "0", - "type": "t_enum(StakingOperation)18835" + "type": "t_enum(StakingOperation)19541" }, { - "astId": 18841, + "astId": 19547, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "payload", "offset": 0, @@ -481,7 +481,7 @@ "type": "t_bytes_storage" }, { - "astId": 18843, + "astId": 19549, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "validator", "offset": 0, @@ -491,12 +491,12 @@ ], "numberOfBytes": "96" }, - "t_struct(StakingChangeLog)18865_storage": { + "t_struct(StakingChangeLog)19571_storage": { "encoding": "inplace", "label": "struct StakingChangeLog", "members": [ { - "astId": 18855, + "astId": 19561, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "nextConfigurationNumber", "offset": 0, @@ -504,7 +504,7 @@ "type": "t_uint64" }, { - "astId": 18858, + "astId": 19564, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "startConfigurationNumber", "offset": 8, @@ -512,22 +512,22 @@ "type": "t_uint64" }, { - "astId": 18864, + "astId": 19570, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "changes", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)" + "type": "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)" } ], "numberOfBytes": "64" }, - "t_struct(StakingRelease)18873_storage": { + "t_struct(StakingRelease)19579_storage": { "encoding": "inplace", "label": "struct StakingRelease", "members": [ { - "astId": 18869, + "astId": 19575, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "releaseAt", "offset": 0, @@ -535,7 +535,7 @@ "type": "t_uint256" }, { - "astId": 18872, + "astId": 19578, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "amount", "offset": 0, @@ -545,12 +545,12 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingReleaseQueue)18895_storage": { + "t_struct(StakingReleaseQueue)19601_storage": { "encoding": "inplace", "label": "struct StakingReleaseQueue", "members": [ { - "astId": 18888, + "astId": 19594, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "lockingDuration", "offset": 0, @@ -558,22 +558,22 @@ "type": "t_uint256" }, { - "astId": 18894, + "astId": 19600, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "releases", "offset": 0, "slot": "1", - "type": "t_mapping(t_address,t_struct(AddressStakingReleases)18884_storage)" + "type": "t_mapping(t_address,t_struct(AddressStakingReleases)19590_storage)" } ], "numberOfBytes": "64" }, - "t_struct(SubnetActorStorage)16379_storage": { + "t_struct(SubnetActorStorage)16306_storage": { "encoding": "inplace", "label": "struct SubnetActorStorage", "members": [ { - "astId": 16286, + "astId": 16213, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "genesisCircSupply", "offset": 0, @@ -581,7 +581,7 @@ "type": "t_uint256" }, { - "astId": 16289, + "astId": 16216, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "lastBottomUpCheckpointHeight", "offset": 0, @@ -589,7 +589,7 @@ "type": "t_uint256" }, { - "astId": 16292, + "astId": 16219, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "minActivationCollateral", "offset": 0, @@ -597,7 +597,7 @@ "type": "t_uint256" }, { - "astId": 16295, + "astId": 16222, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "bottomUpCheckPeriod", "offset": 0, @@ -605,7 +605,7 @@ "type": "t_uint256" }, { - "astId": 16297, + "astId": 16224, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "currentSubnetHash", "offset": 0, @@ -613,7 +613,7 @@ "type": "t_bytes32" }, { - "astId": 16300, + "astId": 16227, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "ipcGatewayAddr", "offset": 0, @@ -621,7 +621,7 @@ "type": "t_address" }, { - "astId": 16303, + "astId": 16230, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "maxMsgsPerBottomUpBatch", "offset": 20, @@ -629,7 +629,7 @@ "type": "t_uint64" }, { - "astId": 16306, + "astId": 16233, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "majorityPercentage", "offset": 28, @@ -637,7 +637,7 @@ "type": "t_uint8" }, { - "astId": 16309, + "astId": 16236, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "powerScale", "offset": 29, @@ -645,7 +645,7 @@ "type": "t_int8" }, { - "astId": 16313, + "astId": 16240, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "consensus", "offset": 30, @@ -653,7 +653,7 @@ "type": "t_enum(ConsensusType)5500" }, { - "astId": 16316, + "astId": 16243, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "bootstrapped", "offset": 31, @@ -661,7 +661,7 @@ "type": "t_bool" }, { - "astId": 16319, + "astId": 16246, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "minValidators", "offset": 0, @@ -669,7 +669,7 @@ "type": "t_uint64" }, { - "astId": 16322, + "astId": 16249, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "killed", "offset": 8, @@ -677,55 +677,55 @@ "type": "t_bool" }, { - "astId": 16326, + "astId": 16253, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "supplySource", "offset": 0, "slot": "7", - "type": "t_struct(Asset)18983_storage" + "type": "t_struct(Asset)19689_storage" }, { - "astId": 16330, + "astId": 16257, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "collateralSource", "offset": 0, "slot": "8", - "type": "t_struct(Asset)18983_storage" + "type": "t_struct(Asset)19689_storage" }, { - "astId": 16334, + "astId": 16261, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "parentId", "offset": 0, "slot": "9", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 16338, + "astId": 16265, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "validatorSet", "offset": 0, "slot": "11", - "type": "t_struct(ValidatorSet)18942_storage" + "type": "t_struct(ValidatorSet)19648_storage" }, { - "astId": 16342, + "astId": 16269, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "changeSet", "offset": 0, "slot": "20", - "type": "t_struct(StakingChangeLog)18865_storage" + "type": "t_struct(StakingChangeLog)19571_storage" }, { - "astId": 16346, + "astId": 16273, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "releaseQueue", "offset": 0, "slot": "22", - "type": "t_struct(StakingReleaseQueue)18895_storage" + "type": "t_struct(StakingReleaseQueue)19601_storage" }, { - "astId": 16351, + "astId": 16278, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "bootstrapNodes", "offset": 0, @@ -733,7 +733,7 @@ "type": "t_mapping(t_address,t_string_storage)" }, { - "astId": 16355, + "astId": 16282, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "bootstrapOwners", "offset": 0, @@ -741,23 +741,23 @@ "type": "t_struct(AddressSet)3459_storage" }, { - "astId": 16361, + "astId": 16288, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "committedCheckpoints", "offset": 0, "slot": "27", - "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)" }, { - "astId": 16366, + "astId": 16293, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "genesisValidators", "offset": 0, "slot": "28", - "type": "t_array(t_struct(Validator)18966_storage)dyn_storage" + "type": "t_array(t_struct(Validator)19672_storage)dyn_storage" }, { - "astId": 16371, + "astId": 16298, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "genesisBalance", "offset": 0, @@ -765,7 +765,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 16375, + "astId": 16302, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "genesisBalanceKeys", "offset": 0, @@ -773,7 +773,7 @@ "type": "t_array(t_address)dyn_storage" }, { - "astId": 16378, + "astId": 16305, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "validatorGater", "offset": 0, @@ -783,12 +783,12 @@ ], "numberOfBytes": "1024" }, - "t_struct(SubnetID)18814_storage": { + "t_struct(SubnetID)19520_storage": { "encoding": "inplace", "label": "struct SubnetID", "members": [ { - "astId": 18809, + "astId": 19515, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "root", "offset": 0, @@ -796,7 +796,7 @@ "type": "t_uint64" }, { - "astId": 18813, + "astId": 19519, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "route", "offset": 0, @@ -806,12 +806,12 @@ ], "numberOfBytes": "64" }, - "t_struct(Validator)18966_storage": { + "t_struct(Validator)19672_storage": { "encoding": "inplace", "label": "struct Validator", "members": [ { - "astId": 18961, + "astId": 19667, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "weight", "offset": 0, @@ -819,7 +819,7 @@ "type": "t_uint256" }, { - "astId": 18963, + "astId": 19669, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "addr", "offset": 0, @@ -827,7 +827,7 @@ "type": "t_address" }, { - "astId": 18965, + "astId": 19671, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "metadata", "offset": 0, @@ -837,12 +837,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ValidatorInfo)18907_storage": { + "t_struct(ValidatorInfo)19613_storage": { "encoding": "inplace", "label": "struct ValidatorInfo", "members": [ { - "astId": 18899, + "astId": 19605, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "federatedPower", "offset": 0, @@ -850,7 +850,7 @@ "type": "t_uint256" }, { - "astId": 18901, + "astId": 19607, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "confirmedCollateral", "offset": 0, @@ -858,7 +858,7 @@ "type": "t_uint256" }, { - "astId": 18903, + "astId": 19609, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "totalCollateral", "offset": 0, @@ -866,7 +866,7 @@ "type": "t_uint256" }, { - "astId": 18906, + "astId": 19612, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "metadata", "offset": 0, @@ -876,20 +876,20 @@ ], "numberOfBytes": "128" }, - "t_struct(ValidatorSet)18942_storage": { + "t_struct(ValidatorSet)19648_storage": { "encoding": "inplace", "label": "struct ValidatorSet", "members": [ { - "astId": 18921, + "astId": 19627, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "permissionMode", "offset": 0, "slot": "0", - "type": "t_enum(PermissionMode)18912" + "type": "t_enum(PermissionMode)19618" }, { - "astId": 18924, + "astId": 19630, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "activeLimit", "offset": 1, @@ -897,7 +897,7 @@ "type": "t_uint16" }, { - "astId": 18927, + "astId": 19633, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "totalConfirmedCollateral", "offset": 0, @@ -905,28 +905,28 @@ "type": "t_uint256" }, { - "astId": 18933, + "astId": 19639, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "validators", "offset": 0, "slot": "2", - "type": "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)" + "type": "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)" }, { - "astId": 18937, + "astId": 19643, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "activeValidators", "offset": 0, "slot": "3", - "type": "t_struct(MinPQ)17743_storage" + "type": "t_struct(MinPQ)17670_storage" }, { - "astId": 18941, + "astId": 19647, "contract": "contracts/SubnetActorDiamond.sol:SubnetActorDiamond", "label": "waitingValidators", "offset": 0, "slot": "6", - "type": "t_struct(MaxPQ)17125_storage" + "type": "t_struct(MaxPQ)17052_storage" } ], "numberOfBytes": "288" diff --git a/contracts/.storage-layouts/SubnetActorModifiers.json b/contracts/.storage-layouts/SubnetActorModifiers.json index dee728907..baf89be37 100644 --- a/contracts/.storage-layouts/SubnetActorModifiers.json +++ b/contracts/.storage-layouts/SubnetActorModifiers.json @@ -1,12 +1,12 @@ { "storage": [ { - "astId": 16393, + "astId": 16320, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "s", "offset": 0, "slot": "0", - "type": "t_struct(SubnetActorStorage)16379_storage" + "type": "t_struct(SubnetActorStorage)16306_storage" } ], "types": { @@ -27,14 +27,14 @@ "label": "bytes32[]", "numberOfBytes": "32" }, - "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage": { - "base": "t_struct(IpcEnvelope)18702_storage", + "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage": { + "base": "t_struct(IpcEnvelope)19408_storage", "encoding": "dynamic_array", "label": "struct IpcEnvelope[]", "numberOfBytes": "32" }, - "t_array(t_struct(Validator)18966_storage)dyn_storage": { - "base": "t_struct(Validator)18966_storage", + "t_array(t_struct(Validator)19672_storage)dyn_storage": { + "base": "t_struct(Validator)19672_storage", "encoding": "dynamic_array", "label": "struct Validator[]", "numberOfBytes": "32" @@ -54,7 +54,7 @@ "label": "bytes", "numberOfBytes": "32" }, - "t_enum(AssetKind)18987": { + "t_enum(AssetKind)19693": { "encoding": "inplace", "label": "enum AssetKind", "numberOfBytes": "1" @@ -64,17 +64,17 @@ "label": "enum ConsensusType", "numberOfBytes": "1" }, - "t_enum(IpcMsgKind)18679": { + "t_enum(IpcMsgKind)19385": { "encoding": "inplace", "label": "enum IpcMsgKind", "numberOfBytes": "1" }, - "t_enum(PermissionMode)18912": { + "t_enum(PermissionMode)19618": { "encoding": "inplace", "label": "enum PermissionMode", "numberOfBytes": "1" }, - "t_enum(StakingOperation)18835": { + "t_enum(StakingOperation)19541": { "encoding": "inplace", "label": "enum StakingOperation", "numberOfBytes": "1" @@ -91,19 +91,19 @@ "numberOfBytes": "32", "value": "t_string_storage" }, - "t_mapping(t_address,t_struct(AddressStakingReleases)18884_storage)": { + "t_mapping(t_address,t_struct(AddressStakingReleases)19590_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct AddressStakingReleases)", "numberOfBytes": "32", - "value": "t_struct(AddressStakingReleases)18884_storage" + "value": "t_struct(AddressStakingReleases)19590_storage" }, - "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)": { + "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => struct ValidatorInfo)", "numberOfBytes": "32", - "value": "t_struct(ValidatorInfo)18907_storage" + "value": "t_struct(ValidatorInfo)19613_storage" }, "t_mapping(t_address,t_uint16)": { "encoding": "mapping", @@ -133,26 +133,26 @@ "numberOfBytes": "32", "value": "t_address" }, - "t_mapping(t_uint16,t_struct(StakingRelease)18873_storage)": { + "t_mapping(t_uint16,t_struct(StakingRelease)19579_storage)": { "encoding": "mapping", "key": "t_uint16", "label": "mapping(uint16 => struct StakingRelease)", "numberOfBytes": "32", - "value": "t_struct(StakingRelease)18873_storage" + "value": "t_struct(StakingRelease)19579_storage" }, - "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)": { + "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct BottomUpCheckpoint)", "numberOfBytes": "32", - "value": "t_struct(BottomUpCheckpoint)18654_storage" + "value": "t_struct(BottomUpCheckpoint)19331_storage" }, - "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)": { + "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)": { "encoding": "mapping", "key": "t_uint64", "label": "mapping(uint64 => struct StakingChange)", "numberOfBytes": "32", - "value": "t_struct(StakingChange)18844_storage" + "value": "t_struct(StakingChange)19550_storage" }, "t_string_storage": { "encoding": "bytes", @@ -174,12 +174,12 @@ ], "numberOfBytes": "64" }, - "t_struct(AddressStakingReleases)18884_storage": { + "t_struct(AddressStakingReleases)19590_storage": { "encoding": "inplace", "label": "struct AddressStakingReleases", "members": [ { - "astId": 18876, + "astId": 19582, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "length", "offset": 0, @@ -187,7 +187,7 @@ "type": "t_uint16" }, { - "astId": 18878, + "astId": 19584, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "startIdx", "offset": 2, @@ -195,30 +195,30 @@ "type": "t_uint16" }, { - "astId": 18883, + "astId": 19589, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "releases", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint16,t_struct(StakingRelease)18873_storage)" + "type": "t_mapping(t_uint16,t_struct(StakingRelease)19579_storage)" } ], "numberOfBytes": "64" }, - "t_struct(Asset)18983_storage": { + "t_struct(Asset)19689_storage": { "encoding": "inplace", "label": "struct Asset", "members": [ { - "astId": 18979, + "astId": 19685, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(AssetKind)18987" + "type": "t_enum(AssetKind)19693" }, { - "astId": 18982, + "astId": 19688, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "tokenAddress", "offset": 1, @@ -228,20 +228,20 @@ ], "numberOfBytes": "32" }, - "t_struct(BottomUpCheckpoint)18654_storage": { + "t_struct(BottomUpCheckpoint)19331_storage": { "encoding": "inplace", "label": "struct BottomUpCheckpoint", "members": [ { - "astId": 18639, + "astId": 19316, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "subnetID", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18642, + "astId": 19319, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "blockHeight", "offset": 0, @@ -249,7 +249,7 @@ "type": "t_uint256" }, { - "astId": 18645, + "astId": 19322, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "blockHash", "offset": 0, @@ -257,7 +257,7 @@ "type": "t_bytes32" }, { - "astId": 18648, + "astId": 19325, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "nextConfigurationNumber", "offset": 0, @@ -265,22 +265,22 @@ "type": "t_uint64" }, { - "astId": 18653, + "astId": 19330, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "msgs", "offset": 0, "slot": "5", - "type": "t_array(t_struct(IpcEnvelope)18702_storage)dyn_storage" + "type": "t_array(t_struct(IpcEnvelope)19408_storage)dyn_storage" } ], "numberOfBytes": "192" }, - "t_struct(FvmAddress)18733_storage": { + "t_struct(FvmAddress)19439_storage": { "encoding": "inplace", "label": "struct FvmAddress", "members": [ { - "astId": 18730, + "astId": 19436, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "addrType", "offset": 0, @@ -288,7 +288,7 @@ "type": "t_uint8" }, { - "astId": 18732, + "astId": 19438, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "payload", "offset": 0, @@ -298,59 +298,59 @@ ], "numberOfBytes": "64" }, - "t_struct(IPCAddress)18958_storage": { + "t_struct(IPCAddress)19664_storage": { "encoding": "inplace", "label": "struct IPCAddress", "members": [ { - "astId": 18954, + "astId": 19660, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "subnetId", "offset": 0, "slot": "0", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 18957, + "astId": 19663, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "rawAddress", "offset": 0, "slot": "2", - "type": "t_struct(FvmAddress)18733_storage" + "type": "t_struct(FvmAddress)19439_storage" } ], "numberOfBytes": "128" }, - "t_struct(IpcEnvelope)18702_storage": { + "t_struct(IpcEnvelope)19408_storage": { "encoding": "inplace", "label": "struct IpcEnvelope", "members": [ { - "astId": 18684, + "astId": 19390, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "kind", "offset": 0, "slot": "0", - "type": "t_enum(IpcMsgKind)18679" + "type": "t_enum(IpcMsgKind)19385" }, { - "astId": 18688, + "astId": 19394, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "to", "offset": 0, "slot": "1", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18692, + "astId": 19398, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "from", "offset": 0, "slot": "5", - "type": "t_struct(IPCAddress)18958_storage" + "type": "t_struct(IPCAddress)19664_storage" }, { - "astId": 18695, + "astId": 19401, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "nonce", "offset": 0, @@ -358,7 +358,7 @@ "type": "t_uint64" }, { - "astId": 18698, + "astId": 19404, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "value", "offset": 0, @@ -366,7 +366,7 @@ "type": "t_uint256" }, { - "astId": 18701, + "astId": 19407, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "message", "offset": 0, @@ -376,42 +376,42 @@ ], "numberOfBytes": "384" }, - "t_struct(MaxPQ)17125_storage": { + "t_struct(MaxPQ)17052_storage": { "encoding": "inplace", "label": "struct MaxPQ", "members": [ { - "astId": 17124, + "astId": 17051, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(MinPQ)17743_storage": { + "t_struct(MinPQ)17670_storage": { "encoding": "inplace", "label": "struct MinPQ", "members": [ { - "astId": 17742, + "astId": 17669, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "inner", "offset": 0, "slot": "0", - "type": "t_struct(PQ)18373_storage" + "type": "t_struct(PQ)18300_storage" } ], "numberOfBytes": "96" }, - "t_struct(PQ)18373_storage": { + "t_struct(PQ)18300_storage": { "encoding": "inplace", "label": "struct PQ", "members": [ { - "astId": 18362, + "astId": 18289, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "size", "offset": 0, @@ -419,7 +419,7 @@ "type": "t_uint16" }, { - "astId": 18367, + "astId": 18294, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "addressToPos", "offset": 0, @@ -427,7 +427,7 @@ "type": "t_mapping(t_address,t_uint16)" }, { - "astId": 18372, + "astId": 18299, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "posToAddress", "offset": 0, @@ -460,20 +460,20 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingChange)18844_storage": { + "t_struct(StakingChange)19550_storage": { "encoding": "inplace", "label": "struct StakingChange", "members": [ { - "astId": 18839, + "astId": 19545, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "op", "offset": 0, "slot": "0", - "type": "t_enum(StakingOperation)18835" + "type": "t_enum(StakingOperation)19541" }, { - "astId": 18841, + "astId": 19547, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "payload", "offset": 0, @@ -481,7 +481,7 @@ "type": "t_bytes_storage" }, { - "astId": 18843, + "astId": 19549, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "validator", "offset": 0, @@ -491,12 +491,12 @@ ], "numberOfBytes": "96" }, - "t_struct(StakingChangeLog)18865_storage": { + "t_struct(StakingChangeLog)19571_storage": { "encoding": "inplace", "label": "struct StakingChangeLog", "members": [ { - "astId": 18855, + "astId": 19561, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "nextConfigurationNumber", "offset": 0, @@ -504,7 +504,7 @@ "type": "t_uint64" }, { - "astId": 18858, + "astId": 19564, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "startConfigurationNumber", "offset": 8, @@ -512,22 +512,22 @@ "type": "t_uint64" }, { - "astId": 18864, + "astId": 19570, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "changes", "offset": 0, "slot": "1", - "type": "t_mapping(t_uint64,t_struct(StakingChange)18844_storage)" + "type": "t_mapping(t_uint64,t_struct(StakingChange)19550_storage)" } ], "numberOfBytes": "64" }, - "t_struct(StakingRelease)18873_storage": { + "t_struct(StakingRelease)19579_storage": { "encoding": "inplace", "label": "struct StakingRelease", "members": [ { - "astId": 18869, + "astId": 19575, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "releaseAt", "offset": 0, @@ -535,7 +535,7 @@ "type": "t_uint256" }, { - "astId": 18872, + "astId": 19578, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "amount", "offset": 0, @@ -545,12 +545,12 @@ ], "numberOfBytes": "64" }, - "t_struct(StakingReleaseQueue)18895_storage": { + "t_struct(StakingReleaseQueue)19601_storage": { "encoding": "inplace", "label": "struct StakingReleaseQueue", "members": [ { - "astId": 18888, + "astId": 19594, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "lockingDuration", "offset": 0, @@ -558,22 +558,22 @@ "type": "t_uint256" }, { - "astId": 18894, + "astId": 19600, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "releases", "offset": 0, "slot": "1", - "type": "t_mapping(t_address,t_struct(AddressStakingReleases)18884_storage)" + "type": "t_mapping(t_address,t_struct(AddressStakingReleases)19590_storage)" } ], "numberOfBytes": "64" }, - "t_struct(SubnetActorStorage)16379_storage": { + "t_struct(SubnetActorStorage)16306_storage": { "encoding": "inplace", "label": "struct SubnetActorStorage", "members": [ { - "astId": 16286, + "astId": 16213, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "genesisCircSupply", "offset": 0, @@ -581,7 +581,7 @@ "type": "t_uint256" }, { - "astId": 16289, + "astId": 16216, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "lastBottomUpCheckpointHeight", "offset": 0, @@ -589,7 +589,7 @@ "type": "t_uint256" }, { - "astId": 16292, + "astId": 16219, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "minActivationCollateral", "offset": 0, @@ -597,7 +597,7 @@ "type": "t_uint256" }, { - "astId": 16295, + "astId": 16222, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "bottomUpCheckPeriod", "offset": 0, @@ -605,7 +605,7 @@ "type": "t_uint256" }, { - "astId": 16297, + "astId": 16224, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "currentSubnetHash", "offset": 0, @@ -613,7 +613,7 @@ "type": "t_bytes32" }, { - "astId": 16300, + "astId": 16227, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "ipcGatewayAddr", "offset": 0, @@ -621,7 +621,7 @@ "type": "t_address" }, { - "astId": 16303, + "astId": 16230, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "maxMsgsPerBottomUpBatch", "offset": 20, @@ -629,7 +629,7 @@ "type": "t_uint64" }, { - "astId": 16306, + "astId": 16233, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "majorityPercentage", "offset": 28, @@ -637,7 +637,7 @@ "type": "t_uint8" }, { - "astId": 16309, + "astId": 16236, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "powerScale", "offset": 29, @@ -645,7 +645,7 @@ "type": "t_int8" }, { - "astId": 16313, + "astId": 16240, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "consensus", "offset": 30, @@ -653,7 +653,7 @@ "type": "t_enum(ConsensusType)5500" }, { - "astId": 16316, + "astId": 16243, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "bootstrapped", "offset": 31, @@ -661,7 +661,7 @@ "type": "t_bool" }, { - "astId": 16319, + "astId": 16246, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "minValidators", "offset": 0, @@ -669,7 +669,7 @@ "type": "t_uint64" }, { - "astId": 16322, + "astId": 16249, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "killed", "offset": 8, @@ -677,55 +677,55 @@ "type": "t_bool" }, { - "astId": 16326, + "astId": 16253, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "supplySource", "offset": 0, "slot": "7", - "type": "t_struct(Asset)18983_storage" + "type": "t_struct(Asset)19689_storage" }, { - "astId": 16330, + "astId": 16257, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "collateralSource", "offset": 0, "slot": "8", - "type": "t_struct(Asset)18983_storage" + "type": "t_struct(Asset)19689_storage" }, { - "astId": 16334, + "astId": 16261, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "parentId", "offset": 0, "slot": "9", - "type": "t_struct(SubnetID)18814_storage" + "type": "t_struct(SubnetID)19520_storage" }, { - "astId": 16338, + "astId": 16265, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "validatorSet", "offset": 0, "slot": "11", - "type": "t_struct(ValidatorSet)18942_storage" + "type": "t_struct(ValidatorSet)19648_storage" }, { - "astId": 16342, + "astId": 16269, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "changeSet", "offset": 0, "slot": "20", - "type": "t_struct(StakingChangeLog)18865_storage" + "type": "t_struct(StakingChangeLog)19571_storage" }, { - "astId": 16346, + "astId": 16273, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "releaseQueue", "offset": 0, "slot": "22", - "type": "t_struct(StakingReleaseQueue)18895_storage" + "type": "t_struct(StakingReleaseQueue)19601_storage" }, { - "astId": 16351, + "astId": 16278, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "bootstrapNodes", "offset": 0, @@ -733,7 +733,7 @@ "type": "t_mapping(t_address,t_string_storage)" }, { - "astId": 16355, + "astId": 16282, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "bootstrapOwners", "offset": 0, @@ -741,23 +741,23 @@ "type": "t_struct(AddressSet)3459_storage" }, { - "astId": 16361, + "astId": 16288, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "committedCheckpoints", "offset": 0, "slot": "27", - "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)18654_storage)" + "type": "t_mapping(t_uint256,t_struct(BottomUpCheckpoint)19331_storage)" }, { - "astId": 16366, + "astId": 16293, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "genesisValidators", "offset": 0, "slot": "28", - "type": "t_array(t_struct(Validator)18966_storage)dyn_storage" + "type": "t_array(t_struct(Validator)19672_storage)dyn_storage" }, { - "astId": 16371, + "astId": 16298, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "genesisBalance", "offset": 0, @@ -765,7 +765,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 16375, + "astId": 16302, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "genesisBalanceKeys", "offset": 0, @@ -773,7 +773,7 @@ "type": "t_array(t_address)dyn_storage" }, { - "astId": 16378, + "astId": 16305, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "validatorGater", "offset": 0, @@ -783,12 +783,12 @@ ], "numberOfBytes": "1024" }, - "t_struct(SubnetID)18814_storage": { + "t_struct(SubnetID)19520_storage": { "encoding": "inplace", "label": "struct SubnetID", "members": [ { - "astId": 18809, + "astId": 19515, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "root", "offset": 0, @@ -796,7 +796,7 @@ "type": "t_uint64" }, { - "astId": 18813, + "astId": 19519, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "route", "offset": 0, @@ -806,12 +806,12 @@ ], "numberOfBytes": "64" }, - "t_struct(Validator)18966_storage": { + "t_struct(Validator)19672_storage": { "encoding": "inplace", "label": "struct Validator", "members": [ { - "astId": 18961, + "astId": 19667, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "weight", "offset": 0, @@ -819,7 +819,7 @@ "type": "t_uint256" }, { - "astId": 18963, + "astId": 19669, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "addr", "offset": 0, @@ -827,7 +827,7 @@ "type": "t_address" }, { - "astId": 18965, + "astId": 19671, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "metadata", "offset": 0, @@ -837,12 +837,12 @@ ], "numberOfBytes": "96" }, - "t_struct(ValidatorInfo)18907_storage": { + "t_struct(ValidatorInfo)19613_storage": { "encoding": "inplace", "label": "struct ValidatorInfo", "members": [ { - "astId": 18899, + "astId": 19605, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "federatedPower", "offset": 0, @@ -850,7 +850,7 @@ "type": "t_uint256" }, { - "astId": 18901, + "astId": 19607, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "confirmedCollateral", "offset": 0, @@ -858,7 +858,7 @@ "type": "t_uint256" }, { - "astId": 18903, + "astId": 19609, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "totalCollateral", "offset": 0, @@ -866,7 +866,7 @@ "type": "t_uint256" }, { - "astId": 18906, + "astId": 19612, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "metadata", "offset": 0, @@ -876,20 +876,20 @@ ], "numberOfBytes": "128" }, - "t_struct(ValidatorSet)18942_storage": { + "t_struct(ValidatorSet)19648_storage": { "encoding": "inplace", "label": "struct ValidatorSet", "members": [ { - "astId": 18921, + "astId": 19627, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "permissionMode", "offset": 0, "slot": "0", - "type": "t_enum(PermissionMode)18912" + "type": "t_enum(PermissionMode)19618" }, { - "astId": 18924, + "astId": 19630, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "activeLimit", "offset": 1, @@ -897,7 +897,7 @@ "type": "t_uint16" }, { - "astId": 18927, + "astId": 19633, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "totalConfirmedCollateral", "offset": 0, @@ -905,28 +905,28 @@ "type": "t_uint256" }, { - "astId": 18933, + "astId": 19639, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "validators", "offset": 0, "slot": "2", - "type": "t_mapping(t_address,t_struct(ValidatorInfo)18907_storage)" + "type": "t_mapping(t_address,t_struct(ValidatorInfo)19613_storage)" }, { - "astId": 18937, + "astId": 19643, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "activeValidators", "offset": 0, "slot": "3", - "type": "t_struct(MinPQ)17743_storage" + "type": "t_struct(MinPQ)17670_storage" }, { - "astId": 18941, + "astId": 19647, "contract": "contracts/lib/LibSubnetActorStorage.sol:SubnetActorModifiers", "label": "waitingValidators", "offset": 0, "slot": "6", - "type": "t_struct(MaxPQ)17125_storage" + "type": "t_struct(MaxPQ)17052_storage" } ], "numberOfBytes": "288" diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index dd59524fb..508f367f3 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -1,64 +1,3 @@ // DO NOT EDIT! This file was generated by build.rs #[macro_use] mod convert; -#[allow(clippy::all)] -pub mod checkpointing_facet; -#[allow(clippy::all)] -pub mod diamond_cut_facet; -#[allow(clippy::all)] -pub mod diamond_loupe_facet; -#[allow(clippy::all)] -pub mod gateway_diamond; -#[allow(clippy::all)] -pub mod gateway_getter_facet; -#[allow(clippy::all)] -pub mod gateway_manager_facet; -#[allow(clippy::all)] -pub mod gateway_messenger_facet; -#[allow(clippy::all)] -pub mod i_diamond; -#[allow(clippy::all)] -pub mod lib_gateway; -#[allow(clippy::all)] -pub mod lib_quorum; -#[allow(clippy::all)] -pub mod lib_staking; -#[allow(clippy::all)] -pub mod lib_staking_change_log; -#[allow(clippy::all)] -pub mod ownership_facet; -#[allow(clippy::all)] -pub mod register_subnet_facet; -#[allow(clippy::all)] -pub mod subnet_actor_checkpointing_facet; -#[allow(clippy::all)] -pub mod subnet_actor_diamond; -#[allow(clippy::all)] -pub mod subnet_actor_getter_facet; -#[allow(clippy::all)] -pub mod subnet_actor_manager_facet; -#[allow(clippy::all)] -pub mod subnet_actor_pause_facet; -#[allow(clippy::all)] -pub mod subnet_actor_reward_facet; -#[allow(clippy::all)] -pub mod subnet_getter_facet; -#[allow(clippy::all)] -pub mod subnet_registry_diamond; -#[allow(clippy::all)] -pub mod top_down_finality_facet; -#[allow(clippy::all)] -pub mod xnet_messaging_facet; - -// The list of contracts need to convert FvmAddress to fvm_shared::Address -fvm_address_conversion!(gateway_manager_facet); -fvm_address_conversion!(gateway_getter_facet); -fvm_address_conversion!(xnet_messaging_facet); -fvm_address_conversion!(gateway_messenger_facet); -fvm_address_conversion!(subnet_actor_checkpointing_facet); -fvm_address_conversion!(subnet_actor_getter_facet); -fvm_address_conversion!(lib_gateway); - -// The list of contracts that need to convert common types between each other -common_type_conversion!(subnet_actor_getter_facet, checkpointing_facet); -common_type_conversion!(subnet_actor_getter_facet, xnet_messaging_facet); diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index 7f838e1a4..cd629c07d 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -80,6 +80,10 @@ error InvalidFederationPayload(); error DuplicatedGenesisValidator(); error NotEnoughGenesisValidators(); error ValidatorPowerChangeDenied(); +error CommitmentAlreadyInitialized(); +error SubnetNoTargetCommitment(); +error ValidatorAlreadyClaimed(); + enum InvalidXnetMessageReason { Sender, diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index 1b55a015a..91f54d2cb 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -16,6 +16,7 @@ import {BatchNotCreated, InvalidBatchEpoch, BatchAlreadyExists, NotEnoughSubnetC import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; +import {LibValidatorRewardParent} from "../../reward/ValidatorRewardParentFacet.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -41,6 +42,11 @@ contract CheckpointingFacet is GatewayActorModifiers { LibGateway.checkMsgLength(checkpoint.msgs); execBottomUpMsgs(checkpoint.msgs, subnet); + + LibValidatorRewardParent.initNewDistribution( + checkpoint.validatorReward.commitment, + checkpoint.subnetID + ); } /// @notice creates a new bottom-up checkpoint diff --git a/contracts/contracts/lib/LibGatewayActorStorage.sol b/contracts/contracts/lib/LibGatewayActorStorage.sol index 4cb63536e..8e98f78f3 100644 --- a/contracts/contracts/lib/LibGatewayActorStorage.sol +++ b/contracts/contracts/lib/LibGatewayActorStorage.sol @@ -102,3 +102,20 @@ contract GatewayActorModifiers { _; } } + +contract SystemContract { + using FilAddress for address; + using FilAddress for address payable; + using AccountHelper for address; + + function _systemActorOnly() private view { + if (!msg.sender.isSystemActor()) { + revert NotSystemActor(); + } + } + + modifier systemActorOnly() { + _systemActorOnly(); + _; + } +} \ No newline at end of file diff --git a/contracts/contracts/lib/LibSubnetActorStorage.sol b/contracts/contracts/lib/LibSubnetActorStorage.sol index 474f84c0f..eaf800ca5 100644 --- a/contracts/contracts/lib/LibSubnetActorStorage.sol +++ b/contracts/contracts/lib/LibSubnetActorStorage.sol @@ -65,25 +65,6 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet address[] genesisBalanceKeys; /// @notice The validator gater, if address(0), no validator gating is performed address validatorGater; - - /// BEGIN Validator Rewards. - - /// @notice The validator rewarder. - /// If address(0), this subnet does not process activity summaries, but instead forwards them to the parent - /// network via bottom-up checkpoints. - /// If address(0), and this is the root network, summaries are discarded (> /dev/null). - /// TODO(rewarder): set this address correctly from the constructor. - address validatorRewarder; - /// @notice Summaries pending to be processed. - /// If the validator rewarder is non-zero, these denote summaries presentable at this level. - /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. - /// Partitioned by subnet ID, in the sequence they must be presented. - /// TODO(rewarder): optimize this pair of data structures. - mapping(SubnetID => bytes32[]) pendingSummaries; - /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. - /// Only used if the validator rewarder is non-zero. - /// TODO(rewarder): optimize this pair of data structures. - mapping(bytes32 => SubnetID) presentableSummaries; } library LibSubnetActorStorage { diff --git a/contracts/contracts/interfaces/IValidatorRewarder.sol b/contracts/contracts/reward/IValidatorRewarder.sol similarity index 81% rename from contracts/contracts/interfaces/IValidatorRewarder.sol rename to contracts/contracts/reward/IValidatorRewarder.sol index 6001dfb22..3a3e8e3de 100644 --- a/contracts/contracts/interfaces/IValidatorRewarder.sol +++ b/contracts/contracts/reward/IValidatorRewarder.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; +import {ActivitySummary} from "./ValidatorReward.sol"; /// @title ValidatorRewarder interface. /// @@ -12,7 +13,6 @@ import {SubnetID} from "../structs/Subnet.sol"; interface IValidatorRewarder { /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and /// disburse any relevant rewards. - /// The /// @dev This method should revert if the summary is invalid; this will cause the - function disburseRewards(SubnetID memory id, ActivitySummary memory summary) external; + function disburseRewards(SubnetID calldata id, address validator, ActivitySummary calldata summary) external; } diff --git a/contracts/contracts/reward/ValidatorActivityTracker.sol b/contracts/contracts/reward/ValidatorActivityTracker.sol new file mode 100644 index 000000000..b0c1344e5 --- /dev/null +++ b/contracts/contracts/reward/ValidatorActivityTracker.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {ValidatorSummary, ActivitySummary} from "./ValidatorReward.sol"; +import {SystemContract} from "../lib/LibGatewayActorStorage.sol"; + + +/// The validator reward facet for the child subnet, i.e. for child subnet to track validators's activies +/// and create the commitment. +contract ValidatorActivityTracker is SystemContract { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @dev The starting height of validator's mining activities since the last purged block + uint64 startHeight; + /// @dev The list of validator who have participated in mining since `startHeight` + EnumerableSet.AddressSet validators; + /// Tracks the number of blocks a validator has committed since `startHeight` + mapping(address => uint64) blocksCommitted; + + /// Validators claim their reward for doing work in the child subnet + function recordValidatorActivity(address validator) external systemActorOnly { + blocksCommitted[validator] += 1; + + if (!validators.contains(validator)) { + validators.add(validator); + } + } + + /// Reads the current validator summary + function getSummary() external view returns(ActivitySummary memory summary) { + summary.blockRange = [startHeight, block.number]; + + // prepare the activities + uint256 num_validators = validators.length(); + + summary.activities = new ValidatorSummary[](num_validators); + for (uint256 i = 0; i < num_validators; ) { + address validator = validators.at(i); + bytes memory metadata = new bytes(0); + + summary.activities[i] = ValidatorSummary({ + validator: validator, + blocksCommitted: blocksCommitted[validator], + metadata: metadata + }); + + unchecked { + i++; + } + } + } + + /// Reads the current validator summary and purge the data accordingly + /// @dev Call this method only when bottom up checkpoint needs to be created + function purgeActivities() external systemActorOnly { + // prepare the activities + uint256 num_validators = validators.length(); + + for (uint256 i = num_validators - 1; i >= 0; ) { + address validator = validators.at(i); + + delete blocksCommitted[validator]; + validators.remove(validator); + + unchecked { + if (i == 0) { break; } + i--; + } + } + + startHeight = uint64(block.number); + } +} diff --git a/contracts/contracts/reward/ValidatorReward.sol b/contracts/contracts/reward/ValidatorReward.sol new file mode 100644 index 000000000..e880cb130 --- /dev/null +++ b/contracts/contracts/reward/ValidatorReward.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +/// The commitment for the validator rewards that child subnet submits to the parent subnet +/// together with a bottom up checkpoint +struct ValidatorRewardCommitment { + /// The commitment for the parent subnet + bytes32 commitment; + + // TODO: add relayed rewarder commitment +} + +/// The summary for a single validator +struct ValidatorSummary { + /// @dev The validator whose activity we're reporting about. + address validator; + /// @dev The number of blocks committed by each validator in the position they appear in the validators array. + /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. + uint64 blocksCommitted; + /// @dev Other metadata + bytes metadata; +} + +/// A summary of validator's activity in the child subnet. This is submitted to the parent for reward distribution. +struct ActivitySummary { + /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. + uint256[2] blockRange; + ValidatorSummary[] activities; +} + +library LibActivitySummary { + function numValidators(ActivitySummary calldata self) internal pure returns(uint64) { + return uint64(self.activities.length); + } + + function commitment(ActivitySummary calldata self) internal pure returns(bytes32) { + return keccak256(abi.encode(self)); + } + + function containsValidator(ActivitySummary calldata self, address validator) internal pure returns(bool) { + uint256 len = self.activities.length; + for (uint256 i = 0; i < len; ) { + if (self.activities[i].validator == validator) { + return true; + } + + unchecked { + i++; + } + } + + return false; + } +} \ No newline at end of file diff --git a/contracts/contracts/reward/ValidatorRewardParentFacet.sol b/contracts/contracts/reward/ValidatorRewardParentFacet.sol new file mode 100644 index 000000000..5be88e76f --- /dev/null +++ b/contracts/contracts/reward/ValidatorRewardParentFacet.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {Pausable} from "../lib/LibPausable.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; +import {ValidatorSummary, ActivitySummary, LibActivitySummary} from "./ValidatorReward.sol"; +import {IValidatorRewarder} from "./IValidatorRewarder.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {SubnetID} from "../structs/Subnet.sol"; + +/// The validator reward facet for the parent subnet, i.e. for validators in the child subnet +/// to claim their reward in the parent subnet, which should be the current subnet this facet +/// is deployed. +contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { + using LibActivitySummary for ActivitySummary; + + /// Validators claim their reward for doing work in the child subnet + function claim(SubnetID calldata subnetId, ActivitySummary calldata summary) external nonReentrant whenNotPaused { + if (!summary.containsValidator(msg.sender)) { + revert NotValidator(msg.sender); + } + + // todo: check subnet is active + + ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); + + if (s.validatorRewarder == address(0)) { + handleRelay(); + } else { + handleDistribution(s, subnetId, summary); + } + } + + function handleRelay() internal pure { + revert("not implemented yet"); + } + + function handleDistribution(ValidatorRewardParentStorage storage s, SubnetID calldata subnetId, ActivitySummary calldata summary) internal { + bytes32 commitment = summary.commitment(); + + LibValidatorRewardParent.ensureValidCommitment(s, commitment, subnetId); + + LibValidatorRewardParent.validatorTryClaim(s, commitment, msg.sender); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, msg.sender, summary); + + LibValidatorRewardParent.tryPurgeCommitment(s, subnetId, commitment, summary.numValidators()); + } +} + +/// The activity summary commiment that is currently under reward distribution +struct RewardDistribution { + /// The checkpoint height that this distribution + uint64 checkpointHeight; + /// The list of validators that have claimed the reward + EnumerableSet.AddressSet claimed; +} + +/// Used by the SubnetActor to track the rewards for each validator +struct ValidatorRewardParentStorage { + /// @notice The contract address for validator rewarder + address validatorRewarder; + /// @notice Summaries pending to be processed. + /// If the validator rewarder is non-zero, these denote summaries presentable at this level. + /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. + /// Partitioned by subnet ID (hash), in the sequence they must be presented. + mapping(bytes32 => EnumerableSet.Bytes32Set) commitmentsToDistribution; + /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. + /// Only used if the validator rewarder is non-zero. + /// TODO(rewarder): optimize this pair of data structures. + mapping(bytes32 => RewardDistribution) distributions; +} + +library LibValidatorRewardParent { + bytes32 private constant NAMESPACE = keccak256("validator.reward.parent"); + + using SubnetIDHelper for SubnetID; + using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.AddressSet; + + function facetStorage() internal pure returns (ValidatorRewardParentStorage storage ds) { + bytes32 position = NAMESPACE; + assembly { + ds.slot := position + } + return ds; + } + + function listCommitments(ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId) internal view returns(bytes32[] memory) { + bytes32 subnetKey = subnetId.toHash(); + return ds.commitmentsToDistribution[subnetKey].values(); + } + + function initNewDistribution(bytes32 commitment, SubnetID calldata subnetId) internal { + ValidatorRewardParentStorage storage ds = facetStorage(); + + bytes32 subnetKey = subnetId.toHash(); + + if (ds.commitmentsToDistribution[subnetKey].contains(commitment)) { + revert CommitmentAlreadyInitialized(); + } + + ds.commitmentsToDistribution[subnetKey].add(commitment); + ds.distributions[commitment].checkpointHeight = uint64(block.number); + } + + function ensureValidCommitment(ValidatorRewardParentStorage storage ds, bytes32 commitment, SubnetID calldata subnetId) internal view { + bytes32 subnetKey = subnetId.toHash(); + + if (!ds.commitmentsToDistribution[subnetKey].contains(commitment)) { + revert SubnetNoTargetCommitment(); + } + + // Note: ideally we should check the commitment actually exists, but we dont have to as + // Note: the code will ensure if commitmentsToDistribution contains the commitment, + // Note: the commitment will have distribution + // if (ds.distributions[commitment].checkpointHeight == 0) { + // revert CommitmentNotFound(); + // } + } + + /// Validator tries to claim the reward. The validator can only claim the reward if the validator + /// has not claimed before + function validatorTryClaim(ValidatorRewardParentStorage storage ds, bytes32 commitment, address validator) internal { + if(ds.distributions[commitment].claimed.contains(validator)) { + revert ValidatorAlreadyClaimed(); + } + + ds.distributions[commitment].claimed.add(validator); + } + + /// Try to remove the commiment in the target subnet when ALL VALIDATORS HAVE CLAIMED. + function tryPurgeCommitment(ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId, bytes32 commitment, uint64 totalValidators) internal { + bytes32 subnetKey = subnetId.toHash(); + + if (ds.distributions[commitment].claimed.length() < totalValidators) { + return; + } + + ds.commitmentsToDistribution[subnetKey].remove(commitment); + delete ds.distributions[commitment]; + } +} \ No newline at end of file diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 8c96fb1b9..598eabdac 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ValidatorRewardCommitment} from "../reward/ValidatorReward.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -29,11 +30,8 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; - /// @dev A commitment to the summary of our chain activity since the previous checkpoint and this one. - bytes32 summary; - /// @dev Summaries relayed upwards from descendants of this subnet. - /// NOTE: Not merkelized to keep it simple, but we will merkelize later to scale better. - RelayedSummary[] relayedSummaries; + /// @dev The validator reward commitment from child subnet to parent subnet + ValidatorRewardCommitment validatorReward; } struct ActivitySummary { diff --git a/contracts/contracts/subnet/SubnetActorRewardFacet.sol b/contracts/contracts/subnet/SubnetActorRewardFacet.sol index 4862e352b..f34ed6f1f 100644 --- a/contracts/contracts/subnet/SubnetActorRewardFacet.sol +++ b/contracts/contracts/subnet/SubnetActorRewardFacet.sol @@ -14,17 +14,6 @@ import {Asset} from "../structs/Subnet.sol"; contract SubnetActorRewardFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using AssetHelper for Asset; - // TODO(rewards): add this function so that relayers can submit summaries to process reward payouts in the root network. - function submitSummary(SubnetID subnetId, ActivitySummary memory summary) external nonReentrant whenNotPaused { - // TODO(rewards): - // 1. Check that the subnet is active. - // 2. Check that the subnet has a non-zero ValidatorRewarder. - // 3. Hash the activity summary to get the commitment. - // 4. Validate that the commitment is pending and presentable, and validate that it matches the expected subnet. - // 5. Send the summary to the ValidatorRewarder#disburseRewards. - // 6. If OK (not reverted), drop the summary from the pending and presentable commitments. - } - /// @notice Validator claims their released collateral. function claim() external nonReentrant whenNotPaused { uint256 amount = LibStaking.claimCollateral(msg.sender); diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 84eaf5456..d826ff63f 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,6 +46,9 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; +import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; + + struct TestSubnetDefinition { GatewayDiamond gateway; address gatewayAddr; @@ -913,7 +916,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHeight: h, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(nextConfigNum))}) }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index adcdc5e96..4d3b853a6 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000fba0fa4d00000000000000000000000000000000000000000000000000000000dc749b0500000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000024ad232000000000000000000000000000000000000000000000000000000005e6de63200000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -97,7 +97,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000279979f5700000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002e72f09ca00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index 0759b98f0..5acf8954a 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,6 +39,8 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; +import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; + contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; using CrossMsgHelper for IpcEnvelope; @@ -1067,7 +1069,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: 0, blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1075,7 +1078,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // failed to create a checkpoint with zero membership weight @@ -1116,7 +1120,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: d, blockHash: keccak256("block"), nextConfigurationNumber: 2, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1139,7 +1144,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1160,7 +1166,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.prank(caller); @@ -1206,7 +1213,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.prank(caller); @@ -1226,7 +1234,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1234,7 +1243,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: 2 * gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block2"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1298,7 +1308,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1359,7 +1370,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1442,7 +1454,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1476,7 +1489,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1520,7 +1534,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // create a checkpoint @@ -1568,7 +1583,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: i * gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block"), nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1631,7 +1647,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 1, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 74f6223cf..3955f24a9 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,6 +33,9 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; +import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; + + contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; using CrossMsgHelper for IpcEnvelope; @@ -163,7 +166,8 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHash: blockhash(block.number), blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.prank(address(saDiamond)); @@ -221,7 +225,8 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHash: blockhash(block.number), blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 088f828fb..1ac22d585 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,6 +45,8 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; +import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; + contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; using CrossMsgHelper for IpcEnvelope; @@ -1348,7 +1350,8 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHeight: batch.blockHeight, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: batch.msgs + msgs: batch.msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1377,7 +1380,8 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHeight: e, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 85f57e135..442b653e9 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,6 +43,8 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; +import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; + contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; using FilAddress for address; @@ -688,7 +690,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -696,7 +699,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 1, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -796,7 +800,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 1, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -804,7 +809,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 1, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1))}) }); vm.deal(address(saDiamond), 100 ether); @@ -833,6 +839,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; + checkpoint.validatorReward = ValidatorRewardCommitment({ commitment: bytes32(uint256(2))}); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -888,7 +895,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 1, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1) )}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -900,7 +908,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 3, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(2))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -911,7 +920,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 2, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(3))}) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -922,7 +932,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 1, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(4))}) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -932,7 +943,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(5))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -945,7 +957,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 1, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(6))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -958,7 +971,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 2, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(7))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -971,7 +985,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 3, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(8))}) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -981,7 +996,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() * 2, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(9))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -994,7 +1010,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod() * 3, blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: new IpcEnvelope[](0) + msgs: new IpcEnvelope[](0), + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(10))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1035,7 +1052,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: saDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -1078,7 +1096,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHeight: 2 * saDiamond.getter().bottomUpCheckPeriod(), blockHash: keccak256("block2"), nextConfigurationNumber: 0, - msgs: msgs + msgs: msgs, + validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1))}) }); hash = keccak256(abi.encode(checkpoint)); diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 19890678d..04b40653e 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -110,6 +110,7 @@ where block_hash, next_configuration_number, msgs, + validatorReward: state.validator_reward().get_activities_summary()?.commitment(), }; // Save the checkpoint in the ledger. @@ -118,6 +119,8 @@ where .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) .context("failed to store checkpoint")?; + state.validator_reward().purge_activities()?; + // Figure out the power updates if there was some change in the configuration. let power_updates = if next_configuration_number == 0 { PowerUpdates(Vec::new()) diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index f53e630a9..65df21af6 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -186,6 +186,8 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { + state.validator_reward_mut().track_block_mined(); + // TODO: Consider doing this async, since it's purely informational and not consensus-critical. let _ = checkpoint::emit_trace_if_check_checkpoint_finalized(&self.gateway, &mut state) .inspect_err(|e| { diff --git a/fendermint/vm/reward/Cargo.toml b/fendermint/vm/reward/Cargo.toml new file mode 100644 index 000000000..40324a289 --- /dev/null +++ b/fendermint/vm/reward/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fendermint_vm_validater_reward" +description = "The top down checkpoint mechanism for ipc protocol integration" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +cid = { workspace = true } +ethers = { workspace = true, option = true } +fvm_shared = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +fendermint_crypto = {path = "../../crypto" } +fendermint_vm_event = { path = "../event" } +fendermint_tracing = { path = "../../tracing" } +ipc-observability = { workspace = true } \ No newline at end of file diff --git a/fendermint/vm/reward/src/lib.rs b/fendermint/vm/reward/src/lib.rs new file mode 100644 index 000000000..c628a40e8 --- /dev/null +++ b/fendermint/vm/reward/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::collections::HashMap; +use std::fmt::Debug; +use fvm_shared::clock::ChainEpoch; +use fendermint_crypto::PublicKey; + +pub struct BlockMined { + validator: PublicKey, + height: ChainEpoch, +} + +#[derive(Debug, Clone)] +pub struct ActivitySummary { + block_range: (ChainEpoch, ChainEpoch), + details: HashMap +} + +/// Tracks the validator activities in the current blockchain +pub trait ValidatorActivityTracker { + type ValidatorSummaryDetail: Clone + Debug + TryInto>; + + /// Mark the validator has mined the target block. + fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()>; + + /// Get the validators activities summary since the checkpoint height + fn get_activities_summary(&self) -> anyhow::Result>; + + /// Purge the current validator activities summary + fn purge_activities(&mut self) -> anyhow::Result<()>; +} \ No newline at end of file From 80eabcb34dbd3f156faa2fc2a37f803e063c0def Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 8 Oct 2024 21:57:39 +0800 Subject: [PATCH 068/111] merkle proof --- Cargo.lock | 41 +++--- Cargo.toml | 2 + contracts/binding/src/lib.rs | 61 ++++++++ .../Activity.sol} | 8 +- .../IValidatorRewarder.sol | 4 +- .../activities/LibActivityMerkleVerifier.sol | 19 +++ .../ValidatorActivityTracker.sol | 4 +- .../ValidatorRewardParentFacet.sol | 135 +++++++++++++----- contracts/contracts/errors/IPCErrors.sol | 1 + .../gateway/router/CheckpointingFacet.sol | 5 +- contracts/contracts/structs/CrossNet.sol | 6 +- contracts/test/IntegrationTestBase.sol | 4 +- .../test/integration/GatewayDiamond.t.sol | 32 ++--- .../integration/GatewayDiamondToken.t.sol | 6 +- contracts/test/integration/MultiSubnet.t.sol | 6 +- .../test/integration/SubnetActorDiamond.t.sol | 36 ++--- fendermint/actors/activity-tracker/Cargo.toml | 35 +++++ fendermint/actors/activity-tracker/src/lib.rs | 86 +++++++++++ .../actors/activity-tracker/src/state.rs | 81 +++++++++++ .../src/activities/mod.rs} | 3 + .../vm/interpreter/src/fvm/checkpoint.rs | 7 - fendermint/vm/interpreter/src/lib.rs | 1 + fendermint/vm/reward/Cargo.toml | 21 --- 23 files changed, 462 insertions(+), 142 deletions(-) rename contracts/contracts/{reward/ValidatorReward.sol => activities/Activity.sol} (89%) rename contracts/contracts/{reward => activities}/IValidatorRewarder.sol (81%) create mode 100644 contracts/contracts/activities/LibActivityMerkleVerifier.sol rename contracts/contracts/{reward => activities}/ValidatorActivityTracker.sol (95%) rename contracts/contracts/{reward => activities}/ValidatorRewardParentFacet.sol (53%) create mode 100644 fendermint/actors/activity-tracker/Cargo.toml create mode 100644 fendermint/actors/activity-tracker/src/lib.rs create mode 100644 fendermint/actors/activity-tracker/src/state.rs rename fendermint/vm/{reward/src/lib.rs => interpreter/src/activities/mod.rs} (90%) delete mode 100644 fendermint/vm/reward/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 1c4886caf..177a9922b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2777,6 +2777,27 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "fendermint_actor_activity_tracker" +version = "0.1.0" +dependencies = [ + "anyhow", + "cid", + "fil_actor_eam", + "fil_actors_evm_shared", + "fil_actors_runtime", + "frc42_dispatch", + "fvm_ipld_blockstore", + "fvm_ipld_encoding", + "fvm_shared", + "hex-literal 0.4.1", + "log", + "multihash 0.18.1", + "num-derive 0.3.3", + "num-traits", + "serde", +] + [[package]] name = "fendermint_actor_chainmetadata" version = "0.1.0" @@ -3472,26 +3493,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "fendermint_vm_validater_reward" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "cid", - "ethers", - "fendermint_crypto", - "fendermint_tracing", - "fendermint_vm_event", - "fvm_ipld_encoding", - "fvm_shared", - "hex", - "ipc-observability", - "serde", - "thiserror", - "tracing", -] - [[package]] name = "ff" version = "0.12.1" diff --git a/Cargo.toml b/Cargo.toml index ccc3db09b..a91fa5cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "fendermint/vm/*", "fendermint/actors", "fendermint/actors/chainmetadata", + "fendermint/actors/activity-tracker", ] [workspace.package] @@ -217,6 +218,7 @@ fil_actor_eam = { git = "https://github.com/filecoin-project/builtin-actors", ta fil_actors_runtime = { git = "https://github.com/filecoin-project/builtin-actors", tag = "v12.0.0" } fendermint_actor_eam = { path = "./fendermint/actors/eam" } +fendermint_actor_activity_tracker= { path = "./fendermint/actors/activity-tracker" } cid = { version = "0.10.1", default-features = false, features = [ "serde-codec", diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index 508f367f3..dd59524fb 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -1,3 +1,64 @@ // DO NOT EDIT! This file was generated by build.rs #[macro_use] mod convert; +#[allow(clippy::all)] +pub mod checkpointing_facet; +#[allow(clippy::all)] +pub mod diamond_cut_facet; +#[allow(clippy::all)] +pub mod diamond_loupe_facet; +#[allow(clippy::all)] +pub mod gateway_diamond; +#[allow(clippy::all)] +pub mod gateway_getter_facet; +#[allow(clippy::all)] +pub mod gateway_manager_facet; +#[allow(clippy::all)] +pub mod gateway_messenger_facet; +#[allow(clippy::all)] +pub mod i_diamond; +#[allow(clippy::all)] +pub mod lib_gateway; +#[allow(clippy::all)] +pub mod lib_quorum; +#[allow(clippy::all)] +pub mod lib_staking; +#[allow(clippy::all)] +pub mod lib_staking_change_log; +#[allow(clippy::all)] +pub mod ownership_facet; +#[allow(clippy::all)] +pub mod register_subnet_facet; +#[allow(clippy::all)] +pub mod subnet_actor_checkpointing_facet; +#[allow(clippy::all)] +pub mod subnet_actor_diamond; +#[allow(clippy::all)] +pub mod subnet_actor_getter_facet; +#[allow(clippy::all)] +pub mod subnet_actor_manager_facet; +#[allow(clippy::all)] +pub mod subnet_actor_pause_facet; +#[allow(clippy::all)] +pub mod subnet_actor_reward_facet; +#[allow(clippy::all)] +pub mod subnet_getter_facet; +#[allow(clippy::all)] +pub mod subnet_registry_diamond; +#[allow(clippy::all)] +pub mod top_down_finality_facet; +#[allow(clippy::all)] +pub mod xnet_messaging_facet; + +// The list of contracts need to convert FvmAddress to fvm_shared::Address +fvm_address_conversion!(gateway_manager_facet); +fvm_address_conversion!(gateway_getter_facet); +fvm_address_conversion!(xnet_messaging_facet); +fvm_address_conversion!(gateway_messenger_facet); +fvm_address_conversion!(subnet_actor_checkpointing_facet); +fvm_address_conversion!(subnet_actor_getter_facet); +fvm_address_conversion!(lib_gateway); + +// The list of contracts that need to convert common types between each other +common_type_conversion!(subnet_actor_getter_facet, checkpointing_facet); +common_type_conversion!(subnet_actor_getter_facet, xnet_messaging_facet); diff --git a/contracts/contracts/reward/ValidatorReward.sol b/contracts/contracts/activities/Activity.sol similarity index 89% rename from contracts/contracts/reward/ValidatorReward.sol rename to contracts/contracts/activities/Activity.sol index e880cb130..a9792598e 100644 --- a/contracts/contracts/reward/ValidatorReward.sol +++ b/contracts/contracts/activities/Activity.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -/// The commitment for the validator rewards that child subnet submits to the parent subnet +/// The commitments for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint -struct ValidatorRewardCommitment { - /// The commitment for the parent subnet - bytes32 commitment; +struct ActivityCommitment { + /// The activity summary for validators + bytes32 summary; // TODO: add relayed rewarder commitment } diff --git a/contracts/contracts/reward/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol similarity index 81% rename from contracts/contracts/reward/IValidatorRewarder.sol rename to contracts/contracts/activities/IValidatorRewarder.sol index 3a3e8e3de..8468ea304 100644 --- a/contracts/contracts/reward/IValidatorRewarder.sol +++ b/contracts/contracts/activities/IValidatorRewarder.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -import {ActivitySummary} from "./ValidatorReward.sol"; +import {ValidatorSummary} from "./Activity.sol"; /// @title ValidatorRewarder interface. /// @@ -14,5 +14,5 @@ interface IValidatorRewarder { /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and /// disburse any relevant rewards. /// @dev This method should revert if the summary is invalid; this will cause the - function disburseRewards(SubnetID calldata id, address validator, ActivitySummary calldata summary) external; + function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external; } diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol new file mode 100644 index 000000000..4c98c085b --- /dev/null +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; +import {InvalidProof} from "../errors/IPCErrors.sol"; +import {ValidatorSummary} from "./Activity.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +/// Verifies the proof to the commitment in subnet activity summary +library LibActivityMerkleVerifier { + function ensureValidProof(bytes32 commitment, ValidatorSummary calldata summary, bytes32[] calldata proof) internal pure { + // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(summary.validator, summary.blocksCommitted, summary.metadata)))); + bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); + if (!valid) { + revert InvalidProof(); + } + } +} diff --git a/contracts/contracts/reward/ValidatorActivityTracker.sol b/contracts/contracts/activities/ValidatorActivityTracker.sol similarity index 95% rename from contracts/contracts/reward/ValidatorActivityTracker.sol rename to contracts/contracts/activities/ValidatorActivityTracker.sol index b0c1344e5..e0bd03228 100644 --- a/contracts/contracts/reward/ValidatorActivityTracker.sol +++ b/contracts/contracts/activities/ValidatorActivityTracker.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ValidatorSummary, ActivitySummary} from "./ValidatorReward.sol"; +import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; import {SystemContract} from "../lib/LibGatewayActorStorage.sol"; @@ -54,7 +54,7 @@ contract ValidatorActivityTracker is SystemContract { /// Reads the current validator summary and purge the data accordingly /// @dev Call this method only when bottom up checkpoint needs to be created - function purgeActivities() external systemActorOnly { + function purge_activities() external systemActorOnly { // prepare the activities uint256 num_validators = validators.length(); diff --git a/contracts/contracts/reward/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardParentFacet.sol similarity index 53% rename from contracts/contracts/reward/ValidatorRewardParentFacet.sol rename to contracts/contracts/activities/ValidatorRewardParentFacet.sol index 5be88e76f..e7e7b9ebc 100644 --- a/contracts/contracts/reward/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardParentFacet.sol @@ -2,14 +2,16 @@ pragma solidity ^0.8.23; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; -import {ValidatorSummary, ActivitySummary, LibActivitySummary} from "./ValidatorReward.sol"; +import {ValidatorSummary, ActivitySummary, LibActivitySummary} from "./Activity.sol"; import {IValidatorRewarder} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; +import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; /// The validator reward facet for the parent subnet, i.e. for validators in the child subnet /// to claim their reward in the parent subnet, which should be the current subnet this facet @@ -18,35 +20,46 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { using LibActivitySummary for ActivitySummary; /// Validators claim their reward for doing work in the child subnet - function claim(SubnetID calldata subnetId, ActivitySummary calldata summary) external nonReentrant whenNotPaused { - if (!summary.containsValidator(msg.sender)) { + function claim( + SubnetID calldata subnetId, + uint64 checkpointHeight, + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) external nonReentrant whenNotPaused { + // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight + // note: will never exist. + + if (msg.sender != summary.validator) { revert NotValidator(msg.sender); } - // todo: check subnet is active - ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); if (s.validatorRewarder == address(0)) { - handleRelay(); - } else { - handleDistribution(s, subnetId, summary); + return handleRelay(); } + + bytes32 commitment = LibValidatorRewardParent.ensureValidCommitment(s, subnetId, checkpointHeight); + LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); + + handleDistribution(s, subnetId, commitment, summary); + } function handleRelay() internal pure { revert("not implemented yet"); } - function handleDistribution(ValidatorRewardParentStorage storage s, SubnetID calldata subnetId, ActivitySummary calldata summary) internal { - bytes32 commitment = summary.commitment(); - - LibValidatorRewardParent.ensureValidCommitment(s, commitment, subnetId); + function handleDistribution( + ValidatorRewardParentStorage storage s, + SubnetID calldata subnetId, + bytes32 commitment, + ValidatorSummary calldata summary + ) internal { + LibValidatorRewardParent.validatorTryClaim(s, commitment, summary.validator); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); - LibValidatorRewardParent.validatorTryClaim(s, commitment, msg.sender); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, msg.sender, summary); - - LibValidatorRewardParent.tryPurgeCommitment(s, subnetId, commitment, summary.numValidators()); + // LibValidatorRewardParent.tryPurgeCommitment(s, subnetId, commitment, summary.numValidators()); } } @@ -54,6 +67,8 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { struct RewardDistribution { /// The checkpoint height that this distribution uint64 checkpointHeight; + /// Total number of valdators to claim the distribution + uint64 totalValidators; /// The list of validators that have claimed the reward EnumerableSet.AddressSet claimed; } @@ -62,63 +77,100 @@ struct RewardDistribution { struct ValidatorRewardParentStorage { /// @notice The contract address for validator rewarder address validatorRewarder; - /// @notice Summaries pending to be processed. + /// @notice Summaries look up pending to be processed. /// If the validator rewarder is non-zero, these denote summaries presentable at this level. /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. - /// Partitioned by subnet ID (hash), in the sequence they must be presented. - mapping(bytes32 => EnumerableSet.Bytes32Set) commitmentsToDistribution; + /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment + mapping(bytes32 => EnumerableMap.Bytes32ToBytes32Map) commitments; /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. /// Only used if the validator rewarder is non-zero. - /// TODO(rewarder): optimize this pair of data structures. mapping(bytes32 => RewardDistribution) distributions; } +/// The payload for list commitments query +struct ListCommimentDetail { + /// The child subnet checkpoint height + uint64 checkpointHeight; + /// The actual commiment of the activities + bytes32 commitment; +} + library LibValidatorRewardParent { bytes32 private constant NAMESPACE = keccak256("validator.reward.parent"); using SubnetIDHelper for SubnetID; - using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; using EnumerableSet for EnumerableSet.AddressSet; - function facetStorage() internal pure returns (ValidatorRewardParentStorage storage ds) { - bytes32 position = NAMESPACE; - assembly { - ds.slot := position - } - return ds; - } + // =========== External library functions ============= + + function listCommitments(SubnetID calldata subnetId) internal view returns(ListCommimentDetail[] memory listDetails) { + ValidatorRewardParentStorage storage ds = facetStorage(); - function listCommitments(ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId) internal view returns(bytes32[] memory) { bytes32 subnetKey = subnetId.toHash(); - return ds.commitmentsToDistribution[subnetKey].values(); + + uint256 size = ds.commitments[subnetKey].length(); + listDetails = new ListCommimentDetail[](size); + + for (uint256 i = 0; i < size; ) { + (bytes32 heightBytes32, bytes32 commitment) = ds.commitments[subnetKey].at(i); + + listDetails[i] = ListCommimentDetail({ + checkpointHeight: uint64(uint256(heightBytes32)), + commitment: commitment + }); + + unchecked { + i++; + } + } + + return listDetails; } - function initNewDistribution(bytes32 commitment, SubnetID calldata subnetId) internal { + function initNewDistribution(uint64 checkpointHeight, bytes32 commitment, SubnetID calldata subnetId) internal { ValidatorRewardParentStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); - if (ds.commitmentsToDistribution[subnetKey].contains(commitment)) { + if (ds.distributions[commitment].checkpointHeight != 0) { revert CommitmentAlreadyInitialized(); } - ds.commitmentsToDistribution[subnetKey].add(commitment); - ds.distributions[commitment].checkpointHeight = uint64(block.number); + ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); + ds.distributions[commitment].checkpointHeight = checkpointHeight; } - function ensureValidCommitment(ValidatorRewardParentStorage storage ds, bytes32 commitment, SubnetID calldata subnetId) internal view { + // ============ Internal library functions ============ + + function facetStorage() internal pure returns (ValidatorRewardParentStorage storage ds) { + bytes32 position = NAMESPACE; + assembly { + ds.slot := position + } + return ds; + } + + function ensureValidCommitment( + ValidatorRewardParentStorage storage ds, + SubnetID calldata subnetId, + uint64 checkpointHeight + ) internal view returns(bytes32) { bytes32 subnetKey = subnetId.toHash(); - if (!ds.commitmentsToDistribution[subnetKey].contains(commitment)) { + (bool exists, bytes32 commitment) = ds.commitments[subnetKey].tryGet(bytes32(uint256(checkpointHeight))); + if (!exists) { revert SubnetNoTargetCommitment(); } // Note: ideally we should check the commitment actually exists, but we dont have to as - // Note: the code will ensure if commitmentsToDistribution contains the commitment, + // Note: the code will ensure if commitments contains the commitment, // Note: the commitment will have distribution // if (ds.distributions[commitment].checkpointHeight == 0) { // revert CommitmentNotFound(); // } + + return commitment; } /// Validator tries to claim the reward. The validator can only claim the reward if the validator @@ -132,14 +184,19 @@ library LibValidatorRewardParent { } /// Try to remove the commiment in the target subnet when ALL VALIDATORS HAVE CLAIMED. - function tryPurgeCommitment(ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId, bytes32 commitment, uint64 totalValidators) internal { + function tryPurgeCommitment( + ValidatorRewardParentStorage storage ds, + SubnetID calldata subnetId, + bytes32 commitment, + uint64 totalValidators + ) internal { bytes32 subnetKey = subnetId.toHash(); if (ds.distributions[commitment].claimed.length() < totalValidators) { return; } - ds.commitmentsToDistribution[subnetKey].remove(commitment); + delete ds.commitments[subnetKey]; delete ds.distributions[commitment]; } } \ No newline at end of file diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index cd629c07d..f65028d14 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -83,6 +83,7 @@ error ValidatorPowerChangeDenied(); error CommitmentAlreadyInitialized(); error SubnetNoTargetCommitment(); error ValidatorAlreadyClaimed(); +error InvalidProof(); enum InvalidXnetMessageReason { diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index 91f54d2cb..b4df27757 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -16,7 +16,7 @@ import {BatchNotCreated, InvalidBatchEpoch, BatchAlreadyExists, NotEnoughSubnetC import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {LibValidatorRewardParent} from "../../reward/ValidatorRewardParentFacet.sol"; +import {LibValidatorRewardParent} from "../../activities/ValidatorRewardParentFacet.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -44,7 +44,8 @@ contract CheckpointingFacet is GatewayActorModifiers { execBottomUpMsgs(checkpoint.msgs, subnet); LibValidatorRewardParent.initNewDistribution( - checkpoint.validatorReward.commitment, + uint64(checkpoint.blockHeight), + checkpoint.activities.summary, checkpoint.subnetID ); } diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 598eabdac..c11d21236 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ValidatorRewardCommitment} from "../reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../activities/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -30,8 +30,8 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; - /// @dev The validator reward commitment from child subnet to parent subnet - ValidatorRewardCommitment validatorReward; + /// @dev The activity commitment from child subnet to parent subnet + ActivityCommitment activities; } struct ActivitySummary { diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index d826ff63f..967319f1a 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,7 +46,7 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; struct TestSubnetDefinition { @@ -917,7 +917,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(nextConfigNum))}) + activities: ActivityCommitment({ summary: bytes32(uint256(nextConfigNum))}) }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index 5acf8954a..d9e45d059 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -1070,7 +1070,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1079,7 +1079,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // failed to create a checkpoint with zero membership weight @@ -1121,7 +1121,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1145,7 +1145,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1167,7 +1167,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.prank(caller); @@ -1214,7 +1214,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.prank(caller); @@ -1235,7 +1235,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1244,7 +1244,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1309,7 +1309,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1371,7 +1371,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1455,7 +1455,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1490,7 +1490,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1535,7 +1535,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // create a checkpoint @@ -1584,7 +1584,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1648,7 +1648,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 3955f24a9..f191f152b 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,7 +33,7 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { @@ -167,7 +167,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.prank(address(saDiamond)); @@ -226,7 +226,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 1ac22d585..5fdee611f 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,7 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -1351,7 +1351,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1381,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 442b653e9..64ada4509 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ValidatorRewardCommitment} from "../../contracts/reward/ValidatorReward.sol"; +import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -691,7 +691,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -700,7 +700,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -801,7 +801,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -810,7 +810,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1))}) + activities: ActivityCommitment({ summary: bytes32(uint256(1))}) }); vm.deal(address(saDiamond), 100 ether); @@ -839,7 +839,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.validatorReward = ValidatorRewardCommitment({ commitment: bytes32(uint256(2))}); + checkpoint.activities = ActivityCommitment({ summary: bytes32(uint256(2))}); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -896,7 +896,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1) )}) + activities: ActivityCommitment({ summary: bytes32(uint256(1) )}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -909,7 +909,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(2))}) + activities: ActivityCommitment({ summary: bytes32(uint256(2))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -921,7 +921,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(3))}) + activities: ActivityCommitment({ summary: bytes32(uint256(3))}) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -933,7 +933,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(4))}) + activities: ActivityCommitment({ summary: bytes32(uint256(4))}) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -944,7 +944,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(5))}) + activities: ActivityCommitment({ summary: bytes32(uint256(5))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -958,7 +958,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(6))}) + activities: ActivityCommitment({ summary: bytes32(uint256(6))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -972,7 +972,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(7))}) + activities: ActivityCommitment({ summary: bytes32(uint256(7))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -986,7 +986,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(8))}) + activities: ActivityCommitment({ summary: bytes32(uint256(8))}) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -997,7 +997,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(9))}) + activities: ActivityCommitment({ summary: bytes32(uint256(9))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1011,7 +1011,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(10))}) + activities: ActivityCommitment({ summary: bytes32(uint256(10))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1053,7 +1053,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(0)}) + activities: ActivityCommitment({ summary: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -1097,7 +1097,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - validatorReward: ValidatorRewardCommitment({ commitment: bytes32(uint256(1))}) + activities: ActivityCommitment({ summary: bytes32(uint256(1))}) }); hash = keccak256(abi.encode(checkpoint)); diff --git a/fendermint/actors/activity-tracker/Cargo.toml b/fendermint/actors/activity-tracker/Cargo.toml new file mode 100644 index 000000000..886262036 --- /dev/null +++ b/fendermint/actors/activity-tracker/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "fendermint_actor_activity_tracker" +description = "Tracks fendermint block mining activities" +license.workspace = true +edition.workspace = true +authors.workspace = true +version = "0.1.0" + +[lib] +## lib is necessary for integration tests +## cdylib is necessary for Wasm build +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = { workspace = true } +cid = { workspace = true } +fil_actor_eam = { workspace = true } +fil_actors_runtime = { workspace = true } +fvm_ipld_blockstore = { workspace = true } +fvm_ipld_encoding = { workspace = true } +fvm_shared = { workspace = true } +log = { workspace = true } +multihash = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true } +hex-literal = { workspace = true } +frc42_dispatch = { workspace = true } + +[dev-dependencies] +fil_actors_evm_shared = { workspace = true } +fil_actors_runtime = { workspace = true, features = ["test_utils"] } + +[features] +fil-actor = ["fil_actors_runtime/fil-actor"] diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs new file mode 100644 index 000000000..7f9d89501 --- /dev/null +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -0,0 +1,86 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fil_actors_runtime::runtime::{ActorCode, Runtime}; +use fil_actors_runtime::{actor_dispatch, ActorError}; +use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::address::Address; +use fvm_shared::{ActorID, MethodNum}; +use num_derive::FromPrimitive; + +pub use crate::state::{ValidatorSummary}; +use crate::state::State; + +mod state; + +#[cfg(feature = "fil-actor")] +fil_actors_runtime::wasm_trampoline!(ActivityTrackerActor); + +pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity"; + +pub struct ActivityTrackerActor; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct BlockedMinedParams { + validator: Address, +} + +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum Method { + BlockMined = frc42_dispatch::method_hash!("BlockMined"), + GetActivities = frc42_dispatch::method_hash!("GetActivities"), + PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), +} + +impl ActivityTrackerActor { + pub fn constructor(rt: &impl Runtime) -> Result<(), ActorError> { + let st = State::new(rt.store())?; + rt.create(&st)?; + + Ok(()) + } + + pub fn block_mined(rt: &impl Runtime, block: BlockedMinedParams) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut State, rt| { + st.incr_validator_block_committed(rt, &block.validator) + })?; + + Ok(()) + } + + pub fn purge_activities(rt: &impl Runtime) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut State, rt| { + st.purge_validator_block_committed(rt)?; + st.reset_start_height(rt) + })?; + + Ok(()) + } + + pub fn get_activities(rt: &impl Runtime) -> Result, ActorError> { + let state: State = rt.state()?; + state.validator_activities(rt) + } + +} + +impl ActorCode for ActivityTrackerActor { + type Methods = Method; + + fn name() -> &'static str { + CHAINMETADATA_ACTOR_NAME + } + + actor_dispatch! { + Constructor => constructor, + PushBlockHash => push_block_hash, + LookbackLen => lookback_len, + GetBlockHash => get_block_hash, + } +} diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs new file mode 100644 index 000000000..299d59649 --- /dev/null +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -0,0 +1,81 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::collections::HashMap; +use cid::Cid; +use fil_actors_runtime::runtime::Runtime; +use fil_actors_runtime::{ActorError, Map2, DEFAULT_HAMT_CONFIG}; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::address::Address; +use fvm_shared::ActorID; +use fvm_shared::clock::ChainEpoch; +use serde::{Deserialize, Serialize}; + +pub type BlockCommittedMap = Map2; +pub type BlockCommitted = u64; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ValidatorSummary { + validator: Address, + block_committed: BlockCommitted, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct State { + pub start_height: ChainEpoch, + pub blocks_committed: Cid, // BlockCommittedMap +} + +impl State { + pub fn new( + store: &BS, + ) -> Result { + let mut deployers_map = BlockCommittedMap::empty(store, DEFAULT_HAMT_CONFIG, "empty"); + Ok(State { start_height: 0, blocks_committed: deployers_map.flush()? }) + } + + pub fn reset_start_height(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { + self.start_height = rt.curr_epoch(); + Ok(()) + } + + pub fn purge_validator_block_committed(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { + let all_validators = self.validator_activities(rt)?; + let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + + for (v, _) in all_validators { + validators.delete(&v)?; + } + + self.blocks_committed = validators.flush()?; + + Ok(()) + } + + pub fn incr_validator_block_committed(&self, rt: &impl Runtime, validator: &Address) -> Result<(), ActorError> { + let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + + let v = if let Some(v) = validators.get(validator)? { + *v + 1 + } else { + 1 + }; + + validators.set(validator, v)?; + + Ok(()) + } + + pub fn validator_activities(&self, rt: &impl Runtime) -> Result, ActorError> { + let mut result = vec![]; + + let validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + validators.for_each(|(k, v)| { + result.push(ValidatorSummary{ validator: k, block_committed: v}); + Ok(()) + })?; + + Ok(result) + } +} + diff --git a/fendermint/vm/reward/src/lib.rs b/fendermint/vm/interpreter/src/activities/mod.rs similarity index 90% rename from fendermint/vm/reward/src/lib.rs rename to fendermint/vm/interpreter/src/activities/mod.rs index c628a40e8..280de1aeb 100644 --- a/fendermint/vm/reward/src/lib.rs +++ b/fendermint/vm/interpreter/src/activities/mod.rs @@ -1,6 +1,9 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +//! Tracks the current blockchain block mining activities and propagates to the parent subnet if +//! needed. + use std::collections::HashMap; use std::fmt::Debug; use fvm_shared::clock::ChainEpoch; diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 04b40653e..f3791f36c 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -96,13 +96,6 @@ where let num_msgs = msgs.len(); - // TODO(rewards): query block producers for the blocks from the last checkpointed epoch to the current one. - // Ideally keep a live cache of block producers, append to it when new blocks are committed, and prune it when generating a checkpoint. - // But for now, we can try to keep it simple and query CometBFT, although that adds latency. - // If we do this, this method seems to be the quickest way: https://docs.cometbft.com/main/rpc/#/Info/block_search - - // TODO(rewards): populate the ActivitySummary struct with the information above, and pass it to the create_bottom_up_checkpoint call. - // Construct checkpoint. let checkpoint = BottomUpCheckpoint { subnet_id, diff --git a/fendermint/vm/interpreter/src/lib.rs b/fendermint/vm/interpreter/src/lib.rs index 40868b6cd..deaa6ed11 100644 --- a/fendermint/vm/interpreter/src/lib.rs +++ b/fendermint/vm/interpreter/src/lib.rs @@ -10,6 +10,7 @@ pub mod signed; #[cfg(feature = "arb")] mod arb; +mod activities; /// Prepare and process transaction proposals. #[async_trait] diff --git a/fendermint/vm/reward/Cargo.toml b/fendermint/vm/reward/Cargo.toml deleted file mode 100644 index 40324a289..000000000 --- a/fendermint/vm/reward/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "fendermint_vm_validater_reward" -description = "The top down checkpoint mechanism for ipc protocol integration" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -anyhow = { workspace = true } -async-trait = { workspace = true } -cid = { workspace = true } -ethers = { workspace = true, option = true } -fvm_shared = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -fendermint_crypto = {path = "../../crypto" } -fendermint_vm_event = { path = "../event" } -fendermint_tracing = { path = "../../tracing" } -ipc-observability = { workspace = true } \ No newline at end of file From 59085f5345fbb6ba9500ca4244199c40cd1bcfe6 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 10 Oct 2024 16:11:49 +0800 Subject: [PATCH 069/111] call state from exec state --- fendermint/vm/interpreter/src/fvm/externs.rs | 11 ++++ .../vm/interpreter/src/fvm/state/exec.rs | 54 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/fendermint/vm/interpreter/src/fvm/externs.rs b/fendermint/vm/interpreter/src/fvm/externs.rs index f17e03f68..9f1b4ad48 100644 --- a/fendermint/vm/interpreter/src/fvm/externs.rs +++ b/fendermint/vm/interpreter/src/fvm/externs.rs @@ -36,6 +36,17 @@ where } } +impl FendermintExterns + where + DB: Blockstore + 'static + Clone,{ + pub fn read_only_clone(&self) -> FendermintExterns> { + FendermintExterns { + blockstore: ReadOnlyBlockstore::new(self.blockstore.clone()), + state_root: self.state_root, + } + } +} + impl Rand for FendermintExterns where DB: Blockstore + 'static, diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 7d5f10dda..fb20f72ae 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -1,6 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use anyhow::Ok; @@ -14,6 +15,7 @@ use fvm::{ state_tree::StateTree, DefaultKernel, }; +use fvm::engine::EnginePool; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::RawBytes; use fvm_shared::{ @@ -26,6 +28,7 @@ use serde_with::serde_as; use crate::fvm::externs::FendermintExterns; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; +use crate::fvm::store::ReadOnlyBlockstore; pub type BlockHash = [u8; 32]; @@ -113,6 +116,8 @@ where /// Indicate whether the parameters have been updated. params_dirty: bool, + + executor_info: ExecutorInfo, } impl FvmExecState @@ -145,8 +150,8 @@ where let engine = multi_engine.get(&nc)?; let externs = FendermintExterns::new(blockstore.clone(), params.state_root); - let machine = DefaultMachine::new(&mc, blockstore, externs)?; - let executor = DefaultExecutor::new(engine, machine)?; + let machine = DefaultMachine::new(&mc, blockstore.clone(), externs)?; + let executor = DefaultExecutor::new(engine.clone(), machine)?; Ok(Self { executor, @@ -159,6 +164,10 @@ where power_scale: params.power_scale, }, params_dirty: false, + executor_info: ExecutorInfo { + engine_pool: engine, + store: blockstore.clone(), + }, }) } @@ -310,6 +319,21 @@ where f(&mut self.params); self.params_dirty = true; } + + pub fn call_state(&self) -> anyhow::Result> { + let externs = self.executor.externs().read_only_clone(); + let machine = DefaultMachine::new( + self.executor.context(), + ReadOnlyBlockstore::new(self.executor_info.store.clone()), + externs + )?; + + Ok(FvmCallState { + executor: RefCell::new( + DefaultExecutor::new(self.executor_info.engine_pool.clone(), machine)? + ) + }) + } } impl HasChainID for FvmExecState @@ -350,3 +374,29 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { }; (ret, Default::default()) } + +/// Tracks the metadata about the executor, so that it can be used to clone itself or create call state +struct ExecutorInfo { + engine_pool: EnginePool, + store: DB, +} + +type CallExecutor = DefaultExecutor< + DefaultKernel, FendermintExterns>>>>, +>; + +/// A state we create for the calling the getters through fvm +pub struct FvmCallState + where + DB: Blockstore + Clone + 'static, +{ + executor: RefCell>, +} + +impl FvmCallState { + pub fn call(&self, msg: Message) -> anyhow::Result { + let mut inner = self.executor.borrow_mut(); + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + Ok(inner.execute_message(msg, ApplyKind::Implicit, raw_length)?) + } +} \ No newline at end of file From 558bf1972c3f7578659ca2ec6da18f15feda5953 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 10 Oct 2024 18:26:58 +0800 Subject: [PATCH 070/111] fmt --- fendermint/vm/interpreter/src/fvm/externs.rs | 7 ++--- .../vm/interpreter/src/fvm/state/exec.rs | 27 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/fendermint/vm/interpreter/src/fvm/externs.rs b/fendermint/vm/interpreter/src/fvm/externs.rs index 9f1b4ad48..7f02b0611 100644 --- a/fendermint/vm/interpreter/src/fvm/externs.rs +++ b/fendermint/vm/interpreter/src/fvm/externs.rs @@ -36,9 +36,10 @@ where } } -impl FendermintExterns - where - DB: Blockstore + 'static + Clone,{ +impl FendermintExterns +where + DB: Blockstore + 'static + Clone, +{ pub fn read_only_clone(&self) -> FendermintExterns> { FendermintExterns { blockstore: ReadOnlyBlockstore::new(self.blockstore.clone()), diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index fb20f72ae..ff984b1c6 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -7,6 +7,7 @@ use std::collections::{HashMap, HashSet}; use anyhow::Ok; use cid::Cid; use fendermint_vm_genesis::PowerScale; +use fvm::engine::EnginePool; use fvm::{ call_manager::DefaultCallManager, engine::MultiEngine, @@ -15,7 +16,6 @@ use fvm::{ state_tree::StateTree, DefaultKernel, }; -use fvm::engine::EnginePool; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::RawBytes; use fvm_shared::{ @@ -26,9 +26,9 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use crate::fvm::externs::FendermintExterns; +use crate::fvm::store::ReadOnlyBlockstore; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; -use crate::fvm::store::ReadOnlyBlockstore; pub type BlockHash = [u8; 32]; @@ -325,13 +325,14 @@ where let machine = DefaultMachine::new( self.executor.context(), ReadOnlyBlockstore::new(self.executor_info.store.clone()), - externs + externs, )?; Ok(FvmCallState { - executor: RefCell::new( - DefaultExecutor::new(self.executor_info.engine_pool.clone(), machine)? - ) + executor: RefCell::new(DefaultExecutor::new( + self.executor_info.engine_pool.clone(), + machine, + )?), }) } } @@ -382,21 +383,25 @@ struct ExecutorInfo { } type CallExecutor = DefaultExecutor< - DefaultKernel, FendermintExterns>>>>, + DefaultKernel< + DefaultCallManager< + DefaultMachine, FendermintExterns>>, + >, + >, >; /// A state we create for the calling the getters through fvm pub struct FvmCallState - where - DB: Blockstore + Clone + 'static, +where + DB: Blockstore + Clone + 'static, { executor: RefCell>, } -impl FvmCallState { +impl FvmCallState { pub fn call(&self, msg: Message) -> anyhow::Result { let mut inner = self.executor.borrow_mut(); let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; Ok(inner.execute_message(msg, ApplyKind::Implicit, raw_length)?) } -} \ No newline at end of file +} From c78383ede8709fa8335e45d5c5c4f956bb20e739 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 11 Oct 2024 17:04:25 +0800 Subject: [PATCH 071/111] integration tests for validator rewards --- Cargo.lock | 1 + contracts/binding/build.rs | 1 + contracts/binding/src/lib.rs | 2 +- contracts/contracts/GatewayDiamond.sol | 4 + contracts/contracts/activities/Activity.sol | 10 +- .../activities/LibActivityMerkleVerifier.sol | 10 +- .../activities/ValidatorActivityTracker.sol | 75 ------- .../activities/ValidatorRewardParentFacet.sol | 33 +-- contracts/contracts/errors/IPCErrors.sol | 1 - .../examples/ValidatorRewarderMap.sol | 17 ++ contracts/tasks/gen-selector-library.ts | 1 + contracts/test/IntegrationTestBase.sol | 26 ++- contracts/test/IntegrationTestPresets.sol | 9 +- .../test/helpers/GatewayFacetsHelper.sol | 6 + contracts/test/helpers/MerkleTreeHelper.sol | 32 +++ contracts/test/helpers/SelectorLibrary.sol | 7 + .../test/integration/GatewayDiamond.t.sol | 175 +++++++++++++--- .../integration/GatewayDiamondToken.t.sol | 5 +- contracts/test/integration/MultiSubnet.t.sol | 4 +- .../test/integration/SubnetActorDiamond.t.sol | 34 ++-- fendermint/actors/activity-tracker/src/lib.rs | 12 +- .../actors/activity-tracker/src/state.rs | 48 ++++- fendermint/app/options/src/genesis.rs | 3 + fendermint/app/src/cmd/genesis.rs | 3 + .../contract-test/tests/staking/state.rs | 2 + .../testing/materializer/src/docker/mod.rs | 1 + fendermint/vm/actor_interface/src/activity.rs | 2 +- fendermint/vm/actor_interface/src/lib.rs | 2 +- fendermint/vm/genesis/Cargo.toml | 1 + fendermint/vm/genesis/src/arb.rs | 2 + fendermint/vm/genesis/src/lib.rs | 2 + .../interpreter/src/fvm/activities/actor.rs | 192 +++++++++++++----- .../interpreter/src/fvm/activities/merkle.rs | 11 +- .../vm/interpreter/src/fvm/activities/mod.rs | 16 +- .../vm/interpreter/src/fvm/checkpoint.rs | 8 +- fendermint/vm/interpreter/src/fvm/exec.rs | 6 +- fendermint/vm/interpreter/src/fvm/mod.rs | 2 +- .../vm/interpreter/src/fvm/state/exec.rs | 12 +- ipc/api/src/evm.rs | 18 +- 39 files changed, 550 insertions(+), 246 deletions(-) delete mode 100644 contracts/contracts/activities/ValidatorActivityTracker.sol create mode 100644 contracts/contracts/examples/ValidatorRewarderMap.sol diff --git a/Cargo.lock b/Cargo.lock index 844a2ceaf..d73cd71f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3324,6 +3324,7 @@ dependencies = [ "fvm_shared", "hex", "ipc-api", + "ipc-types", "multihash 0.18.1", "num-traits", "quickcheck", diff --git a/contracts/binding/build.rs b/contracts/binding/build.rs index 75d56a51a..211589562 100644 --- a/contracts/binding/build.rs +++ b/contracts/binding/build.rs @@ -91,6 +91,7 @@ fn main() { "SubnetActorCheckpointingFacet", "SubnetActorGetterFacet", "LibGateway", + "CheckpointingFacet", ]; let modules = fvm_address_conversion.into_iter().map(camel_to_snake); diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index 81be776be..a171bbe17 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -53,12 +53,12 @@ pub mod xnet_messaging_facet; // The list of contracts need to convert FvmAddress to fvm_shared::Address fvm_address_conversion!(gateway_manager_facet); fvm_address_conversion!(gateway_getter_facet); -fvm_address_conversion!(checkpointing_facet); fvm_address_conversion!(xnet_messaging_facet); fvm_address_conversion!(gateway_messenger_facet); fvm_address_conversion!(subnet_actor_checkpointing_facet); fvm_address_conversion!(subnet_actor_getter_facet); fvm_address_conversion!(lib_gateway); +fvm_address_conversion!(checkpointing_facet); // The list of contracts that need to convert common types between each other common_type_conversion!(subnet_actor_getter_facet, checkpointing_facet); diff --git a/contracts/contracts/GatewayDiamond.sol b/contracts/contracts/GatewayDiamond.sol index 9a87be704..d4ea5135c 100644 --- a/contracts/contracts/GatewayDiamond.sol +++ b/contracts/contracts/GatewayDiamond.sol @@ -13,6 +13,7 @@ import {LibGateway} from "./lib/LibGateway.sol"; import {SubnetID} from "./structs/Subnet.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {BATCH_PERIOD, MAX_MSGS_PER_BATCH} from "./structs/CrossNet.sol"; +import {LibValidatorRewardParent} from "./activities/ValidatorRewardParentFacet.sol"; error FunctionNotFound(bytes4 _functionSelector); @@ -30,6 +31,7 @@ contract GatewayDiamond { SubnetID networkName; Validator[] genesisValidators; bytes32 commitSha; + address validatorRewarder; } constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params) { @@ -76,6 +78,8 @@ contract GatewayDiamond { // set initial validators and update membership Membership memory initial = Membership({configurationNumber: 0, validators: params.genesisValidators}); LibGateway.updateMembership(initial); + + LibValidatorRewardParent.updateRewarder(params.validatorRewarder); } function _fallback() internal { diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index a9792598e..ded2f21bb 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -10,7 +10,7 @@ struct ActivityCommitment { // TODO: add relayed rewarder commitment } -/// The summary for a single validator +/// The summary for a single validator struct ValidatorSummary { /// @dev The validator whose activity we're reporting about. address validator; @@ -29,15 +29,15 @@ struct ActivitySummary { } library LibActivitySummary { - function numValidators(ActivitySummary calldata self) internal pure returns(uint64) { + function numValidators(ActivitySummary calldata self) internal pure returns (uint64) { return uint64(self.activities.length); } - function commitment(ActivitySummary calldata self) internal pure returns(bytes32) { + function commitment(ActivitySummary calldata self) internal pure returns (bytes32) { return keccak256(abi.encode(self)); } - function containsValidator(ActivitySummary calldata self, address validator) internal pure returns(bool) { + function containsValidator(ActivitySummary calldata self, address validator) internal pure returns (bool) { uint256 len = self.activities.length; for (uint256 i = 0; i < len; ) { if (self.activities[i].validator == validator) { @@ -51,4 +51,4 @@ library LibActivitySummary { return false; } -} \ No newline at end of file +} diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 4c98c085b..c359a6fb3 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -8,9 +8,15 @@ import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProo /// Verifies the proof to the commitment in subnet activity summary library LibActivityMerkleVerifier { - function ensureValidProof(bytes32 commitment, ValidatorSummary calldata summary, bytes32[] calldata proof) internal pure { + function ensureValidProof( + bytes32 commitment, + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) internal pure { // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(summary.validator, summary.blocksCommitted, summary.metadata)))); + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(summary.validator, summary.blocksCommitted, summary.metadata))) + ); bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); if (!valid) { revert InvalidProof(); diff --git a/contracts/contracts/activities/ValidatorActivityTracker.sol b/contracts/contracts/activities/ValidatorActivityTracker.sol deleted file mode 100644 index e0bd03228..000000000 --- a/contracts/contracts/activities/ValidatorActivityTracker.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.23; - -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; -import {SystemContract} from "../lib/LibGatewayActorStorage.sol"; - - -/// The validator reward facet for the child subnet, i.e. for child subnet to track validators's activies -/// and create the commitment. -contract ValidatorActivityTracker is SystemContract { - using EnumerableSet for EnumerableSet.AddressSet; - - /// @dev The starting height of validator's mining activities since the last purged block - uint64 startHeight; - /// @dev The list of validator who have participated in mining since `startHeight` - EnumerableSet.AddressSet validators; - /// Tracks the number of blocks a validator has committed since `startHeight` - mapping(address => uint64) blocksCommitted; - - /// Validators claim their reward for doing work in the child subnet - function recordValidatorActivity(address validator) external systemActorOnly { - blocksCommitted[validator] += 1; - - if (!validators.contains(validator)) { - validators.add(validator); - } - } - - /// Reads the current validator summary - function getSummary() external view returns(ActivitySummary memory summary) { - summary.blockRange = [startHeight, block.number]; - - // prepare the activities - uint256 num_validators = validators.length(); - - summary.activities = new ValidatorSummary[](num_validators); - for (uint256 i = 0; i < num_validators; ) { - address validator = validators.at(i); - bytes memory metadata = new bytes(0); - - summary.activities[i] = ValidatorSummary({ - validator: validator, - blocksCommitted: blocksCommitted[validator], - metadata: metadata - }); - - unchecked { - i++; - } - } - } - - /// Reads the current validator summary and purge the data accordingly - /// @dev Call this method only when bottom up checkpoint needs to be created - function purge_activities() external systemActorOnly { - // prepare the activities - uint256 num_validators = validators.length(); - - for (uint256 i = num_validators - 1; i >= 0; ) { - address validator = validators.at(i); - - delete blocksCommitted[validator]; - validators.remove(validator); - - unchecked { - if (i == 0) { break; } - i--; - } - } - - startHeight = uint64(block.number); - } -} diff --git a/contracts/contracts/activities/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardParentFacet.sol index e7e7b9ebc..8f515dc09 100644 --- a/contracts/contracts/activities/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardParentFacet.sol @@ -7,7 +7,7 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; -import {ValidatorSummary, ActivitySummary, LibActivitySummary} from "./Activity.sol"; +import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; import {IValidatorRewarder} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; @@ -17,8 +17,6 @@ import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { - using LibActivitySummary for ActivitySummary; - /// Validators claim their reward for doing work in the child subnet function claim( SubnetID calldata subnetId, @@ -43,11 +41,11 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); handleDistribution(s, subnetId, commitment, summary); - } function handleRelay() internal pure { - revert("not implemented yet"); + // no opt for now + return; } function handleDistribution( @@ -58,8 +56,6 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { ) internal { LibValidatorRewardParent.validatorTryClaim(s, commitment, summary.validator); IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); - - // LibValidatorRewardParent.tryPurgeCommitment(s, subnetId, commitment, summary.numValidators()); } } @@ -87,7 +83,7 @@ struct ValidatorRewardParentStorage { mapping(bytes32 => RewardDistribution) distributions; } -/// The payload for list commitments query +/// The payload for list commitments query struct ListCommimentDetail { /// The child subnet checkpoint height uint64 checkpointHeight; @@ -104,7 +100,9 @@ library LibValidatorRewardParent { // =========== External library functions ============= - function listCommitments(SubnetID calldata subnetId) internal view returns(ListCommimentDetail[] memory listDetails) { + function listCommitments( + SubnetID calldata subnetId + ) internal view returns (ListCommimentDetail[] memory listDetails) { ValidatorRewardParentStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); @@ -128,6 +126,11 @@ library LibValidatorRewardParent { return listDetails; } + function updateRewarder(address rewarder) internal { + ValidatorRewardParentStorage storage ds = facetStorage(); + ds.validatorRewarder = rewarder; + } + function initNewDistribution(uint64 checkpointHeight, bytes32 commitment, SubnetID calldata subnetId) internal { ValidatorRewardParentStorage storage ds = facetStorage(); @@ -155,7 +158,7 @@ library LibValidatorRewardParent { ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId, uint64 checkpointHeight - ) internal view returns(bytes32) { + ) internal view returns (bytes32) { bytes32 subnetKey = subnetId.toHash(); (bool exists, bytes32 commitment) = ds.commitments[subnetKey].tryGet(bytes32(uint256(checkpointHeight))); @@ -175,8 +178,12 @@ library LibValidatorRewardParent { /// Validator tries to claim the reward. The validator can only claim the reward if the validator /// has not claimed before - function validatorTryClaim(ValidatorRewardParentStorage storage ds, bytes32 commitment, address validator) internal { - if(ds.distributions[commitment].claimed.contains(validator)) { + function validatorTryClaim( + ValidatorRewardParentStorage storage ds, + bytes32 commitment, + address validator + ) internal { + if (ds.distributions[commitment].claimed.contains(validator)) { revert ValidatorAlreadyClaimed(); } @@ -199,4 +206,4 @@ library LibValidatorRewardParent { delete ds.commitments[subnetKey]; delete ds.distributions[commitment]; } -} \ No newline at end of file +} diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index f65028d14..15be11f4a 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -85,7 +85,6 @@ error SubnetNoTargetCommitment(); error ValidatorAlreadyClaimed(); error InvalidProof(); - enum InvalidXnetMessageReason { Sender, DstSubnet, diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol new file mode 100644 index 000000000..6124bc68c --- /dev/null +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; +import {ValidatorSummary} from "../activities/Activity.sol"; +import {SubnetID} from "../structs/Subnet.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; + +contract ValidatorRewarderMap is IValidatorRewarder { + using SubnetIDHelper for SubnetID; + + mapping(address => uint64) public blocksCommitted; + + function disburseRewards(SubnetID calldata /*id*/, ValidatorSummary calldata summary) external { + blocksCommitted[summary.validator] += summary.blocksCommitted; + } +} \ No newline at end of file diff --git a/contracts/tasks/gen-selector-library.ts b/contracts/tasks/gen-selector-library.ts index 31a4e045b..e7fb7f1f7 100644 --- a/contracts/tasks/gen-selector-library.ts +++ b/contracts/tasks/gen-selector-library.ts @@ -32,6 +32,7 @@ task('gen-selector-library', 'Generates a Solidity library with contract selecto 'RegisterSubnetFacet', 'SubnetGetterFacet', 'SubnetActorMock', + 'ValidatorRewardParentFacet', ] const resolveSelectors = async (contractName: string) => { diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 967319f1a..578bd3422 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -21,6 +21,7 @@ import {GatewayMessengerFacet} from "../contracts/gateway/GatewayMessengerFacet. import {GatewayManagerFacet} from "../contracts/gateway/GatewayManagerFacet.sol"; import {CheckpointingFacet} from "../contracts/gateway/router/CheckpointingFacet.sol"; +import {ValidatorRewardParentFacet} from "../contracts/activities/ValidatorRewardParentFacet.sol"; import {XnetMessagingFacet} from "../contracts/gateway/router/XnetMessagingFacet.sol"; import {TopDownFinalityFacet} from "../contracts/gateway/router/TopDownFinalityFacet.sol"; @@ -46,8 +47,7 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; - +import {ActivityCommitment} from "../contracts/activities/Activity.sol"; struct TestSubnetDefinition { GatewayDiamond gateway; @@ -127,6 +127,7 @@ contract TestRegistry is Test, TestParams { contract TestGatewayActor is Test, TestParams { bytes4[] gwCheckpointingFacetSelectors; + bytes4[] gwValidatorRewardFacetSelectors; bytes4[] gwXnetMessagingFacetSelectors; bytes4[] gwTopDownFinalityFacetSelectors; @@ -142,6 +143,7 @@ contract TestGatewayActor is Test, TestParams { GatewayDiamond gatewayDiamond; constructor() { + gwValidatorRewardFacetSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardParentFacet"); gwCheckpointingFacetSelectors = SelectorLibrary.resolveSelectors("CheckpointingFacet"); gwXnetMessagingFacetSelectors = SelectorLibrary.resolveSelectors("XnetMessagingFacet"); gwTopDownFinalityFacetSelectors = SelectorLibrary.resolveSelectors("TopDownFinalityFacet"); @@ -314,7 +316,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); return params; } @@ -326,13 +329,15 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); return params; } function createGatewayDiamond(GatewayDiamond.ConstructorParams memory params) public returns (GatewayDiamond) { CheckpointingFacet checkpointingFacet = new CheckpointingFacet(); + ValidatorRewardParentFacet validatorRewardParentFacet = new ValidatorRewardParentFacet(); XnetMessagingFacet xnetMessagingFacet = new XnetMessagingFacet(); TopDownFinalityFacet topDownFinalityFacet = new TopDownFinalityFacet(); GatewayManagerFacet manager = new GatewayManagerFacet(); @@ -342,7 +347,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, DiamondLoupeFacet louper = new DiamondLoupeFacet(); OwnershipFacet ownership = new OwnershipFacet(); - IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](9); + IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](10); gwDiamondCut[0] = ( IDiamond.FacetCut({ @@ -415,6 +420,15 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, functionSelectors: gwOwnershipSelectors }) ); + + gwDiamondCut[9] = ( + IDiamond.FacetCut({ + facetAddress: address(validatorRewardParentFacet), + action: IDiamond.FacetCutAction.Add, + functionSelectors: gwValidatorRewardFacetSelectors + }) + ); + gatewayDiamond = new GatewayDiamond(gwDiamondCut, params); return gatewayDiamond; @@ -917,7 +931,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(nextConfigNum))}) + activities: ActivityCommitment({summary: bytes32(uint256(nextConfigNum))}) }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/IntegrationTestPresets.sol b/contracts/test/IntegrationTestPresets.sol index da155ad6c..dceca44f3 100644 --- a/contracts/test/IntegrationTestPresets.sol +++ b/contracts/test/IntegrationTestPresets.sol @@ -32,7 +32,8 @@ contract L1GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); return params; @@ -59,7 +60,8 @@ contract L2GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); return params; @@ -89,7 +91,8 @@ contract L3GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); return params; diff --git a/contracts/test/helpers/GatewayFacetsHelper.sol b/contracts/test/helpers/GatewayFacetsHelper.sol index d35889134..0c656b102 100644 --- a/contracts/test/helpers/GatewayFacetsHelper.sol +++ b/contracts/test/helpers/GatewayFacetsHelper.sol @@ -7,6 +7,7 @@ import {GatewayManagerFacet} from "../../contracts/gateway/GatewayManagerFacet.s import {GatewayMessengerFacet} from "../../contracts/gateway/GatewayMessengerFacet.sol"; import {TopDownFinalityFacet} from "../../contracts/gateway/router/TopDownFinalityFacet.sol"; import {CheckpointingFacet} from "../../contracts/gateway/router/CheckpointingFacet.sol"; +import {ValidatorRewardParentFacet} from "../../contracts/activities/ValidatorRewardParentFacet.sol"; import {XnetMessagingFacet} from "../../contracts/gateway/router/XnetMessagingFacet.sol"; import {GatewayDiamond} from "../../contracts/GatewayDiamond.sol"; import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; @@ -49,6 +50,11 @@ library GatewayFacetsHelper { } // + function validatorReward(GatewayDiamond gw) internal pure returns (ValidatorRewardParentFacet) { + ValidatorRewardParentFacet facet = ValidatorRewardParentFacet(address(gw)); + return facet; + } + function ownership(GatewayDiamond gw) internal pure returns (OwnershipFacet) { OwnershipFacet facet = OwnershipFacet(address(gw)); return facet; diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index 0307d7090..a89116ed7 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -31,4 +31,36 @@ library MerkleTreeHelper { return (root, proofs); } + + function createMerkleProofsForActivities( + address[] memory addrs, + uint64[] memory blocksMined, + bytes[] memory metadatas + ) internal returns (bytes32, bytes32[][] memory) { + Merkle merkleTree = new Merkle(); + + if (addrs.length != blocksMined.length) { + revert("different array lengths btw blocks mined and addrs"); + } + if (addrs.length != metadatas.length) { + revert("different array lengths btw metadatas and addrs"); + } + uint256 len = addrs.length; + + bytes32 root; + bytes32[][] memory proofs = new bytes32[][](len); + bytes32[] memory data = new bytes32[](len); + for (uint256 i = 0; i < len; i++) { + data[i] = keccak256(bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i], metadatas[i])))); + } + + root = merkleTree.getRoot(data); + // get proof + for (uint256 i = 0; i < len; i++) { + bytes32[] memory proof = merkleTree.getProof(data, i); + proofs[i] = proof; + } + + return (root, proofs); + } } diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 4d3b853a6..e58a32b4e 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -122,6 +122,13 @@ library SelectorLibrary { (bytes4[]) ); } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardParentFacet"))) { + return + abi.decode( + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000116e6e14200000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } revert(string.concat("Selectors not found for facet: ", facetName)); } } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index d9e45d059..de8c79378 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,9 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivityCommitment, ValidatorSummary} from "../../contracts/activities/Activity.sol"; +import {LibValidatorRewardParent} from "../../contracts/activities/ValidatorRewardParentFacet.sol"; +import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -103,7 +105,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT } function testGatewayDiamond_LoupeFunction() public view { - require(gatewayDiamond.diamondLouper().facets().length == 9, "unexpected length"); + require(gatewayDiamond.diamondLouper().facets().length == 10, "unexpected length"); require( gatewayDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" @@ -200,7 +202,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); GatewayDiamond dep = createGatewayDiamond(constructorParams); @@ -233,7 +236,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: 100, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](2); @@ -737,7 +741,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -765,7 +770,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -792,7 +798,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -820,7 +827,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -1070,7 +1078,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1079,7 +1087,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // failed to create a checkpoint with zero membership weight @@ -1121,7 +1129,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1145,7 +1153,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1167,7 +1175,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.prank(caller); @@ -1214,7 +1222,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.prank(caller); @@ -1235,7 +1243,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1244,7 +1252,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1309,7 +1317,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1371,7 +1379,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1455,7 +1463,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1490,7 +1498,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1535,7 +1543,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // create a checkpoint @@ -1584,7 +1592,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1648,7 +1656,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.prank(caller); @@ -1670,7 +1678,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(0) }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -1727,5 +1736,123 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT } } + // ============== Test Activities =============== + function testGatewayDiamond_ValidatorClaimMiningReward_Works() public { + address caller = address(saDiamond); + + ValidatorRewarderMap v = new ValidatorRewarderMap(); + { + GatewayDiamond.ConstructorParams memory constructorParams = GatewayDiamond.ConstructorParams({ + networkName: SubnetID({root: ROOTNET_CHAINID, route: new address[](0)}), + bottomUpCheckPeriod: DEFAULT_CHECKPOINT_PERIOD, + majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, + genesisValidators: new Validator[](0), + activeValidatorsLimit: 100, + commitSha: DEFAULT_COMMIT_SHA, + validatorRewarder: address(v) + }); + gatewayDiamond = createGatewayDiamond(constructorParams); + } + + vm.startPrank(caller); + vm.deal(caller, DEFAULT_COLLATERAL_AMOUNT + DEFAULT_CROSS_MSG_FEE); + registerSubnet(DEFAULT_COLLATERAL_AMOUNT, caller); + vm.stopPrank(); + + (SubnetID memory subnetId, , , , ) = getSubnet(address(caller)); + (bool exist, Subnet memory subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); + require(exist, "subnet does not exist"); + require(subnetInfo.circSupply == 0, "unexpected initial circulation supply"); + + gatewayDiamond.manager().fund{value: DEFAULT_COLLATERAL_AMOUNT}( + subnetId, + FvmAddressHelper.from(address(caller)) + ); + (, subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); + require(subnetInfo.circSupply == DEFAULT_COLLATERAL_AMOUNT, "unexpected circulation supply after funding"); + + (, address[] memory addrs, ) = TestUtils.getFourValidators(vm); + bytes[] memory metadata = new bytes[](addrs.length); + uint64[] memory blocksMined = new uint64[](addrs.length); + + blocksMined[0] = 1; + blocksMined[1] = 2; + + (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + metadata + ); + BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ + subnetID: subnetId, + blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), + blockHash: keccak256("block1"), + nextConfigurationNumber: 1, + msgs: new IpcEnvelope[](0), + activities: ActivityCommitment({summary: activityRoot}) + }); + + vm.prank(caller); + gatewayDiamond.checkpointer().commitCheckpoint(checkpoint); + + vm.startPrank(addrs[0]); + vm.deal(addrs[0], 1 ether); + gatewayDiamond.validatorReward().claim( + subnetId, + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + ValidatorSummary({ + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proofs[0] + ); + + vm.startPrank(addrs[1]); + vm.deal(addrs[1], 1 ether); + gatewayDiamond.validatorReward().claim( + subnetId, + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + ValidatorSummary({ + validator: addrs[1], + blocksCommitted: blocksMined[1], + metadata: metadata[1] + }), + proofs[1] + ); + + vm.startPrank(addrs[2]); + vm.deal(addrs[2], 1 ether); + gatewayDiamond.validatorReward().claim( + subnetId, + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + ValidatorSummary({ + validator: addrs[2], + blocksCommitted: blocksMined[2], + metadata: metadata[2] + }), + proofs[2] + ); + + vm.startPrank(addrs[3]); + vm.deal(addrs[3], 1 ether); + gatewayDiamond.validatorReward().claim( + subnetId, + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + ValidatorSummary({ + validator: addrs[3], + blocksCommitted: blocksMined[3], + metadata: metadata[3] + }), + proofs[3] + ); + + // check + assert(v.blocksCommitted(addrs[0]) == 1); + assert(v.blocksCommitted(addrs[1]) == 2); + assert(v.blocksCommitted(addrs[2]) == 0); + assert(v.blocksCommitted(addrs[3]) == 0); + } + function callback() public view {} } diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index f191f152b..9d1d02139 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -35,7 +35,6 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; - contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; using CrossMsgHelper for IpcEnvelope; @@ -167,7 +166,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.prank(address(saDiamond)); @@ -226,7 +225,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 5fdee611f..71f8d2355 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -1351,7 +1351,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1381,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 64ada4509..4a7bd38d4 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -691,7 +691,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -700,7 +700,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -801,7 +801,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -810,7 +810,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(1))}) + activities: ActivityCommitment({summary: bytes32(uint256(1))}) }); vm.deal(address(saDiamond), 100 ether); @@ -839,7 +839,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivityCommitment({ summary: bytes32(uint256(2))}); + checkpoint.activities = ActivityCommitment({summary: bytes32(uint256(2))}); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -896,7 +896,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(1) )}) + activities: ActivityCommitment({summary: bytes32(uint256(1))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -909,7 +909,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(2))}) + activities: ActivityCommitment({summary: bytes32(uint256(2))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -921,7 +921,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(3))}) + activities: ActivityCommitment({summary: bytes32(uint256(3))}) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -933,7 +933,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(4))}) + activities: ActivityCommitment({summary: bytes32(uint256(4))}) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -944,7 +944,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(5))}) + activities: ActivityCommitment({summary: bytes32(uint256(5))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -958,7 +958,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(6))}) + activities: ActivityCommitment({summary: bytes32(uint256(6))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -972,7 +972,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(7))}) + activities: ActivityCommitment({summary: bytes32(uint256(7))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -986,7 +986,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(8))}) + activities: ActivityCommitment({summary: bytes32(uint256(8))}) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -997,7 +997,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(9))}) + activities: ActivityCommitment({summary: bytes32(uint256(9))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1011,7 +1011,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(10))}) + activities: ActivityCommitment({summary: bytes32(uint256(10))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1053,7 +1053,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivityCommitment({summary: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -1097,7 +1097,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(1))}) + activities: ActivityCommitment({summary: bytes32(uint256(1))}) }); hash = keccak256(abi.encode(checkpoint)); diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 4679d15ee..2d9bf6039 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -1,18 +1,18 @@ // Copyright 2021-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use fil_actors_runtime::actor_error; +use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fil_actors_runtime::runtime::{ActorCode, Runtime}; use fil_actors_runtime::{actor_dispatch, ActorError}; -use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::tuple::*; use fvm_shared::address::Address; -use fvm_shared::METHOD_CONSTRUCTOR; use fvm_shared::clock::ChainEpoch; +use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; -use fil_actors_runtime::actor_error; -pub use crate::state::{ValidatorSummary}; use crate::state::State; +pub use crate::state::ValidatorSummary; mod state; @@ -76,10 +76,10 @@ impl ActivityTrackerActor { let state: State = rt.state()?; let activities = state.validator_activities(rt)?; Ok(GetActivitiesResult { - activities, start_height: state.start_height + activities, + start_height: state.start_height, }) } - } impl ActorCode for ActivityTrackerActor { diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs index d5ca0d174..257f78110 100644 --- a/fendermint/actors/activity-tracker/src/state.rs +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -26,11 +26,12 @@ pub struct State { } impl State { - pub fn new( - store: &BS, - ) -> Result { + pub fn new(store: &BS) -> Result { let mut deployers_map = BlockCommittedMap::empty(store, DEFAULT_HAMT_CONFIG, "empty"); - Ok(State { start_height: 0, blocks_committed: deployers_map.flush()? }) + Ok(State { + start_height: 0, + blocks_committed: deployers_map.flush()?, + }) } pub fn reset_start_height(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { @@ -40,7 +41,12 @@ impl State { pub fn purge_validator_block_committed(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { let all_validators = self.validator_activities(rt)?; - let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + let mut validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; for v in all_validators { validators.delete(&v.validator)?; @@ -51,8 +57,17 @@ impl State { Ok(()) } - pub fn incr_validator_block_committed(&self, rt: &impl Runtime, validator: &Address) -> Result<(), ActorError> { - let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + pub fn incr_validator_block_committed( + &self, + rt: &impl Runtime, + validator: &Address, + ) -> Result<(), ActorError> { + let mut validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; let v = if let Some(v) = validators.get(validator)? { *v + 1 @@ -65,16 +80,27 @@ impl State { Ok(()) } - pub fn validator_activities(&self, rt: &impl Runtime) -> Result, ActorError> { + pub fn validator_activities( + &self, + rt: &impl Runtime, + ) -> Result, ActorError> { let mut result = vec![]; - let validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + let validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; validators.for_each(|k, v| { - result.push(ValidatorSummary{ validator: k, block_committed: *v, metadata: vec![]}); + result.push(ValidatorSummary { + validator: k, + block_committed: *v, + metadata: vec![], + }); Ok(()) })?; Ok(result) } } - diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 740384758..f44f1a916 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -194,6 +194,9 @@ pub struct GenesisIpcGatewayArgs { /// Maximum number of active validators. #[arg(long, short = 'v', default_value = "100")] pub active_validators_limit: u16, + + #[arg(long, value_parser = parse_eth_address, default_value = "0xff00000000000000000000000000000000000065")] + pub validator_rewarder: Address, } #[derive(Args, Debug, Clone)] diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index 134c9ad4d..c817d9cba 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -271,6 +271,7 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh bottom_up_check_period: args.bottom_up_check_period, majority_percentage: args.majority_percentage, active_validators_limit: args.active_validators_limit, + validator_rewarder: args.validator_rewarder, }; let ipc_params = match genesis.ipc { @@ -336,6 +337,8 @@ async fn new_genesis_from_parent( bottom_up_check_period: genesis_info.bottom_up_checkpoint_period, majority_percentage: genesis_info.majority_percentage, active_validators_limit: genesis_info.active_validators_limit, + // default to zero address, need to call setter separately + validator_rewarder: Address::from(EthAddress([0; 20])), }, }; let mut genesis = Genesis { diff --git a/fendermint/testing/contract-test/tests/staking/state.rs b/fendermint/testing/contract-test/tests/staking/state.rs index f1af4eaee..2f4b5f5f3 100644 --- a/fendermint/testing/contract-test/tests/staking/state.rs +++ b/fendermint/testing/contract-test/tests/staking/state.rs @@ -566,6 +566,7 @@ impl arbitrary::Arbitrary<'_> for StakingState { bottom_up_check_period: 1 + u.choose_index(100)? as u64, majority_percentage: 51 + u8::arbitrary(u)? % 50, active_validators_limit: 1 + u.choose_index(100)? as u16, + validator_rewarder: Address::from(EthAddress([0; 20])), }, }; @@ -594,6 +595,7 @@ impl arbitrary::Arbitrary<'_> for StakingState { bottom_up_check_period: 1 + u.choose_index(100)? as u64, majority_percentage: 51 + u8::arbitrary(u)? % 50, active_validators_limit: num_max_validators as u16, + validator_rewarder: Address::from(EthAddress([0; 20])), }, }; diff --git a/fendermint/testing/materializer/src/docker/mod.rs b/fendermint/testing/materializer/src/docker/mod.rs index f31cd0675..2ce6819f4 100644 --- a/fendermint/testing/materializer/src/docker/mod.rs +++ b/fendermint/testing/materializer/src/docker/mod.rs @@ -694,6 +694,7 @@ impl Materializer for DockerMaterializer { bottom_up_check_period: 1, majority_percentage: 67, active_validators_limit: 100, + validator_rewarder: fvm_shared::address::Address::from(EthAddress([0; 20])), }, }), }; diff --git a/fendermint/vm/actor_interface/src/activity.rs b/fendermint/vm/actor_interface/src/activity.rs index b0191ee4d..51353cacd 100644 --- a/fendermint/vm/actor_interface/src/activity.rs +++ b/fendermint/vm/actor_interface/src/activity.rs @@ -1,4 +1,4 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -define_id!(ACTIVITY_TRACKER { id: 99 }); \ No newline at end of file +define_id!(ACTIVITY_TRACKER { id: 99 }); diff --git a/fendermint/vm/actor_interface/src/lib.rs b/fendermint/vm/actor_interface/src/lib.rs index 81f0b15e3..8958fb232 100644 --- a/fendermint/vm/actor_interface/src/lib.rs +++ b/fendermint/vm/actor_interface/src/lib.rs @@ -43,6 +43,7 @@ macro_rules! define_singleton { } pub mod account; +pub mod activity; pub mod burntfunds; pub mod chainmetadata; pub mod cron; @@ -57,4 +58,3 @@ pub mod multisig; pub mod placeholder; pub mod reward; pub mod system; -pub mod activity; diff --git a/fendermint/vm/genesis/Cargo.toml b/fendermint/vm/genesis/Cargo.toml index c44bfbe96..27214281c 100644 --- a/fendermint/vm/genesis/Cargo.toml +++ b/fendermint/vm/genesis/Cargo.toml @@ -34,6 +34,7 @@ quickcheck = { workspace = true } quickcheck_macros = { workspace = true } hex = { workspace = true } serde_json = { workspace = true } +ipc-types = {workspace = true} # Enable arb on self for tests. fendermint_vm_genesis = { path = ".", features = ["arb"] } diff --git a/fendermint/vm/genesis/src/arb.rs b/fendermint/vm/genesis/src/arb.rs index 99128cd0d..ed18e5779 100644 --- a/fendermint/vm/genesis/src/arb.rs +++ b/fendermint/vm/genesis/src/arb.rs @@ -9,6 +9,7 @@ use fendermint_crypto::SecretKey; use fendermint_testing::arb::{ArbSubnetID, ArbTokenAmount}; use fendermint_vm_core::Timestamp; use fvm_shared::{address::Address, version::NetworkVersion}; +use ipc_types::EthAddress; use quickcheck::{Arbitrary, Gen}; use rand::{rngs::StdRng, SeedableRng}; @@ -126,6 +127,7 @@ impl Arbitrary for ipc::GatewayParams { bottom_up_check_period: u64::arbitrary(g).max(1), majority_percentage: u8::arbitrary(g) % 50 + 51, active_validators_limit: u16::arbitrary(g) % 100 + 1, + validator_rewarder: Address::from(EthAddress([0; 20])), } } } diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index ae87b4a1d..355f14e36 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -226,6 +226,7 @@ impl From for PermissionModeParams { /// IPC related data structures. pub mod ipc { use fendermint_vm_encoding::IsHumanReadable; + use fvm_shared::address::Address; use ipc_api::subnet_id::SubnetID; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -243,6 +244,7 @@ pub mod ipc { pub bottom_up_check_period: u64, pub majority_percentage: u8, pub active_validators_limit: u16, + pub validator_rewarder: Address, } } diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs index 56e6680b4..549bcf808 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/actor.rs @@ -1,21 +1,25 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use fvm::executor::{ApplyKind, ApplyRet, Executor}; -use fvm_shared::clock::ChainEpoch; -use fendermint_actor_activity_tracker::{ValidatorSummary}; -use fendermint_vm_actor_interface::system; use crate::fvm::activities::{ActivitySummary, BlockMined, ValidatorActivityTracker}; +use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; +use anyhow::Context; +use fendermint_actor_activity_tracker::{GetActivitiesResult, ValidatorSummary}; use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_actor_interface::system; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::clock::ChainEpoch; -pub struct ActorActivityTracker<'a, E> { - pub(crate) executor: &'a mut E, +pub struct ActorActivityTracker<'a, DB: Blockstore + Clone + 'static> { + pub(crate) executor: &'a mut FvmExecState, pub(crate) epoch: ChainEpoch, } -impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> { +impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker + for ActorActivityTracker<'a, DB> +{ type ValidatorSummaryDetail = ValidatorSummary; fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { @@ -37,34 +41,35 @@ impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> gas_premium: Default::default(), }; - self.apply_implicit_message(msg)?; + self.executor.execute_implicit(msg)?; Ok(()) } - fn get_activities_summary(&self) -> anyhow::Result> { - // let msg = FvmMessage { - // from: system::SYSTEM_ACTOR_ADDR, - // to: ACTIVITY_TRACKER_ACTOR_ADDR, - // sequence: self.epoch as u64, - // // exclude this from gas restriction - // gas_limit: i64::MAX as u64, - // method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, - // params: fvm_ipld_encoding::RawBytes::default(), - // value: Default::default(), - // version: Default::default(), - // gas_fee_cap: Default::default(), - // gas_premium: Default::default(), - // }; - // - // let apply_ret = self.apply_implicit_message(msg)?; - // let r = - // fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) - // .context("failed to parse validator activities")?; - // Ok(ActivitySummary { - // block_range: (r.start_height, self.epoch), - // details: r.activities, - // }) - todo!() + fn get_activities_summary( + &self, + ) -> anyhow::Result> { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: self.epoch as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, + params: fvm_ipld_encoding::RawBytes::default(), + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let apply_ret = self.executor.call_state()?.call(msg)?; + let r = fvm_ipld_encoding::from_slice::( + &apply_ret.msg_receipt.return_data, + ) + .context("failed to parse validator activities")?; + Ok(ActivitySummary { + details: r.activities, + }) } fn purge_activities(&mut self) -> anyhow::Result<()> { @@ -82,23 +87,116 @@ impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> gas_premium: Default::default(), }; - self.apply_implicit_message(msg)?; + self.executor.execute_implicit(msg)?; Ok(()) } } -impl <'a, E: Executor> ActorActivityTracker<'a, E> { - fn apply_implicit_message<>( - &mut self, - msg: FvmMessage, - ) -> anyhow::Result { - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = self.executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; +// impl<'a, E: Executor> ActorActivityTracker<'a, E> { +// fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { +// let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; +// let apply_ret = self +// .executor +// .execute_message(msg, ApplyKind::Implicit, raw_length)?; +// +// if let Some(err) = apply_ret.failure_info { +// anyhow::bail!("failed to apply activity tracker messages: {}", err) +// } else { +// Ok(apply_ret) +// } +// } +// } - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to apply activity tracker messages: {}", err) - } else { - Ok(apply_ret) - } - } -} +// pub struct ActorActivityTracker<'a, E> { +// pub(crate) executor: &'a mut E, +// pub(crate) epoch: ChainEpoch, +// } +// +// impl<'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> { +// type ValidatorSummaryDetail = ValidatorSummary; +// +// fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { +// let params = fendermint_actor_activity_tracker::BlockedMinedParams { +// validator: fvm_shared::address::Address::from(EthAddress::from(block.validator)), +// }; +// +// let msg = FvmMessage { +// from: system::SYSTEM_ACTOR_ADDR, +// to: ACTIVITY_TRACKER_ACTOR_ADDR, +// sequence: self.epoch as u64, +// // exclude this from gas restriction +// gas_limit: i64::MAX as u64, +// method_num: fendermint_actor_activity_tracker::Method::BlockMined as u64, +// params: fvm_ipld_encoding::RawBytes::serialize(params)?, +// value: Default::default(), +// version: Default::default(), +// gas_fee_cap: Default::default(), +// gas_premium: Default::default(), +// }; +// +// self.apply_implicit_message(msg)?; +// Ok(()) +// } +// +// fn get_activities_summary( +// &self, +// ) -> anyhow::Result> { +// let msg = FvmMessage { +// from: system::SYSTEM_ACTOR_ADDR, +// to: ACTIVITY_TRACKER_ACTOR_ADDR, +// sequence: self.epoch as u64, +// // exclude this from gas restriction +// gas_limit: i64::MAX as u64, +// method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, +// params: fvm_ipld_encoding::RawBytes::default(), +// value: Default::default(), +// version: Default::default(), +// gas_fee_cap: Default::default(), +// gas_premium: Default::default(), +// }; +// +// let apply_ret = self.executor.caller().apply_implicit_message(msg)?; +// // let r = +// // fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) +// // .context("failed to parse validator activities")?; +// // Ok(ActivitySummary { +// // block_range: (r.start_height, self.epoch), +// // details: r.activities, +// // }) +// todo!() +// } +// +// fn purge_activities(&mut self) -> anyhow::Result<()> { +// let msg = FvmMessage { +// from: system::SYSTEM_ACTOR_ADDR, +// to: ACTIVITY_TRACKER_ACTOR_ADDR, +// sequence: self.epoch as u64, +// // exclude this from gas restriction +// gas_limit: i64::MAX as u64, +// method_num: fendermint_actor_activity_tracker::Method::PurgeActivities as u64, +// params: fvm_ipld_encoding::RawBytes::default(), +// value: Default::default(), +// version: Default::default(), +// gas_fee_cap: Default::default(), +// gas_premium: Default::default(), +// }; +// +// self.apply_implicit_message(msg)?; +// Ok(()) +// } +// } +// +// impl<'a, E: Executor> ActorActivityTracker<'a, E> { +// fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { +// let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; +// let apply_ret = self +// .executor +// .execute_message(msg, ApplyKind::Implicit, raw_length)?; +// +// if let Some(err) = apply_ret.failure_info { +// anyhow::bail!("failed to apply activity tracker messages: {}", err) +// } else { +// Ok(apply_ret) +// } +// } +// } diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs index db79ff40e..9fb374ef4 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs @@ -33,8 +33,13 @@ impl TryFrom<&[ValidatorSummary]> for MerkleProofGen { let values = values .iter() .map(|t| { - payload_to_evm_address(t.validator.payload()) - .map(|addr| vec![format!("{addr:?}"), t.block_committed.to_string(), hex::encode(&t.metadata)]) + payload_to_evm_address(t.validator.payload()).map(|addr| { + vec![ + format!("{addr:?}"), + t.block_committed.to_string(), + hex::encode(&t.metadata), + ] + }) }) .collect::>>()?; @@ -42,4 +47,4 @@ impl TryFrom<&[ValidatorSummary]> for MerkleProofGen { .context("failed to construct Merkle tree")?; Ok(MerkleProofGen { tree }) } -} \ No newline at end of file +} diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs index 99ab16674..22c5ddf15 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/mod.rs @@ -7,11 +7,11 @@ pub mod actor; mod merkle; -use std::fmt::Debug; +use crate::fvm::activities::merkle::MerkleProofGen; use fendermint_actor_activity_tracker::ValidatorSummary; use fendermint_crypto::PublicKey; use ipc_api::checkpoint::ActivityCommitment; -use crate::fvm::activities::merkle::MerkleProofGen; +use std::fmt::Debug; pub struct BlockMined { pub(crate) validator: PublicKey, @@ -19,7 +19,7 @@ pub struct BlockMined { #[derive(Debug, Clone)] pub struct ActivitySummary { - pub details: Vec + pub details: Vec, } /// Tracks the validator activities in the current blockchain @@ -30,7 +30,9 @@ pub trait ValidatorActivityTracker { fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()>; /// Get the validators activities summary since the checkpoint height - fn get_activities_summary(&self) -> anyhow::Result>; + fn get_activities_summary( + &self, + ) -> anyhow::Result>; /// Purge the current validator activities summary fn purge_activities(&mut self) -> anyhow::Result<()>; @@ -39,8 +41,8 @@ pub trait ValidatorActivityTracker { impl ActivitySummary { pub fn commitment(&self) -> anyhow::Result { let gen = MerkleProofGen::try_from(self.details.as_slice())?; - Ok(ActivityCommitment{ - summary: gen.root().to_fixed_bytes().to_vec() + Ok(ActivityCommitment { + summary: gen.root().to_fixed_bytes().to_vec(), }) } -} \ No newline at end of file +} diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 38dea8846..fd9a23a2e 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -104,7 +104,11 @@ where block_hash, next_configuration_number, msgs, - activities: state.activities_tracker().get_activities_summary()?.commitment()?.try_into()?, + activities: state + .activities_tracker() + .get_activities_summary()? + .commitment()? + .try_into()?, }; // Save the checkpoint in the ledger. @@ -248,7 +252,7 @@ where msgs: convert_tokenizables(cp.msgs)?, activities: checkpoint::ActivityCommitment { summary: cp.activities.summary, - } + }, }; // We mustn't do these in parallel because of how nonces are fetched. diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 930fda16d..3f86b8dee 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -12,9 +12,9 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; +use crate::fvm::activities::BlockMined; use crate::fvm::gas::{GasMarket, GasUtilization}; use crate::ExecInterpreter; -use crate::fvm::activities::BlockMined; use crate::fvm::activities::ValidatorActivityTracker; @@ -206,7 +206,9 @@ where async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { if let Some(pubkey) = state.validator_pubkey() { - state.activities_tracker().track_block_mined(BlockMined { validator: pubkey })?; + state + .activities_tracker() + .track_block_mined(BlockMined { validator: pubkey })?; } state.update_gas_market()?; diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 0f979036a..c42a8b7f2 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -12,11 +12,11 @@ pub mod state; pub mod store; pub mod upgrades; +pub mod activities; #[cfg(any(test, feature = "bundle"))] pub mod bundle; pub mod gas; pub(crate) mod topdown; -pub mod activities; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index ea932b8c2..bfef8c6a3 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -26,12 +26,12 @@ use fvm_shared::{ use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use crate::fvm::activities::actor::ActorActivityTracker; use crate::fvm::externs::FendermintExterns; use crate::fvm::gas::actor::ActorGasMarket; use crate::fvm::store::ReadOnlyBlockstore; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; -use crate::fvm::activities::actor::ActorActivityTracker; pub type BlockHash = [u8; 32]; @@ -40,7 +40,8 @@ pub type ActorAddressMap = HashMap; /// The result of the message application bundled with any delegated addresses of event emitters. pub type ExecResult = anyhow::Result<(ApplyRet, ActorAddressMap)>; -pub type StateExecutor = DefaultExecutor>>>>; +pub type StateExecutor = + DefaultExecutor>>>>; /// Parts of the state which evolve during the lifetime of the chain. #[serde_as] @@ -283,8 +284,11 @@ where self.executor.context().network.chain_id } - pub fn activities_tracker(&mut self) -> ActorActivityTracker> { - ActorActivityTracker { epoch: self.block_height(), executor: &mut self.executor } + pub fn activities_tracker(&mut self) -> ActorActivityTracker { + ActorActivityTracker { + epoch: self.block_height(), + executor: self, + } } /// Collect all the event emitters' delegated addresses, for those who have any. diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 72f8fdf13..51d707c17 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -4,8 +4,8 @@ //! Type conversion for IPC Agent struct with solidity contract struct use crate::address::IPCAddress; -use crate::checkpoint::{BottomUpCheckpoint, ActivityCommitment}; use crate::checkpoint::BottomUpMsgBatch; +use crate::checkpoint::{ActivityCommitment, BottomUpCheckpoint}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -18,10 +18,9 @@ use fvm_shared::address::{Address, Payload}; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use ipc_actors_abis::{ - gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, - register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, + checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, + lib_gateway, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, subnet_actor_getter_facet, top_down_finality_facet, xnet_messaging_facet, - checkpointing_facet }; /// The type conversion for IPC structs to evm solidity contracts. We need this convenient macro because @@ -126,11 +125,12 @@ macro_rules! bottom_up_checkpoint_conversion { type Error = anyhow::Error; fn try_from(c: ActivityCommitment) -> Result { - Ok( - $module::ActivityCommitment { - summary: c.summary.try_into().map_err(|_| anyhow!("cannot convert bytes32"))?, - } - ) + Ok($module::ActivityCommitment { + summary: c + .summary + .try_into() + .map_err(|_| anyhow!("cannot convert bytes32"))?, + }) } } From 4f03a21889f4413c7934f9d2eecbc1a2b86a5bc8 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 11 Oct 2024 17:23:40 +0800 Subject: [PATCH 072/111] update deployment --- .../activities/ValidatorRewardParentFacet.sol | 15 ++++++++++++++- contracts/contracts/errors/IPCErrors.sol | 1 + contracts/contracts/lib/LibDiamond.sol | 2 +- contracts/tasks/deploy-gateway.ts | 6 +++++- contracts/test/helpers/SelectorLibrary.sol | 2 +- contracts/test/integration/GatewayDiamond.t.sol | 8 ++++---- .../test/integration/SubnetActorDiamond.t.sol | 10 +++++----- contracts/test/integration/SubnetRegistry.t.sol | 4 ++-- 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/contracts/contracts/activities/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardParentFacet.sol index 8f515dc09..8a669dce1 100644 --- a/contracts/contracts/activities/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardParentFacet.sol @@ -6,17 +6,30 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; -import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotOwner} from "../errors/IPCErrors.sol"; import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; import {IValidatorRewarder} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; /// The validator reward facet for the parent subnet, i.e. for validators in the child subnet /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { + event RewarderUpdated(address oldRewarder, address newRewarder); + + function updateRewarder(address newRewarder) external { + if (msg.sender != LibDiamond.contractOwner()) { + revert NotOwner(); + } + + ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); + emit RewarderUpdated(s.validatorRewarder, newRewarder); + s.validatorRewarder = newRewarder; + } + /// Validators claim their reward for doing work in the child subnet function claim( SubnetID calldata subnetId, diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index 15be11f4a..2cdc7ba96 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -84,6 +84,7 @@ error CommitmentAlreadyInitialized(); error SubnetNoTargetCommitment(); error ValidatorAlreadyClaimed(); error InvalidProof(); +error NotOwner(); enum InvalidXnetMessageReason { Sender, diff --git a/contracts/contracts/lib/LibDiamond.sol b/contracts/contracts/lib/LibDiamond.sol index e60f520c2..476086d12 100644 --- a/contracts/contracts/lib/LibDiamond.sol +++ b/contracts/contracts/lib/LibDiamond.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.23; import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; import {IDiamond} from "../interfaces/IDiamond.sol"; +import {NotOwner} from "../errors/IPCErrors.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); error NoSelectorsProvidedForFacetForCut(address _facetAddress); diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts index d8971e82d..34dc394ce 100644 --- a/contracts/tasks/deploy-gateway.ts +++ b/contracts/tasks/deploy-gateway.ts @@ -13,6 +13,8 @@ const gatewayConstructorParams = { }, genesisValidators: [], commitSha: undefined, // Will be set later. + /// default to no rewarder + validatorRewarder: "0x0000000000000000000000000000000000000000", } task('deploy-gateway') @@ -67,7 +69,8 @@ async function deployFacets(hre: HardhatRuntimeEnvironment, deployer: string): P }, { name: 'TopDownFinalityFacet', libraries: ['AccountHelper'] }, { name: 'OwnershipFacet' }, - ] + { name: 'ValidatorRewardParentFacet' }, + ]; return await Deployments.deploy(hre, deployer, ...facets) } @@ -76,6 +79,7 @@ async function deployGatewayDiamond( hre: HardhatRuntimeEnvironment, deployer: string, facets: Deployments, + ): Promise { gatewayConstructorParams.networkName.root = await hre.getChainId() gatewayConstructorParams.commitSha = hre.ethers.utils.formatBytes32String(gitCommitSha()) diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index e58a32b4e..db5eec8d1 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardParentFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000116e6e14200000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000216e6e1420000000000000000000000000000000000000000000000000000000097d1471400000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index de8c79378..f993ea874 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -66,7 +66,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT require(owner != newOwner, "ownership should be updated"); require(newOwner == address(1), "new owner not address 1"); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gatewayDiamond.ownership().transferOwnership(address(1)); } @@ -144,7 +144,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT ); //test that other user cannot call diamondcut to add function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); @@ -165,7 +165,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT //test that other user cannot call diamondcut to replace function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); @@ -183,7 +183,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT //test that other user cannot call diamondcut to remove function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 4a7bd38d4..64a4c3176 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -1135,7 +1135,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { ); //test that other user cannot call diamondcut to add function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1155,7 +1155,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { //test that other user cannot call diamondcut to replace function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1173,7 +1173,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { //test that other user cannot call diamondcut to remove function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1792,14 +1792,14 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { function testSubnetActorDiamond_PauseUnpause_NotOwner() public { vm.prank(vm.addr(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamond.pauser().pause(); saDiamond.pauser().pause(); require(saDiamond.pauser().paused(), "not paused"); vm.prank(vm.addr(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamond.pauser().unpause(); saDiamond.pauser().unpause(); diff --git a/contracts/test/integration/SubnetRegistry.t.sol b/contracts/test/integration/SubnetRegistry.t.sol index 95c7d8b87..b6642fb23 100644 --- a/contracts/test/integration/SubnetRegistry.t.sol +++ b/contracts/test/integration/SubnetRegistry.t.sol @@ -101,7 +101,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.permissionMode = PermissionMode.Collateral; vm.prank(address(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); s.register().newSubnetActor(params); } @@ -308,7 +308,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { // Test only owner can update vm.prank(address(1)); // Set a different address as the sender - vm.expectRevert(abi.encodeWithSelector(LibDiamond.NotOwner.selector)); // Expected revert message + vm.expectRevert(abi.encodeWithSelector(NotOwner.selector)); // Expected revert message registrySubnetGetterFacet.updateReferenceSubnetContract( newGetterFacet, newManagerFacet, From c85761acb764018cbe3b014ee8c77caf41bc0b9d Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 15 Oct 2024 21:35:46 +0800 Subject: [PATCH 073/111] deploy map --- .../examples/ValidatorRewarderMap.sol | 2 -- contracts/tasks/deploy-gateway.ts | 6 +++-- contracts/tasks/index.ts | 1 + contracts/tasks/validator-rewarder.ts | 22 +++++++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 contracts/tasks/validator-rewarder.ts diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 6124bc68c..c601e9502 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -4,10 +4,8 @@ pragma solidity ^0.8.23; import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; import {ValidatorSummary} from "../activities/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; -import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; contract ValidatorRewarderMap is IValidatorRewarder { - using SubnetIDHelper for SubnetID; mapping(address => uint64) public blocksCommitted; diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts index 34dc394ce..a6b59f583 100644 --- a/contracts/tasks/deploy-gateway.ts +++ b/contracts/tasks/deploy-gateway.ts @@ -14,7 +14,7 @@ const gatewayConstructorParams = { genesisValidators: [], commitSha: undefined, // Will be set later. /// default to no rewarder - validatorRewarder: "0x0000000000000000000000000000000000000000", + validatorRewarder: process.env.VALIDATOR_REWARDER || "0x0000000000000000000000000000000000000000", } task('deploy-gateway') @@ -69,7 +69,7 @@ async function deployFacets(hre: HardhatRuntimeEnvironment, deployer: string): P }, { name: 'TopDownFinalityFacet', libraries: ['AccountHelper'] }, { name: 'OwnershipFacet' }, - { name: 'ValidatorRewardParentFacet' }, + { name: 'ValidatorRewardParentFacet', libraries: ['SubnetIDHelper'] }, ]; return await Deployments.deploy(hre, deployer, ...facets) @@ -84,6 +84,8 @@ async function deployGatewayDiamond( gatewayConstructorParams.networkName.root = await hre.getChainId() gatewayConstructorParams.commitSha = hre.ethers.utils.formatBytes32String(gitCommitSha()) + console.log("deploy gateway with params", gatewayConstructorParams); + const deployments = await Deployments.deploy(hre, deployer, { name: 'GatewayDiamond', args: [facets.asFacetCuts(), gatewayConstructorParams], diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 901e611bc..d0ab1c692 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -5,4 +5,5 @@ import './deploy-registry' import './deploy' import './upgrade' import './validator-gater' +import './validator-rewarder' import './gen-selector-library' diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts new file mode 100644 index 000000000..679c8fc29 --- /dev/null +++ b/contracts/tasks/validator-rewarder.ts @@ -0,0 +1,22 @@ +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { Deployments } from './lib' + +// step 1. deploy the validator rewarder +// sample command: pnpm exec hardhat validator-rewarder-deploy --network calibrationnet +task('validator-rewarder-deploy') + .setDescription('Deploy example subnet validator rewarder contract') + .setAction(async (_: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + console.log( + `Deploying validator rewarder contract with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + await Deployments.deploy(hre, deployer, { + name: 'ValidatorRewarderMap', + libraries: [], + }) + }) From 385b4ab53c5faac8db064378314f89a4c399659e Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 15 Oct 2024 21:58:35 +0800 Subject: [PATCH 074/111] init activity --- fendermint/actors/activity-tracker/src/lib.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 2d9bf6039..03d150228 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -11,7 +11,7 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; -use crate::state::State; +pub use crate::state::State; pub use crate::state::ValidatorSummary; mod state; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 833427236..5bce33711 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -19,7 +19,8 @@ use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, cron, eam, gas, init, ipc, reward, system, EMPTY_ARR, + account, activity, burntfunds, chainmetadata, cron, eam, gas, init, ipc, reward, system, + EMPTY_ARR, }; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; @@ -447,6 +448,17 @@ impl GenesisBuilder { ) .context("failed to create gas market actor")?; + let tracker_state = fendermint_actor_activity_tracker::State::new(state.store())?; + state + .create_custom_actor( + fendermint_actor_activity_tracker::IPC_ACTIVITY_TRACKER_ACTOR_NAME, + activity::ACTIVITY_TRACKER_ACTOR_ID, + &tracker_state, + TokenAmount::zero(), + None, + ) + .context("failed to create activity tracker actor")?; + // STAGE 2: Create non-builtin accounts which do not have a fixed ID. // The next ID is going to be _after_ the accounts, which have already been assigned an ID by the `Init` actor. From cc1ebf22ad210daa639750bae7ba0a3ff5beec07 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 15 Oct 2024 22:08:10 +0800 Subject: [PATCH 075/111] human readble string --- fendermint/vm/genesis/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index 355f14e36..25a5bf4fc 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -244,6 +244,7 @@ pub mod ipc { pub bottom_up_check_period: u64, pub majority_percentage: u8, pub active_validators_limit: u16, + #[serde_as(as = "IsHumanReadable")] pub validator_rewarder: Address, } } From 9d10df287ca8144d396f71b554884d1f97816165 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 15 Oct 2024 22:12:23 +0800 Subject: [PATCH 076/111] update build --- fendermint/actors/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/actors/build.rs b/fendermint/actors/build.rs index fc1a180e9..a3003ada6 100644 --- a/fendermint/actors/build.rs +++ b/fendermint/actors/build.rs @@ -8,7 +8,7 @@ use std::path::Path; use std::process::{Command, Stdio}; use std::thread; -const ACTORS: &[&str] = &["chainmetadata", "eam", "gas_market"]; +const ACTORS: &[&str] = &["chainmetadata", "eam", "gas_market", "activity_tracker"]; const FILES_TO_WATCH: &[&str] = &["Cargo.toml", "src"]; From 983ca6de5f990bfe5c637a9e78def5816adf0015 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 15 Oct 2024 22:15:41 +0800 Subject: [PATCH 077/111] update name --- fendermint/actors/activity-tracker/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 03d150228..8d52d41b7 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -19,7 +19,7 @@ mod state; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(ActivityTrackerActor); -pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity"; +pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity_tracker"; pub struct ActivityTrackerActor; From f8f379d153957f83f55b4ece9eb90f2f40772f9b Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 16 Oct 2024 13:49:04 +0800 Subject: [PATCH 078/111] fix errors --- fendermint/actors/activity-tracker/src/lib.rs | 5 +- .../interpreter/src/fvm/activities/actor.rs | 123 ++---------------- 2 files changed, 17 insertions(+), 111 deletions(-) diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 8d52d41b7..791bc688a 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -10,6 +10,7 @@ use fvm_shared::address::Address; use fvm_shared::clock::ChainEpoch; use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; +use serde::{Deserialize, Serialize}; pub use crate::state::State; pub use crate::state::ValidatorSummary; @@ -28,7 +29,7 @@ pub struct BlockedMinedParams { pub validator: Address, } -#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct GetActivitiesResult { pub activities: Vec, pub start_height: ChainEpoch, @@ -73,6 +74,8 @@ impl ActivityTrackerActor { } pub fn get_activities(rt: &impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; let activities = state.validator_activities(rt)?; Ok(GetActivitiesResult { diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs index 549bcf808..a9f2b4788 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/actor.rs @@ -9,6 +9,7 @@ use fendermint_actor_activity_tracker::{GetActivitiesResult, ValidatorSummary}; use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::system; +use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; use fvm_shared::clock::ChainEpoch; @@ -41,7 +42,7 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker gas_premium: Default::default(), }; - self.executor.execute_implicit(msg)?; + self.apply_implicit_message(msg)?; Ok(()) } @@ -87,116 +88,18 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker gas_premium: Default::default(), }; - self.executor.execute_implicit(msg)?; + self.apply_implicit_message(msg)?; Ok(()) } } -// impl<'a, E: Executor> ActorActivityTracker<'a, E> { -// fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { -// let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; -// let apply_ret = self -// .executor -// .execute_message(msg, ApplyKind::Implicit, raw_length)?; -// -// if let Some(err) = apply_ret.failure_info { -// anyhow::bail!("failed to apply activity tracker messages: {}", err) -// } else { -// Ok(apply_ret) -// } -// } -// } - -// pub struct ActorActivityTracker<'a, E> { -// pub(crate) executor: &'a mut E, -// pub(crate) epoch: ChainEpoch, -// } -// -// impl<'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> { -// type ValidatorSummaryDetail = ValidatorSummary; -// -// fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { -// let params = fendermint_actor_activity_tracker::BlockedMinedParams { -// validator: fvm_shared::address::Address::from(EthAddress::from(block.validator)), -// }; -// -// let msg = FvmMessage { -// from: system::SYSTEM_ACTOR_ADDR, -// to: ACTIVITY_TRACKER_ACTOR_ADDR, -// sequence: self.epoch as u64, -// // exclude this from gas restriction -// gas_limit: i64::MAX as u64, -// method_num: fendermint_actor_activity_tracker::Method::BlockMined as u64, -// params: fvm_ipld_encoding::RawBytes::serialize(params)?, -// value: Default::default(), -// version: Default::default(), -// gas_fee_cap: Default::default(), -// gas_premium: Default::default(), -// }; -// -// self.apply_implicit_message(msg)?; -// Ok(()) -// } -// -// fn get_activities_summary( -// &self, -// ) -> anyhow::Result> { -// let msg = FvmMessage { -// from: system::SYSTEM_ACTOR_ADDR, -// to: ACTIVITY_TRACKER_ACTOR_ADDR, -// sequence: self.epoch as u64, -// // exclude this from gas restriction -// gas_limit: i64::MAX as u64, -// method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, -// params: fvm_ipld_encoding::RawBytes::default(), -// value: Default::default(), -// version: Default::default(), -// gas_fee_cap: Default::default(), -// gas_premium: Default::default(), -// }; -// -// let apply_ret = self.executor.caller().apply_implicit_message(msg)?; -// // let r = -// // fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) -// // .context("failed to parse validator activities")?; -// // Ok(ActivitySummary { -// // block_range: (r.start_height, self.epoch), -// // details: r.activities, -// // }) -// todo!() -// } -// -// fn purge_activities(&mut self) -> anyhow::Result<()> { -// let msg = FvmMessage { -// from: system::SYSTEM_ACTOR_ADDR, -// to: ACTIVITY_TRACKER_ACTOR_ADDR, -// sequence: self.epoch as u64, -// // exclude this from gas restriction -// gas_limit: i64::MAX as u64, -// method_num: fendermint_actor_activity_tracker::Method::PurgeActivities as u64, -// params: fvm_ipld_encoding::RawBytes::default(), -// value: Default::default(), -// version: Default::default(), -// gas_fee_cap: Default::default(), -// gas_premium: Default::default(), -// }; -// -// self.apply_implicit_message(msg)?; -// Ok(()) -// } -// } -// -// impl<'a, E: Executor> ActorActivityTracker<'a, E> { -// fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { -// let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; -// let apply_ret = self -// .executor -// .execute_message(msg, ApplyKind::Implicit, raw_length)?; -// -// if let Some(err) = apply_ret.failure_info { -// anyhow::bail!("failed to apply activity tracker messages: {}", err) -// } else { -// Ok(apply_ret) -// } -// } -// } +impl<'a, DB: Blockstore + Clone + 'static> ActorActivityTracker<'a, DB> { + fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { + let (apply_ret, _) = self.executor.execute_implicit(msg)?; + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to apply activity tracker messages: {}", err) + } else { + Ok(apply_ret) + } + } +} From 9e6136bdd6553a943443d1d3ee3c4fe5259a5a64 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 16 Oct 2024 14:08:35 +0800 Subject: [PATCH 079/111] add commit --- fendermint/actors/activity-tracker/src/state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs index 257f78110..7377b78b3 100644 --- a/fendermint/actors/activity-tracker/src/state.rs +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -58,7 +58,7 @@ impl State { } pub fn incr_validator_block_committed( - &self, + &mut self, rt: &impl Runtime, validator: &Address, ) -> Result<(), ActorError> { @@ -77,6 +77,8 @@ impl State { validators.set(validator, v)?; + self.blocks_committed = validators.flush()?; + Ok(()) } From cd273252b70b21809d6964ae3b8bb95c16e2135a Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 16 Oct 2024 14:53:26 +0800 Subject: [PATCH 080/111] fmt code --- contracts/contracts/gateway/router/CheckpointingFacet.sol | 3 --- contracts/contracts/lib/LibGateway.sol | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index b4df27757..f92764a61 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -56,7 +56,6 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @param membershipWeight - the total weight of the membership function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, - // TODO(rewarder) ActivitySummary calldata summary, bytes32 membershipRootHash, uint256 membershipWeight ) external systemActorOnly { @@ -76,8 +75,6 @@ contract CheckpointingFacet is GatewayActorModifiers { majorityPercentage: s.majorityPercentage }); - // TODO(rewarder): emit an ActivitySummaryCommittedevent so relayers can pick it up. - LibGateway.storeBottomUpCheckpoint(checkpoint); } diff --git a/contracts/contracts/lib/LibGateway.sol b/contracts/contracts/lib/LibGateway.sol index f7c3e84ad..eb96ea800 100644 --- a/contracts/contracts/lib/LibGateway.sol +++ b/contracts/contracts/lib/LibGateway.sol @@ -82,6 +82,7 @@ library LibGateway { b.subnetID = checkpoint.subnetID; b.nextConfigurationNumber = checkpoint.nextConfigurationNumber; b.blockHeight = checkpoint.blockHeight; + b.activities = checkpoint.activities; uint256 msgLength = checkpoint.msgs.length; for (uint256 i; i < msgLength; ) { From 32262a24e0cd523b936591dfc852483b444f8984 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 16 Oct 2024 22:15:39 +0800 Subject: [PATCH 081/111] update contract --- contracts/contracts/activities/Activity.sol | 44 +++--------- .../activities/LibActivityMerkleVerifier.sol | 4 +- .../activities/ValidatorRewardParentFacet.sol | 70 ++++++++++--------- .../gateway/router/CheckpointingFacet.sol | 5 +- contracts/contracts/structs/CrossNet.sol | 18 +---- .../subnet/SubnetActorRewardFacet.sol | 1 - contracts/test/IntegrationTestBase.sol | 4 +- contracts/test/helpers/MerkleTreeHelper.sol | 6 +- contracts/test/helpers/SelectorLibrary.sol | 6 +- .../test/integration/GatewayDiamond.t.sol | 49 +++++++------ .../integration/GatewayDiamondToken.t.sol | 6 +- contracts/test/integration/MultiSubnet.t.sol | 6 +- .../test/integration/SubnetActorDiamond.t.sol | 36 +++++----- .../interpreter/src/fvm/activities/actor.rs | 6 +- .../interpreter/src/fvm/activities/merkle.rs | 10 +-- .../vm/interpreter/src/fvm/activities/mod.rs | 18 ++--- .../vm/interpreter/src/fvm/checkpoint.rs | 7 +- ipc/api/src/checkpoint.rs | 11 +-- ipc/api/src/evm.rs | 18 ++--- 19 files changed, 155 insertions(+), 170 deletions(-) diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index ded2f21bb..e246136a2 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -/// The commitments for the child subnet activities that should be submitted to the parent subnet +/// The summary for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint -struct ActivityCommitment { - /// The activity summary for validators - bytes32 summary; +struct ActivitySummary { + /// The total number of distintive validators that have mined + uint64 totalActiveValidators; + /// The activity commitment for validators + bytes32 commitment; // TODO: add relayed rewarder commitment } /// The summary for a single validator struct ValidatorSummary { + /// @dev The child subnet checkpoint height associated with this summary + uint64 checkpointHeight; /// @dev The validator whose activity we're reporting about. address validator; /// @dev The number of blocks committed by each validator in the position they appear in the validators array. @@ -20,35 +24,3 @@ struct ValidatorSummary { /// @dev Other metadata bytes metadata; } - -/// A summary of validator's activity in the child subnet. This is submitted to the parent for reward distribution. -struct ActivitySummary { - /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. - uint256[2] blockRange; - ValidatorSummary[] activities; -} - -library LibActivitySummary { - function numValidators(ActivitySummary calldata self) internal pure returns (uint64) { - return uint64(self.activities.length); - } - - function commitment(ActivitySummary calldata self) internal pure returns (bytes32) { - return keccak256(abi.encode(self)); - } - - function containsValidator(ActivitySummary calldata self, address validator) internal pure returns (bool) { - uint256 len = self.activities.length; - for (uint256 i = 0; i < len; ) { - if (self.activities[i].validator == validator) { - return true; - } - - unchecked { - i++; - } - } - - return false; - } -} diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index c359a6fb3..dafc56b54 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -15,7 +15,9 @@ library LibActivityMerkleVerifier { ) internal pure { // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash bytes32 leaf = keccak256( - bytes.concat(keccak256(abi.encode(summary.validator, summary.blocksCommitted, summary.metadata))) + bytes.concat(keccak256(abi.encode( + summary.checkpointHeight, summary.validator, summary.blocksCommitted, summary.metadata + ))) ); bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); if (!valid) { diff --git a/contracts/contracts/activities/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardParentFacet.sol index 8a669dce1..a4bc8c8e8 100644 --- a/contracts/contracts/activities/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardParentFacet.sol @@ -7,7 +7,7 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotOwner} from "../errors/IPCErrors.sol"; -import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; +import {ValidatorSummary} from "./Activity.sol"; import {IValidatorRewarder} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; @@ -33,7 +33,6 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { /// Validators claim their reward for doing work in the child subnet function claim( SubnetID calldata subnetId, - uint64 checkpointHeight, ValidatorSummary calldata summary, bytes32[] calldata proof ) external nonReentrant whenNotPaused { @@ -50,32 +49,17 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { return handleRelay(); } - bytes32 commitment = LibValidatorRewardParent.ensureValidCommitment(s, subnetId, checkpointHeight); - LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); - - handleDistribution(s, subnetId, commitment, summary); + LibValidatorRewardParent.handleDistribution(s, subnetId, summary, proof); } function handleRelay() internal pure { // no opt for now return; } - - function handleDistribution( - ValidatorRewardParentStorage storage s, - SubnetID calldata subnetId, - bytes32 commitment, - ValidatorSummary calldata summary - ) internal { - LibValidatorRewardParent.validatorTryClaim(s, commitment, summary.validator); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); - } } /// The activity summary commiment that is currently under reward distribution struct RewardDistribution { - /// The checkpoint height that this distribution - uint64 checkpointHeight; /// Total number of valdators to claim the distribution uint64 totalValidators; /// The list of validators that have claimed the reward @@ -93,7 +77,8 @@ struct ValidatorRewardParentStorage { mapping(bytes32 => EnumerableMap.Bytes32ToBytes32Map) commitments; /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. /// Only used if the validator rewarder is non-zero. - mapping(bytes32 => RewardDistribution) distributions; + /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment + mapping(bytes32 => mapping(uint64 => RewardDistribution)) distributions; } /// The payload for list commitments query @@ -144,17 +129,22 @@ library LibValidatorRewardParent { ds.validatorRewarder = rewarder; } - function initNewDistribution(uint64 checkpointHeight, bytes32 commitment, SubnetID calldata subnetId) internal { + function initNewDistribution( + uint64 checkpointHeight, + bytes32 commitment, + uint64 totalActiveValidators, + SubnetID calldata subnetId + ) internal { ValidatorRewardParentStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); - if (ds.distributions[commitment].checkpointHeight != 0) { + if (ds.distributions[subnetKey][checkpointHeight].totalValidators != 0) { revert CommitmentAlreadyInitialized(); } ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); - ds.distributions[commitment].checkpointHeight = checkpointHeight; + ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; } // ============ Internal library functions ============ @@ -167,13 +157,26 @@ library LibValidatorRewardParent { return ds; } + function handleDistribution( + ValidatorRewardParentStorage storage s, + SubnetID calldata subnetId, + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) internal { + bytes32 subnetKey = subnetId.toHash(); + + bytes32 commitment = ensureValidCommitment(s, subnetKey, summary.checkpointHeight); + LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); + + validatorTryClaim(s, subnetKey, summary.checkpointHeight, summary.validator); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); + } + function ensureValidCommitment( ValidatorRewardParentStorage storage ds, - SubnetID calldata subnetId, + bytes32 subnetKey, uint64 checkpointHeight ) internal view returns (bytes32) { - bytes32 subnetKey = subnetId.toHash(); - (bool exists, bytes32 commitment) = ds.commitments[subnetKey].tryGet(bytes32(uint256(checkpointHeight))); if (!exists) { revert SubnetNoTargetCommitment(); @@ -193,30 +196,33 @@ library LibValidatorRewardParent { /// has not claimed before function validatorTryClaim( ValidatorRewardParentStorage storage ds, - bytes32 commitment, + bytes32 subnetKey, + uint64 checkpointHeight, address validator ) internal { - if (ds.distributions[commitment].claimed.contains(validator)) { + if (ds.distributions[subnetKey][checkpointHeight].claimed.contains(validator)) { revert ValidatorAlreadyClaimed(); } - ds.distributions[commitment].claimed.add(validator); + ds.distributions[subnetKey][checkpointHeight].claimed.add(validator); } /// Try to remove the commiment in the target subnet when ALL VALIDATORS HAVE CLAIMED. function tryPurgeCommitment( ValidatorRewardParentStorage storage ds, SubnetID calldata subnetId, - bytes32 commitment, - uint64 totalValidators + uint64 checkpointHeight ) internal { bytes32 subnetKey = subnetId.toHash(); - if (ds.distributions[commitment].claimed.length() < totalValidators) { + uint256 total = uint256(ds.distributions[subnetKey][checkpointHeight].totalValidators); + uint256 claimed = ds.distributions[subnetKey][checkpointHeight].claimed.length(); + + if (claimed < total) { return; } delete ds.commitments[subnetKey]; - delete ds.distributions[commitment]; + delete ds.distributions[subnetKey][checkpointHeight]; } } diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index f92764a61..1c975cac5 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol"; -import {BottomUpCheckpoint, ActivitySummary, ActivitySummaryCommitted} from "../../structs/CrossNet.sol"; +import {BottomUpCheckpoint} from "../../structs/CrossNet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; import {LibQuorum} from "../../lib/LibQuorum.sol"; import {Subnet} from "../../structs/Subnet.sol"; @@ -45,7 +45,8 @@ contract CheckpointingFacet is GatewayActorModifiers { LibValidatorRewardParent.initNewDistribution( uint64(checkpoint.blockHeight), - checkpoint.activities.summary, + checkpoint.activities.commitment, + checkpoint.activities.totalActiveValidators, checkpoint.subnetID ); } diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index c11d21236..1dc78be10 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ActivityCommitment} from "../activities/Activity.sol"; +import {ActivitySummary} from "../activities/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -30,22 +30,10 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; - /// @dev The activity commitment from child subnet to parent subnet - ActivityCommitment activities; + /// @dev The activity summary from child subnet to parent subnet + ActivitySummary activities; } -struct ActivitySummary { - /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. - uint256[2] blockRange; - /// @dev The validators whose activity we're reporting about. - address[] validators; - /// @dev The number of blocks committed by each validator in the position they appear in the validators array. - /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. - uint64[] blocksCommitted; -} - -event ActivitySummaryCommitted(bytes32 indexed commitment, ActivitySummary summary); - struct RelayedSummary { /// @dev The subnet IDs whose activity is being relayed. SubnetID subnet; diff --git a/contracts/contracts/subnet/SubnetActorRewardFacet.sol b/contracts/contracts/subnet/SubnetActorRewardFacet.sol index f34ed6f1f..1b8b84b7d 100644 --- a/contracts/contracts/subnet/SubnetActorRewardFacet.sol +++ b/contracts/contracts/subnet/SubnetActorRewardFacet.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {ActivitySummary} from "../structs/CrossNet.sol"; import {QuorumObjKind} from "../structs/Quorum.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 578bd3422..a1afc2159 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -47,7 +47,7 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ActivityCommitment} from "../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../contracts/activities/Activity.sol"; struct TestSubnetDefinition { GatewayDiamond gateway; @@ -931,7 +931,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(nextConfigNum))}) + activities: ActivitySummary({totalActiveValidators: uint64(validators.length), commitment: bytes32(uint256(nextConfigNum))}) }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index a89116ed7..de6d5bd30 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -35,6 +35,7 @@ library MerkleTreeHelper { function createMerkleProofsForActivities( address[] memory addrs, uint64[] memory blocksMined, + uint64[] memory checkpointHeights, bytes[] memory metadatas ) internal returns (bytes32, bytes32[][] memory) { Merkle merkleTree = new Merkle(); @@ -45,13 +46,16 @@ library MerkleTreeHelper { if (addrs.length != metadatas.length) { revert("different array lengths btw metadatas and addrs"); } + if (addrs.length != checkpointHeights.length) { + revert("different array lengths btw checkpointHeights and addrs"); + } uint256 len = addrs.length; bytes32 root; bytes32[][] memory proofs = new bytes32[][](len); bytes32[] memory data = new bytes32[](len); for (uint256 i = 0; i < len; i++) { - data[i] = keccak256(bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i], metadatas[i])))); + data[i] = keccak256(bytes.concat(keccak256(abi.encode(checkpointHeights[i], addrs[i], blocksMined[i], metadatas[i])))); } root = merkleTree.getRoot(data); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index db5eec8d1..ab362484a 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000024ad232000000000000000000000000000000000000000000000000000000005e6de63200000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff2000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -97,7 +97,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { return abi.decode( - hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002e72f09ca00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021b6bda5d00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardParentFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000216e6e1420000000000000000000000000000000000000000000000000000000097d1471400000000000000000000000000000000000000000000000000000000", + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026be7503e0000000000000000000000000000000000000000000000000000000097d1471400000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index f993ea874..14de0791d 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivityCommitment, ValidatorSummary} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary, ValidatorSummary} from "../../contracts/activities/Activity.sol"; import {LibValidatorRewardParent} from "../../contracts/activities/ValidatorRewardParentFacet.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; @@ -1078,7 +1078,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1087,7 +1087,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // failed to create a checkpoint with zero membership weight @@ -1129,7 +1129,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1153,7 +1153,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1175,7 +1175,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); @@ -1222,7 +1222,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); @@ -1243,7 +1243,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1252,7 +1252,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1317,7 +1317,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1379,7 +1379,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1463,7 +1463,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1498,7 +1498,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1543,7 +1543,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1592,7 +1592,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1656,7 +1656,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); @@ -1774,13 +1774,20 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT (, address[] memory addrs, ) = TestUtils.getFourValidators(vm); bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); + uint64[] memory checkpointHeights = new uint64[](addrs.length); blocksMined[0] = 1; blocksMined[1] = 2; + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, + checkpointHeights, metadata ); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1789,7 +1796,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: activityRoot}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: activityRoot}) }); vm.prank(caller); @@ -1799,8 +1806,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT vm.deal(addrs[0], 1 ether); gatewayDiamond.validatorReward().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), validator: addrs[0], blocksCommitted: blocksMined[0], metadata: metadata[0] @@ -1812,8 +1819,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT vm.deal(addrs[1], 1 ether); gatewayDiamond.validatorReward().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), validator: addrs[1], blocksCommitted: blocksMined[1], metadata: metadata[1] @@ -1825,8 +1832,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT vm.deal(addrs[2], 1 ether); gatewayDiamond.validatorReward().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), validator: addrs[2], blocksCommitted: blocksMined[2], metadata: metadata[2] @@ -1838,8 +1845,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT vm.deal(addrs[3], 1 ether); gatewayDiamond.validatorReward().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), validator: addrs[3], blocksCommitted: blocksMined[3], metadata: metadata[3] diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 9d1d02139..9b1b4e50b 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,7 +33,7 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -166,7 +166,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(address(saDiamond)); @@ -225,7 +225,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 71f8d2355..7313e35c6 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,7 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -1351,7 +1351,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1381,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 64a4c3176..deb9933e5 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -691,7 +691,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -700,7 +700,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -801,7 +801,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -810,7 +810,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(1))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); vm.deal(address(saDiamond), 100 ether); @@ -839,7 +839,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivityCommitment({summary: bytes32(uint256(2))}); + checkpoint.activities = ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -896,7 +896,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(1))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -909,7 +909,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(2))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -921,7 +921,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(3))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(3))}) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -933,7 +933,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(4))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(4))}) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -944,7 +944,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(5))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(5))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -958,7 +958,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(6))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(6))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -972,7 +972,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(7))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(7))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -986,7 +986,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(8))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(8))}) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -997,7 +997,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(9))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(9))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1011,7 +1011,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({summary: bytes32(uint256(10))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(10))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1053,7 +1053,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -1097,7 +1097,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({summary: bytes32(uint256(1))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); hash = keccak256(abi.encode(checkpoint)); diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs index a9f2b4788..87ddad1cb 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/actor.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::activities::{ActivitySummary, BlockMined, ValidatorActivityTracker}; +use crate::fvm::activities::{ActivityDetails, BlockMined, ValidatorActivityTracker}; use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; use anyhow::Context; @@ -48,7 +48,7 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker fn get_activities_summary( &self, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let msg = FvmMessage { from: system::SYSTEM_ACTOR_ADDR, to: ACTIVITY_TRACKER_ACTOR_ADDR, @@ -68,7 +68,7 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker &apply_ret.msg_receipt.return_data, ) .context("failed to parse validator activities")?; - Ok(ActivitySummary { + Ok(ActivityDetails { details: r.activities, }) } diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs index 9fb374ef4..0aa2ff0b0 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs @@ -3,6 +3,7 @@ use anyhow::Context; use fendermint_actor_activity_tracker::ValidatorSummary; +use fvm_shared::clock::ChainEpoch; use ipc_api::evm::payload_to_evm_address; use ipc_observability::lazy_static; use merkle_tree_rs::format::Raw; @@ -12,7 +13,7 @@ pub type Hash = ethers::types::H256; lazy_static!( /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; ); /// The merkle tree based proof verification to interact with solidity contracts @@ -26,15 +27,14 @@ impl MerkleProofGen { } } -impl TryFrom<&[ValidatorSummary]> for MerkleProofGen { - type Error = anyhow::Error; - - fn try_from(values: &[ValidatorSummary]) -> Result { +impl MerkleProofGen { + pub fn new(values: &[ValidatorSummary], checkpoint_height: ChainEpoch) -> anyhow::Result { let values = values .iter() .map(|t| { payload_to_evm_address(t.validator.payload()).map(|addr| { vec![ + checkpoint_height.to_string(), format!("{addr:?}"), t.block_committed.to_string(), hex::encode(&t.metadata), diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs index 22c5ddf15..251c47bd0 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/mod.rs @@ -10,7 +10,8 @@ mod merkle; use crate::fvm::activities::merkle::MerkleProofGen; use fendermint_actor_activity_tracker::ValidatorSummary; use fendermint_crypto::PublicKey; -use ipc_api::checkpoint::ActivityCommitment; +use fvm_shared::clock::ChainEpoch; +use ipc_api::checkpoint::ActivitySummary; use std::fmt::Debug; pub struct BlockMined { @@ -18,7 +19,7 @@ pub struct BlockMined { } #[derive(Debug, Clone)] -pub struct ActivitySummary { +pub struct ActivityDetails { pub details: Vec, } @@ -32,17 +33,18 @@ pub trait ValidatorActivityTracker { /// Get the validators activities summary since the checkpoint height fn get_activities_summary( &self, - ) -> anyhow::Result>; + ) -> anyhow::Result>; /// Purge the current validator activities summary fn purge_activities(&mut self) -> anyhow::Result<()>; } -impl ActivitySummary { - pub fn commitment(&self) -> anyhow::Result { - let gen = MerkleProofGen::try_from(self.details.as_slice())?; - Ok(ActivityCommitment { - summary: gen.root().to_fixed_bytes().to_vec(), +impl ActivityDetails { + pub fn commitment(&self, checkpoint_height: ChainEpoch) -> anyhow::Result { + let gen = MerkleProofGen::new(self.details.as_slice(), checkpoint_height)?; + Ok(ActivitySummary { + total_active_validators: self.details.len() as u64, + commitment: gen.root().to_fixed_bytes().to_vec(), }) } } diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index fd9a23a2e..013b6f626 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -107,7 +107,7 @@ where activities: state .activities_tracker() .get_activities_summary()? - .commitment()? + .commitment(height.value() as i64)? .try_into()?, }; @@ -250,8 +250,9 @@ where block_hash: cp.block_hash, next_configuration_number: cp.next_configuration_number, msgs: convert_tokenizables(cp.msgs)?, - activities: checkpoint::ActivityCommitment { - summary: cp.activities.summary, + activities: checkpoint::ActivitySummary { + total_active_validators: cp.activities.total_active_validators, + commitment: cp.activities.commitment, }, }; diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 4efc04b7f..f88008629 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -76,14 +76,15 @@ pub struct BottomUpMsgBatch { pub msgs: Vec, } -#[serde_as] -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] /// The commitments for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint -pub struct ActivityCommitment { +#[serde_as] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ActivitySummary { + pub total_active_validators: u64, /// The activity summary for validators #[serde_as(as = "HumanReadable")] - pub summary: Vec, + pub commitment: Vec, // TODO: add relayed activity commitment } @@ -106,7 +107,7 @@ pub struct BottomUpCheckpoint { /// The list of messages for execution pub msgs: Vec, /// The activity commitment from child subnet to parent subnet - pub activities: ActivityCommitment, + pub activities: ActivitySummary, } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 51d707c17..562eddd93 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -5,7 +5,7 @@ use crate::address::IPCAddress; use crate::checkpoint::BottomUpMsgBatch; -use crate::checkpoint::{ActivityCommitment, BottomUpCheckpoint}; +use crate::checkpoint::{ActivitySummary, BottomUpCheckpoint}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -121,13 +121,14 @@ macro_rules! cross_msg_types { /// The type conversion between different bottom up checkpoint definition in ethers and sdk macro_rules! bottom_up_checkpoint_conversion { ($module:ident) => { - impl TryFrom for $module::ActivityCommitment { + impl TryFrom for $module::ActivitySummary { type Error = anyhow::Error; - fn try_from(c: ActivityCommitment) -> Result { - Ok($module::ActivityCommitment { - summary: c - .summary + fn try_from(c: ActivitySummary) -> Result { + Ok($module::ActivitySummary { + total_active_validators: c.total_active_validators, + commitment: c + .commitment .try_into() .map_err(|_| anyhow!("cannot convert bytes32"))?, }) @@ -167,8 +168,9 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map(IpcEnvelope::try_from) .collect::, _>>()?, - activities: ActivityCommitment { - summary: value.activities.summary.to_vec(), + activities: ActivitySummary { + total_active_validators: value.activities.total_active_validators, + commitment: value.activities.commitment.to_vec(), }, }) } From 957baa27667b514271b05b1334ef51b1b071243f Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 17 Oct 2024 22:46:48 +0800 Subject: [PATCH 082/111] shift reward facet to subnet actor --- contracts/contracts/GatewayDiamond.sol | 4 - contracts/contracts/SubnetActorDiamond.sol | 7 + contracts/contracts/SubnetRegistryDiamond.sol | 7 + ...rentFacet.sol => ValidatorRewardFacet.sol} | 40 ++--- .../examples/ValidatorRewarderMap.sol | 22 ++- .../gateway/router/CheckpointingFacet.sol | 10 +- .../lib/LibSubnetRegistryStorage.sol | 6 + contracts/tasks/deploy-gateway.ts | 3 - contracts/tasks/gen-selector-library.ts | 2 +- contracts/tasks/validator-rewarder.ts | 13 ++ contracts/test/IntegrationTestBase.sol | 49 +++--- contracts/test/IntegrationTestPresets.sol | 9 +- .../test/helpers/GatewayFacetsHelper.sol | 7 - contracts/test/helpers/SelectorLibrary.sol | 4 +- .../test/helpers/SubnetActorFacetsHelper.sol | 6 + .../test/integration/GatewayDiamond.t.sol | 152 ++---------------- .../test/integration/SubnetActorDiamond.t.sol | 137 +++++++++++++++- .../test/integration/SubnetRegistry.t.sol | 8 +- .../invariants/SubnetRegistryInvariants.t.sol | 3 + .../handlers/SubnetRegistryHandler.sol | 5 +- fendermint/actors/activity-tracker/src/lib.rs | 14 ++ 21 files changed, 287 insertions(+), 221 deletions(-) rename contracts/contracts/activities/{ValidatorRewardParentFacet.sol => ValidatorRewardFacet.sol} (86%) diff --git a/contracts/contracts/GatewayDiamond.sol b/contracts/contracts/GatewayDiamond.sol index d4ea5135c..9a87be704 100644 --- a/contracts/contracts/GatewayDiamond.sol +++ b/contracts/contracts/GatewayDiamond.sol @@ -13,7 +13,6 @@ import {LibGateway} from "./lib/LibGateway.sol"; import {SubnetID} from "./structs/Subnet.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {BATCH_PERIOD, MAX_MSGS_PER_BATCH} from "./structs/CrossNet.sol"; -import {LibValidatorRewardParent} from "./activities/ValidatorRewardParentFacet.sol"; error FunctionNotFound(bytes4 _functionSelector); @@ -31,7 +30,6 @@ contract GatewayDiamond { SubnetID networkName; Validator[] genesisValidators; bytes32 commitSha; - address validatorRewarder; } constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params) { @@ -78,8 +76,6 @@ contract GatewayDiamond { // set initial validators and update membership Membership memory initial = Membership({configurationNumber: 0, validators: params.genesisValidators}); LibGateway.updateMembership(initial); - - LibValidatorRewardParent.updateRewarder(params.validatorRewarder); } function _fallback() internal { diff --git a/contracts/contracts/SubnetActorDiamond.sol b/contracts/contracts/SubnetActorDiamond.sol index 5abb6cfb3..dc36e7ba8 100644 --- a/contracts/contracts/SubnetActorDiamond.sol +++ b/contracts/contracts/SubnetActorDiamond.sol @@ -15,6 +15,8 @@ import {SubnetIDHelper} from "./lib/SubnetIDHelper.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AssetHelper} from "./lib/AssetHelper.sol"; +import {LibValidatorReward} from "./activities/ValidatorRewardFacet.sol"; + error FunctionNotFound(bytes4 _functionSelector); contract SubnetActorDiamond { @@ -37,6 +39,7 @@ contract SubnetActorDiamond { Asset collateralSource; SubnetID parentId; address validatorGater; + address validatorRewarder; } constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params, address owner) { @@ -102,6 +105,10 @@ contract SubnetActorDiamond { if (params.validatorGater != address(0)) { s.validatorGater = params.validatorGater; } + + if (params.validatorRewarder != address(0)) { + LibValidatorReward.updateRewarder(params.validatorRewarder); + } } function _fallback() internal { diff --git a/contracts/contracts/SubnetRegistryDiamond.sol b/contracts/contracts/SubnetRegistryDiamond.sol index bd57a65b9..250dfffb6 100644 --- a/contracts/contracts/SubnetRegistryDiamond.sol +++ b/contracts/contracts/SubnetRegistryDiamond.sol @@ -25,6 +25,7 @@ contract SubnetRegistryDiamond { address diamondCutFacet; address diamondLoupeFacet; address ownershipFacet; + address validatorRewardFacet; bytes4[] subnetActorGetterSelectors; bytes4[] subnetActorManagerSelectors; bytes4[] subnetActorRewarderSelectors; @@ -33,6 +34,7 @@ contract SubnetRegistryDiamond { bytes4[] subnetActorDiamondCutSelectors; bytes4[] subnetActorDiamondLoupeSelectors; bytes4[] subnetActorOwnershipSelectors; + bytes4[] validatorRewardSelectors; SubnetCreationPrivileges creationPrivileges; } @@ -64,6 +66,9 @@ contract SubnetRegistryDiamond { if (params.ownershipFacet == address(0)) { revert FacetCannotBeZero(); } + if (params.validatorRewardFacet == address(0)) { + revert FacetCannotBeZero(); + } LibDiamond.setContractOwner(msg.sender); LibDiamond.diamondCut({_diamondCut: _diamondCut, _init: address(0), _calldata: new bytes(0)}); @@ -83,6 +88,7 @@ contract SubnetRegistryDiamond { s.SUBNET_ACTOR_DIAMOND_CUT_FACET = params.diamondCutFacet; s.SUBNET_ACTOR_LOUPE_FACET = params.diamondLoupeFacet; s.SUBNET_ACTOR_OWNERSHIP_FACET = params.ownershipFacet; + s.VALIDATOR_REWARD_FACET = params.validatorRewardFacet; s.subnetActorGetterSelectors = params.subnetActorGetterSelectors; s.subnetActorManagerSelectors = params.subnetActorManagerSelectors; @@ -92,6 +98,7 @@ contract SubnetRegistryDiamond { s.subnetActorDiamondCutSelectors = params.subnetActorDiamondCutSelectors; s.subnetActorDiamondLoupeSelectors = params.subnetActorDiamondLoupeSelectors; s.subnetActorOwnershipSelectors = params.subnetActorOwnershipSelectors; + s.validatorRewardSelectors = params.validatorRewardSelectors; s.creationPrivileges = params.creationPrivileges; } diff --git a/contracts/contracts/activities/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol similarity index 86% rename from contracts/contracts/activities/ValidatorRewardParentFacet.sol rename to contracts/contracts/activities/ValidatorRewardFacet.sol index a4bc8c8e8..fd6e6b021 100644 --- a/contracts/contracts/activities/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -17,17 +17,11 @@ import {LibDiamond} from "../lib/LibDiamond.sol"; /// The validator reward facet for the parent subnet, i.e. for validators in the child subnet /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. -contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { +contract ValidatorRewardFacet is ReentrancyGuard, Pausable { event RewarderUpdated(address oldRewarder, address newRewarder); - function updateRewarder(address newRewarder) external { - if (msg.sender != LibDiamond.contractOwner()) { - revert NotOwner(); - } - - ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); - emit RewarderUpdated(s.validatorRewarder, newRewarder); - s.validatorRewarder = newRewarder; + function batchClaim(SubnetID calldata subnetId) external { + revert("todo"); } /// Validators claim their reward for doing work in the child subnet @@ -43,13 +37,13 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { revert NotValidator(msg.sender); } - ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); if (s.validatorRewarder == address(0)) { return handleRelay(); } - LibValidatorRewardParent.handleDistribution(s, subnetId, summary, proof); + LibValidatorReward.handleDistribution(s, subnetId, summary, proof); } function handleRelay() internal pure { @@ -67,7 +61,7 @@ struct RewardDistribution { } /// Used by the SubnetActor to track the rewards for each validator -struct ValidatorRewardParentStorage { +struct ValidatorRewardStorage { /// @notice The contract address for validator rewarder address validatorRewarder; /// @notice Summaries look up pending to be processed. @@ -89,7 +83,7 @@ struct ListCommimentDetail { bytes32 commitment; } -library LibValidatorRewardParent { +library LibValidatorReward { bytes32 private constant NAMESPACE = keccak256("validator.reward.parent"); using SubnetIDHelper for SubnetID; @@ -101,7 +95,7 @@ library LibValidatorRewardParent { function listCommitments( SubnetID calldata subnetId ) internal view returns (ListCommimentDetail[] memory listDetails) { - ValidatorRewardParentStorage storage ds = facetStorage(); + ValidatorRewardStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); @@ -125,7 +119,7 @@ library LibValidatorRewardParent { } function updateRewarder(address rewarder) internal { - ValidatorRewardParentStorage storage ds = facetStorage(); + ValidatorRewardStorage storage ds = facetStorage(); ds.validatorRewarder = rewarder; } @@ -135,7 +129,7 @@ library LibValidatorRewardParent { uint64 totalActiveValidators, SubnetID calldata subnetId ) internal { - ValidatorRewardParentStorage storage ds = facetStorage(); + ValidatorRewardStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); @@ -149,7 +143,7 @@ library LibValidatorRewardParent { // ============ Internal library functions ============ - function facetStorage() internal pure returns (ValidatorRewardParentStorage storage ds) { + function facetStorage() internal pure returns (ValidatorRewardStorage storage ds) { bytes32 position = NAMESPACE; assembly { ds.slot := position @@ -158,7 +152,7 @@ library LibValidatorRewardParent { } function handleDistribution( - ValidatorRewardParentStorage storage s, + ValidatorRewardStorage storage s, SubnetID calldata subnetId, ValidatorSummary calldata summary, bytes32[] calldata proof @@ -173,7 +167,7 @@ library LibValidatorRewardParent { } function ensureValidCommitment( - ValidatorRewardParentStorage storage ds, + ValidatorRewardStorage storage ds, bytes32 subnetKey, uint64 checkpointHeight ) internal view returns (bytes32) { @@ -195,7 +189,7 @@ library LibValidatorRewardParent { /// Validator tries to claim the reward. The validator can only claim the reward if the validator /// has not claimed before function validatorTryClaim( - ValidatorRewardParentStorage storage ds, + ValidatorRewardStorage storage ds, bytes32 subnetKey, uint64 checkpointHeight, address validator @@ -207,9 +201,9 @@ library LibValidatorRewardParent { ds.distributions[subnetKey][checkpointHeight].claimed.add(validator); } - /// Try to remove the commiment in the target subnet when ALL VALIDATORS HAVE CLAIMED. - function tryPurgeCommitment( - ValidatorRewardParentStorage storage ds, + /// Try to remove the distribution in the target subnet when ALL VALIDATORS HAVE CLAIMED. + function tryPurgeDistribution( + ValidatorRewardStorage storage ds, SubnetID calldata subnetId, uint64 checkpointHeight ) internal { diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index c601e9502..f5a0c74f7 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -5,11 +5,31 @@ import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; import {ValidatorSummary} from "../activities/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; +/// An example validator rewarder implementation that tracks the accumulated +/// reward for each valdiator only. contract ValidatorRewarderMap is IValidatorRewarder { + SubnetID public subnetId; + address public owner; mapping(address => uint64) public blocksCommitted; - function disburseRewards(SubnetID calldata /*id*/, ValidatorSummary calldata summary) external { + constructor() { + owner = msg.sender; + } + + function setSubnet(SubnetID calldata id) external { + require(msg.sender == owner, "not owner"); + require(id.route.length > 0, "root not allowed"); + + subnetId = id; + } + + function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external { + require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); + + address actor = id.route[id.route.length - 1]; + require(actor == msg.sender, "not from subnet"); + blocksCommitted[summary.validator] += summary.blocksCommitted; } } \ No newline at end of file diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index 1c975cac5..ea91ab86d 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -16,7 +16,7 @@ import {BatchNotCreated, InvalidBatchEpoch, BatchAlreadyExists, NotEnoughSubnetC import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {LibValidatorRewardParent} from "../../activities/ValidatorRewardParentFacet.sol"; +import {LibValidatorReward} from "../../activities/ValidatorRewardFacet.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -43,7 +43,7 @@ contract CheckpointingFacet is GatewayActorModifiers { execBottomUpMsgs(checkpoint.msgs, subnet); - LibValidatorRewardParent.initNewDistribution( + LibValidatorReward.initNewDistribution( uint64(checkpoint.blockHeight), checkpoint.activities.commitment, checkpoint.activities.totalActiveValidators, @@ -64,8 +64,10 @@ contract CheckpointingFacet is GatewayActorModifiers { revert CheckpointAlreadyExists(); } - // TODO(rewarder): compute the commitment to the summary and set it in the checkpoint. - // Collect summaries to relay and put them in the checkpoint. Reset the pending summaries map. + // TODO(rewarder): step 1. call fvm ActivityTrackerActor::get_summary to generate the summary + // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 + // TODO: (if there is more time, should wrap param checkpoint with another data structure) + // TODO(rewarder): step 3. call fvm ActivityTrackerActor::purge_activities to purge the activities LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index fed633a02..3983c76ee 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -12,6 +12,7 @@ struct SubnetRegistryActorStorage { // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_MANAGER_FACET; // solhint-disable-next-line var-name-mixedcase + /// TODO: this should be removed as it's for collateral withdraw only, not rewarder address SUBNET_ACTOR_REWARD_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_CHECKPOINTING_FACET; @@ -23,11 +24,14 @@ struct SubnetRegistryActorStorage { address SUBNET_ACTOR_LOUPE_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_OWNERSHIP_FACET; + // solhint-disable-next-line var-name-mixedcase + address VALIDATOR_REWARD_FACET; /// The subnet actor getter facet functions selectors bytes4[] subnetActorGetterSelectors; /// The subnet actor manager facet functions selectors bytes4[] subnetActorManagerSelectors; /// The subnet actor reward facet functions selectors + /// TODO: this should be removed as it's for collateral withdraw only, not rewarder bytes4[] subnetActorRewarderSelectors; /// The subnet actor checkpointing facet functions selectors bytes4[] subnetActorCheckpointerSelectors; @@ -39,6 +43,8 @@ struct SubnetRegistryActorStorage { bytes4[] subnetActorDiamondLoupeSelectors; /// The subnet actor ownership facet functions selectors bytes4[] subnetActorOwnershipSelectors; + /// The validator reward facet functions selectors + bytes4[] validatorRewardSelectors; /// @notice Mapping that tracks the deployed subnet actors per user. /// Key is the hash of Subnet ID, values are addresses. /// mapping owner => nonce => subnet diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts index a6b59f583..f463242c9 100644 --- a/contracts/tasks/deploy-gateway.ts +++ b/contracts/tasks/deploy-gateway.ts @@ -13,8 +13,6 @@ const gatewayConstructorParams = { }, genesisValidators: [], commitSha: undefined, // Will be set later. - /// default to no rewarder - validatorRewarder: process.env.VALIDATOR_REWARDER || "0x0000000000000000000000000000000000000000", } task('deploy-gateway') @@ -69,7 +67,6 @@ async function deployFacets(hre: HardhatRuntimeEnvironment, deployer: string): P }, { name: 'TopDownFinalityFacet', libraries: ['AccountHelper'] }, { name: 'OwnershipFacet' }, - { name: 'ValidatorRewardParentFacet', libraries: ['SubnetIDHelper'] }, ]; return await Deployments.deploy(hre, deployer, ...facets) diff --git a/contracts/tasks/gen-selector-library.ts b/contracts/tasks/gen-selector-library.ts index e7fb7f1f7..512060afb 100644 --- a/contracts/tasks/gen-selector-library.ts +++ b/contracts/tasks/gen-selector-library.ts @@ -32,7 +32,7 @@ task('gen-selector-library', 'Generates a Solidity library with contract selecto 'RegisterSubnetFacet', 'SubnetGetterFacet', 'SubnetActorMock', - 'ValidatorRewardParentFacet', + 'ValidatorRewardFacet', ] const resolveSelectors = async (contractName: string) => { diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index 679c8fc29..a71365f92 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -20,3 +20,16 @@ task('validator-rewarder-deploy') libraries: [], }) }) + +// step 2. claim the validator rewarder +// sample command: pnpm exec hardhat validator-rewarder-claim --network calibrationnet +task('validator-rewarder-deploy') + .setDescription('Claim reward from example subnet validator rewarder contract') + .setAction(async (_: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [validator] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(validator) + + // todo + }) \ No newline at end of file diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index a1afc2159..8d7252e3e 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -21,7 +21,6 @@ import {GatewayMessengerFacet} from "../contracts/gateway/GatewayMessengerFacet. import {GatewayManagerFacet} from "../contracts/gateway/GatewayManagerFacet.sol"; import {CheckpointingFacet} from "../contracts/gateway/router/CheckpointingFacet.sol"; -import {ValidatorRewardParentFacet} from "../contracts/activities/ValidatorRewardParentFacet.sol"; import {XnetMessagingFacet} from "../contracts/gateway/router/XnetMessagingFacet.sol"; import {TopDownFinalityFacet} from "../contracts/gateway/router/TopDownFinalityFacet.sol"; @@ -48,6 +47,8 @@ import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; import {ActivitySummary} from "../contracts/activities/Activity.sol"; +import {ValidatorRewarderMap} from "../contracts/examples/ValidatorRewarderMap.sol"; +import {ValidatorRewardFacet} from "../contracts/activities/ValidatorRewardFacet.sol"; struct TestSubnetDefinition { GatewayDiamond gateway; @@ -127,7 +128,6 @@ contract TestRegistry is Test, TestParams { contract TestGatewayActor is Test, TestParams { bytes4[] gwCheckpointingFacetSelectors; - bytes4[] gwValidatorRewardFacetSelectors; bytes4[] gwXnetMessagingFacetSelectors; bytes4[] gwTopDownFinalityFacetSelectors; @@ -143,7 +143,6 @@ contract TestGatewayActor is Test, TestParams { GatewayDiamond gatewayDiamond; constructor() { - gwValidatorRewardFacetSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardParentFacet"); gwCheckpointingFacetSelectors = SelectorLibrary.resolveSelectors("CheckpointingFacet"); gwXnetMessagingFacetSelectors = SelectorLibrary.resolveSelectors("XnetMessagingFacet"); gwTopDownFinalityFacetSelectors = SelectorLibrary.resolveSelectors("TopDownFinalityFacet"); @@ -168,6 +167,7 @@ contract TestSubnetActor is Test, TestParams { bytes4[] saCutterSelectors; bytes4[] saLouperSelectors; bytes4[] saOwnershipSelectors; + bytes4[] validatorRewardSelectors; SubnetActorDiamond saDiamond; SubnetActorMock saMock; @@ -182,6 +182,7 @@ contract TestSubnetActor is Test, TestParams { saCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); saLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); saOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); } function defaultSubnetActorParamsWith( @@ -210,7 +211,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: source, collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -234,7 +236,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: source, collateralSource: collateral, - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -268,7 +271,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: Asset({kind: AssetKind.ERC20, tokenAddress: tokenAddress}), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -316,8 +320,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); return params; } @@ -329,15 +332,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); return params; } function createGatewayDiamond(GatewayDiamond.ConstructorParams memory params) public returns (GatewayDiamond) { CheckpointingFacet checkpointingFacet = new CheckpointingFacet(); - ValidatorRewardParentFacet validatorRewardParentFacet = new ValidatorRewardParentFacet(); XnetMessagingFacet xnetMessagingFacet = new XnetMessagingFacet(); TopDownFinalityFacet topDownFinalityFacet = new TopDownFinalityFacet(); GatewayManagerFacet manager = new GatewayManagerFacet(); @@ -347,7 +348,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, DiamondLoupeFacet louper = new DiamondLoupeFacet(); OwnershipFacet ownership = new OwnershipFacet(); - IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](10); + IDiamond.FacetCut[] memory gwDiamondCut = new IDiamond.FacetCut[](9); gwDiamondCut[0] = ( IDiamond.FacetCut({ @@ -421,14 +422,6 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); - gwDiamondCut[9] = ( - IDiamond.FacetCut({ - facetAddress: address(validatorRewardParentFacet), - action: IDiamond.FacetCutAction.Add, - functionSelectors: gwValidatorRewardFacetSelectors - }) - ); - gatewayDiamond = new GatewayDiamond(gwDiamondCut, params); return gatewayDiamond; @@ -506,8 +499,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, DiamondLoupeFacet louper = new DiamondLoupeFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); OwnershipFacet ownership = new OwnershipFacet(); + ValidatorRewardFacet validatorReward = new ValidatorRewardFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](8); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](9); diamondCut[0] = ( IDiamond.FacetCut({ @@ -573,6 +567,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); + diamondCut[8] = ( + IDiamond.FacetCut({ + facetAddress: address(validatorReward), + action: IDiamond.FacetCutAction.Add, + functionSelectors: validatorRewardSelectors + }) + ); SubnetActorDiamond diamond = new SubnetActorDiamond(diamondCut, params, address(this)); return diamond; @@ -623,7 +624,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, permissionMode: _permissionMode, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); saDiamond = createSubnetActor(params); } @@ -654,7 +656,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, permissionMode: _permissionMode, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: _validatorGater + validatorGater: _validatorGater, + validatorRewarder: address(new ValidatorRewarderMap()) }); saDiamond = createSubnetActor(params); } diff --git a/contracts/test/IntegrationTestPresets.sol b/contracts/test/IntegrationTestPresets.sol index dceca44f3..da155ad6c 100644 --- a/contracts/test/IntegrationTestPresets.sol +++ b/contracts/test/IntegrationTestPresets.sol @@ -32,8 +32,7 @@ contract L1GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); return params; @@ -60,8 +59,7 @@ contract L2GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); return params; @@ -91,8 +89,7 @@ contract L3GatewayActorDiamond is IntegrationTestBase { majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: DEFAULT_ACTIVE_VALIDATORS_LIMIT, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); return params; diff --git a/contracts/test/helpers/GatewayFacetsHelper.sol b/contracts/test/helpers/GatewayFacetsHelper.sol index 0c656b102..5034bb350 100644 --- a/contracts/test/helpers/GatewayFacetsHelper.sol +++ b/contracts/test/helpers/GatewayFacetsHelper.sol @@ -7,7 +7,6 @@ import {GatewayManagerFacet} from "../../contracts/gateway/GatewayManagerFacet.s import {GatewayMessengerFacet} from "../../contracts/gateway/GatewayMessengerFacet.sol"; import {TopDownFinalityFacet} from "../../contracts/gateway/router/TopDownFinalityFacet.sol"; import {CheckpointingFacet} from "../../contracts/gateway/router/CheckpointingFacet.sol"; -import {ValidatorRewardParentFacet} from "../../contracts/activities/ValidatorRewardParentFacet.sol"; import {XnetMessagingFacet} from "../../contracts/gateway/router/XnetMessagingFacet.sol"; import {GatewayDiamond} from "../../contracts/GatewayDiamond.sol"; import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; @@ -49,12 +48,6 @@ library GatewayFacetsHelper { return facet; } - // - function validatorReward(GatewayDiamond gw) internal pure returns (ValidatorRewardParentFacet) { - ValidatorRewardParentFacet facet = ValidatorRewardParentFacet(address(gw)); - return facet; - } - function ownership(GatewayDiamond gw) internal pure returns (OwnershipFacet) { OwnershipFacet facet = OwnershipFacet(address(gw)); return facet; diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index ab362484a..e9722b2b6 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -104,7 +104,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("RegisterSubnetFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000018471614600000000000000000000000000000000000000000000000000000000", + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001611941f900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -122,7 +122,7 @@ library SelectorLibrary { (bytes4[]) ); } - if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardParentFacet"))) { + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026be7503e0000000000000000000000000000000000000000000000000000000097d1471400000000000000000000000000000000000000000000000000000000", diff --git a/contracts/test/helpers/SubnetActorFacetsHelper.sol b/contracts/test/helpers/SubnetActorFacetsHelper.sol index 4d7a24002..fcea95a4f 100644 --- a/contracts/test/helpers/SubnetActorFacetsHelper.sol +++ b/contracts/test/helpers/SubnetActorFacetsHelper.sol @@ -9,6 +9,7 @@ import {SubnetActorGetterFacet} from "../../contracts/subnet/SubnetActorGetterFa import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; library SubnetActorFacetsHelper { function manager(address sa) internal pure returns (SubnetActorManagerFacet) { @@ -48,6 +49,11 @@ library SubnetActorFacetsHelper { // + function validatorReward(SubnetActorDiamond sa) internal pure returns (ValidatorRewardFacet) { + ValidatorRewardFacet facet = ValidatorRewardFacet(address(sa)); + return facet; + } + function manager(SubnetActorDiamond sa) internal pure returns (SubnetActorManagerFacet) { SubnetActorManagerFacet facet = SubnetActorManagerFacet(address(sa)); return facet; diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index 14de0791d..db2f720ab 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,9 +39,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivitySummary, ValidatorSummary} from "../../contracts/activities/Activity.sol"; -import {LibValidatorRewardParent} from "../../contracts/activities/ValidatorRewardParentFacet.sol"; -import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -105,7 +103,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT } function testGatewayDiamond_LoupeFunction() public view { - require(gatewayDiamond.diamondLouper().facets().length == 10, "unexpected length"); + require(gatewayDiamond.diamondLouper().facets().length == 9, "unexpected length"); require( gatewayDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" @@ -202,8 +200,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); GatewayDiamond dep = createGatewayDiamond(constructorParams); @@ -236,8 +233,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: 100, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](2); @@ -741,8 +737,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -770,8 +765,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -798,8 +792,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -827,8 +820,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -1678,8 +1670,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, genesisValidators: new Validator[](0), activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(0) + commitSha: DEFAULT_COMMIT_SHA }); gatewayDiamond = createGatewayDiamond(constructorParams); @@ -1736,130 +1727,5 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT } } - // ============== Test Activities =============== - function testGatewayDiamond_ValidatorClaimMiningReward_Works() public { - address caller = address(saDiamond); - - ValidatorRewarderMap v = new ValidatorRewarderMap(); - { - GatewayDiamond.ConstructorParams memory constructorParams = GatewayDiamond.ConstructorParams({ - networkName: SubnetID({root: ROOTNET_CHAINID, route: new address[](0)}), - bottomUpCheckPeriod: DEFAULT_CHECKPOINT_PERIOD, - majorityPercentage: DEFAULT_MAJORITY_PERCENTAGE, - genesisValidators: new Validator[](0), - activeValidatorsLimit: 100, - commitSha: DEFAULT_COMMIT_SHA, - validatorRewarder: address(v) - }); - gatewayDiamond = createGatewayDiamond(constructorParams); - } - - vm.startPrank(caller); - vm.deal(caller, DEFAULT_COLLATERAL_AMOUNT + DEFAULT_CROSS_MSG_FEE); - registerSubnet(DEFAULT_COLLATERAL_AMOUNT, caller); - vm.stopPrank(); - - (SubnetID memory subnetId, , , , ) = getSubnet(address(caller)); - (bool exist, Subnet memory subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); - require(exist, "subnet does not exist"); - require(subnetInfo.circSupply == 0, "unexpected initial circulation supply"); - - gatewayDiamond.manager().fund{value: DEFAULT_COLLATERAL_AMOUNT}( - subnetId, - FvmAddressHelper.from(address(caller)) - ); - (, subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); - require(subnetInfo.circSupply == DEFAULT_COLLATERAL_AMOUNT, "unexpected circulation supply after funding"); - - (, address[] memory addrs, ) = TestUtils.getFourValidators(vm); - bytes[] memory metadata = new bytes[](addrs.length); - uint64[] memory blocksMined = new uint64[](addrs.length); - uint64[] memory checkpointHeights = new uint64[](addrs.length); - - blocksMined[0] = 1; - blocksMined[1] = 2; - - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - - (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( - addrs, - blocksMined, - checkpointHeights, - metadata - ); - BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ - subnetID: subnetId, - blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), - blockHash: keccak256("block1"), - nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: activityRoot}) - }); - - vm.prank(caller); - gatewayDiamond.checkpointer().commitCheckpoint(checkpoint); - - vm.startPrank(addrs[0]); - vm.deal(addrs[0], 1 ether); - gatewayDiamond.validatorReward().claim( - subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proofs[0] - ); - - vm.startPrank(addrs[1]); - vm.deal(addrs[1], 1 ether); - gatewayDiamond.validatorReward().claim( - subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[1], - blocksCommitted: blocksMined[1], - metadata: metadata[1] - }), - proofs[1] - ); - - vm.startPrank(addrs[2]); - vm.deal(addrs[2], 1 ether); - gatewayDiamond.validatorReward().claim( - subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[2], - blocksCommitted: blocksMined[2], - metadata: metadata[2] - }), - proofs[2] - ); - - vm.startPrank(addrs[3]); - vm.deal(addrs[3], 1 ether); - gatewayDiamond.validatorReward().claim( - subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[3], - blocksCommitted: blocksMined[3], - metadata: metadata[3] - }), - proofs[3] - ); - - // check - assert(v.blocksCommitted(addrs[0]) == 1); - assert(v.blocksCommitted(addrs[1]) == 2); - assert(v.blocksCommitted(addrs[2]) == 0); - assert(v.blocksCommitted(addrs[3]) == 0); - } - function callback() public view {} } diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index deb9933e5..af5328d63 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,9 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary, ValidatorSummary} from "../../contracts/activities/Activity.sol"; +import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; +import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -342,7 +344,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { permissionMode: PermissionMode.Collateral, supplySource: native, collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }), address(saDupGetterFaucet), address(saDupMangerFaucet), @@ -2329,6 +2332,136 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { require(address(gatewayAddress).balance == DEFAULT_MIN_VALIDATOR_STAKE, "gateway post claim balance wrong"); } + // ============== Test Activities =============== + function testGatewayDiamond_ValidatorClaimMiningReward_Works() public { + gatewayAddress = address(gatewayDiamond); + + Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)}); + + SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith( + gatewayAddress, + SubnetID(ROOTNET_CHAINID, new address[](0)), + source, + AssetHelper.native() + ); + ValidatorRewarderMap m = new ValidatorRewarderMap(); + params.validatorRewarder = address(m); + + saDiamond = createSubnetActor(params); + + SubnetID memory tmpId = SubnetID(ROOTNET_CHAINID, new address[](1)); + tmpId.route[0] = address(saDiamond); + m.setSubnet(tmpId); + + address caller = address(saDiamond); + vm.startPrank(caller); + vm.deal(caller, DEFAULT_COLLATERAL_AMOUNT + DEFAULT_CROSS_MSG_FEE); + registerSubnet(DEFAULT_COLLATERAL_AMOUNT, caller); + vm.stopPrank(); + + (SubnetID memory subnetId, , , , ) = getSubnet(address(caller)); + (bool exist, Subnet memory subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); + require(exist, "subnet does not exist"); + require(subnetInfo.circSupply == 0, "unexpected initial circulation supply"); + require(tmpId.route[0] == subnetId.route[0], "address not match"); + + gatewayDiamond.manager().fund{value: DEFAULT_COLLATERAL_AMOUNT}( + subnetId, + FvmAddressHelper.from(address(caller)) + ); + (, subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); + require(subnetInfo.circSupply == DEFAULT_COLLATERAL_AMOUNT, "unexpected circulation supply after funding"); + + (, address[] memory addrs, ) = TestUtils.getFourValidators(vm); + bytes[] memory metadata = new bytes[](addrs.length); + uint64[] memory blocksMined = new uint64[](addrs.length); + uint64[] memory checkpointHeights = new uint64[](addrs.length); + + blocksMined[0] = 1; + blocksMined[1] = 2; + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ + subnetID: subnetId, + blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), + blockHash: keccak256("block1"), + nextConfigurationNumber: 1, + msgs: new IpcEnvelope[](0), + activities: ActivitySummary({totalActiveValidators: 1, commitment: activityRoot}) + }); + + vm.prank(caller); + gatewayDiamond.checkpointer().commitCheckpoint(checkpoint); + + vm.startPrank(addrs[0]); + vm.deal(addrs[0], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proofs[0] + ); + + vm.startPrank(addrs[1]); + vm.deal(addrs[1], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[1], + blocksCommitted: blocksMined[1], + metadata: metadata[1] + }), + proofs[1] + ); + + vm.startPrank(addrs[2]); + vm.deal(addrs[2], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[2], + blocksCommitted: blocksMined[2], + metadata: metadata[2] + }), + proofs[2] + ); + + vm.startPrank(addrs[3]); + vm.deal(addrs[3], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[3], + blocksCommitted: blocksMined[3], + metadata: metadata[3] + }), + proofs[3] + ); + + // check + assert(m.blocksCommitted(addrs[0]) == 1); + assert(m.blocksCommitted(addrs[1]) == 2); + assert(m.blocksCommitted(addrs[2]) == 0); + assert(m.blocksCommitted(addrs[3]) == 0); + } + // ----------------------------------------------------------------------------------------------------------------- // Tests for validator gater // ----------------------------------------------------------------------------------------------------------------- diff --git a/contracts/test/integration/SubnetRegistry.t.sol b/contracts/test/integration/SubnetRegistry.t.sol index b6642fb23..68e99f550 100644 --- a/contracts/test/integration/SubnetRegistry.t.sol +++ b/contracts/test/integration/SubnetRegistry.t.sol @@ -29,6 +29,8 @@ import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; import {AssetHelper} from "../../contracts/lib/AssetHelper.sol"; import {RegistryFacetsHelper} from "../helpers/RegistryFacetsHelper.sol"; import {DiamondFacetsHelper} from "../helpers/DiamondFacetsHelper.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; +import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -66,6 +68,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); + params.validatorRewardFacet = address(new ValidatorRewardFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -75,6 +78,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); params.creationPrivileges = SubnetCreationPrivileges.Unrestricted; @@ -170,6 +174,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { new SubnetRegistryDiamond(diamondCut, params); params.ownershipFacet = address(8); + params.validatorRewardFacet = address(9); new SubnetRegistryDiamond(diamondCut, params); } @@ -257,7 +262,8 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { permissionMode: PermissionMode.Collateral, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); registrySubnetFacet.newSubnetActor(params); diff --git a/contracts/test/invariants/SubnetRegistryInvariants.t.sol b/contracts/test/invariants/SubnetRegistryInvariants.t.sol index 4dd42e247..e26c32a2e 100644 --- a/contracts/test/invariants/SubnetRegistryInvariants.t.sol +++ b/contracts/test/invariants/SubnetRegistryInvariants.t.sol @@ -18,6 +18,7 @@ import {SubnetGetterFacet} from "../../contracts/subnetregistry/SubnetGetterFace import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; import {IntegrationTestBase, TestRegistry} from "../IntegrationTestBase.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -51,6 +52,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); + params.validatorRewardFacet = address(new ValidatorRewardFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -60,6 +62,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); registryDiamond = createSubnetRegistry(params); registryHandler = new SubnetRegistryHandler(registryDiamond); diff --git a/contracts/test/invariants/handlers/SubnetRegistryHandler.sol b/contracts/test/invariants/handlers/SubnetRegistryHandler.sol index 688383197..a842c2106 100644 --- a/contracts/test/invariants/handlers/SubnetRegistryHandler.sol +++ b/contracts/test/invariants/handlers/SubnetRegistryHandler.sol @@ -11,6 +11,8 @@ import {SubnetRegistryDiamond} from "../../../contracts/SubnetRegistryDiamond.so import {ConsensusType} from "../../../contracts/enums/ConsensusType.sol"; import {SubnetID, PermissionMode} from "../../../contracts/structs/Subnet.sol"; import {AssetHelper} from "../../../contracts/lib/AssetHelper.sol"; +import {ValidatorRewarderMap} from "../../../contracts/examples/ValidatorRewarderMap.sol"; + import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {RegistryFacetsHelper} from "../../helpers/RegistryFacetsHelper.sol"; @@ -124,7 +126,8 @@ contract SubnetRegistryHandler is CommonBase, StdCheats, StdUtils { permissionMode: PermissionMode.Collateral, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); address owner = getRandomOldAddressOrNewOne(seed); diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 791bc688a..20ca09273 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -35,6 +35,13 @@ pub struct GetActivitiesResult { pub start_height: ChainEpoch, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct GetActivitySummaryResult { + pub commitment: [u8; 32], + /// Total number validators that have mined blocks + pub total_active_validators: u64, +} + #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { @@ -42,6 +49,7 @@ pub enum Method { BlockMined = frc42_dispatch::method_hash!("BlockMined"), GetActivities = frc42_dispatch::method_hash!("GetActivities"), PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), + GetSummary = frc42_dispatch::method_hash!("GetSummary"), } impl ActivityTrackerActor { @@ -73,6 +81,11 @@ impl ActivityTrackerActor { Ok(()) } + pub fn get_summary(_rt: &impl Runtime) -> Result { + let dummy = GetActivitySummaryResult{ commitment: [0; 32], total_active_validators: 10 }; + Ok(dummy) + } + pub fn get_activities(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; @@ -97,5 +110,6 @@ impl ActorCode for ActivityTrackerActor { BlockMined => block_mined, GetActivities => get_activities, PurgeActivities => purge_activities, + GetSummary => get_summary, } } From 25252bea530c42b6f12d63e12123f14a610aad8d Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Thu, 17 Oct 2024 22:53:10 +0800 Subject: [PATCH 083/111] update comments --- contracts/contracts/gateway/router/CheckpointingFacet.sol | 1 + fendermint/actors/activity-tracker/src/lib.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index ea91ab86d..797b9ad46 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -68,6 +68,7 @@ contract CheckpointingFacet is GatewayActorModifiers { // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 // TODO: (if there is more time, should wrap param checkpoint with another data structure) // TODO(rewarder): step 3. call fvm ActivityTrackerActor::purge_activities to purge the activities + // TODO(rewarder): step 4. emit validator details as event LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 20ca09273..d9dc7f298 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -40,6 +40,8 @@ pub struct GetActivitySummaryResult { pub commitment: [u8; 32], /// Total number validators that have mined blocks pub total_active_validators: u64, + /// The validator details + pub activities: Vec, } #[derive(FromPrimitive)] From 92ebc67ccb7a7468520277548135c7d888d7d509 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 18 Oct 2024 15:11:53 +0800 Subject: [PATCH 084/111] migrate to event --- contracts/contracts/activities/Activity.sol | 31 ++++++++ .../activities/IValidatorRewarder.sol | 12 ++++ .../activities/LibActivityMerkleVerifier.sol | 8 ++- .../activities/ValidatorRewardFacet.sol | 70 +++++++++++-------- .../examples/ValidatorRewarderMap.sol | 6 +- .../gateway/router/CheckpointingFacet.sol | 39 ++++++++--- .../subnet/SubnetActorCheckpointingFacet.sol | 8 +++ contracts/tasks/deploy-gateway.ts | 5 +- contracts/tasks/validator-rewarder.ts | 13 ---- contracts/test/IntegrationTestBase.sol | 36 +++++++++- contracts/test/helpers/MerkleTreeHelper.sol | 4 +- contracts/test/helpers/SelectorLibrary.sol | 2 +- .../test/integration/SubnetActorDiamond.t.sol | 48 ++++--------- fendermint/actors/activity-tracker/src/lib.rs | 3 +- fendermint/app/options/src/genesis.rs | 3 - fendermint/app/src/cmd/genesis.rs | 3 - .../contract-test/tests/staking/state.rs | 2 - .../testing/materializer/src/docker/mod.rs | 1 - fendermint/vm/genesis/src/arb.rs | 1 - fendermint/vm/genesis/src/lib.rs | 2 - .../vm/interpreter/src/fvm/checkpoint.rs | 34 +++++++-- .../vm/interpreter/src/fvm/state/ipc.rs | 22 ++++++ ipc/api/src/subnet.rs | 1 + ipc/cli/src/commands/subnet/create.rs | 12 ++++ ipc/provider/src/lib.rs | 2 + ipc/provider/src/manager/evm/manager.rs | 1 + 26 files changed, 254 insertions(+), 115 deletions(-) diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index e246136a2..a643a8a4c 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -1,6 +1,25 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; +import {SubnetID} from "../structs/Subnet.sol"; + +event ActivityReportCreated(uint64 checkpointHeight, ActivityReport report); + +/// The full validator activities report +struct ActivityReport { + ValidatorActivityReport[] validators; +} + +struct ValidatorActivityReport { + /// @dev The validator whose activity we're reporting about. + address validator; + /// @dev The number of blocks committed by each validator in the position they appear in the validators array. + /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. + uint64 blocksCommitted; + /// @dev Other metadata + bytes metadata; +} + /// The summary for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint struct ActivitySummary { @@ -24,3 +43,15 @@ struct ValidatorSummary { /// @dev Other metadata bytes metadata; } + +/// The proof required for validators to claim rewards +struct ValidatorClaimProof { + ValidatorSummary summary; + bytes32[] proof; +} + +/// The proofs to batch claim validator rewards +struct BatchClaimProofs { + SubnetID subnetId; + ValidatorClaimProof[] proofs; +} \ No newline at end of file diff --git a/contracts/contracts/activities/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol index 8468ea304..eb47b7fed 100644 --- a/contracts/contracts/activities/IValidatorRewarder.sol +++ b/contracts/contracts/activities/IValidatorRewarder.sol @@ -16,3 +16,15 @@ interface IValidatorRewarder { /// @dev This method should revert if the summary is invalid; this will cause the function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external; } + +/// @title Validator reward setup interface +/// +/// @dev This is used to initialize a reward distribution +interface IValidatorRewardSetup { + function initDistribution( + SubnetID calldata subnetId, + uint64 checkpointHeight, + bytes32 commitment, + uint64 totalActiveValidators + ) external; +} diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index dafc56b54..3ff06a4ae 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -15,9 +15,11 @@ library LibActivityMerkleVerifier { ) internal pure { // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash bytes32 leaf = keccak256( - bytes.concat(keccak256(abi.encode( - summary.checkpointHeight, summary.validator, summary.blocksCommitted, summary.metadata - ))) + bytes.concat( + keccak256( + abi.encode(summary.checkpointHeight, summary.validator, summary.blocksCommitted, summary.metadata) + ) + ) ); bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); if (!valid) { diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index fd6e6b021..cc45fe0cc 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -6,9 +6,9 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; -import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotOwner} from "../errors/IPCErrors.sol"; -import {ValidatorSummary} from "./Activity.sol"; -import {IValidatorRewarder} from "./IValidatorRewarder.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; +import {ValidatorSummary, BatchClaimProofs} from "./Activity.sol"; +import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; @@ -18,10 +18,16 @@ import {LibDiamond} from "../lib/LibDiamond.sol"; /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardFacet is ReentrancyGuard, Pausable { - event RewarderUpdated(address oldRewarder, address newRewarder); - - function batchClaim(SubnetID calldata subnetId) external { - revert("todo"); + function batchClaim( + BatchClaimProofs calldata payload + ) external nonReentrant whenNotPaused { + uint256 len = payload.proofs.length; + for (uint256 i = 0; i < len; ) { + _claim(payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); + unchecked { + i++; + } + } } /// Validators claim their reward for doing work in the child subnet @@ -30,6 +36,15 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { ValidatorSummary calldata summary, bytes32[] calldata proof ) external nonReentrant whenNotPaused { + _claim(subnetId, summary, proof); + } + + function handleRelay() internal pure { + // no opt for now + return; + } + + function _claim(SubnetID calldata subnetId, ValidatorSummary calldata summary, bytes32[] calldata proof) internal { // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight // note: will never exist. @@ -45,11 +60,6 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { LibValidatorReward.handleDistribution(s, subnetId, summary, proof); } - - function handleRelay() internal pure { - // no opt for now - return; - } } /// The activity summary commiment that is currently under reward distribution @@ -92,6 +102,24 @@ library LibValidatorReward { // =========== External library functions ============= + function initNewDistribution( + SubnetID calldata subnetId, + uint64 checkpointHeight, + bytes32 commitment, + uint64 totalActiveValidators + ) internal { + ValidatorRewardStorage storage ds = facetStorage(); + + bytes32 subnetKey = subnetId.toHash(); + + if (ds.distributions[subnetKey][checkpointHeight].totalValidators != 0) { + revert CommitmentAlreadyInitialized(); + } + + ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); + ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; + } + function listCommitments( SubnetID calldata subnetId ) internal view returns (ListCommimentDetail[] memory listDetails) { @@ -123,24 +151,6 @@ library LibValidatorReward { ds.validatorRewarder = rewarder; } - function initNewDistribution( - uint64 checkpointHeight, - bytes32 commitment, - uint64 totalActiveValidators, - SubnetID calldata subnetId - ) internal { - ValidatorRewardStorage storage ds = facetStorage(); - - bytes32 subnetKey = subnetId.toHash(); - - if (ds.distributions[subnetKey][checkpointHeight].totalValidators != 0) { - revert CommitmentAlreadyInitialized(); - } - - ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); - ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; - } - // ============ Internal library functions ============ function facetStorage() internal pure returns (ValidatorRewardStorage storage ds) { diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index f5a0c74f7..1dfa33df5 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -5,7 +5,7 @@ import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; import {ValidatorSummary} from "../activities/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; -/// An example validator rewarder implementation that tracks the accumulated +/// An example validator rewarder implementation that tracks the accumulated /// reward for each valdiator only. contract ValidatorRewarderMap is IValidatorRewarder { SubnetID public subnetId; @@ -26,10 +26,10 @@ contract ValidatorRewarderMap is IValidatorRewarder { function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external { require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); - + address actor = id.route[id.route.length - 1]; require(actor == msg.sender, "not from subnet"); blocksCommitted[summary.validator] += summary.blocksCommitted; } -} \ No newline at end of file +} diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index 797b9ad46..a88cf1026 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -16,7 +16,8 @@ import {BatchNotCreated, InvalidBatchEpoch, BatchAlreadyExists, NotEnoughSubnetC import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {LibValidatorReward} from "../../activities/ValidatorRewardFacet.sol"; + +import {ActivityReportCreated, ActivityReport} from "../../activities/Activity.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -42,13 +43,35 @@ contract CheckpointingFacet is GatewayActorModifiers { LibGateway.checkMsgLength(checkpoint.msgs); execBottomUpMsgs(checkpoint.msgs, subnet); + } + + /// @notice creates a new bottom-up checkpoint with activity report + /// @param checkpoint - a bottom-up checkpoint + /// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight + /// @param membershipWeight - the total weight of the membership + /// @param activityReport - the validator validator report + function createBUChptWithActivities( + BottomUpCheckpoint calldata checkpoint, + bytes32 membershipRootHash, + uint256 membershipWeight, + ActivityReport calldata activityReport + ) external systemActorOnly { + if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { + revert CheckpointAlreadyExists(); + } + + LibQuorum.createQuorumInfo({ + self: s.checkpointQuorumMap, + objHeight: checkpoint.blockHeight, + objHash: keccak256(abi.encode(checkpoint)), + membershipRootHash: membershipRootHash, + membershipWeight: membershipWeight, + majorityPercentage: s.majorityPercentage + }); + + LibGateway.storeBottomUpCheckpoint(checkpoint); - LibValidatorReward.initNewDistribution( - uint64(checkpoint.blockHeight), - checkpoint.activities.commitment, - checkpoint.activities.totalActiveValidators, - checkpoint.subnetID - ); + emit ActivityReportCreated(uint64(checkpoint.blockHeight), activityReport); } /// @notice creates a new bottom-up checkpoint @@ -65,7 +88,7 @@ contract CheckpointingFacet is GatewayActorModifiers { } // TODO(rewarder): step 1. call fvm ActivityTrackerActor::get_summary to generate the summary - // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 + // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 // TODO: (if there is more time, should wrap param checkpoint with another data structure) // TODO(rewarder): step 3. call fvm ActivityTrackerActor::purge_activities to purge the activities // TODO(rewarder): step 4. emit validator details as event diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index 84aefa146..c130b2ad5 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -13,6 +13,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {LibGateway} from "../lib/LibGateway.sol"; +import {LibValidatorReward} from "../activities/ValidatorRewardFacet.sol"; contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using EnumerableSet for EnumerableSet.AddressSet; @@ -49,6 +50,13 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); + LibValidatorReward.initNewDistribution( + checkpoint.subnetID, + uint64(checkpoint.blockHeight), + checkpoint.activities.commitment, + checkpoint.activities.totalActiveValidators + ); + // confirming the changes in membership in the child LibStaking.confirmChange(checkpoint.nextConfigurationNumber); } diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts index f463242c9..de2226d1b 100644 --- a/contracts/tasks/deploy-gateway.ts +++ b/contracts/tasks/deploy-gateway.ts @@ -67,7 +67,7 @@ async function deployFacets(hre: HardhatRuntimeEnvironment, deployer: string): P }, { name: 'TopDownFinalityFacet', libraries: ['AccountHelper'] }, { name: 'OwnershipFacet' }, - ]; + ] return await Deployments.deploy(hre, deployer, ...facets) } @@ -76,12 +76,11 @@ async function deployGatewayDiamond( hre: HardhatRuntimeEnvironment, deployer: string, facets: Deployments, - ): Promise { gatewayConstructorParams.networkName.root = await hre.getChainId() gatewayConstructorParams.commitSha = hre.ethers.utils.formatBytes32String(gitCommitSha()) - console.log("deploy gateway with params", gatewayConstructorParams); + console.log('deploy gateway with params', gatewayConstructorParams) const deployments = await Deployments.deploy(hre, deployer, { name: 'GatewayDiamond', diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index a71365f92..02f7f5adf 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -19,17 +19,4 @@ task('validator-rewarder-deploy') name: 'ValidatorRewarderMap', libraries: [], }) - }) - -// step 2. claim the validator rewarder -// sample command: pnpm exec hardhat validator-rewarder-claim --network calibrationnet -task('validator-rewarder-deploy') - .setDescription('Claim reward from example subnet validator rewarder contract') - .setAction(async (_: TaskArguments, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - - const [validator] = await hre.getUnnamedAccounts() - const balance = await hre.ethers.provider.getBalance(validator) - - // todo }) \ No newline at end of file diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 8d7252e3e..09a78abc1 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -934,7 +934,41 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: uint64(validators.length), commitment: bytes32(uint256(nextConfigNum))}) + activities: ActivitySummary({ + totalActiveValidators: uint64(validators.length), + commitment: bytes32(uint256(nextConfigNum)) + }) + }); + + vm.deal(address(saDiamond), 100 ether); + + bytes32 hash = keccak256(abi.encode(checkpoint)); + + for (uint256 i = 0; i < n; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKeys[i], hash); + signatures[i] = abi.encodePacked(r, s, v); + } + + vm.prank(validators[0]); + saDiamond.checkpointer().submitCheckpoint(checkpoint, validators, signatures); + } + + function confirmChange(address[] memory validators, uint256[] memory privKeys, ActivitySummary memory activities) internal { + uint256 n = validators.length; + + bytes[] memory signatures = new bytes[](n); + + (uint64 nextConfigNum, ) = saDiamond.getter().getConfigurationNumbers(); + + uint256 h = saDiamond.getter().lastBottomUpCheckpointHeight() + saDiamond.getter().bottomUpCheckPeriod(); + + BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ + subnetID: saDiamond.getter().getParent().createSubnetId(address(saDiamond)), + blockHeight: h, + blockHash: keccak256(abi.encode(h)), + nextConfigurationNumber: nextConfigNum - 1, + msgs: new IpcEnvelope[](0), + activities: activities }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index de6d5bd30..e3b7ed21f 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -55,7 +55,9 @@ library MerkleTreeHelper { bytes32[][] memory proofs = new bytes32[][](len); bytes32[] memory data = new bytes32[](len); for (uint256 i = 0; i < len; i++) { - data[i] = keccak256(bytes.concat(keccak256(abi.encode(checkpointHeights[i], addrs[i], blocksMined[i], metadatas[i])))); + data[i] = keccak256( + bytes.concat(keccak256(abi.encode(checkpointHeights[i], addrs[i], blocksMined[i], metadatas[i]))) + ); } root = merkleTree.getRoot(data); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index e9722b2b6..e5bc13b63 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026be7503e0000000000000000000000000000000000000000000000000000000097d1471400000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000336293a3b000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000229b0e5700000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index af5328d63..ffb7e4137 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -2346,33 +2346,24 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { ); ValidatorRewarderMap m = new ValidatorRewarderMap(); params.validatorRewarder = address(m); - + params.minValidators = 2; + params.permissionMode = PermissionMode.Federated; + saDiamond = createSubnetActor(params); - SubnetID memory tmpId = SubnetID(ROOTNET_CHAINID, new address[](1)); - tmpId.route[0] = address(saDiamond); - m.setSubnet(tmpId); - - address caller = address(saDiamond); - vm.startPrank(caller); - vm.deal(caller, DEFAULT_COLLATERAL_AMOUNT + DEFAULT_CROSS_MSG_FEE); - registerSubnet(DEFAULT_COLLATERAL_AMOUNT, caller); - vm.stopPrank(); - - (SubnetID memory subnetId, , , , ) = getSubnet(address(caller)); - (bool exist, Subnet memory subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); - require(exist, "subnet does not exist"); - require(subnetInfo.circSupply == 0, "unexpected initial circulation supply"); - require(tmpId.route[0] == subnetId.route[0], "address not match"); + SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1)); + subnetId.route[0] = address(saDiamond); + m.setSubnet(subnetId); - gatewayDiamond.manager().fund{value: DEFAULT_COLLATERAL_AMOUNT}( - subnetId, - FvmAddressHelper.from(address(caller)) - ); - (, subnetInfo) = gatewayDiamond.getter().getSubnet(subnetId); - require(subnetInfo.circSupply == DEFAULT_COLLATERAL_AMOUNT, "unexpected circulation supply after funding"); + (address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4); + + uint256[] memory powers = new uint256[](4); + powers[0] = 10000; + powers[1] = 10000; + powers[2] = 10000; + powers[3] = 10000; + saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); - (, address[] memory addrs, ) = TestUtils.getFourValidators(vm); bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); uint64[] memory checkpointHeights = new uint64[](addrs.length); @@ -2391,17 +2382,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { checkpointHeights, metadata ); - BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ - subnetID: subnetId, - blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), - blockHash: keccak256("block1"), - nextConfigurationNumber: 1, - msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: activityRoot}) - }); - vm.prank(caller); - gatewayDiamond.checkpointer().commitCheckpoint(checkpoint); + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot})); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index d9dc7f298..97b6f69d0 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -84,7 +84,8 @@ impl ActivityTrackerActor { } pub fn get_summary(_rt: &impl Runtime) -> Result { - let dummy = GetActivitySummaryResult{ commitment: [0; 32], total_active_validators: 10 }; + // todo + let dummy = GetActivitySummaryResult{ commitment: [0; 32], total_active_validators: 10, activities: vec![] }; Ok(dummy) } diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index f44f1a916..740384758 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -194,9 +194,6 @@ pub struct GenesisIpcGatewayArgs { /// Maximum number of active validators. #[arg(long, short = 'v', default_value = "100")] pub active_validators_limit: u16, - - #[arg(long, value_parser = parse_eth_address, default_value = "0xff00000000000000000000000000000000000065")] - pub validator_rewarder: Address, } #[derive(Args, Debug, Clone)] diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index c817d9cba..134c9ad4d 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -271,7 +271,6 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh bottom_up_check_period: args.bottom_up_check_period, majority_percentage: args.majority_percentage, active_validators_limit: args.active_validators_limit, - validator_rewarder: args.validator_rewarder, }; let ipc_params = match genesis.ipc { @@ -337,8 +336,6 @@ async fn new_genesis_from_parent( bottom_up_check_period: genesis_info.bottom_up_checkpoint_period, majority_percentage: genesis_info.majority_percentage, active_validators_limit: genesis_info.active_validators_limit, - // default to zero address, need to call setter separately - validator_rewarder: Address::from(EthAddress([0; 20])), }, }; let mut genesis = Genesis { diff --git a/fendermint/testing/contract-test/tests/staking/state.rs b/fendermint/testing/contract-test/tests/staking/state.rs index 2f4b5f5f3..f1af4eaee 100644 --- a/fendermint/testing/contract-test/tests/staking/state.rs +++ b/fendermint/testing/contract-test/tests/staking/state.rs @@ -566,7 +566,6 @@ impl arbitrary::Arbitrary<'_> for StakingState { bottom_up_check_period: 1 + u.choose_index(100)? as u64, majority_percentage: 51 + u8::arbitrary(u)? % 50, active_validators_limit: 1 + u.choose_index(100)? as u16, - validator_rewarder: Address::from(EthAddress([0; 20])), }, }; @@ -595,7 +594,6 @@ impl arbitrary::Arbitrary<'_> for StakingState { bottom_up_check_period: 1 + u.choose_index(100)? as u64, majority_percentage: 51 + u8::arbitrary(u)? % 50, active_validators_limit: num_max_validators as u16, - validator_rewarder: Address::from(EthAddress([0; 20])), }, }; diff --git a/fendermint/testing/materializer/src/docker/mod.rs b/fendermint/testing/materializer/src/docker/mod.rs index 2ce6819f4..f31cd0675 100644 --- a/fendermint/testing/materializer/src/docker/mod.rs +++ b/fendermint/testing/materializer/src/docker/mod.rs @@ -694,7 +694,6 @@ impl Materializer for DockerMaterializer { bottom_up_check_period: 1, majority_percentage: 67, active_validators_limit: 100, - validator_rewarder: fvm_shared::address::Address::from(EthAddress([0; 20])), }, }), }; diff --git a/fendermint/vm/genesis/src/arb.rs b/fendermint/vm/genesis/src/arb.rs index ed18e5779..491872716 100644 --- a/fendermint/vm/genesis/src/arb.rs +++ b/fendermint/vm/genesis/src/arb.rs @@ -127,7 +127,6 @@ impl Arbitrary for ipc::GatewayParams { bottom_up_check_period: u64::arbitrary(g).max(1), majority_percentage: u8::arbitrary(g) % 50 + 51, active_validators_limit: u16::arbitrary(g) % 100 + 1, - validator_rewarder: Address::from(EthAddress([0; 20])), } } } diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index 25a5bf4fc..3d9753800 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -244,8 +244,6 @@ pub mod ipc { pub bottom_up_check_period: u64, pub majority_percentage: u8, pub active_validators_limit: u16, - #[serde_as(as = "IsHumanReadable")] - pub validator_rewarder: Address, } } diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 013b6f626..73b8b9449 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -19,6 +19,8 @@ use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::BottomUpCheckpoint; use fendermint_vm_genesis::{Power, Validator, ValidatorKey}; +use ipc_api::evm::payload_to_evm_address; + use ipc_actors_abis::checkpointing_facet as checkpoint; use ipc_actors_abis::gateway_getter_facet as getter; use ipc_api::staking::ConfigurationNumber; @@ -97,6 +99,10 @@ where let num_msgs = msgs.len(); + let activities = state + .activities_tracker() + .get_activities_summary()?; + // Construct checkpoint. let checkpoint = BottomUpCheckpoint { subnet_id, @@ -104,17 +110,33 @@ where block_hash, next_configuration_number, msgs, - activities: state - .activities_tracker() - .get_activities_summary()? - .commitment(height.value() as i64)? - .try_into()?, + activities: activities.commitment(height.value() as i64)?.try_into()?, }; // Save the checkpoint in the ledger. // Pass in the current power table, because these are the validators who can sign this checkpoint. + + // gateway + // .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) + // .context("failed to store checkpoint")?; + + let report = checkpoint::ActivityReport { + validators: activities + .details + .into_iter() + .map(|v| Ok(checkpoint::ValidatorActivityReport { + validator: payload_to_evm_address(v.validator.payload())?, + blocks_committed: v.block_committed, + metadata: ethers::types::Bytes::from(v.metadata), + })).collect::>>()? + }; gateway - .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) + .create_bu_ckpt_with_activities( + state, + checkpoint.clone(), + &curr_power_table.0, + report + ) .context("failed to store checkpoint")?; state.activities_tracker().purge_activities()?; diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 12caa26c6..5c0852fca 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -138,6 +138,28 @@ impl GatewayCaller { }) } + /// Insert a new checkpoint at the period boundary. + pub fn create_bu_ckpt_with_activities( + &self, + state: &mut FvmExecState, + checkpoint: checkpointing_facet::BottomUpCheckpoint, + power_table: &[Validator], + activities: checkpointing_facet::ActivityReport, + ) -> anyhow::Result<()> { + // Construct a Merkle tree from the power table, which we can use to validate validator set membership + // when the signatures are submitted in transactions for accumulation. + let tree = + ValidatorMerkleTree::new(power_table).context("failed to create validator tree")?; + + let total_power = power_table.iter().fold(et::U256::zero(), |p, v| { + p.saturating_add(et::U256::from(v.power.0)) + }); + + self.checkpointing.call(state, |c| { + c.create_bu_chpt_with_activities(checkpoint, tree.root_hash().0, total_power, activities) + }) + } + /// Retrieve checkpoints which have not reached a quorum. pub fn incomplete_checkpoints( &self, diff --git a/ipc/api/src/subnet.rs b/ipc/api/src/subnet.rs index 386df79a3..3213d3502 100644 --- a/ipc/api/src/subnet.rs +++ b/ipc/api/src/subnet.rs @@ -88,6 +88,7 @@ pub struct ConstructParams { pub supply_source: Asset, pub collateral_source: Asset, pub validator_gater: Address, + pub validator_rewarder: Address, } /// Consensus types supported by hierarchical consensus diff --git a/ipc/cli/src/commands/subnet/create.rs b/ipc/cli/src/commands/subnet/create.rs index f170827f0..4cedd64ab 100644 --- a/ipc/cli/src/commands/subnet/create.rs +++ b/ipc/cli/src/commands/subnet/create.rs @@ -42,6 +42,12 @@ impl CreateSubnet { .clone() .unwrap_or(ZERO_ADDRESS.to_string()); let validator_gater = require_fil_addr_from_str(&raw_addr)?; + + let raw_addr = arguments + .validator_rewarder + .clone() + .unwrap_or(ZERO_ADDRESS.to_string()); + let validator_rewarder = require_fil_addr_from_str(&raw_addr)?; let addr = provider .create_subnet( from, @@ -57,6 +63,7 @@ impl CreateSubnet { supply_source, collateral_source, validator_gater, + validator_rewarder, ) .await?; @@ -165,6 +172,11 @@ pub struct CreateSubnetArgs { help = "The address of validator gating contract. None if validator gating is disabled" )] pub validator_gater: Option, + #[arg( + long, + help = "The address of validator rewarder contract." + )] + pub validator_rewarder: Option, #[arg( long, help = "The kind of collateral source of a subnet on its parent subnet: native or erc20", diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index 5fa469f30..b0faebd51 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -256,6 +256,7 @@ impl IpcProvider { supply_source: Asset, collateral_source: Asset, validator_gater: Address, + validator_rewarder: Address, ) -> anyhow::Result
{ let conn = self.get_connection(&parent)?; @@ -275,6 +276,7 @@ impl IpcProvider { supply_source, collateral_source, validator_gater, + validator_rewarder, }; conn.manager() diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 21fc3416f..47ca4a938 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -278,6 +278,7 @@ impl SubnetManager for EthSubnetManager { supply_source: register_subnet_facet::Asset::try_from(params.supply_source)?, collateral_source: register_subnet_facet::Asset::try_from(params.collateral_source)?, validator_gater: payload_to_evm_address(params.validator_gater.payload())?, + validator_rewarder: payload_to_evm_address(params.validator_rewarder.payload())?, }; tracing::info!("creating subnet on evm with params: {params:?}"); From e7edce1d0d23667c593dc2dbbfa37f10ac1cc114 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 18 Oct 2024 16:24:35 +0800 Subject: [PATCH 085/111] update impl --- Cargo.toml | 1 + fendermint/actors/activity-tracker/src/lib.rs | 6 +- fendermint/app/src/tmconv.rs | 63 ----- .../testing/contract-test/tests/gas_market.rs | 142 +++++------ fendermint/vm/genesis/src/lib.rs | 1 - fendermint/vm/interpreter/Cargo.toml | 1 + fendermint/vm/interpreter/src/chain.rs | 1 - .../vm/interpreter/src/fvm/checkpoint.rs | 24 +- fendermint/vm/interpreter/src/fvm/exec.rs | 11 +- .../vm/interpreter/src/fvm/gas/actor.rs | 225 ------------------ fendermint/vm/interpreter/src/fvm/gas/mod.rs | 48 ---- .../vm/interpreter/src/fvm/state/exec.rs | 13 +- .../vm/interpreter/src/fvm/state/ipc.rs | 7 +- ipc/cli/src/commands/subnet/create.rs | 5 +- 14 files changed, 103 insertions(+), 445 deletions(-) delete mode 100644 fendermint/vm/interpreter/src/fvm/gas/actor.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/gas/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 689d38476..cfe34ad09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "fendermint/actors", "fendermint/actors/api", "fendermint/actors/chainmetadata", + "fendermint/actors/activity-tracker", "fendermint/actors/eam", "fendermint/actors/gas_market/eip1559", ] diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 97b6f69d0..ec075cbca 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -85,7 +85,11 @@ impl ActivityTrackerActor { pub fn get_summary(_rt: &impl Runtime) -> Result { // todo - let dummy = GetActivitySummaryResult{ commitment: [0; 32], total_active_validators: 10, activities: vec![] }; + let dummy = GetActivitySummaryResult { + commitment: [0; 32], + total_active_validators: 10, + activities: vec![], + }; Ok(dummy) } diff --git a/fendermint/app/src/tmconv.rs b/fendermint/app/src/tmconv.rs index 765d3ca6d..cd1e1e786 100644 --- a/fendermint/app/src/tmconv.rs +++ b/fendermint/app/src/tmconv.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT //! Conversions to Tendermint data types. use anyhow::{anyhow, bail, Context}; -use fendermint_actor_gas_market::SetConstants; use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Power, Validator}; use fendermint_vm_interpreter::fvm::{ @@ -11,13 +10,11 @@ use fendermint_vm_interpreter::fvm::{ }; use fendermint_vm_message::signed::DomainHash; use fendermint_vm_snapshot::{SnapshotItem, SnapshotManifest}; -use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, error::ExitCode, event::StampedEvent, ActorID}; use prost::Message; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, num::NonZeroU32}; use tendermint::abci::{response, Code, Event, EventAttribute}; -use tendermint_rpc::Client; use crate::{app::AppError, BlockHeight}; @@ -27,25 +24,6 @@ struct SnapshotMetadata { state_params: FvmStateParams, } -/// The end block update for cometbft -pub struct EndBlockUpdate { - pub max_gas: Option, - pub validators: PowerUpdates, -} - -impl EndBlockUpdate { - pub fn new(power: PowerUpdates) -> Self { - Self { - max_gas: None, - validators: power, - } - } - - pub fn update_gas(&mut self, constants: SetConstants) { - self.max_gas = Some(constants.block_gas_limit); - } -} - /// IPLD encoding of data types we know we must be able to encode. macro_rules! ipld_encode { ($var:ident) => { @@ -54,31 +32,6 @@ macro_rules! ipld_encode { }; } -pub(crate) async fn to_end_block( - client: &C, - height: ChainEpoch, - value: EndBlockUpdate, -) -> anyhow::Result { - let validator_updates = - to_validator_updates(value.validators.0).context("failed to convert validator updates")?; - - let mut consensus_param_updates = None; - if let Some(max_gas) = value.max_gas { - let mut consensus_params = client - .consensus_params(tendermint::block::Height::try_from(height)?) - .await? - .consensus_params; - consensus_params.block.max_gas = max_gas as i64; - consensus_param_updates = Some(consensus_params); - } - - Ok(response::EndBlock { - validator_updates, - consensus_param_updates, - events: Vec::new(), // TODO: Events from epoch transitions? - }) -} - /// Response to delivery where the input was blatantly invalid. /// This indicates that the validator who made the block was Byzantine. pub fn invalid_deliver_tx(err: AppError, description: String) -> response::DeliverTx { @@ -111,22 +64,6 @@ pub fn invalid_query(err: AppError, description: String) -> response::Query { } } -/// Convert validator power to tendermint validator update. -/// TODO: the import is quite strange, `Validator` and `Power` are imported from `genesis` crate, -/// TODO: which should be from a `type` or `validator` crate. -pub fn to_validator_updates( - validators: Vec>, -) -> anyhow::Result> { - let mut updates = vec![]; - for v in validators { - updates.push(tendermint::validator::Update { - pub_key: tendermint::PublicKey::try_from(v.public_key)?, - power: tendermint::vote::Power::try_from(v.power.0)?, - }); - } - Ok(updates) -} - pub fn to_deliver_tx( ret: FvmApplyRet, domain_hash: Option, diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs index 7440521a4..5695d08eb 100644 --- a/fendermint/testing/contract-test/tests/gas_market.rs +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -3,22 +3,24 @@ mod staking; +use anyhow::Context; use async_trait::async_trait; -use fendermint_actor_gas_market_eip1559::Constants; +use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; use fendermint_contract_test::Tester; use fendermint_crypto::{PublicKey, SecretKey}; use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::gas_market::GAS_MARKET_ACTOR_ADDR; -use fendermint_vm_actor_interface::system::SYSTEM_ACTOR_ADDR; +use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; +use fendermint_vm_actor_interface::system; use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; +use fendermint_vm_interpreter::fvm::gas::GasMarket; +use fendermint_vm_interpreter::fvm::state::FvmExecState; use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; use fendermint_vm_interpreter::fvm::FvmMessageInterpreter; -use fvm::executor::{ApplyKind, Executor}; -use fvm_ipld_encoding::RawBytes; use fvm_shared::address::Address; use fvm_shared::bigint::Zero; +use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use fvm_shared::message::Message; use fvm_shared::version::NetworkVersion; @@ -29,15 +31,15 @@ use tendermint_rpc::Client; lazy_static! { static ref ADDR: Address = - Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap(); + Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); static ref ADDR2: Address = - Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap(); + Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); } const CHAIN_NAME: &str = "mychain"; type I = FvmMessageInterpreter; // returns a seeded secret key which is guaranteed to be the same every time -fn rand_secret_key() -> SecretKey { +fn my_secret_key() -> SecretKey { SecretKey::random(&mut StdRng::seed_from_u64(123)) } @@ -50,7 +52,7 @@ async fn default_tester() -> (Tester, PublicKey) { async fn tester_with_upgrader( upgrade_scheduler: UpgradeScheduler, ) -> (Tester, PublicKey) { - let validator = rand_secret_key().public_key(); + let validator = my_secret_key().public_key(); let interpreter: FvmMessageInterpreter = FvmMessageInterpreter::new(NeverCallClient, None, 1.05, 1.05, false, upgrade_scheduler); @@ -87,13 +89,16 @@ async fn test_gas_market_base_fee_oscillation() { let (mut tester, _) = default_tester().await; let num_msgs = 10; - let block_gas_limit = 6178630; - let base_gas_limit = block_gas_limit / num_msgs; + let total_gas_limit = 6178630; + let base_gas_limit = total_gas_limit / num_msgs; + + let mut gas_constants = SetConstants::default(); + gas_constants.block_gas_limit = total_gas_limit; let messages = (0..num_msgs) .map(|i| Message { version: 0, - from: *ADDR, + from: ADDR.clone(), to: Address::new_id(10), sequence: i, value: TokenAmount::from_atto(1), @@ -105,24 +110,24 @@ async fn test_gas_market_base_fee_oscillation() { }) .collect::>(); - let producer = rand_secret_key().public_key(); - - // block 1: set the gas constants + // iterate over all the upgrades let height = 1; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); tester - .execute_msgs(vec![custom_gas_limit(block_gas_limit)]) + .modify_exec_state(|mut state| async { + state.gas_market_mut().set_constants(gas_constants); + Ok((state, ())) + }) .await .unwrap(); tester.end_block(height).await.unwrap(); tester.commit().await.unwrap(); - // let height = 2; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); let before_reading = tester .modify_exec_state(|mut state| async { - let reading = state.read_gas_market()?; + let reading = current_reading(&mut state, height)?; Ok((state, reading)) }) .await @@ -132,10 +137,10 @@ async fn test_gas_market_base_fee_oscillation() { tester.commit().await.unwrap(); let height = 3; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); let post_full_block_reading = tester .modify_exec_state(|mut state| async { - let reading = state.read_gas_market()?; + let reading = current_reading(&mut state, height)?; Ok((state, reading)) }) .await @@ -148,10 +153,10 @@ async fn test_gas_market_base_fee_oscillation() { ); let height = 4; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); let post_empty_block_reading = tester .modify_exec_state(|mut state| async { - let reading = state.read_gas_market()?; + let reading = current_reading(&mut state, height)?; Ok((state, reading)) }) .await @@ -177,8 +182,8 @@ async fn test_gas_market_premium_distribution() { let messages = (0..num_msgs) .map(|i| Message { version: 0, - from: *ADDR, - to: *ADDR2, + from: ADDR.clone(), + to: ADDR2.clone(), sequence: i, value: TokenAmount::from_atto(1), method_num: 0, @@ -189,11 +194,12 @@ async fn test_gas_market_premium_distribution() { }) .collect::>(); - let proposer = rand_secret_key().public_key(); - // iterate over all the upgrades let height = 1; - tester.begin_block(height, proposer).await.unwrap(); + tester + .begin_block_with_validator(height, Some(validator)) + .await + .unwrap(); let initial_balance = tester .modify_exec_state(|state| async { let tree = state.state_tree(); @@ -232,85 +238,85 @@ async fn test_gas_market_premium_distribution() { async fn test_gas_market_upgrade() { let mut upgrader = UpgradeScheduler::new(); - // Initial block gas limit is determined by the default constants. - let initial_block_gas_limit = Constants::default().block_gas_limit; - let updated_block_gas_limit = 200; - - // Attach an upgrade at epoch 2 that changes the gas limit to 200. + let total_gas_limit = 100; upgrader .add( - Upgrade::new(CHAIN_NAME, 2, Some(1), move |state| { + Upgrade::new(CHAIN_NAME, 1, Some(1), |state| { println!( "[Upgrade at height {}] Update gas market params", state.block_height() ); - state.execute_with_executor(|executor| { - // cannot capture updated_block_gas_limit due to Upgrade::new wanting a fn pointer. - let msg = custom_gas_limit(200); - executor.execute_message(msg, ApplyKind::Implicit, 0)?; - Ok(()) - }) + + let mut gas_constants = SetConstants::default(); + gas_constants.block_gas_limit = 100; + + state.gas_market_mut().set_constants(gas_constants); + + Ok(()) }) .unwrap(), ) .unwrap(); - // Create a new tester with the upgrader attached. let (mut tester, _) = tester_with_upgrader(upgrader).await; - let producer = rand_secret_key().public_key(); - - // At height 1, simply read the block gas limit and ensure it's the default. let height = 1; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); let reading = tester .modify_exec_state(|mut state| async { - let reading = state.read_gas_market()?; + let reading = current_reading(&mut state, height)?; Ok((state, reading)) }) .await .unwrap(); - assert_eq!( - reading.block_gas_limit, initial_block_gas_limit, - "block gas limit should be the default as per constants" + assert_ne!( + reading.block_gas_limit, total_gas_limit, + "gas limit should not equal at start" ); tester.end_block(height).await.unwrap(); tester.commit().await.unwrap(); - // The upgrade above should have updated the gas limit to 200. let height = 2; - tester.begin_block(height, producer).await.unwrap(); + tester.begin_block(height).await.unwrap(); let reading = tester .modify_exec_state(|mut state| async { - let reading = state.read_gas_market()?; + let reading = current_reading(&mut state, height)?; Ok((state, reading)) }) .await .unwrap(); assert_eq!( - reading.block_gas_limit, updated_block_gas_limit, - "gas limit post-upgrade should be {updated_block_gas_limit}" + reading.block_gas_limit, total_gas_limit, + "gas limit should equal after upgrade" ); } -fn custom_gas_limit(block_gas_limit: u64) -> Message { - let gas_constants = fendermint_actor_gas_market_eip1559::SetConstants { - block_gas_limit, - ..Default::default() - }; - - Message { - version: 0, - from: SYSTEM_ACTOR_ADDR, +pub fn current_reading( + state: &mut FvmExecState, + block_height: ChainEpoch, +) -> anyhow::Result { + let msg = Message { + from: system::SYSTEM_ACTOR_ADDR, to: GAS_MARKET_ACTOR_ADDR, - sequence: 0, + sequence: block_height as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, + params: fvm_ipld_encoding::RawBytes::default(), value: Default::default(), - method_num: fendermint_actor_gas_market_eip1559::Method::SetConstants as u64, - params: RawBytes::serialize(&gas_constants).unwrap(), - gas_limit: 10000000, + version: Default::default(), gas_fee_cap: Default::default(), gas_premium: Default::default(), + }; + let (apply_ret, _) = state.execute_implicit(msg)?; + + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to read gas market state: {}", err); } + + let r = fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse gas market readying")?; + Ok(r) } #[derive(Clone)] diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index 3d9753800..ae87b4a1d 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -226,7 +226,6 @@ impl From for PermissionModeParams { /// IPC related data structures. pub mod ipc { use fendermint_vm_encoding::IsHumanReadable; - use fvm_shared::address::Address; use ipc_api::subnet_id::SubnetID; use serde::{Deserialize, Serialize}; use serde_with::serde_as; diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 8fffec2f6..39c6f2e34 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -24,6 +24,7 @@ fendermint_rpc = { path = "../../rpc" } fendermint_tracing = { path = "../../tracing" } fendermint_actors = { path = "../../actors" } fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" } +fendermint_actor_activity_tracker = { path = "../../actors/activity-tracker" } fendermint_actor_gas_market_eip1559 = { path = "../../actors/gas_market/eip1559" } fendermint_actor_eam = { workspace = true } fendermint_testing = { path = "../../testing", optional = true } diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 50d6224a2..034090a30 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,6 +1,5 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::gas::GasMarket; use crate::fvm::state::ipc::GatewayCaller; use crate::fvm::store::ReadOnlyBlockstore; use crate::fvm::{topdown, BlockGasLimit, FvmApplyRet, PowerUpdates}; diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 73b8b9449..aa56f5086 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -99,9 +99,7 @@ where let num_msgs = msgs.len(); - let activities = state - .activities_tracker() - .get_activities_summary()?; + let activities = state.activities_tracker().get_activities_summary()?; // Construct checkpoint. let checkpoint = BottomUpCheckpoint { @@ -124,19 +122,17 @@ where validators: activities .details .into_iter() - .map(|v| Ok(checkpoint::ValidatorActivityReport { - validator: payload_to_evm_address(v.validator.payload())?, - blocks_committed: v.block_committed, - metadata: ethers::types::Bytes::from(v.metadata), - })).collect::>>()? + .map(|v| { + Ok(checkpoint::ValidatorActivityReport { + validator: payload_to_evm_address(v.validator.payload())?, + blocks_committed: v.block_committed, + metadata: ethers::types::Bytes::from(v.metadata), + }) + }) + .collect::>>()?, }; gateway - .create_bu_ckpt_with_activities( - state, - checkpoint.clone(), - &curr_power_table.0, - report - ) + .create_bu_ckpt_with_activities(state, checkpoint.clone(), &curr_power_table.0, report) .context("failed to store checkpoint")?; state.activities_tracker().purge_activities()?; diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 86bd74ebc..1e71ebf83 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -5,6 +5,7 @@ use anyhow::Context; use async_trait::async_trait; use std::collections::HashMap; +use crate::ExecInterpreter; use fendermint_vm_actor_interface::{chainmetadata, cron, system}; use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; @@ -12,12 +13,6 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; -use crate::fvm::activities::BlockMined; -use crate::fvm::gas::{GasMarket, GasUtilization}; -use crate::ExecInterpreter; - -use crate::fvm::activities::ValidatorActivityTracker; - use super::{ checkpoint::{self, PowerUpdates}, observe::{CheckpointFinalized, MsgExec, MsgExecPurpose}, @@ -170,10 +165,6 @@ where let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - state - .gas_market_mut() - .record_utilization(GasUtilization::from(&apply_ret)); - (apply_ret, emitters, latency) }; diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs deleted file mode 100644 index 6efe35588..000000000 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::gas::{Available, CommitRet, Gas, GasMarket, GasUtilization}; -use crate::fvm::FvmMessage; -use anyhow::{anyhow, Context}; - -use fendermint_actor_gas_market::{BlockGasUtilizationRet, GasMarketReading, SetConstants}; -use fendermint_crypto::PublicKey; -use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; -use fendermint_vm_actor_interface::{reward, system}; -use fvm::executor::{ApplyKind, ApplyRet, Executor}; -use fvm_shared::address::Address; -use fvm_shared::clock::ChainEpoch; -use fvm_shared::econ::TokenAmount; -use fvm_shared::METHOD_SEND; - -#[derive(Default)] -pub struct ActorGasMarket { - /// The base fee for fvm - base_fee: TokenAmount, - /// The total gas premium for the miner - gas_premium: TokenAmount, - /// The block gas limit - block_gas_limit: Gas, - /// The accumulated gas usage so far - block_gas_used: Gas, - /// Pending update to the underlying gas actor - constant_update: Option, -} - -impl GasMarket for ActorGasMarket { - type Constant = SetConstants; - - fn set_constants(&mut self, constants: Self::Constant) { - self.constant_update = Some(constants); - } - - fn available(&self) -> Available { - Available { - block_gas: self.block_gas_limit - self.block_gas_used.min(self.block_gas_limit), - } - } - - fn record_utilization(&mut self, utilization: GasUtilization) { - self.gas_premium += utilization.gas_premium; - self.block_gas_used += utilization.gas_used; - - // sanity check - if self.block_gas_used >= self.block_gas_limit { - tracing::warn!("out of block gas, vm execution more than available gas limit"); - } - } -} - -impl ActorGasMarket { - pub fn current_reading( - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, - params: fvm_ipld_encoding::RawBytes::default(), - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to read gas market state: {}", err); - } - - let r = - fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) - .context("failed to parse gas market readying")?; - Ok(r) - } - - pub fn create( - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let reading = Self::current_reading(executor, block_height)?; - Ok(Self { - base_fee: reading.base_fee, - gas_premium: TokenAmount::from_atto(0), - block_gas_limit: reading.block_gas_limit, - block_gas_used: 0, - constant_update: None, - }) - } - - pub fn take_constant_update(&mut self) -> Option { - self.constant_update.take() - } - - pub fn commit( - &self, - executor: &mut E, - block_height: ChainEpoch, - validator: Option, - ) -> anyhow::Result { - self.distribute_reward(executor, block_height, validator)?; - self.commit_constants(executor, block_height)?; - self.commit_utilization(executor, block_height) - } - - fn distribute_reward( - &self, - executor: &mut E, - block_height: ChainEpoch, - validator: Option, - ) -> anyhow::Result<()> { - if validator.is_none() || self.gas_premium.is_zero() { - return Ok(()); - } - - let validator = validator.unwrap(); - let validator = Address::from(EthAddress::new_secp256k1(&validator.serialize())?); - - let msg = FvmMessage { - from: reward::REWARD_ACTOR_ADDR, - to: validator, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: METHOD_SEND, - params: fvm_ipld_encoding::RawBytes::default(), - value: self.gas_premium.clone(), - - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - self.apply_implicit_message(msg, executor)?; - - Ok(()) - } - - fn commit_constants( - &self, - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result<()> { - let Some(ref constants) = self.constant_update else { - return Ok(()); - }; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::SetConstants as u64, - params: fvm_ipld_encoding::RawBytes::serialize(constants)?, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - self.apply_implicit_message(msg, executor)?; - - Ok(()) - } - - fn commit_utilization( - &self, - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let block_gas_used = self.block_gas_used.min(self.block_gas_limit); - let params = fvm_ipld_encoding::RawBytes::serialize( - fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, - )?; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, - params, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let apply_ret = self.apply_implicit_message(msg, executor)?; - let r = fvm_ipld_encoding::from_slice::( - &apply_ret.msg_receipt.return_data, - ) - .context("failed to parse gas utilization result")?; - Ok(CommitRet { - base_fee: r.base_fee, - }) - } - - fn apply_implicit_message( - &self, - msg: FvmMessage, - executor: &mut E, - ) -> anyhow::Result { - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update EIP1559 gas state: {}", err) - } else { - Ok(apply_ret) - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs deleted file mode 100644 index 3d189276f..000000000 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use fvm::executor::ApplyRet; -use fvm_shared::econ::TokenAmount; - -pub mod actor; - -pub type Gas = u64; - -pub struct Available { - pub block_gas: Gas, -} - -pub struct CommitRet { - pub base_fee: TokenAmount, -} - -pub struct GasUtilization { - gas_used: Gas, - gas_premium: TokenAmount, -} - -/// The gas market for fendermint. This should be backed by an fvm actor. -pub trait GasMarket { - /// The constant parameters that determines the readings of gas market, such as block gas limit. - type Constant; - - /// Update the constants of the gas market. If the gas market is actor based, then it's recommended - /// to flush at EndBlock. - #[allow(dead_code)] - fn set_constants(&mut self, constants: Self::Constant); - - /// Obtain the current block gas available for execution - fn available(&self) -> Available; - - /// Tracks the amount of gas consumed by a transaction - fn record_utilization(&mut self, gas: GasUtilization); -} - -impl From<&ApplyRet> for GasUtilization { - fn from(ret: &ApplyRet) -> Self { - Self { - gas_used: ret.msg_receipt.gas_used, - gas_premium: ret.miner_tip.clone(), - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index c067f5ff3..f26f634bf 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -4,8 +4,10 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use crate::fvm::activities::actor::ActorActivityTracker; use crate::fvm::externs::FendermintExterns; use crate::fvm::gas::BlockGasTracker; +use crate::fvm::store::ReadOnlyBlockstore; use anyhow::Ok; use cid::Cid; use fendermint_actors_api::gas_market::Reading; @@ -39,9 +41,6 @@ pub type ActorAddressMap = HashMap; /// The result of the message application bundled with any delegated addresses of event emitters. pub type ExecResult = anyhow::Result<(ApplyRet, ActorAddressMap)>; -pub type StateExecutor = - DefaultExecutor>>>>; - /// Parts of the state which evolve during the lifetime of the chain. #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] @@ -119,9 +118,6 @@ where params: FvmUpdatableParams, /// Indicate whether the parameters have been updated. params_dirty: bool, - /// Keeps track of block gas usage during execution, and takes care of updating - /// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). - gas_market: ActorGasMarket, executor_info: ExecutorInfo, } @@ -156,8 +152,8 @@ where let engine = multi_engine.get(&nc)?; let externs = FendermintExterns::new(blockstore.clone(), params.state_root); - let machine = DefaultMachine::new(&mc, blockstore, externs)?; - let mut executor = DefaultExecutor::new(engine, machine)?; + let machine = DefaultMachine::new(&mc, blockstore.clone(), externs)?; + let mut executor = DefaultExecutor::new(engine.clone(), machine)?; let block_gas_tracker = BlockGasTracker::create(&mut executor)?; @@ -173,7 +169,6 @@ where power_scale: params.power_scale, }, params_dirty: false, - gas_market, executor_info: ExecutorInfo { engine_pool: engine, diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 5c0852fca..0cbd97313 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -156,7 +156,12 @@ impl GatewayCaller { }); self.checkpointing.call(state, |c| { - c.create_bu_chpt_with_activities(checkpoint, tree.root_hash().0, total_power, activities) + c.create_bu_chpt_with_activities( + checkpoint, + tree.root_hash().0, + total_power, + activities, + ) }) } diff --git a/ipc/cli/src/commands/subnet/create.rs b/ipc/cli/src/commands/subnet/create.rs index 4cedd64ab..fb1769d46 100644 --- a/ipc/cli/src/commands/subnet/create.rs +++ b/ipc/cli/src/commands/subnet/create.rs @@ -172,10 +172,7 @@ pub struct CreateSubnetArgs { help = "The address of validator gating contract. None if validator gating is disabled" )] pub validator_gater: Option, - #[arg( - long, - help = "The address of validator rewarder contract." - )] + #[arg(long, help = "The address of validator rewarder contract.")] pub validator_rewarder: Option, #[arg( long, From 4fb53b9f9a2afeda0dc6d8733631fd2ccbf17442 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 21 Oct 2024 20:43:01 +0800 Subject: [PATCH 086/111] impl ipc cli --- Cargo.lock | 1 + contracts/binding/build.rs | 1 + contracts/binding/src/lib.rs | 2 + contracts/contracts/activities/Activity.sol | 2 +- .../activities/ValidatorRewardFacet.sol | 4 +- contracts/tasks/validator-rewarder.ts | 2 +- contracts/test/IntegrationTestBase.sol | 6 +- contracts/test/helpers/SelectorLibrary.sol | 6 +- .../test/integration/SubnetActorDiamond.t.sol | 6 +- contracts/test/mocks/SubnetActorMock.sol | 9 +- ipc/api/src/checkpoint.rs | 18 +++ ipc/api/src/evm.rs | 22 ++- ipc/cli/src/commands/mod.rs | 4 + ipc/cli/src/commands/validator/batch_claim.rs | 55 +++++++ ipc/cli/src/commands/validator/list.rs | 52 ++++++ ipc/cli/src/commands/validator/mod.rs | 33 ++++ ipc/provider/Cargo.toml | 2 + ipc/provider/src/lib.rs | 42 ++++- ipc/provider/src/manager/evm/manager.rs | 151 +++++++++++++++++- ipc/provider/src/manager/subnet.rs | 33 +++- 20 files changed, 425 insertions(+), 26 deletions(-) create mode 100644 ipc/cli/src/commands/validator/batch_claim.rs create mode 100644 ipc/cli/src/commands/validator/list.rs create mode 100644 ipc/cli/src/commands/validator/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 05600e77d..6d8b6cf60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5181,6 +5181,7 @@ dependencies = [ "ipc_actors_abis", "libsecp256k1", "log", + "merkle-tree-rs", "num-derive 0.3.3", "num-traits", "prometheus", diff --git a/contracts/binding/build.rs b/contracts/binding/build.rs index 211589562..1c45e66a4 100644 --- a/contracts/binding/build.rs +++ b/contracts/binding/build.rs @@ -60,6 +60,7 @@ fn main() { "LibStakingChangeLog", "LibGateway", "LibQuorum", + "ValidatorRewardFacet", ] { let module_name = camel_to_snake(contract_name); let input_path = diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index a171bbe17..a617bead0 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -48,6 +48,8 @@ pub mod subnet_registry_diamond; #[allow(clippy::all)] pub mod top_down_finality_facet; #[allow(clippy::all)] +pub mod validator_reward_facet; +#[allow(clippy::all)] pub mod xnet_messaging_facet; // The list of contracts need to convert FvmAddress to fvm_shared::Address diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index a643a8a4c..36f37e684 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -54,4 +54,4 @@ struct ValidatorClaimProof { struct BatchClaimProofs { SubnetID subnetId; ValidatorClaimProof[] proofs; -} \ No newline at end of file +} diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index cc45fe0cc..5af82f010 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -18,9 +18,7 @@ import {LibDiamond} from "../lib/LibDiamond.sol"; /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardFacet is ReentrancyGuard, Pausable { - function batchClaim( - BatchClaimProofs calldata payload - ) external nonReentrant whenNotPaused { + function batchClaim(BatchClaimProofs calldata payload) external nonReentrant whenNotPaused { uint256 len = payload.proofs.length; for (uint256 i = 0; i < len; ) { _claim(payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index 02f7f5adf..679c8fc29 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -19,4 +19,4 @@ task('validator-rewarder-deploy') name: 'ValidatorRewarderMap', libraries: [], }) - }) \ No newline at end of file + }) diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 09a78abc1..53d17edbf 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -953,7 +953,11 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, saDiamond.checkpointer().submitCheckpoint(checkpoint, validators, signatures); } - function confirmChange(address[] memory validators, uint256[] memory privKeys, ActivitySummary memory activities) internal { + function confirmChange( + address[] memory validators, + uint256[] memory privKeys, + ActivitySummary memory activities + ) internal { uint256 n = validators.length; bytes[] memory signatures = new bytes[](n); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index e5bc13b63..0aca74346 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff2000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff200000000000000000000000000000000000000000000000000000000ed915e7d000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -118,14 +118,14 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorMock"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001210fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000350a14bf00000000000000000000000000000000000000000000000000000000c7ebdaef000000000000000000000000000000000000000000000000000000003ae247130000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000008456cb59000000000000000000000000000000000000000000000000000000005c975abb000000000000000000000000000000000000000000000000000000004d9013a10000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee00000000000000000000000000000000000000000000000000000000dcda897300000000000000000000000000000000000000000000000000000000a694fc3a0000000000000000000000000000000000000000000000000000000079979f57000000000000000000000000000000000000000000000000000000003f4ba83a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d10fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000350a14bf00000000000000000000000000000000000000000000000000000000c7ebdaef000000000000000000000000000000000000000000000000000000003ae247130000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000004d9013a10000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee00000000000000000000000000000000000000000000000000000000dcda897300000000000000000000000000000000000000000000000000000000a694fc3a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000336293a3b000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000229b0e5700000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000221dc19cc000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index ffb7e4137..622584771 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -82,7 +82,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } function testSubnetActorDiamondReal_LoupeFunction() public view { - require(saDiamond.diamondLouper().facets().length == 8, "unexpected length"); + require(saDiamond.diamondLouper().facets().length == 9, "unexpected length"); require( saDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" @@ -2348,7 +2348,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { params.validatorRewarder = address(m); params.minValidators = 2; params.permissionMode = PermissionMode.Federated; - + saDiamond = createSubnetActor(params); SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1)); @@ -2356,7 +2356,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { m.setSubnet(subnetId); (address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4); - + uint256[] memory powers = new uint256[](4); powers[0] = 10000; powers[1] = 10000; diff --git a/contracts/test/mocks/SubnetActorMock.sol b/contracts/test/mocks/SubnetActorMock.sol index b5ff8a123..152901b1c 100644 --- a/contracts/test/mocks/SubnetActorMock.sol +++ b/contracts/test/mocks/SubnetActorMock.sol @@ -3,16 +3,9 @@ pragma solidity ^0.8.23; import {SubnetActorManagerFacet} from "../../contracts/subnet/SubnetActorManagerFacet.sol"; import {LibStaking} from "../../contracts/lib/LibStaking.sol"; -import {SubnetActorPauseFacet} from "../../contracts/subnet/SubnetActorPauseFacet.sol"; import {SubnetActorRewardFacet} from "../../contracts/subnet/SubnetActorRewardFacet.sol"; -import {SubnetActorCheckpointingFacet} from "../../contracts/subnet/SubnetActorCheckpointingFacet.sol"; -contract SubnetActorMock is - SubnetActorPauseFacet, - SubnetActorManagerFacet, - SubnetActorRewardFacet, - SubnetActorCheckpointingFacet -{ +contract SubnetActorMock is SubnetActorManagerFacet, SubnetActorRewardFacet { function confirmChange(uint64 _configurationNumber) external { LibStaking.confirmChange(_configurationNumber); } diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index f88008629..d67e648f4 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -78,6 +78,24 @@ pub struct BottomUpMsgBatch { /// The commitments for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ValidatorSummary { + /// The checkpoint height in the child subnet + pub checkpoint_height: u64, + /// The validator address + pub validator: Address, + /// The number of blocks mined + pub blocks_committed: u64, + /// The extra metadata attached to the validator + pub metadata: Vec, +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ValidatorClaimProof { + pub summary: ValidatorSummary, + pub proof: Vec<[u8; 32]>, +} + #[serde_as] #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct ActivitySummary { diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 562eddd93..3000591da 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -4,8 +4,8 @@ //! Type conversion for IPC Agent struct with solidity contract struct use crate::address::IPCAddress; -use crate::checkpoint::BottomUpMsgBatch; use crate::checkpoint::{ActivitySummary, BottomUpCheckpoint}; +use crate::checkpoint::{BottomUpMsgBatch, ValidatorClaimProof}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -20,7 +20,8 @@ use fvm_shared::econ::TokenAmount; use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, - subnet_actor_getter_facet, top_down_finality_facet, xnet_messaging_facet, + subnet_actor_getter_facet, top_down_finality_facet, validator_reward_facet, + xnet_messaging_facet, }; /// The type conversion for IPC structs to evm solidity contracts. We need this convenient macro because @@ -246,6 +247,7 @@ base_type_conversion!(gateway_getter_facet); base_type_conversion!(gateway_messenger_facet); base_type_conversion!(lib_gateway); base_type_conversion!(checkpointing_facet); +base_type_conversion!(validator_reward_facet); cross_msg_types!(checkpointing_facet); cross_msg_types!(gateway_getter_facet); @@ -275,6 +277,22 @@ impl TryFrom for AssetKind { } } +impl TryFrom for validator_reward_facet::ValidatorClaimProof { + type Error = anyhow::Error; + + fn try_from(v: ValidatorClaimProof) -> Result { + Ok(Self { + proof: v.proof, + summary: validator_reward_facet::ValidatorSummary { + checkpoint_height: v.summary.checkpoint_height, + validator: payload_to_evm_address(v.summary.validator.payload())?, + blocks_committed: v.summary.blocks_committed, + metadata: ethers::types::Bytes::from(v.summary.metadata), + }, + }) + } +} + /// Convert the ipc SubnetID type to a vec of evm addresses. It extracts all the children addresses /// in the subnet id and turns them as a vec of evm addresses. pub fn subnet_id_to_evm_addresses( diff --git a/ipc/cli/src/commands/mod.rs b/ipc/cli/src/commands/mod.rs index 782c497fd..54c803fa2 100644 --- a/ipc/cli/src/commands/mod.rs +++ b/ipc/cli/src/commands/mod.rs @@ -8,6 +8,7 @@ mod crossmsg; // mod daemon; mod subnet; mod util; +mod validator; mod wallet; use crate::commands::checkpoint::CheckpointCommandsArgs; @@ -30,6 +31,7 @@ use std::path::Path; use std::str::FromStr; use crate::commands::config::ConfigCommandsArgs; +use crate::commands::validator::ValidatorCommandsArgs; use crate::commands::wallet::WalletCommandsArgs; use subnet::SubnetCommandsArgs; @@ -47,6 +49,7 @@ enum Commands { CrossMsg(CrossMsgsCommandsArgs), Checkpoint(CheckpointCommandsArgs), Util(UtilCommandsArgs), + Validator(ValidatorCommandsArgs), } #[derive(Debug, Parser)] @@ -133,6 +136,7 @@ pub async fn cli() -> anyhow::Result<()> { Commands::Wallet(args) => args.handle(global).await, Commands::Checkpoint(args) => args.handle(global).await, Commands::Util(args) => args.handle(global).await, + Commands::Validator(args) => args.handle(global).await, }; r.with_context(|| format!("error processing command {:?}", args.command)) diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs new file mode 100644 index 000000000..3a7c5da9a --- /dev/null +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -0,0 +1,55 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +use crate::commands::get_ipc_provider; +use crate::{CommandLineHandler, GlobalArguments}; +use async_trait::async_trait; +use clap::Args; +use fvm_shared::{address::Address, clock::ChainEpoch}; +use ipc_api::subnet_id::SubnetID; +use std::str::FromStr; + +#[derive(Debug, Args)] +#[command(about = "validator batch claim rewards for a target subnet")] +pub(crate) struct BatchClaimArgs { + #[arg(long, help = "The JSON RPC server url for ipc agent")] + pub validator: String, + #[arg(long, help = "The checkpoint height to claim from")] + pub from: ChainEpoch, + #[arg(long, help = "The checkpoint height to claim to")] + pub to: ChainEpoch, + #[arg(long, help = "The source subnet that generated the reward")] + pub reward_source_subnet: String, + #[arg(long, help = "The subnet to claim reward from")] + pub reward_claim_subnet: String, +} + +pub(crate) struct BatchClaim; + +#[async_trait] +impl CommandLineHandler for BatchClaim { + type Arguments = BatchClaimArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("batch claim operation with args: {:?}", arguments); + + let provider = get_ipc_provider(global)?; + let reward_source_subnet = SubnetID::from_str(&arguments.reward_source_subnet)?; + let reward_claim_subnet = SubnetID::from_str(&arguments.reward_claim_subnet)?; + let validator = Address::from_str(&arguments.validator)?; + + provider + .batch_claim( + &reward_claim_subnet, + &reward_source_subnet, + arguments.from, + arguments.to, + &validator, + ) + .await?; + + println!("rewards claimed"); + + Ok(()) + } +} diff --git a/ipc/cli/src/commands/validator/list.rs b/ipc/cli/src/commands/validator/list.rs new file mode 100644 index 000000000..7209de34d --- /dev/null +++ b/ipc/cli/src/commands/validator/list.rs @@ -0,0 +1,52 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +use crate::commands::get_ipc_provider; +use crate::{CommandLineHandler, GlobalArguments}; +use async_trait::async_trait; +use clap::Args; +use fvm_shared::{address::Address, clock::ChainEpoch}; +use ipc_api::subnet_id::SubnetID; +use std::str::FromStr; + +#[derive(Debug, Args)] +#[command(about = "validator list activities in a subnet")] +pub(crate) struct ListActivitiesArgs { + #[arg(long, help = "The JSON RPC server url for ipc agent")] + pub validator: String, + #[arg(long, help = "The checkpoint height to claim from")] + pub from: ChainEpoch, + #[arg(long, help = "The checkpoint height to claim to")] + pub to: ChainEpoch, + #[arg(long, help = "The subnet to list activities from")] + pub subnet: String, +} + +pub(crate) struct ListActivities; + +#[async_trait] +impl CommandLineHandler for ListActivities { + type Arguments = ListActivitiesArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("list validator activities with args: {:?}", arguments); + + let provider = get_ipc_provider(global)?; + let subnet = SubnetID::from_str(&arguments.subnet)?; + let validator = Address::from_str(&arguments.validator)?; + + let r = provider + .list_validator_activities(&subnet, &validator, arguments.from, arguments.to) + .await?; + + println!("found total {} entries", r.len()); + for v in r { + println!("checkpoint height: {}", v.checkpoint_height); + println!(" addr: {}", v.validator); + println!(" metadata: {}", hex::encode(v.metadata)); + println!(" locks_committed: {}", v.blocks_committed); + } + + Ok(()) + } +} diff --git a/ipc/cli/src/commands/validator/mod.rs b/ipc/cli/src/commands/validator/mod.rs new file mode 100644 index 000000000..2845904c9 --- /dev/null +++ b/ipc/cli/src/commands/validator/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +mod batch_claim; +mod list; + +use crate::commands::validator::batch_claim::{BatchClaim, BatchClaimArgs}; +use crate::commands::validator::list::{ListActivities, ListActivitiesArgs}; +use crate::{CommandLineHandler, GlobalArguments}; +use clap::{Args, Subcommand}; + +#[derive(Debug, Args)] +#[command(name = "validator", about = "validator reward related commands")] +#[command(args_conflicts_with_subcommands = true)] +pub(crate) struct ValidatorCommandsArgs { + #[command(subcommand)] + command: Commands, +} + +impl ValidatorCommandsArgs { + pub async fn handle(&self, global: &GlobalArguments) -> anyhow::Result<()> { + match &self.command { + Commands::BatchClaim(args) => BatchClaim::handle(global, args).await, + Commands::ListValidatorActivities(args) => ListActivities::handle(global, args).await, + } + } +} + +#[derive(Debug, Subcommand)] +pub(crate) enum Commands { + BatchClaim(BatchClaimArgs), + ListValidatorActivities(ListActivitiesArgs), +} diff --git a/ipc/provider/Cargo.toml b/ipc/provider/Cargo.toml index 100e413ae..758f1306d 100644 --- a/ipc/provider/Cargo.toml +++ b/ipc/provider/Cargo.toml @@ -44,6 +44,8 @@ fil_actors_runtime = { workspace = true } fvm_ipld_encoding = { workspace = true } fvm_shared = { workspace = true } +merkle-tree-rs = { path = "../../ext/merkle-tree-rs" } + ipc-types = { workspace = true } ipc-wallet = { workspace = true, features = ["with-ethers"] } ipc-api = { workspace = true } diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index b0faebd51..234ee1cf7 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -9,7 +9,7 @@ use config::Config; use fvm_shared::{ address::Address, clock::ChainEpoch, crypto::signature::SignatureType, econ::TokenAmount, }; -use ipc_api::checkpoint::{BottomUpCheckpointBundle, QuorumReachedEvent}; +use ipc_api::checkpoint::{BottomUpCheckpointBundle, QuorumReachedEvent, ValidatorSummary}; use ipc_api::evm::payload_to_evm_address; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; use ipc_api::subnet::{Asset, PermissionMode}; @@ -745,6 +745,46 @@ impl IpcProvider { .set_federated_power(from, subnet, validators, public_keys, federated_power) .await } + + pub async fn list_validator_activities( + &self, + subnet: &SubnetID, + validator: &Address, + from: ChainEpoch, + to: ChainEpoch, + ) -> anyhow::Result> { + let conn = self.get_connection(subnet)?; + conn.manager() + .get_validator_activities(validator, from, to) + .await + } + + pub async fn batch_claim( + &self, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + from: ChainEpoch, + to: ChainEpoch, + validator: &Address, + ) -> anyhow::Result<()> { + let conn = self.get_connection(reward_source_subnet)?; + + let proofs = conn + .manager() + .get_validator_claim_proofs(validator, from, to) + .await?; + if proofs.is_empty() { + return Err(anyhow!( + "address {} has no reward to claim", + validator.to_string() + )); + } + + let conn = self.get_connection(reward_claim_subnet)?; + conn.manager() + .batch_claim(validator, reward_claim_subnet, reward_source_subnet, proofs) + .await + } } /// Lotus JSON keytype format diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 3062c4d64..43a62c411 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -11,7 +11,7 @@ use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, lib_quorum, lib_staking_change_log, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_getter_facet, subnet_actor_manager_facet, - subnet_actor_reward_facet, + subnet_actor_reward_facet, validator_reward_facet, }; use ipc_api::evm::{fil_to_eth_amount, payload_to_evm_address, subnet_id_to_evm_addresses}; use ipc_api::validator::from_contract_validators; @@ -27,7 +27,7 @@ use crate::config::Subnet; use crate::lotus::message::ipc::SubnetInfo; use crate::manager::subnet::{ BottomUpCheckpointRelayer, GetBlockHashResult, SubnetGenesisInfo, TopDownFinalityQuery, - TopDownQueryPayload, + TopDownQueryPayload, ValidatorRewarder, }; use crate::manager::{EthManager, SubnetManager}; use anyhow::{anyhow, Context, Result}; @@ -44,12 +44,16 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo, ValidatorStakingInfo}; use ipc_api::subnet::ConstructParams; use ipc_api::subnet_id::SubnetID; +use ipc_observability::lazy_static; use ipc_wallet::{EthKeyAddress, EvmKeyStore, PersistentKeyStore}; +use merkle_tree_rs::format::Raw; +use merkle_tree_rs::standard::{LeafType, StandardMerkleTree}; use num_traits::ToPrimitive; use std::result; @@ -1279,6 +1283,149 @@ impl BottomUpCheckpointRelayer for EthSubnetManager { } } +lazy_static!( + /// ABI types of the Merkle tree which contains validator addresses and their voting power. + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; +); + +#[async_trait] +impl ValidatorRewarder for EthSubnetManager { + async fn get_validator_claim_proofs( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result> { + let contract = checkpointing_facet::CheckpointingFacet::new( + self.ipc_contract_info.gateway_addr, + Arc::new(self.ipc_contract_info.provider.clone()), + ); + + let ev = contract + .event::() + .from_block(from_checkpoint as u64) + .to_block(to_checkpoint as u64) + .address(ValueOrArray::Value(contract.address())); + + let validator_evm_addr = payload_to_evm_address(validator_addr.payload())?; + + let mut proofs = vec![]; + for (event, meta) in query_with_meta(ev, contract.client()).await? { + tracing::debug!("found event at height: {}", meta.block_number); + + let mut activities = vec![]; + let mut maybe_validator = None; + for validator in event.report.validators { + let payload = vec![ + event.checkpoint_height.to_string(), + format!("{:?}", validator.validator), + validator.blocks_committed.to_string(), + hex::encode(validator.metadata.as_ref()), + ]; + + if validator.validator == validator_evm_addr { + let summary = ValidatorSummary { + checkpoint_height: event.checkpoint_height, + validator: *validator_addr, + blocks_committed: validator.blocks_committed, + metadata: validator.metadata.to_vec(), + }; + maybe_validator = Some((payload.clone(), summary)); + } + + activities.push(payload); + } + + let tree = StandardMerkleTree::::of(&activities, &VALIDATOR_SUMMARY_FIELDS) + .context("failed to construct Merkle tree")?; + + let Some((payload, summary)) = maybe_validator else { + tracing::info!( + "target validator address has not activities in epoch {}", + meta.block_number + ); + continue; + }; + let proof = tree.get_proof(LeafType::LeafBytes(payload))?; + + proofs.push(ValidatorClaimProof { + summary, + proof: proof.into_iter().map(|v| v.into()).collect(), + }); + } + + Ok(proofs) + } + + /// Get the reward for specific validator in a subnet + async fn get_validator_activities( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result> { + let contract = checkpointing_facet::CheckpointingFacet::new( + self.ipc_contract_info.gateway_addr, + Arc::new(self.ipc_contract_info.provider.clone()), + ); + + let ev = contract + .event::() + .from_block(from_checkpoint as u64) + .to_block(to_checkpoint as u64) + .address(ValueOrArray::Value(contract.address())); + + let mut activities = vec![]; + let validator_eth_addr = payload_to_evm_address(validator_addr.payload())?; + + for (event, meta) in query_with_meta(ev, contract.client()).await? { + tracing::debug!("found event at height: {}", meta.block_number); + for validator in event.report.validators { + if validator.validator != validator_eth_addr { + continue; + } + + activities.push(ValidatorSummary { + validator: *validator_addr, + checkpoint_height: event.checkpoint_height, + blocks_committed: validator.blocks_committed, + metadata: validator.metadata.to_vec(), + }); + } + } + + Ok(activities) + } + + /// Claim the reward in batch + async fn batch_claim( + &self, + submitter: &Address, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + proofs: Vec, + ) -> Result<()> { + let signer = Arc::new(self.get_signer(submitter)?); + let contract = validator_reward_facet::ValidatorRewardFacet::new( + contract_address_from_subnet(reward_claim_subnet)?, + signer.clone(), + ); + + let call = contract.batch_claim(validator_reward_facet::BatchClaimProofs { + subnet_id: validator_reward_facet::SubnetID::try_from(reward_source_subnet)?, + proofs: proofs + .into_iter() + .map(validator_reward_facet::ValidatorClaimProof::try_from) + .collect::>>()?, + }); + let call = call_with_premium_estimation(signer, call).await?; + + call.send().await?; + + Ok(()) + } +} + /// Receives an input `FunctionCall` and returns a new instance /// after estimating an optimal `gas_premium` for the transaction pub(crate) async fn call_with_premium_estimation( diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index cc47ab093..35c7626d0 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -9,6 +9,7 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; @@ -20,7 +21,9 @@ use crate::lotus::message::ipc::SubnetInfo; /// Trait to interact with a subnet and handle its lifecycle. #[async_trait] -pub trait SubnetManager: Send + Sync + TopDownFinalityQuery + BottomUpCheckpointRelayer { +pub trait SubnetManager: + Send + Sync + TopDownFinalityQuery + BottomUpCheckpointRelayer + ValidatorRewarder +{ /// Deploys a new subnet actor on the `parent` subnet and with the /// configuration passed in `ConstructParams`. /// The result of the function is the ID address for the subnet actor from which the final @@ -279,3 +282,31 @@ pub trait BottomUpCheckpointRelayer: Send + Sync { /// Get the current epoch in the current subnet async fn current_epoch(&self) -> Result; } + +/// The validator reward related functions, such as check reward and claim reward for mining blocks +/// in the child subnet +#[async_trait] +pub trait ValidatorRewarder: Send + Sync { + /// Obtain the proofs needed for the validator to batch claim the rewards + async fn get_validator_claim_proofs( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result>; + /// Get the reward for specific validator in the current subnet gateway + async fn get_validator_activities( + &self, + validator: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result>; + /// Claim the reward in batches + async fn batch_claim( + &self, + submitter: &Address, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + proofs: Vec, + ) -> Result<()>; +} From 0fc593ff5b48eecc1c4d318a5f00ddee5e8c5543 Mon Sep 17 00:00:00 2001 From: raulk Date: Tue, 22 Oct 2024 14:52:13 +0100 Subject: [PATCH 087/111] fix clippy. --- .../testing/contract-test/tests/gas_market.rs | 142 +++++++++--------- .../contract-test/tests/staking/machine.rs | 2 + fendermint/vm/actor_interface/src/ipc.rs | 1 + fendermint/vm/genesis/src/arb.rs | 1 - 4 files changed, 71 insertions(+), 75 deletions(-) diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs index 5695d08eb..7440521a4 100644 --- a/fendermint/testing/contract-test/tests/gas_market.rs +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -3,24 +3,22 @@ mod staking; -use anyhow::Context; use async_trait::async_trait; -use fendermint_actor_gas_market::{GasMarketReading, SetConstants}; +use fendermint_actor_gas_market_eip1559::Constants; use fendermint_contract_test::Tester; use fendermint_crypto::{PublicKey, SecretKey}; use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; -use fendermint_vm_actor_interface::system; +use fendermint_vm_actor_interface::gas_market::GAS_MARKET_ACTOR_ADDR; +use fendermint_vm_actor_interface::system::SYSTEM_ACTOR_ADDR; use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; -use fendermint_vm_interpreter::fvm::gas::GasMarket; -use fendermint_vm_interpreter::fvm::state::FvmExecState; use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; use fendermint_vm_interpreter::fvm::FvmMessageInterpreter; +use fvm::executor::{ApplyKind, Executor}; +use fvm_ipld_encoding::RawBytes; use fvm_shared::address::Address; use fvm_shared::bigint::Zero; -use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use fvm_shared::message::Message; use fvm_shared::version::NetworkVersion; @@ -31,15 +29,15 @@ use tendermint_rpc::Client; lazy_static! { static ref ADDR: Address = - Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); + Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap(); static ref ADDR2: Address = - Address::new_secp256k1(&my_secret_key().public_key().serialize()).unwrap(); + Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap(); } const CHAIN_NAME: &str = "mychain"; type I = FvmMessageInterpreter; // returns a seeded secret key which is guaranteed to be the same every time -fn my_secret_key() -> SecretKey { +fn rand_secret_key() -> SecretKey { SecretKey::random(&mut StdRng::seed_from_u64(123)) } @@ -52,7 +50,7 @@ async fn default_tester() -> (Tester, PublicKey) { async fn tester_with_upgrader( upgrade_scheduler: UpgradeScheduler, ) -> (Tester, PublicKey) { - let validator = my_secret_key().public_key(); + let validator = rand_secret_key().public_key(); let interpreter: FvmMessageInterpreter = FvmMessageInterpreter::new(NeverCallClient, None, 1.05, 1.05, false, upgrade_scheduler); @@ -89,16 +87,13 @@ async fn test_gas_market_base_fee_oscillation() { let (mut tester, _) = default_tester().await; let num_msgs = 10; - let total_gas_limit = 6178630; - let base_gas_limit = total_gas_limit / num_msgs; - - let mut gas_constants = SetConstants::default(); - gas_constants.block_gas_limit = total_gas_limit; + let block_gas_limit = 6178630; + let base_gas_limit = block_gas_limit / num_msgs; let messages = (0..num_msgs) .map(|i| Message { version: 0, - from: ADDR.clone(), + from: *ADDR, to: Address::new_id(10), sequence: i, value: TokenAmount::from_atto(1), @@ -110,24 +105,24 @@ async fn test_gas_market_base_fee_oscillation() { }) .collect::>(); - // iterate over all the upgrades + let producer = rand_secret_key().public_key(); + + // block 1: set the gas constants let height = 1; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); tester - .modify_exec_state(|mut state| async { - state.gas_market_mut().set_constants(gas_constants); - Ok((state, ())) - }) + .execute_msgs(vec![custom_gas_limit(block_gas_limit)]) .await .unwrap(); tester.end_block(height).await.unwrap(); tester.commit().await.unwrap(); + // let height = 2; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); let before_reading = tester .modify_exec_state(|mut state| async { - let reading = current_reading(&mut state, height)?; + let reading = state.read_gas_market()?; Ok((state, reading)) }) .await @@ -137,10 +132,10 @@ async fn test_gas_market_base_fee_oscillation() { tester.commit().await.unwrap(); let height = 3; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); let post_full_block_reading = tester .modify_exec_state(|mut state| async { - let reading = current_reading(&mut state, height)?; + let reading = state.read_gas_market()?; Ok((state, reading)) }) .await @@ -153,10 +148,10 @@ async fn test_gas_market_base_fee_oscillation() { ); let height = 4; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); let post_empty_block_reading = tester .modify_exec_state(|mut state| async { - let reading = current_reading(&mut state, height)?; + let reading = state.read_gas_market()?; Ok((state, reading)) }) .await @@ -182,8 +177,8 @@ async fn test_gas_market_premium_distribution() { let messages = (0..num_msgs) .map(|i| Message { version: 0, - from: ADDR.clone(), - to: ADDR2.clone(), + from: *ADDR, + to: *ADDR2, sequence: i, value: TokenAmount::from_atto(1), method_num: 0, @@ -194,12 +189,11 @@ async fn test_gas_market_premium_distribution() { }) .collect::>(); + let proposer = rand_secret_key().public_key(); + // iterate over all the upgrades let height = 1; - tester - .begin_block_with_validator(height, Some(validator)) - .await - .unwrap(); + tester.begin_block(height, proposer).await.unwrap(); let initial_balance = tester .modify_exec_state(|state| async { let tree = state.state_tree(); @@ -238,85 +232,85 @@ async fn test_gas_market_premium_distribution() { async fn test_gas_market_upgrade() { let mut upgrader = UpgradeScheduler::new(); - let total_gas_limit = 100; + // Initial block gas limit is determined by the default constants. + let initial_block_gas_limit = Constants::default().block_gas_limit; + let updated_block_gas_limit = 200; + + // Attach an upgrade at epoch 2 that changes the gas limit to 200. upgrader .add( - Upgrade::new(CHAIN_NAME, 1, Some(1), |state| { + Upgrade::new(CHAIN_NAME, 2, Some(1), move |state| { println!( "[Upgrade at height {}] Update gas market params", state.block_height() ); - - let mut gas_constants = SetConstants::default(); - gas_constants.block_gas_limit = 100; - - state.gas_market_mut().set_constants(gas_constants); - - Ok(()) + state.execute_with_executor(|executor| { + // cannot capture updated_block_gas_limit due to Upgrade::new wanting a fn pointer. + let msg = custom_gas_limit(200); + executor.execute_message(msg, ApplyKind::Implicit, 0)?; + Ok(()) + }) }) .unwrap(), ) .unwrap(); + // Create a new tester with the upgrader attached. let (mut tester, _) = tester_with_upgrader(upgrader).await; + let producer = rand_secret_key().public_key(); + + // At height 1, simply read the block gas limit and ensure it's the default. let height = 1; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); let reading = tester .modify_exec_state(|mut state| async { - let reading = current_reading(&mut state, height)?; + let reading = state.read_gas_market()?; Ok((state, reading)) }) .await .unwrap(); - assert_ne!( - reading.block_gas_limit, total_gas_limit, - "gas limit should not equal at start" + assert_eq!( + reading.block_gas_limit, initial_block_gas_limit, + "block gas limit should be the default as per constants" ); tester.end_block(height).await.unwrap(); tester.commit().await.unwrap(); + // The upgrade above should have updated the gas limit to 200. let height = 2; - tester.begin_block(height).await.unwrap(); + tester.begin_block(height, producer).await.unwrap(); let reading = tester .modify_exec_state(|mut state| async { - let reading = current_reading(&mut state, height)?; + let reading = state.read_gas_market()?; Ok((state, reading)) }) .await .unwrap(); assert_eq!( - reading.block_gas_limit, total_gas_limit, - "gas limit should equal after upgrade" + reading.block_gas_limit, updated_block_gas_limit, + "gas limit post-upgrade should be {updated_block_gas_limit}" ); } -pub fn current_reading( - state: &mut FvmExecState, - block_height: ChainEpoch, -) -> anyhow::Result { - let msg = Message { - from: system::SYSTEM_ACTOR_ADDR, +fn custom_gas_limit(block_gas_limit: u64) -> Message { + let gas_constants = fendermint_actor_gas_market_eip1559::SetConstants { + block_gas_limit, + ..Default::default() + }; + + Message { + version: 0, + from: SYSTEM_ACTOR_ADDR, to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, - params: fvm_ipld_encoding::RawBytes::default(), + sequence: 0, value: Default::default(), - version: Default::default(), + method_num: fendermint_actor_gas_market_eip1559::Method::SetConstants as u64, + params: RawBytes::serialize(&gas_constants).unwrap(), + gas_limit: 10000000, gas_fee_cap: Default::default(), gas_premium: Default::default(), - }; - let (apply_ret, _) = state.execute_implicit(msg)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to read gas market state: {}", err); } - - let r = fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) - .context("failed to parse gas market readying")?; - Ok(r) } #[derive(Clone)] diff --git a/fendermint/testing/contract-test/tests/staking/machine.rs b/fendermint/testing/contract-test/tests/staking/machine.rs index 7aed62ea5..4926475f5 100644 --- a/fendermint/testing/contract-test/tests/staking/machine.rs +++ b/fendermint/testing/contract-test/tests/staking/machine.rs @@ -120,6 +120,7 @@ impl StateMachine for StakingMachine { token_address: ethers::types::Address::zero(), }, validator_gater: EthAddress::from(ethers::types::Address::zero()).into(), + validator_rewarder: Default::default(), }; eprintln!("\n> PARENT IPC: {parent_ipc:?}"); @@ -285,6 +286,7 @@ impl StateMachine for StakingMachine { block_hash: *block_hash, next_configuration_number: *next_configuration_number, msgs: Vec::new(), + activities: Default::default(), }; let checkpoint_hash = checkpoint.clone().abi_hash(); diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index f57b3c36d..14ab371f4 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -534,6 +534,7 @@ pub mod subnet { ], next_configuration_number: 1, msgs: vec![], + activities: Default::default(), }; let param_type = BottomUpCheckpoint::param_type(); diff --git a/fendermint/vm/genesis/src/arb.rs b/fendermint/vm/genesis/src/arb.rs index 491872716..99128cd0d 100644 --- a/fendermint/vm/genesis/src/arb.rs +++ b/fendermint/vm/genesis/src/arb.rs @@ -9,7 +9,6 @@ use fendermint_crypto::SecretKey; use fendermint_testing::arb::{ArbSubnetID, ArbTokenAmount}; use fendermint_vm_core::Timestamp; use fvm_shared::{address::Address, version::NetworkVersion}; -use ipc_types::EthAddress; use quickcheck::{Arbitrary, Gen}; use rand::{rngs::StdRng, SeedableRng}; From e239a05b49be3983bdf726c1a59f842d857dba70 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 22 Oct 2024 22:13:02 +0800 Subject: [PATCH 088/111] update deployment related --- contracts/tasks/deploy-registry.ts | 4 ++++ fendermint/vm/actor_interface/src/ipc.rs | 6 ++++++ fendermint/vm/interpreter/src/genesis.rs | 3 +++ 3 files changed, 13 insertions(+) diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index f087bd8b0..2ae9245ec 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -41,6 +41,7 @@ task('deploy-registry') { name: 'DiamondCutFacet' }, { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, + { name: 'ValidatorRewardFacet' }, ) const registryFacets = await Deployments.deploy( @@ -85,6 +86,7 @@ task('deploy-registry') diamondCutFacet: subnetActorFacets.addresses['DiamondCutFacet'], diamondLoupeFacet: subnetActorFacets.addresses['DiamondLoupeFacet'], ownershipFacet: subnetActorFacets.addresses['OwnershipFacet'], + validatorRewardFacet: subnetActorFacets.addresses['ValidatorRewardFacet'], subnetActorGetterSelectors: selectors(subnetActorFacets.contracts['SubnetActorGetterFacet']), subnetActorManagerSelectors: selectors(subnetActorFacets.contracts['SubnetActorManagerFacet']), @@ -94,6 +96,8 @@ task('deploy-registry') subnetActorDiamondCutSelectors: selectors(subnetActorFacets.contracts['DiamondCutFacet']), subnetActorDiamondLoupeSelectors: selectors(subnetActorFacets.contracts['DiamondLoupeFacet']), subnetActorOwnershipSelectors: selectors(subnetActorFacets.contracts['OwnershipFacet']), + validatorRewardSelectors: selectors(subnetActorFacets.contracts['ValidatorRewardFacet']), + creationPrivileges: Number(mode), } diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index 14ab371f4..be5248fb5 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -129,6 +129,10 @@ lazy_static! { name: "SubnetGetterFacet", abi: ia::subnet_getter_facet::SUBNETGETTERFACET_ABI.to_owned(), }, + EthFacet { + name: "ValidatorRewardFacet", + abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), + }, ], }, ), @@ -470,6 +474,7 @@ pub mod registry { pub diamond_cut_facet: Address, pub diamond_loupe_facet: Address, pub ownership_facet: Address, + pub validator_reward_facet: Address, pub subnet_getter_selectors: Vec, pub subnet_manager_selectors: Vec, pub subnet_rewarder_selectors: Vec, @@ -478,6 +483,7 @@ pub mod registry { pub subnet_actor_diamond_cut_selectors: Vec, pub subnet_actor_diamond_loupe_selectors: Vec, pub subnet_actor_ownership_selectors: Vec, + pub validator_reward_selectors: Vec, pub creation_privileges: u8, // 0 = Unrestricted, 1 = Owner. } } diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 48b39ef33..9d9b6a48e 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -582,6 +582,7 @@ fn deploy_contracts( let diamond_loupe_facet = facets.remove(0); let diamond_cut_facet = facets.remove(0); let ownership_facet = facets.remove(0); + let validator_reward_facet = facets.remove(0); debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); @@ -595,6 +596,7 @@ fn deploy_contracts( diamond_cut_facet: diamond_cut_facet.facet_address, diamond_loupe_facet: diamond_loupe_facet.facet_address, ownership_facet: ownership_facet.facet_address, + validator_reward_facet: validator_reward_facet.facet_address, subnet_getter_selectors: getter_facet.function_selectors, subnet_manager_selectors: manager_facet.function_selectors, subnet_rewarder_selectors: rewarder_facet.function_selectors, @@ -603,6 +605,7 @@ fn deploy_contracts( subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, subnet_actor_ownership_selectors: ownership_facet.function_selectors, + validator_reward_selectors: validator_reward_facet.function_selectors, creation_privileges: 0, }; From 125e6ec86a58a321a18c247a59e0cffdc4c97941 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 22 Oct 2024 23:03:25 +0800 Subject: [PATCH 089/111] update batch claim params --- contracts/contracts/activities/Activity.sol | 2 +- .../activities/ValidatorRewardFacet.sol | 35 +++++++++++++------ contracts/test/helpers/SelectorLibrary.sol | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index 36f37e684..56700d749 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -50,7 +50,7 @@ struct ValidatorClaimProof { bytes32[] proof; } -/// The proofs to batch claim validator rewards +/// The proofs to batch claim validator rewards in a specific subnet struct BatchClaimProofs { SubnetID subnetId; ValidatorClaimProof[] proofs; diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index 5af82f010..226c38924 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -18,13 +18,10 @@ import {LibDiamond} from "../lib/LibDiamond.sol"; /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardFacet is ReentrancyGuard, Pausable { - function batchClaim(BatchClaimProofs calldata payload) external nonReentrant whenNotPaused { - uint256 len = payload.proofs.length; + function batchClaim(BatchClaimProofs[] calldata payload) external nonReentrant whenNotPaused { + uint256 len = payload.length; for (uint256 i = 0; i < len; ) { - _claim(payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); - unchecked { - i++; - } + _batchClaimInSubnet(payload[i]); } } @@ -34,15 +31,35 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { ValidatorSummary calldata summary, bytes32[] calldata proof ) external nonReentrant whenNotPaused { - _claim(subnetId, summary, proof); + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + _claim(s, subnetId, summary, proof); } + // ======== Internal functions =========== + function handleRelay() internal pure { // no opt for now return; } - function _claim(SubnetID calldata subnetId, ValidatorSummary calldata summary, bytes32[] calldata proof) internal { + function _batchClaimInSubnet(BatchClaimProofs calldata payload) internal { + uint256 len = payload.proofs.length; + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + + for (uint256 i = 0; i < len; ) { + _claim(s, payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); + unchecked { + i++; + } + } + } + + function _claim( + ValidatorRewardStorage storage s, + SubnetID calldata subnetId, + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) internal { // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight // note: will never exist. @@ -50,8 +67,6 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { revert NotValidator(msg.sender); } - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - if (s.validatorRewarder == address(0)) { return handleRelay(); } diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 0aca74346..121b8c330 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000221dc19cc000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000023cca8af0000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } From de66b719ce2ff4dc1f3aab6111992faa78236ea3 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Tue, 22 Oct 2024 23:36:18 +0800 Subject: [PATCH 090/111] batch claim tests --- .../activities/ValidatorRewardFacet.sol | 3 + .../test/integration/SubnetActorDiamond.t.sol | 209 +++++++++++++++++- 2 files changed, 211 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index 226c38924..b18e8b551 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -22,6 +22,9 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { uint256 len = payload.length; for (uint256 i = 0; i < len; ) { _batchClaimInSubnet(payload[i]); + unchecked { + i++; + } } } diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 622584771..8a7cb3c68 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivitySummary, ValidatorSummary} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary, ValidatorSummary, BatchClaimProofs, ValidatorClaimProof} from "../../contracts/activities/Activity.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; @@ -2444,6 +2444,213 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { assert(m.blocksCommitted(addrs[3]) == 0); } + function testGatewayDiamond_ValidatorBatchClaimMiningReward_Works() public { + ValidatorRewarderMap m = new ValidatorRewarderMap(); + { + gatewayAddress = address(gatewayDiamond); + + Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)}); + + SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith( + gatewayAddress, + SubnetID(ROOTNET_CHAINID, new address[](0)), + source, + AssetHelper.native() + ); + params.validatorRewarder = address(m); + params.minValidators = 2; + params.permissionMode = PermissionMode.Federated; + + saDiamond = createSubnetActor(params); + } + + SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1)); + subnetId.route[0] = address(saDiamond); + m.setSubnet(subnetId); + + (address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4); + + { + uint256[] memory powers = new uint256[](4); + powers[0] = 10000; + powers[1] = 10000; + powers[2] = 10000; + powers[3] = 10000; + saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); + } + + bytes[] memory metadata = new bytes[](addrs.length); + uint64[] memory blocksMined = new uint64[](addrs.length); + uint64[] memory checkpointHeights = new uint64[](addrs.length); + + blocksMined[0] = 1; + blocksMined[1] = 2; + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + + (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + + vm.startPrank(addrs[0]); + vm.deal(addrs[0], 1 ether); + + BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); + ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); + claimProofs[0] = ValidatorClaimProof({ + summary: ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proof: proofs1[0] + }); + claimProofs[1] = ValidatorClaimProof({ + summary: ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2, + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proof: proofs2[0] + }); + + batchProofs[0] = BatchClaimProofs({ + subnetId: subnetId, + proofs: claimProofs + }); + + saDiamond.validatorReward().batchClaim(batchProofs); + + // check + assert(m.blocksCommitted(addrs[0]) == 2); + } + + + function testGatewayDiamond_ValidatorBatchClaimMiningReward_NoDoubleClaim() public { + ValidatorRewarderMap m = new ValidatorRewarderMap(); + { + gatewayAddress = address(gatewayDiamond); + + Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)}); + + SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith( + gatewayAddress, + SubnetID(ROOTNET_CHAINID, new address[](0)), + source, + AssetHelper.native() + ); + params.validatorRewarder = address(m); + params.minValidators = 2; + params.permissionMode = PermissionMode.Federated; + + saDiamond = createSubnetActor(params); + } + + SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1)); + subnetId.route[0] = address(saDiamond); + m.setSubnet(subnetId); + + (address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4); + + { + uint256[] memory powers = new uint256[](4); + powers[0] = 10000; + powers[1] = 10000; + powers[2] = 10000; + powers[3] = 10000; + saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); + } + + bytes[] memory metadata = new bytes[](addrs.length); + uint64[] memory blocksMined = new uint64[](addrs.length); + uint64[] memory checkpointHeights = new uint64[](addrs.length); + + blocksMined[0] = 1; + blocksMined[1] = 2; + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; + + (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + + vm.startPrank(addrs[0]); + vm.deal(addrs[0], 1 ether); + + BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); + ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); + claimProofs[0] = ValidatorClaimProof({ + summary: ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proof: proofs1[0] + }); + claimProofs[1] = ValidatorClaimProof({ + summary: ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proof: proofs1[0] + }); + + batchProofs[0] = BatchClaimProofs({ + subnetId: subnetId, + proofs: claimProofs + }); + + vm.expectRevert(ValidatorAlreadyClaimed.selector); + saDiamond.validatorReward().batchClaim(batchProofs); + } + // ----------------------------------------------------------------------------------------------------------------- // Tests for validator gater // ----------------------------------------------------------------------------------------------------------------- From c74a21cbb1d990ce19e8b5099854f3f3b18d479c Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 15:42:59 +0800 Subject: [PATCH 091/111] add events --- fendermint/app/src/app.rs | 15 ++++++-- fendermint/testing/contract-test/src/lib.rs | 4 +- fendermint/vm/interpreter/src/chain.rs | 8 ++-- .../vm/interpreter/src/fvm/checkpoint.rs | 5 ++- fendermint/vm/interpreter/src/fvm/exec.rs | 23 +++++++++-- fendermint/vm/interpreter/src/fvm/mod.rs | 2 +- .../vm/interpreter/src/fvm/state/exec.rs | 3 -- .../vm/interpreter/src/fvm/state/ipc.rs | 21 +++++----- ipc/api/src/checkpoint.rs | 6 +++ ipc/api/src/evm.rs | 17 ++++++++- ipc/cli/src/commands/validator/batch_claim.rs | 14 +++++-- ipc/provider/src/lib.rs | 38 ++++++++++++------- ipc/provider/src/manager/evm/manager.rs | 17 ++++----- ipc/provider/src/manager/subnet.rs | 5 +-- 14 files changed, 120 insertions(+), 58 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 86e2313da..2b6178fcb 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -22,7 +22,7 @@ use fendermint_vm_interpreter::fvm::state::{ FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; -use fendermint_vm_interpreter::fvm::{BlockGasLimit, FvmApplyRet, PowerUpdates}; +use fendermint_vm_interpreter::fvm::{EndBlockOutput, FvmApplyRet}; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ @@ -424,7 +424,7 @@ where Message = Vec, BeginOutput = FvmApplyRet, DeliverOutput = BytesMessageApplyRes, - EndOutput = (PowerUpdates, BlockGasLimit), + EndOutput = EndBlockOutput, >, I: CheckInterpreter< State = FvmExecState>, @@ -794,7 +794,11 @@ where tracing::debug!(height = request.height, "end block"); // End the interpreter for this block. - let (power_updates, new_block_gas_limit) = self + let EndBlockOutput { + power_updates, + block_gas_limit: new_block_gas_limit, + events, + } = self .modify_exec_state(|s| self.interpreter.end(s)) .await .context("end failed")?; @@ -823,7 +827,10 @@ where let ret = response::EndBlock { validator_updates, consensus_param_updates, - events: Vec::new(), // TODO: Return events from epoch transitions. + events: events + .into_iter() + .flat_map(|(stamped, emitters)| to_events("event", stamped, emitters)) + .collect::>(), }; Ok(ret) diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index ebe835633..8d6e3f9cf 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -9,7 +9,7 @@ use std::{future::Future, sync::Arc}; use fendermint_crypto::PublicKey; use fendermint_vm_genesis::Genesis; -use fendermint_vm_interpreter::fvm::{BlockGasLimit, PowerUpdates}; +use fendermint_vm_interpreter::fvm::EndBlockOutput; use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; use fendermint_vm_interpreter::{ fvm::{ @@ -67,7 +67,7 @@ where Message = FvmMessage, BeginOutput = FvmApplyRet, DeliverOutput = FvmApplyRet, - EndOutput = (PowerUpdates, BlockGasLimit), + EndOutput = EndBlockOutput, >, { pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result { diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 034090a30..d3fe2489e 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::fvm::state::ipc::GatewayCaller; use crate::fvm::store::ReadOnlyBlockstore; -use crate::fvm::{topdown, BlockGasLimit, FvmApplyRet, PowerUpdates}; +use crate::fvm::{topdown, EndBlockOutput, FvmApplyRet}; use crate::selector::{GasLimitSelector, MessageSelector}; use crate::{ fvm::state::FvmExecState, @@ -246,7 +246,7 @@ where Message = VerifiableMessage, DeliverOutput = SignedMessageApplyRes, State = FvmExecState, - EndOutput = (PowerUpdates, BlockGasLimit), + EndOutput = EndBlockOutput, >, { // The state consists of the resolver pool, which this interpreter needs, and the rest of the @@ -434,10 +434,10 @@ where let (state, out) = self.inner.end(state).await?; // Update any component that needs to know about changes in the power table. - if !out.0 .0.is_empty() { + if !out.power_updates.0.is_empty() { let power_updates = out + .power_updates .0 - .0 .iter() .map(|v| { let vk = ValidatorKey::from(v.public_key.0); diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index aa56f5086..21696d5cb 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -36,6 +36,7 @@ use super::{ ValidatorContext, }; use crate::fvm::activities::ValidatorActivityTracker; +use crate::fvm::exec::BlockEndEvents; /// Validator voting power snapshot. #[derive(Debug, Clone, PartialEq, Eq)] @@ -53,6 +54,7 @@ pub struct PowerUpdates(pub Vec>); pub fn maybe_create_checkpoint( gateway: &GatewayCaller, state: &mut FvmExecState, + event_tracker: &mut BlockEndEvents, ) -> anyhow::Result> where DB: Blockstore + Sync + Send + Clone + 'static, @@ -131,9 +133,10 @@ where }) .collect::>>()?, }; - gateway + let ret = gateway .create_bu_ckpt_with_activities(state, checkpoint.clone(), &curr_power_table.0, report) .context("failed to store checkpoint")?; + event_tracker.push((ret.apply_ret.events, ret.emitters)); state.activities_tracker().purge_activities()?; diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index d7c400c8d..00814b4fa 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -14,11 +14,15 @@ use async_trait::async_trait; use fendermint_vm_actor_interface::{chainmetadata, cron, system}; use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; +use fvm_shared::event::StampedEvent; use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use std::collections::HashMap; use tendermint_rpc::Client; +pub type Event = (Vec, HashMap); +pub type BlockEndEvents = Vec; + /// The return value extended with some things from the message that /// might not be available to the caller, because of the message lookups /// and transformations that happen along the way, e.g. where we need @@ -33,6 +37,13 @@ pub struct FvmApplyRet { pub emitters: HashMap, } +pub struct EndBlockOutput { + pub power_updates: PowerUpdates, + pub block_gas_limit: BlockGasLimit, + /// The end block events to be recorded + pub events: BlockEndEvents, +} + #[async_trait] impl ExecInterpreter for FvmMessageInterpreter where @@ -46,7 +57,7 @@ where /// Return validator power updates and the next base fee. /// Currently ignoring events as there aren't any emitted by the smart contract, /// but keep in mind that if there were, those would have to be propagated. - type EndOutput = (PowerUpdates, BlockGasLimit); + type EndOutput = EndBlockOutput; async fn begin( &self, @@ -190,6 +201,8 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { + let mut block_end_events = BlockEndEvents::default(); + if let Some(pubkey) = state.block_producer() { state .activities_tracker() @@ -208,7 +221,7 @@ where }); let updates = if let Some((checkpoint, updates)) = - checkpoint::maybe_create_checkpoint(&self.gateway, &mut state) + checkpoint::maybe_create_checkpoint(&self.gateway, &mut state, &mut block_end_events) .context("failed to create checkpoint")? { // Asynchronously broadcast signature, if validating. @@ -256,7 +269,11 @@ where PowerUpdates::default() }; - let ret = (updates, next_gas_market.block_gas_limit); + let ret = EndBlockOutput { + power_updates: updates, + block_gas_limit: next_gas_market.block_gas_limit, + events: block_end_events, + }; Ok((state, ret)) } } diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index d19a1d32a..670714315 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -21,7 +21,7 @@ pub(crate) mod topdown; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; -pub use exec::FvmApplyRet; +pub use exec::{EndBlockOutput, FvmApplyRet}; use fendermint_crypto::{PublicKey, SecretKey}; pub use fendermint_vm_message::query::FvmQuery; use fvm_ipld_blockstore::Blockstore; diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 1a6c37b7f..f26f634bf 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -41,9 +41,6 @@ pub type ActorAddressMap = HashMap; /// The result of the message application bundled with any delegated addresses of event emitters. pub type ExecResult = anyhow::Result<(ApplyRet, ActorAddressMap)>; -pub type StateExecutor = - DefaultExecutor>>>>; - /// Parts of the state which evolve during the lifetime of the chain. #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 0cbd97313..7fb7143df 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -145,7 +145,7 @@ impl GatewayCaller { checkpoint: checkpointing_facet::BottomUpCheckpoint, power_table: &[Validator], activities: checkpointing_facet::ActivityReport, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { // Construct a Merkle tree from the power table, which we can use to validate validator set membership // when the signatures are submitted in transactions for accumulation. let tree = @@ -155,14 +155,17 @@ impl GatewayCaller { p.saturating_add(et::U256::from(v.power.0)) }); - self.checkpointing.call(state, |c| { - c.create_bu_chpt_with_activities( - checkpoint, - tree.root_hash().0, - total_power, - activities, - ) - }) + Ok(self + .checkpointing + .call_with_return(state, |c| { + c.create_bu_chpt_with_activities( + checkpoint, + tree.root_hash().0, + total_power, + activities, + ) + })? + .into_return()) } /// Retrieve checkpoints which have not reached a quorum. diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index d67e648f4..5edb1a7d8 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -90,6 +90,12 @@ pub struct ValidatorSummary { pub metadata: Vec, } +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct BatchClaimProofs { + pub subnet_id: SubnetID, + pub proofs: Vec, +} + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct ValidatorClaimProof { pub summary: ValidatorSummary, diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 096a3a428..7722a19d0 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -4,7 +4,7 @@ //! Type conversion for IPC Agent struct with solidity contract struct use crate::address::IPCAddress; -use crate::checkpoint::{ActivitySummary, BottomUpCheckpoint}; +use crate::checkpoint::{ActivitySummary, BatchClaimProofs, BottomUpCheckpoint}; use crate::checkpoint::{BottomUpMsgBatch, ValidatorClaimProof}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; @@ -322,6 +322,21 @@ pub fn fil_to_eth_amount(amount: &TokenAmount) -> anyhow::Result { Ok(U256::from_dec_str(&str)?) } +impl TryFrom for validator_reward_facet::BatchClaimProofs { + type Error = anyhow::Error; + + fn try_from(v: BatchClaimProofs) -> Result { + Ok(Self { + subnet_id: validator_reward_facet::SubnetID::try_from(&v.subnet_id)?, + proofs: v + .proofs + .into_iter() + .map(validator_reward_facet::ValidatorClaimProof::try_from) + .collect::, _>>()?, + }) + } +} + impl TryFrom for top_down_finality_facet::StakingChange { type Error = anyhow::Error; diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs index 3a7c5da9a..a47beec02 100644 --- a/ipc/cli/src/commands/validator/batch_claim.rs +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -18,7 +18,10 @@ pub(crate) struct BatchClaimArgs { pub from: ChainEpoch, #[arg(long, help = "The checkpoint height to claim to")] pub to: ChainEpoch, - #[arg(long, help = "The source subnet that generated the reward")] + #[arg( + long, + help = "The source subnets that generated the reward, use , to separate subnets" + )] pub reward_source_subnet: String, #[arg(long, help = "The subnet to claim reward from")] pub reward_claim_subnet: String, @@ -34,14 +37,19 @@ impl CommandLineHandler for BatchClaim { log::debug!("batch claim operation with args: {:?}", arguments); let provider = get_ipc_provider(global)?; - let reward_source_subnet = SubnetID::from_str(&arguments.reward_source_subnet)?; + + let reward_source_subnets = arguments + .reward_source_subnet + .split(',') + .map(SubnetID::from_str) + .collect::, _>>()?; let reward_claim_subnet = SubnetID::from_str(&arguments.reward_claim_subnet)?; let validator = Address::from_str(&arguments.validator)?; provider .batch_claim( &reward_claim_subnet, - &reward_source_subnet, + &reward_source_subnets, arguments.from, arguments.to, &validator, diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index 234ee1cf7..1b211da0c 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -9,7 +9,9 @@ use config::Config; use fvm_shared::{ address::Address, clock::ChainEpoch, crypto::signature::SignatureType, econ::TokenAmount, }; -use ipc_api::checkpoint::{BottomUpCheckpointBundle, QuorumReachedEvent, ValidatorSummary}; +use ipc_api::checkpoint::{ + BatchClaimProofs, BottomUpCheckpointBundle, QuorumReachedEvent, ValidatorSummary, +}; use ipc_api::evm::payload_to_evm_address; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; use ipc_api::subnet::{Asset, PermissionMode}; @@ -762,27 +764,35 @@ impl IpcProvider { pub async fn batch_claim( &self, reward_claim_subnet: &SubnetID, - reward_source_subnet: &SubnetID, + reward_source_subnets: &[SubnetID], from: ChainEpoch, to: ChainEpoch, validator: &Address, ) -> anyhow::Result<()> { - let conn = self.get_connection(reward_source_subnet)?; - - let proofs = conn - .manager() - .get_validator_claim_proofs(validator, from, to) - .await?; - if proofs.is_empty() { - return Err(anyhow!( - "address {} has no reward to claim", - validator.to_string() - )); + let mut batch_proofs = vec![]; + for source_subnet in reward_source_subnets { + let conn = self.get_connection(source_subnet)?; + + let proofs = conn + .manager() + .get_validator_claim_proofs(validator, from, to) + .await?; + if proofs.is_empty() { + return Err(anyhow!( + "address {} has no reward to claim", + validator.to_string() + )); + } + + batch_proofs.push(BatchClaimProofs { + subnet_id: source_subnet.clone(), + proofs, + }); } let conn = self.get_connection(reward_claim_subnet)?; conn.manager() - .batch_claim(validator, reward_claim_subnet, reward_source_subnet, proofs) + .batch_claim(validator, reward_claim_subnet, batch_proofs) .await } } diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 43a62c411..39366f443 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -43,7 +43,7 @@ use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, I256, U256 use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ - BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; @@ -1402,8 +1402,7 @@ impl ValidatorRewarder for EthSubnetManager { &self, submitter: &Address, reward_claim_subnet: &SubnetID, - reward_source_subnet: &SubnetID, - proofs: Vec, + payloads: Vec, ) -> Result<()> { let signer = Arc::new(self.get_signer(submitter)?); let contract = validator_reward_facet::ValidatorRewardFacet::new( @@ -1411,13 +1410,11 @@ impl ValidatorRewarder for EthSubnetManager { signer.clone(), ); - let call = contract.batch_claim(validator_reward_facet::BatchClaimProofs { - subnet_id: validator_reward_facet::SubnetID::try_from(reward_source_subnet)?, - proofs: proofs - .into_iter() - .map(validator_reward_facet::ValidatorClaimProof::try_from) - .collect::>>()?, - }); + let p = payloads + .into_iter() + .map(validator_reward_facet::BatchClaimProofs::try_from) + .collect::>>()?; + let call = contract.batch_claim(p); let call = call_with_premium_estimation(signer, call).await?; call.send().await?; diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index 35c7626d0..62cbf983b 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -8,7 +8,7 @@ use async_trait::async_trait; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ - BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; @@ -306,7 +306,6 @@ pub trait ValidatorRewarder: Send + Sync { &self, submitter: &Address, reward_claim_subnet: &SubnetID, - reward_source_subnet: &SubnetID, - proofs: Vec, + payloads: Vec, ) -> Result<()>; } From 91298ca71b3225a592842692b8f7295e150c6a51 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 16:09:55 +0800 Subject: [PATCH 092/111] remove checkpoitn height --- .../activities/LibActivityMerkleVerifier.sol | 2 +- contracts/test/helpers/MerkleTreeHelper.sol | 6 +--- .../test/integration/SubnetActorDiamond.t.sol | 33 ------------------- .../interpreter/src/fvm/activities/merkle.rs | 6 ++-- .../vm/interpreter/src/fvm/activities/mod.rs | 5 ++- .../vm/interpreter/src/fvm/checkpoint.rs | 2 +- ipc/provider/src/manager/evm/manager.rs | 3 +- 7 files changed, 8 insertions(+), 49 deletions(-) diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 3ff06a4ae..433f57185 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -17,7 +17,7 @@ library LibActivityMerkleVerifier { bytes32 leaf = keccak256( bytes.concat( keccak256( - abi.encode(summary.checkpointHeight, summary.validator, summary.blocksCommitted, summary.metadata) + abi.encode(summary.validator, summary.blocksCommitted, summary.metadata) ) ) ); diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index e3b7ed21f..ec166cd81 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -35,7 +35,6 @@ library MerkleTreeHelper { function createMerkleProofsForActivities( address[] memory addrs, uint64[] memory blocksMined, - uint64[] memory checkpointHeights, bytes[] memory metadatas ) internal returns (bytes32, bytes32[][] memory) { Merkle merkleTree = new Merkle(); @@ -46,9 +45,6 @@ library MerkleTreeHelper { if (addrs.length != metadatas.length) { revert("different array lengths btw metadatas and addrs"); } - if (addrs.length != checkpointHeights.length) { - revert("different array lengths btw checkpointHeights and addrs"); - } uint256 len = addrs.length; bytes32 root; @@ -56,7 +52,7 @@ library MerkleTreeHelper { bytes32[] memory data = new bytes32[](len); for (uint256 i = 0; i < len; i++) { data[i] = keccak256( - bytes.concat(keccak256(abi.encode(checkpointHeights[i], addrs[i], blocksMined[i], metadatas[i]))) + bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i], metadatas[i]))) ); } diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 8a7cb3c68..c7a8c30a9 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -2366,20 +2366,13 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); - uint64[] memory checkpointHeights = new uint64[](addrs.length); blocksMined[0] = 1; blocksMined[1] = 2; - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, - checkpointHeights, metadata ); @@ -2481,32 +2474,19 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); - uint64[] memory checkpointHeights = new uint64[](addrs.length); blocksMined[0] = 1; blocksMined[1] = 2; - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, - checkpointHeights, metadata ); - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, - checkpointHeights, metadata ); @@ -2586,32 +2566,19 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); - uint64[] memory checkpointHeights = new uint64[](addrs.length); blocksMined[0] = 1; blocksMined[1] = 2; - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); - (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, - checkpointHeights, metadata ); - checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, blocksMined, - checkpointHeights, metadata ); diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs index 0aa2ff0b0..53f2e7c3a 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs @@ -3,7 +3,6 @@ use anyhow::Context; use fendermint_actor_activity_tracker::ValidatorSummary; -use fvm_shared::clock::ChainEpoch; use ipc_api::evm::payload_to_evm_address; use ipc_observability::lazy_static; use merkle_tree_rs::format::Raw; @@ -13,7 +12,7 @@ pub type Hash = ethers::types::H256; lazy_static!( /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; ); /// The merkle tree based proof verification to interact with solidity contracts @@ -28,13 +27,12 @@ impl MerkleProofGen { } impl MerkleProofGen { - pub fn new(values: &[ValidatorSummary], checkpoint_height: ChainEpoch) -> anyhow::Result { + pub fn new(values: &[ValidatorSummary]) -> anyhow::Result { let values = values .iter() .map(|t| { payload_to_evm_address(t.validator.payload()).map(|addr| { vec![ - checkpoint_height.to_string(), format!("{addr:?}"), t.block_committed.to_string(), hex::encode(&t.metadata), diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs index 251c47bd0..1c509c228 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/mod.rs @@ -10,7 +10,6 @@ mod merkle; use crate::fvm::activities::merkle::MerkleProofGen; use fendermint_actor_activity_tracker::ValidatorSummary; use fendermint_crypto::PublicKey; -use fvm_shared::clock::ChainEpoch; use ipc_api::checkpoint::ActivitySummary; use std::fmt::Debug; @@ -40,8 +39,8 @@ pub trait ValidatorActivityTracker { } impl ActivityDetails { - pub fn commitment(&self, checkpoint_height: ChainEpoch) -> anyhow::Result { - let gen = MerkleProofGen::new(self.details.as_slice(), checkpoint_height)?; + pub fn commitment(&self) -> anyhow::Result { + let gen = MerkleProofGen::new(self.details.as_slice())?; Ok(ActivitySummary { total_active_validators: self.details.len() as u64, commitment: gen.root().to_fixed_bytes().to_vec(), diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 21696d5cb..1f52e9a01 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -110,7 +110,7 @@ where block_hash, next_configuration_number, msgs, - activities: activities.commitment(height.value() as i64)?.try_into()?, + activities: activities.commitment()?.try_into()?, }; // Save the checkpoint in the ledger. diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 39366f443..cfc78277b 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -1285,7 +1285,7 @@ impl BottomUpCheckpointRelayer for EthSubnetManager { lazy_static!( /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; ); #[async_trait] @@ -1317,7 +1317,6 @@ impl ValidatorRewarder for EthSubnetManager { let mut maybe_validator = None; for validator in event.report.validators { let payload = vec![ - event.checkpoint_height.to_string(), format!("{:?}", validator.validator), validator.blocks_committed.to_string(), hex::encode(validator.metadata.as_ref()), From 06538e20aee684804b1803c5c63b09dd86d3d6e5 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 16:51:27 +0800 Subject: [PATCH 093/111] filter end block events --- fendermint/eth/api/src/apis/eth.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index 8e6b30281..ab8df7cf8 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -963,9 +963,9 @@ where while height <= to_height { if let Ok(block_results) = data.tm().block_results(height).await { - if let Some(tx_results) = block_results.txs_results { - let block_number = et::U64::from(height.value()); + let block_number = et::U64::from(height.value()); + if let Some(tx_results) = block_results.txs_results { let block = data .block_by_height(et::BlockNumber::Number(block_number)) .await?; @@ -1009,6 +1009,26 @@ where log_index_start += tx_result.events.len(); } + } else if let Some(events) = block_results.end_block_events { + let emitters = from_tm::collect_emitters(&events); + + // Filter by address. + if !addrs.is_empty() && addrs.intersection(&emitters).next().is_none() { + continue; + } + + // all zero indicating it's system contract call + let tx_hash = et::TxHash::zero(); + let tx_idx = et::U64::zero(); + let block_hash = et::H256::zero(); + + let mut tx_logs = + from_tm::to_logs(&events, block_hash, block_number, tx_hash, tx_idx, 0)?; + + // Filter by topic. + tx_logs.retain(|log| matches_topics(&filter, log)); + + logs.append(&mut tx_logs); } } else { break; From 547da7609e73a9d1ccf8edd3868a06cd27d84cae Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 18:28:17 +0800 Subject: [PATCH 094/111] fix genesis --- fendermint/actors/activity-tracker/src/lib.rs | 2 +- fendermint/vm/interpreter/src/genesis.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 5240a69bc..ec075cbca 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -12,7 +12,7 @@ use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; use serde::{Deserialize, Serialize}; -use crate::state::State; +pub use crate::state::State; pub use crate::state::ValidatorSummary; mod state; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 9d9b6a48e..c7458de3f 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -19,7 +19,8 @@ use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, cron, eam, gas_market, init, ipc, reward, system, EMPTY_ARR, + account, activity, burntfunds, chainmetadata, cron, eam, gas_market, init, ipc, reward, system, + EMPTY_ARR, }; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; @@ -452,6 +453,17 @@ impl GenesisBuilder { ) .context("failed to create default eip1559 gas market actor")?; + let tracker_state = fendermint_actor_activity_tracker::State::new(state.store())?; + state + .create_custom_actor( + fendermint_actor_activity_tracker::IPC_ACTIVITY_TRACKER_ACTOR_NAME, + activity::ACTIVITY_TRACKER_ACTOR_ID, + &tracker_state, + TokenAmount::zero(), + None, + ) + .context("failed to create activity tracker actor")?; + // STAGE 2: Create non-builtin accounts which do not have a fixed ID. // The next ID is going to be _after_ the accounts, which have already been assigned an ID by the `Init` actor. From 6f91bb6ed52c386d623b95315d58af31cdffc77d Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 19:00:09 +0800 Subject: [PATCH 095/111] patch end block events --- fendermint/eth/api/src/apis/eth.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index ab8df7cf8..7e0af9213 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -1009,7 +1009,9 @@ where log_index_start += tx_result.events.len(); } - } else if let Some(events) = block_results.end_block_events { + } + + if let Some(events) = block_results.end_block_events { let emitters = from_tm::collect_emitters(&events); // Filter by address. From ec8c0eb37ce6ba1b37dff418f570d673375baf80 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 23 Oct 2024 21:51:50 +0800 Subject: [PATCH 096/111] patch deployment --- .../subnetregistry/RegisterSubnetFacet.sol | 8 +++++- contracts/tasks/deploy-registry.ts | 10 ++++++-- contracts/tasks/validator-rewarder.ts | 25 +++++++++++++++++++ ipc/cli/src/commands/validator/batch_claim.rs | 6 ++--- ipc/provider/src/lib.rs | 5 +++- 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol b/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol index 30babb8f7..dea2c5eb7 100644 --- a/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol +++ b/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol @@ -28,7 +28,7 @@ contract RegisterSubnetFacet is ReentrancyGuard { ensurePrivileges(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](8); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](9); // set the diamond cut for subnet getter diamondCut[0] = IDiamond.FacetCut({ @@ -80,6 +80,12 @@ contract RegisterSubnetFacet is ReentrancyGuard { functionSelectors: s.subnetActorOwnershipSelectors }); + diamondCut[8] = IDiamond.FacetCut({ + facetAddress: s.VALIDATOR_REWARD_FACET, + action: IDiamond.FacetCutAction.Add, + functionSelectors: s.validatorRewardSelectors + }); + // slither-disable-next-line reentrancy-benign subnetAddr = address(new SubnetActorDiamond(diamondCut, _params, msg.sender)); diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index 2ae9245ec..ff3070bdd 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -37,11 +37,17 @@ task('deploy-registry') }, { name: 'SubnetActorPauseFacet' }, { name: 'SubnetActorRewardFacet' }, - { name: 'SubnetActorCheckpointingFacet' }, + { + name: 'SubnetActorCheckpointingFacet', + libraries: ['SubnetIDHelper'], + }, { name: 'DiamondCutFacet' }, { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, - { name: 'ValidatorRewardFacet' }, + { + name: 'ValidatorRewardFacet', + libraries: ['SubnetIDHelper'], + }, ) const registryFacets = await Deployments.deploy( diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index 679c8fc29..0f7d784e8 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -1,6 +1,7 @@ import { task } from 'hardhat/config' import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' import { Deployments } from './lib' +import { artifacts, ethers } from 'hardhat' // step 1. deploy the validator rewarder // sample command: pnpm exec hardhat validator-rewarder-deploy --network calibrationnet @@ -20,3 +21,27 @@ task('validator-rewarder-deploy') libraries: [], }) }) + +// step 2. set the subnet for the rewarder +// sample command: pnpm exec hardhat validator-rewarder-set-subnet --network calibrationnet 314159 +task('validator-rewarder-set-subnet') + .setDescription('Deploy example subnet validator rewarder contract') + .addPositionalParam('root', 'the chain id of parent subnet') + .addPositionalParam('address', 'the address of the subnet actor contract, L2 only') + .setAction(async (args: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + console.log( + `Set validator rewarder subnet with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + // only L2 for now + const subnetId = { root: args.root, route: [args.address] } + console.log('pointing to', subnetId) + + const contracts = await Deployments.resolve(hre, 'ValidatorRewarderMap') + const contract = contracts.contracts.ValidatorRewarderMap + await contract.setSubnet(subnetId) + }) \ No newline at end of file diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs index a47beec02..6cdb106ba 100644 --- a/ipc/cli/src/commands/validator/batch_claim.rs +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -20,9 +20,9 @@ pub(crate) struct BatchClaimArgs { pub to: ChainEpoch, #[arg( long, - help = "The source subnets that generated the reward, use , to separate subnets" + help = "The source subnets that generated the reward, use ',' to separate subnets" )] - pub reward_source_subnet: String, + pub reward_source_subnets: String, #[arg(long, help = "The subnet to claim reward from")] pub reward_claim_subnet: String, } @@ -39,7 +39,7 @@ impl CommandLineHandler for BatchClaim { let provider = get_ipc_provider(global)?; let reward_source_subnets = arguments - .reward_source_subnet + .reward_source_subnets .split(',') .map(SubnetID::from_str) .collect::, _>>()?; diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index 1b211da0c..f6c7cb27c 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -790,7 +790,10 @@ impl IpcProvider { }); } - let conn = self.get_connection(reward_claim_subnet)?; + let parent = reward_claim_subnet + .parent() + .ok_or_else(|| anyhow!("no parent found"))?; + let conn = self.get_connection(&parent)?; conn.manager() .batch_claim(validator, reward_claim_subnet, batch_proofs) .await From 58031021541b278b6c630cda9f5526ea44bcd761 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 15 Nov 2024 14:55:44 +0800 Subject: [PATCH 097/111] remove unused todo and comments --- fendermint/actors/activity-tracker/src/lib.rs | 21 ------------------- .../vm/interpreter/src/fvm/checkpoint.rs | 5 ----- 2 files changed, 26 deletions(-) diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index ec075cbca..791bc688a 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -35,15 +35,6 @@ pub struct GetActivitiesResult { pub start_height: ChainEpoch, } -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct GetActivitySummaryResult { - pub commitment: [u8; 32], - /// Total number validators that have mined blocks - pub total_active_validators: u64, - /// The validator details - pub activities: Vec, -} - #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { @@ -51,7 +42,6 @@ pub enum Method { BlockMined = frc42_dispatch::method_hash!("BlockMined"), GetActivities = frc42_dispatch::method_hash!("GetActivities"), PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), - GetSummary = frc42_dispatch::method_hash!("GetSummary"), } impl ActivityTrackerActor { @@ -83,16 +73,6 @@ impl ActivityTrackerActor { Ok(()) } - pub fn get_summary(_rt: &impl Runtime) -> Result { - // todo - let dummy = GetActivitySummaryResult { - commitment: [0; 32], - total_active_validators: 10, - activities: vec![], - }; - Ok(dummy) - } - pub fn get_activities(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; @@ -117,6 +97,5 @@ impl ActorCode for ActivityTrackerActor { BlockMined => block_mined, GetActivities => get_activities, PurgeActivities => purge_activities, - GetSummary => get_summary, } } diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 1f52e9a01..3592cb3d0 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -115,11 +115,6 @@ where // Save the checkpoint in the ledger. // Pass in the current power table, because these are the validators who can sign this checkpoint. - - // gateway - // .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) - // .context("failed to store checkpoint")?; - let report = checkpoint::ActivityReport { validators: activities .details From 568a734ddc5aae23e369793a7ced8dab3a2cc94b Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 15 Nov 2024 16:33:06 +0800 Subject: [PATCH 098/111] merge with main --- ipc/provider/src/manager/evm/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 20eb15c8c..503cdfbfa 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -1414,7 +1414,7 @@ impl ValidatorRewarder for EthSubnetManager { .map(validator_reward_facet::BatchClaimProofs::try_from) .collect::>>()?; let call = contract.batch_claim(p); - let call = call_with_premium_estimation(signer, call).await?; + let call = call_with_premium_and_pending_block(signer, call).await?; call.send().await?; From 69e04a958a9f2d564b89feaab587f421030ce200 Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 18:14:01 +0700 Subject: [PATCH 099/111] feat(node): integration rewards, review comments. (#1205) Co-authored-by: cryptoAtwill Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> --- Cargo.lock | 2 +- contracts/binding/Cargo.toml | 2 +- contracts/contracts/activities/Activity.sol | 92 +++--- .../activities/IValidatorRewarder.sol | 4 +- .../activities/LibActivityMerkleVerifier.sol | 21 +- .../activities/ValidatorRewardFacet.sol | 89 +++--- .../examples/ValidatorRewarderMap.sol | 22 +- .../gateway/router/CheckpointingFacet.sol | 8 +- .../lib/LibSubnetRegistryStorage.sol | 2 +- contracts/contracts/structs/CrossNet.sol | 4 +- .../subnet/SubnetActorCheckpointingFacet.sol | 4 +- contracts/foundry.toml | 1 + contracts/tasks/deploy-registry.ts | 6 +- contracts/tasks/validator-rewarder.ts | 2 +- contracts/test/IntegrationTestBase.sol | 15 +- contracts/test/helpers/ActivityHelper.sol | 48 +++ contracts/test/helpers/MerkleTreeHelper.sol | 11 +- contracts/test/helpers/SelectorLibrary.sol | 6 +- .../test/integration/GatewayDiamond.t.sol | 33 +- .../integration/GatewayDiamondToken.t.sol | 7 +- contracts/test/integration/MultiSubnet.t.sol | 7 +- .../test/integration/SubnetActorDiamond.t.sol | 181 ++++------- docs/fendermint/diagrams/activity_rollup1.png | Bin 0 -> 119471 bytes docs/fendermint/subnet_activities.md | 78 +++++ extras/axelar-token/foundry.toml | 3 +- extras/linked-token/foundry.toml | 2 +- .../linked-token/test/MultiSubnetTest.t.sol | 12 +- fendermint/actors/activity-tracker/Cargo.toml | 4 +- fendermint/actors/activity-tracker/src/lib.rs | 102 +++--- .../actors/activity-tracker/src/state.rs | 108 ++----- .../actors/activity-tracker/src/types.rs | 28 ++ fendermint/eth/api/Cargo.toml | 2 +- fendermint/eth/api/src/apis/eth.rs | 1 + fendermint/testing/contract-test/Cargo.toml | 2 +- fendermint/vm/actor_interface/src/ipc.rs | 11 +- fendermint/vm/interpreter/Cargo.toml | 1 + .../interpreter/src/fvm/activities/actor.rs | 105 ------ .../vm/interpreter/src/fvm/activities/mod.rs | 49 --- .../vm/interpreter/src/fvm/activity/actor.rs | 75 +++++ .../fvm/{activities => activity}/merkle.rs | 26 +- .../vm/interpreter/src/fvm/activity/mod.rs | 156 +++++++++ .../vm/interpreter/src/fvm/checkpoint.rs | 88 +++--- fendermint/vm/interpreter/src/fvm/exec.rs | 6 +- fendermint/vm/interpreter/src/fvm/externs.rs | 12 - fendermint/vm/interpreter/src/fvm/mod.rs | 2 +- .../vm/interpreter/src/fvm/state/exec.rs | 63 +--- .../vm/interpreter/src/fvm/state/ipc.rs | 23 +- fendermint/vm/message/Cargo.toml | 2 +- ipc/api/src/checkpoint.rs | 72 +++-- ipc/api/src/evm.rs | 93 +++--- ipc/cli/src/commands/validator/batch_claim.rs | 17 +- ipc/cli/src/commands/validator/list.rs | 5 +- ipc/provider/Cargo.toml | 2 +- ipc/provider/src/lib.rs | 39 +-- ipc/provider/src/manager/evm/manager.rs | 299 +++++++++++++----- ipc/provider/src/manager/subnet.rs | 26 +- ipc/wallet/Cargo.toml | 2 +- 57 files changed, 1147 insertions(+), 936 deletions(-) create mode 100644 contracts/test/helpers/ActivityHelper.sol create mode 100644 docs/fendermint/diagrams/activity_rollup1.png create mode 100644 docs/fendermint/subnet_activities.md create mode 100644 fendermint/actors/activity-tracker/src/types.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/activities/actor.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/activities/mod.rs create mode 100644 fendermint/vm/interpreter/src/fvm/activity/actor.rs rename fendermint/vm/interpreter/src/fvm/{activities => activity}/merkle.rs (55%) create mode 100644 fendermint/vm/interpreter/src/fvm/activity/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 731ca28a4..0c7a060ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2769,7 +2769,6 @@ version = "0.1.0" dependencies = [ "anyhow", "cid", - "fil_actor_eam", "fil_actors_evm_shared", "fil_actors_runtime", "frc42_dispatch", @@ -2782,6 +2781,7 @@ dependencies = [ "num-derive 0.3.3", "num-traits", "serde", + "serde_tuple", ] [[package]] diff --git a/contracts/binding/Cargo.toml b/contracts/binding/Cargo.toml index fc82cff9b..e3a27d90d 100644 --- a/contracts/binding/Cargo.toml +++ b/contracts/binding/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] ethers = { workspace = true, features = ["abigen", "ws"] } -fvm_shared = { workspace = true, features = ["crypto"] } +fvm_shared = { workspace = true } anyhow = { workspace = true } [build-dependencies] diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index 56700d749..cdb6982dc 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -3,55 +3,63 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -event ActivityReportCreated(uint64 checkpointHeight, ActivityReport report); +// Event to be emitted within the subnet when a new activity summary has been recorded. +event ActivityRollupRecorded(uint64 checkpointHeight, FullActivityRollup rollup); -/// The full validator activities report -struct ActivityReport { - ValidatorActivityReport[] validators; +// Carries a set of reports summarising various aspects of the activity that took place in the subnet between the +// previous checkpoint and the checkpoint this summary is committed into. If this is the first checkpoint, the summary +// contains information about the subnet's activity since genesis. +// In the future we'll be having more kinds of activity reports here. +struct FullActivityRollup { + /// A report of consensus-level activity that took place in the subnet between the previous checkpoint + /// and the checkpoint this summary is committed into. + /// @dev If there is a configuration change applied at this checkpoint, this carries information + /// about the _old_ validator set. + Consensus.FullSummary consensus; } -struct ValidatorActivityReport { - /// @dev The validator whose activity we're reporting about. - address validator; - /// @dev The number of blocks committed by each validator in the position they appear in the validators array. - /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. - uint64 blocksCommitted; - /// @dev Other metadata - bytes metadata; +// Compressed representation of the activity summary that can be embedded in checkpoints to propagate up the hierarchy. +struct CompressedActivityRollup { + Consensus.CompressedSummary consensus; } -/// The summary for the child subnet activities that should be submitted to the parent subnet -/// together with a bottom up checkpoint -struct ActivitySummary { - /// The total number of distintive validators that have mined - uint64 totalActiveValidators; - /// The activity commitment for validators - bytes32 commitment; +/// Namespace for consensus-level activity summaries. +library Consensus { + type MerkleHash is bytes32; - // TODO: add relayed rewarder commitment -} + // Aggregated stats for consensus-level activity. + struct AggregatedStats { + /// The total number of unique validators that have mined within this period. + uint64 totalActiveValidators; + /// The total number of blocks committed by all validators during this period. + uint64 totalNumBlocksCommitted; + } -/// The summary for a single validator -struct ValidatorSummary { - /// @dev The child subnet checkpoint height associated with this summary - uint64 checkpointHeight; - /// @dev The validator whose activity we're reporting about. - address validator; - /// @dev The number of blocks committed by each validator in the position they appear in the validators array. - /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. - uint64 blocksCommitted; - /// @dev Other metadata - bytes metadata; -} + // The full activity summary for consensus-level activity. + struct FullSummary { + AggregatedStats stats; + /// The breakdown of activity per validator. + ValidatorData[] data; + } -/// The proof required for validators to claim rewards -struct ValidatorClaimProof { - ValidatorSummary summary; - bytes32[] proof; -} + // The compresed representation of the activity summary for consensus-level activity suitable for embedding in a checkpoint. + struct CompressedSummary { + AggregatedStats stats; + /// The commitment for the validator details, so that we don't have to transmit them in full. + MerkleHash dataRootCommitment; + } + + struct ValidatorData { + /// @dev The validator whose activity we're reporting about, identified by the Ethereum address corresponding + /// to its secp256k1 pubkey. + address validator; + /// @dev The number of blocks committed by this validator during the summarised period. + uint64 blocksCommitted; + } -/// The proofs to batch claim validator rewards in a specific subnet -struct BatchClaimProofs { - SubnetID subnetId; - ValidatorClaimProof[] proofs; + /// The payload for validators to claim rewards + struct ValidatorClaim { + ValidatorData data; + MerkleHash[] proof; + } } diff --git a/contracts/contracts/activities/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol index eb47b7fed..052b233de 100644 --- a/contracts/contracts/activities/IValidatorRewarder.sol +++ b/contracts/contracts/activities/IValidatorRewarder.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -import {ValidatorSummary} from "./Activity.sol"; +import {Consensus} from "./Activity.sol"; /// @title ValidatorRewarder interface. /// @@ -14,7 +14,7 @@ interface IValidatorRewarder { /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and /// disburse any relevant rewards. /// @dev This method should revert if the summary is invalid; this will cause the - function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external; + function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) external; } /// @title Validator reward setup interface diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 433f57185..579e64d5f 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -3,25 +3,24 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; import {InvalidProof} from "../errors/IPCErrors.sol"; -import {ValidatorSummary} from "./Activity.sol"; +import {Consensus} from "./Activity.sol"; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; /// Verifies the proof to the commitment in subnet activity summary library LibActivityMerkleVerifier { function ensureValidProof( bytes32 commitment, - ValidatorSummary calldata summary, - bytes32[] calldata proof + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof ) internal pure { // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash - bytes32 leaf = keccak256( - bytes.concat( - keccak256( - abi.encode(summary.validator, summary.blocksCommitted, summary.metadata) - ) - ) - ); - bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(detail.validator, detail.blocksCommitted)))); + // converting proof to bytes32[] + bytes32[] memory proofBytes = new bytes32[](proof.length); + for (uint256 i = 0; i < proof.length; i++) { + proofBytes[i] = Consensus.MerkleHash.unwrap(proof[i]); + } + bool valid = MerkleProof.verify({proof: proofBytes, root: commitment, leaf: leaf}); if (!valid) { revert InvalidProof(); } diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index b18e8b551..903ac8740 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -1,27 +1,32 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {Consensus} from "./Activity.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; +import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; -import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; -import {ValidatorSummary, BatchClaimProofs} from "./Activity.sol"; -import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; -import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; -import {LibDiamond} from "../lib/LibDiamond.sol"; /// The validator reward facet for the parent subnet, i.e. for validators in the child subnet /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardFacet is ReentrancyGuard, Pausable { - function batchClaim(BatchClaimProofs[] calldata payload) external nonReentrant whenNotPaused { - uint256 len = payload.length; + function batchSubnetClaim( + SubnetID calldata subnet, + uint64[] calldata checkpointHeights, + Consensus.ValidatorClaim[] calldata claims + ) external nonReentrant whenNotPaused { + require(checkpointHeights.length == claims.length, "length mismatch"); + uint256 len = claims.length; for (uint256 i = 0; i < len; ) { - _batchClaimInSubnet(payload[i]); + _claim(subnet, checkpointHeights[i], claims[i].data, claims[i].proof); unchecked { i++; } @@ -30,43 +35,33 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { /// Validators claim their reward for doing work in the child subnet function claim( - SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + SubnetID calldata subnet, + uint64 checkpointHeight, + Consensus.ValidatorData calldata data, + Consensus.MerkleHash[] calldata proof ) external nonReentrant whenNotPaused { - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - _claim(s, subnetId, summary, proof); + _claim(subnet, checkpointHeight, data, proof); } // ======== Internal functions =========== function handleRelay() internal pure { - // no opt for now + // no-op for now return; } - function _batchClaimInSubnet(BatchClaimProofs calldata payload) internal { - uint256 len = payload.proofs.length; - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - - for (uint256 i = 0; i < len; ) { - _claim(s, payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); - unchecked { - i++; - } - } - } - function _claim( - ValidatorRewardStorage storage s, SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + uint64 checkpointHeight, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof ) internal { + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight // note: will never exist. - if (msg.sender != summary.validator) { + if (msg.sender != detail.validator) { revert NotValidator(msg.sender); } @@ -74,7 +69,7 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { return handleRelay(); } - LibValidatorReward.handleDistribution(s, subnetId, summary, proof); + LibValidatorReward.handleDistribution(subnetId, checkpointHeight, detail, proof); } } @@ -102,7 +97,7 @@ struct ValidatorRewardStorage { } /// The payload for list commitments query -struct ListCommimentDetail { +struct ListCommitmentDetail { /// The child subnet checkpoint height uint64 checkpointHeight; /// The actual commiment of the activities @@ -121,7 +116,7 @@ library LibValidatorReward { function initNewDistribution( SubnetID calldata subnetId, uint64 checkpointHeight, - bytes32 commitment, + Consensus.MerkleHash commitment, uint64 totalActiveValidators ) internal { ValidatorRewardStorage storage ds = facetStorage(); @@ -132,24 +127,24 @@ library LibValidatorReward { revert CommitmentAlreadyInitialized(); } - ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); + ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), Consensus.MerkleHash.unwrap(commitment)); ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; } function listCommitments( SubnetID calldata subnetId - ) internal view returns (ListCommimentDetail[] memory listDetails) { + ) internal view returns (ListCommitmentDetail[] memory listDetails) { ValidatorRewardStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); uint256 size = ds.commitments[subnetKey].length(); - listDetails = new ListCommimentDetail[](size); + listDetails = new ListCommitmentDetail[](size); for (uint256 i = 0; i < size; ) { (bytes32 heightBytes32, bytes32 commitment) = ds.commitments[subnetKey].at(i); - listDetails[i] = ListCommimentDetail({ + listDetails[i] = ListCommitmentDetail({ checkpointHeight: uint64(uint256(heightBytes32)), commitment: commitment }); @@ -178,18 +173,20 @@ library LibValidatorReward { } function handleDistribution( - ValidatorRewardStorage storage s, SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + uint64 checkpointHeight, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof ) internal { + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + bytes32 subnetKey = subnetId.toHash(); - bytes32 commitment = ensureValidCommitment(s, subnetKey, summary.checkpointHeight); - LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); + bytes32 commitment = ensureValidCommitment(s, subnetKey, checkpointHeight); + LibActivityMerkleVerifier.ensureValidProof(commitment, detail, proof); - validatorTryClaim(s, subnetKey, summary.checkpointHeight, summary.validator); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); + validatorTryClaim(s, subnetKey, checkpointHeight, detail.validator); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, detail); } function ensureValidCommitment( diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 1dfa33df5..8f0ffd7d8 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -2,34 +2,30 @@ pragma solidity ^0.8.23; import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; -import {ValidatorSummary} from "../activities/Activity.sol"; +import {Consensus} from "../activities/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/// An example validator rewarder implementation that tracks the accumulated -/// reward for each valdiator only. -contract ValidatorRewarderMap is IValidatorRewarder { +/// An example validator rewarder implementation that only tracks the cumulative number of +/// blocks committed by each validator. +contract ValidatorRewarderMap is IValidatorRewarder, Ownable { SubnetID public subnetId; - address public owner; mapping(address => uint64) public blocksCommitted; - constructor() { - owner = msg.sender; - } + constructor() Ownable(msg.sender) {} - function setSubnet(SubnetID calldata id) external { - require(msg.sender == owner, "not owner"); + function setSubnet(SubnetID calldata id) external onlyOwner { require(id.route.length > 0, "root not allowed"); - subnetId = id; } - function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external { + function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) external { require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); address actor = id.route[id.route.length - 1]; require(actor == msg.sender, "not from subnet"); - blocksCommitted[summary.validator] += summary.blocksCommitted; + blocksCommitted[detail.validator] += detail.blocksCommitted; } } diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index a88cf1026..fafbecc0c 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -17,7 +17,7 @@ import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {ActivityReportCreated, ActivityReport} from "../../activities/Activity.sol"; +import {ActivityRollupRecorded, FullActivityRollup} from "../../activities/Activity.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -49,12 +49,12 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @param checkpoint - a bottom-up checkpoint /// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight /// @param membershipWeight - the total weight of the membership - /// @param activityReport - the validator validator report + /// @param fullSummary - the full validators' activities summary function createBUChptWithActivities( BottomUpCheckpoint calldata checkpoint, bytes32 membershipRootHash, uint256 membershipWeight, - ActivityReport calldata activityReport + FullActivityRollup calldata fullSummary ) external systemActorOnly { if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { revert CheckpointAlreadyExists(); @@ -71,7 +71,7 @@ contract CheckpointingFacet is GatewayActorModifiers { LibGateway.storeBottomUpCheckpoint(checkpoint); - emit ActivityReportCreated(uint64(checkpoint.blockHeight), activityReport); + emit ActivityRollupRecorded(uint64(checkpoint.blockHeight), fullSummary); } /// @notice creates a new bottom-up checkpoint diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index 3983c76ee..c40381513 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -11,8 +11,8 @@ struct SubnetRegistryActorStorage { address SUBNET_ACTOR_GETTER_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_MANAGER_FACET; - // solhint-disable-next-line var-name-mixedcase /// TODO: this should be removed as it's for collateral withdraw only, not rewarder + // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_REWARD_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_CHECKPOINTING_FACET; diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 1dc78be10..9d00cda8d 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ActivitySummary} from "../activities/Activity.sol"; +import {CompressedActivityRollup} from "../activities/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -31,7 +31,7 @@ struct BottomUpCheckpoint { /// @dev Batch of messages to execute. IpcEnvelope[] msgs; /// @dev The activity summary from child subnet to parent subnet - ActivitySummary activities; + CompressedActivityRollup activities; } struct RelayedSummary { diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index c130b2ad5..ad540d61f 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -53,8 +53,8 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, LibValidatorReward.initNewDistribution( checkpoint.subnetID, uint64(checkpoint.blockHeight), - checkpoint.activities.commitment, - checkpoint.activities.totalActiveValidators + checkpoint.activities.consensus.dataRootCommitment, + checkpoint.activities.consensus.stats.totalActiveValidators ); // confirming the changes in membership in the child diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 2d7761d10..eb7626a0e 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -16,6 +16,7 @@ remappings = [ "murky/=lib/murky/src/", ] allow_paths = ["../node_modules"] +solc = "0.8.23" [fuzz] runs = 512 diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index 51d4ac0ae..50c0491e4 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -37,17 +37,17 @@ task('deploy-registry') }, { name: 'SubnetActorPauseFacet' }, { name: 'SubnetActorRewardFacet' }, - { + { name: 'SubnetActorCheckpointingFacet', libraries: ['SubnetIDHelper'], }, { name: 'DiamondCutFacet' }, { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, - { + { name: 'ValidatorRewardFacet', libraries: ['SubnetIDHelper'], - }, + }, ) const registryFacets = await Deployments.deploy( diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index 0f7d784e8..095ffa935 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -44,4 +44,4 @@ task('validator-rewarder-set-subnet') const contracts = await Deployments.resolve(hre, 'ValidatorRewarderMap') const contract = contracts.contracts.ValidatorRewarderMap await contract.setSubnet(subnetId) - }) \ No newline at end of file + }) diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 53d17edbf..066c2e72e 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,7 +46,7 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ActivitySummary} from "../contracts/activities/Activity.sol"; +import {FullActivityRollup, CompressedActivityRollup, Consensus} from "../contracts/activities/Activity.sol"; import {ValidatorRewarderMap} from "../contracts/examples/ValidatorRewarderMap.sol"; import {ValidatorRewardFacet} from "../contracts/activities/ValidatorRewardFacet.sol"; @@ -934,9 +934,14 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({ - totalActiveValidators: uint64(validators.length), - commitment: bytes32(uint256(nextConfigNum)) + activities: CompressedActivityRollup({ + consensus: Consensus.CompressedSummary({ + stats: Consensus.AggregatedStats({ + totalActiveValidators: uint64(validators.length), + totalNumBlocksCommitted: 3 + }), + dataRootCommitment: Consensus.MerkleHash.wrap(bytes32(uint256(nextConfigNum))) + }) }) }); @@ -956,7 +961,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function confirmChange( address[] memory validators, uint256[] memory privKeys, - ActivitySummary memory activities + CompressedActivityRollup memory activities ) internal { uint256 n = validators.length; diff --git a/contracts/test/helpers/ActivityHelper.sol b/contracts/test/helpers/ActivityHelper.sol new file mode 100644 index 000000000..037db293f --- /dev/null +++ b/contracts/test/helpers/ActivityHelper.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {Consensus, CompressedActivityRollup} from "../../contracts/activities/Activity.sol"; + +library ActivityHelper { + function newCompressedActivityRollup( + uint64 totalActiveValidators, + uint64 totalNumBlocksCommitted, + bytes32 detailsRootCommitment + ) internal pure returns (CompressedActivityRollup memory compressed) { + Consensus.CompressedSummary memory summary = newCompressedSummary( + totalActiveValidators, + totalNumBlocksCommitted, + detailsRootCommitment + ); + compressed.consensus = summary; + return compressed; + } + + function newCompressedSummary( + uint64 totalActiveValidators, + uint64 totalNumBlocksCommitted, + bytes32 detailsRootCommitment + ) internal pure returns (Consensus.CompressedSummary memory summary) { + summary.stats.totalActiveValidators = totalActiveValidators; + summary.stats.totalNumBlocksCommitted = totalNumBlocksCommitted; + summary.dataRootCommitment = Consensus.MerkleHash.wrap(detailsRootCommitment); + } + + function wrapBytes32Array(bytes32[] memory data) internal pure returns (Consensus.MerkleHash[] memory wrapped) { + uint256 length = data.length; + + if (length == 0) { + return wrapped; + } + + wrapped = new Consensus.MerkleHash[](data.length); + for (uint256 i = 0; i < length; ) { + wrapped[i] = Consensus.MerkleHash.wrap(data[i]); + unchecked { + i++; + } + } + + return wrapped; + } +} diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index ec166cd81..f3b911eaf 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -34,26 +34,21 @@ library MerkleTreeHelper { function createMerkleProofsForActivities( address[] memory addrs, - uint64[] memory blocksMined, - bytes[] memory metadatas + uint64[] memory blocksMined ) internal returns (bytes32, bytes32[][] memory) { Merkle merkleTree = new Merkle(); if (addrs.length != blocksMined.length) { revert("different array lengths btw blocks mined and addrs"); } - if (addrs.length != metadatas.length) { - revert("different array lengths btw metadatas and addrs"); - } + uint256 len = addrs.length; bytes32 root; bytes32[][] memory proofs = new bytes32[][](len); bytes32[] memory data = new bytes32[](len); for (uint256 i = 0; i < len; i++) { - data[i] = keccak256( - bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i], metadatas[i]))) - ); + data[i] = keccak256(bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i])))); } root = merkleTree.getRoot(data); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 121b8c330..9dfe76e4c 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff200000000000000000000000000000000000000000000000000000000ed915e7d000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a0316672000000000000000000000000000000000000000000000000000000002ea952910000000000000000000000000000000000000000000000000000000036bfdf6700000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -97,7 +97,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021b6bda5d00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002dcab3d7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000023cca8af0000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000202eca6eb00000000000000000000000000000000000000000000000000000000f9d3434c00000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index db2f720ab..69e53166a 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,8 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -1070,7 +1071,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1079,7 +1080,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // failed to create a checkpoint with zero membership weight @@ -1121,7 +1122,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1145,7 +1146,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1167,7 +1168,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1214,7 +1215,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1235,7 +1236,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1244,7 +1245,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1309,7 +1310,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1371,7 +1372,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1455,7 +1456,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1490,7 +1491,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1535,7 +1536,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1584,7 +1585,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1648,7 +1649,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 9b1b4e50b..3b239b599 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,7 +33,8 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -166,7 +167,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(address(saDiamond)); @@ -225,7 +226,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 7313e35c6..1bf25a3f0 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,8 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -1351,7 +1352,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1382,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index c7a8c30a9..ecfd54c18 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,9 +43,10 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivitySummary, ValidatorSummary, BatchClaimProofs, ValidatorClaimProof} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -694,7 +695,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -703,7 +704,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -804,7 +805,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -813,7 +814,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -842,7 +843,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}); + checkpoint.activities = ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -899,7 +900,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -912,7 +913,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -924,7 +925,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(3))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -936,7 +937,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(4))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -947,7 +948,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(5))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -961,7 +962,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(6))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -975,7 +976,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(7))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -989,7 +990,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(8))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -1000,7 +1001,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(9))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1014,7 +1015,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(10))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1056,7 +1057,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -1100,7 +1101,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); hash = keccak256(abi.encode(checkpoint)); @@ -2364,7 +2365,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { powers[3] = 10000; saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2372,62 +2372,45 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot)); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); saDiamond.validatorReward().claim( subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proofs[0] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + ActivityHelper.wrapBytes32Array(proofs[0]) ); vm.startPrank(addrs[1]); vm.deal(addrs[1], 1 ether); saDiamond.validatorReward().claim( subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[1], - blocksCommitted: blocksMined[1], - metadata: metadata[1] - }), - proofs[1] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[1], blocksCommitted: blocksMined[1]}), + ActivityHelper.wrapBytes32Array(proofs[1]) ); vm.startPrank(addrs[2]); vm.deal(addrs[2], 1 ether); saDiamond.validatorReward().claim( subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[2], - blocksCommitted: blocksMined[2], - metadata: metadata[2] - }), - proofs[2] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[2], blocksCommitted: blocksMined[2]}), + ActivityHelper.wrapBytes32Array(proofs[2]) ); vm.startPrank(addrs[3]); vm.deal(addrs[3], 1 ether); saDiamond.validatorReward().claim( subnetId, - ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[3], - blocksCommitted: blocksMined[3], - metadata: metadata[3] - }), - proofs[3] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[3], blocksCommitted: blocksMined[3]}), + ActivityHelper.wrapBytes32Array(proofs[3]) ); // check @@ -2472,7 +2455,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); } - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2480,55 +2462,41 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot1)); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot2)); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); - BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); - ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); - claimProofs[0] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); - claimProofs[1] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2, - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs2[0] - }); + Consensus.ValidatorClaim[] memory claimProofs = new Consensus.ValidatorClaim[](2); + uint64[] memory checkpointHeights = new uint64[](2); + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - batchProofs[0] = BatchClaimProofs({ - subnetId: subnetId, - proofs: claimProofs + claimProofs[0] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs1[0]) + }); + claimProofs[1] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs2[0]) }); - saDiamond.validatorReward().batchClaim(batchProofs); + saDiamond.validatorReward().batchSubnetClaim(subnetId, checkpointHeights, claimProofs); // check assert(m.blocksCommitted(addrs[0]) == 2); } - function testGatewayDiamond_ValidatorBatchClaimMiningReward_NoDoubleClaim() public { ValidatorRewarderMap m = new ValidatorRewarderMap(); { @@ -2564,7 +2532,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); } - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2572,50 +2539,36 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot1)); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot2)); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); - BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); - ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); - claimProofs[0] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); - claimProofs[1] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); + Consensus.ValidatorClaim[] memory claimProofs = new Consensus.ValidatorClaim[](2); + uint64[] memory heights = new uint64[](2); - batchProofs[0] = BatchClaimProofs({ - subnetId: subnetId, - proofs: claimProofs + heights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + heights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + claimProofs[0] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs1[0]) + }); + claimProofs[1] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs2[0]) }); vm.expectRevert(ValidatorAlreadyClaimed.selector); - saDiamond.validatorReward().batchClaim(batchProofs); + saDiamond.validatorReward().batchSubnetClaim(subnetId, heights, claimProofs); } // ----------------------------------------------------------------------------------------------------------------- diff --git a/docs/fendermint/diagrams/activity_rollup1.png b/docs/fendermint/diagrams/activity_rollup1.png new file mode 100644 index 0000000000000000000000000000000000000000..6a2dc98cb771c9d6f72de8816e959a08b9c2d21a GIT binary patch literal 119471 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

CY8Mp1Eup zBail5%08w>6}!C1LTsr?bxG#(5W#N|Misgv?L4|IOw6>8*sQ{m3qB?k>N!o2xTX@z z+VQ>$EPqBwS(AO)@yA%@I^QS>hE9Grnb{x{jVX;%#rtV&$B{Y-+5NT|KP>v0V(--BP|$0$=Kzy^%(I zQB&id!xGcR1ZhbZ??!V8Le=;xQ&go(*SZ555FK@z6}%UoI2_ARuLt^o(KO%@8bbHR zN7^E=Pg(^N-WQq5R1Sq>1qyM=enund7yH7^d=&dV(JSA-vzA5Ud=o(+Gn3g@p79x( z9STP#CZi}4RmBCEyp~a>0`82HKBolZ8Lm@Q_!i+986r~TO*w&rl9dtON!3vwb&3yH z%Kw(x=eGQpn&hfs8IxYIt*LVR@BP~5sV~N2TT-R@Bl#AowZ1A}WK4&WZQnp`>(8Wa zIY#Jhgv3{msN0N;@ihbAB@*i|!hA|yTiIF^UB?5fiu*{^wUAO7zrXDd@{1^L$bf?6 z0X5+N6c0{5U)bhHxY-UVE|sMryf`H$y+uVAA7Qx`GZDX*&A51nWuWUGVn0S>YoodO zY{GCV-lzFOjPNz4xC`EP$X*Qhg8@vWW<6DoMty+4j0$c0u;JmhR0=yJ1v?m>9o#N! zYXDIuC5U|j$Rf3RgpVLGurf8?j}F=w6U~OH3Ftq3@8D4TYtHPX`qj}px#^*Vk4fa^)t{MW+=54Buseya92#HzR&9yBh#%C+}x^dJ(BnlK|-nG0*Kkq zV-t~8c_!Z^EBZZ$BlJZgG0$5gr-)hV)yn~3x8GsP$Srn4Y0+3BQI}I<^m$Ln1Hyff zJUW0F0G@B&w2eutpt2ouG{wuz!+LyU-^00K%e6#|h=P_H>AjJgv`23EGhACy@lir+ zJ4CTk(;*g!3v5BLxz7>Dsh0HsLU_&=mYw3D%wR$^m*Kr;!@A1Hy^b)2-3-Oi|(6qhS~3lCbVq$>L~7X}n$E>PbW4q$cz*}sw-qza7ovPTTc ztnBEuC%_VA`(VjB^eHD}<2psq--zu~$ax4Fi?sq$*}14JLo#PJ8&{%bZW{FI56FoC zu}}HnG_;Tn*`n z`w@!QHLa8JBO~>@e8tdqYDr8zbi9#fz}K8Ck{mBuH-sW7;mRZDF)S$-lbP}=Er>cX zwcMFm!(=D5zE|4XoaP@D8=lg$5f)5%jwy-E`}JGrATuh}&*(I;EGyG+ZIQju(8LSy zgm`7qTd@NUN#Sw!oy1Wnb2OlDi^oNRj^sz5%4K78dJ zv6_V6cX7&aQra{1d%_Q7;q{w=Ei-ma)W2o}{BQYNHAk(zLcla-D~r0lR`lBXJd8Wf zJ;6H5`rtPy>wYD_>9Ha&)S2mT8|f}YKnh(e`;dBFTOYk$-A~sVc_Ry3JmU+Wzije+ zB*1H)^x4p{e6+s_W%v6{wUeoc10nS?_=a`r3OYOjm#k}H5@nA=m-eozc%ipRF7vL> zC20}$=fyHZ-eiW~bM8|M0A&LP0V*+$_0>CSa_&bdCO{58w(7{O-~88ZlsnK|qQyDP$7K4WpzkPi4XRN8MQm>lB^>X98~*K z>abWd#2kNFilxdYaW7sx6oK3n&azVa$S@7RF-znzyE1uQcEF)3BWrSE)w7s5SRCie zmrBYCe}to36mLbbQYC*CYwU44>BQ5;B*o&p`}3_{aq&#B7s_IzUv z#^=XMA;JmB&SNMRB`v5U`T8*EpS>W48cno*SO(S?pf9XmNX{{tZGo(RJk{x6I^zSh zZtP&^DtR+o-qyOOFd7S)^PT%7(f6=SS}J7pNkN=asYU$(ErPpWZ&MZ3aq29=>|?s5 zq>NNdwH^Y>^>Qts%%?0AE2i=oHC-)`FfjWB9`CDy56sL8H%sD=Mh6pkVC*7?tQc%I zlX5^(b+*t=J3p4c*$a6NjqU1H?I7!km}=)wFP*o=ugrDAreH6B3+qj#Q7-7phRK5Jny04J_51NvO^kL zHyIoN>AOpGRRFZZ*w|7YAvD`~3JRbX5F)i$VD%3M#N>U-l+7f(x3tdYYN&O|zK_9n z30iQkGp`SCy601wUq-_ML|TiG$14<6cZ#=``-BmQqkt}kW?|z)lvq;LQaL)_*A6)~T{XPWuG}#ARKw*!usBBpt5ZN#fQ!7C;Kld& zL=Bd@1n_)Hp~$$K)}|1K2Fl>IE<%3t2qmQ?T+}fKy&PMxBWZW2FRG} zDEBdZF9%%c0K81&i%lempoPbU*1KH!9@Ib!l^wXz|Me;Uh+i+K1Rm?dx}+r{T?K_t zccvE5b1{gYKVbb*EJH^L!5BL(+XW%>}>hS@u6+?Lsz>(W6PP{mg zKXj8G6}eoLie91`!0>*jWgdPWmk~jwP|3NB7pDf2YX;=F z`969A+5ZBUj1;M(hTgN_imGx)-j`_+HxS&RSxJzegwlR%2!?eZ|>sE2_TKLNM-S z-)8bnM>$j&-ZF`h6hv4T*tHXNDi;cQVCmJWq#Neyd!E1ZEe>otey_-0wsq%oY+o9b z9VG~($56M+k6uMAE6GTW_G%fT`-=&5cMY#2C@q16_ipAUQSYt0HKM%H@eP1LA>Z!K zPu$E!M`2gY2ge$jkTa_X8EI%y{SDpGY|1qquU@}cqZGL|fVHIucKs;dP~c>jGyhBZ zM&(DONwCD?+d5DIM*Wz69)0R=T_8~}-Z%c(>@YO)F)lxvkL<~r4e9qYr|%Qgd3k3J z-|p{Y`A91k1T-r-EiDRNY)zt~PM{~r^K%X%lB6l4Xte&qe!pyi1TW`c=@ZuHG%<^P zIT_-0PZJbYT5*qMrFw93pDO0l+a#PtJN!B>Yc^Le#cMm1i*fqCm?uWz(F^KOuwR$#V2q4jinBDTgFP%+T)%csR2xkhTp}` z9S^wo!2D}c7enqnr|v2Rr?`yVml~&el~|(R`Pl*MP4>~tYZ6s0&dDQkHX{qBCVUd! zEPsS|Zo2E1DJ?aZX=RDd6Om@zu(}WY@ED7Fyk|VM8X}m0Uh6Ms;&5*0&TLjKQ?>~W zoQ&Z-oGEIwL#)Unb*OA{ra8~$+vJVwLYzCtDui{&^*NntdR)A#0joh$|3o~JpK(%OA+p3TvUhdA8p(UusTH()Yn0xh}xb5Vs&f15U}}>9(jSu08gYV*+8^ZO3Ia{ zTL1hej^s;R^E$4s)Q_iQWIObW zYm(B{pAU$Dke)N#q*Zj+!b)OC`(3FnqDmq4-`0D_Rot4d{qA!q3vk@k?l^TtiS%CzN zK&_C`uPwu|8o!gX;7NCi_$2B8@8GeXHrFPc@!{Eqjr)Q41dDZw4vJ(*nWc$HrKeiW z;vfgEWBn*Ht(!PTH48!ogV=@8t^xP%*?Y$(y~;!%XbpI*f{`KTuAnf~x=XJ?HK-&) z7JvDvO;|v+XTk@!=;|sL0|Q&2)R^X?}539p^dsq3kS7Ue-6adW6o2 zcqmzZ1j(+mBah}ET-ER^BawDWrW~hVS`Y5)mMb={fvZFc4&V`>_d06mLBrAmU!m>@%ebQlpx86l`5yKAMb80xXZWu_HDI zR3Dr)8;k`mDcFO1g2(71&gl4Es{zlcQL+wtV-FwyP=1?@9{6_!E6%$1ZTeZs;bsZv0ibVop zdd-y(ZZ@TPYJGLI^VoV$Y2AC0TW%2_Hw@iJzPb8~?f(Y}&T zVwoYYn*}l%=+arJat;HFz5^MsLX*$v5g$Rhf{@=Dh(;UqWYlGX*y&{84M$}6)kq*`UV*8W6>=Hfw_0|g6>}Ga3YCJ`Ngli$fHYhA^TGzwSR5X43F8#bUm%m7)SK$SV z?exjF1fc{d=Q$7A>3&MPJ`n~2g0ngid>$gEgYs31FUbYo>vFZb7^*6D%x z62TA(la|t{3`L%_edsGR+pNc#T}O%S@5#3zFMFSf^tEcy(CeF4?)|ed=zo}!W?q$m zuGMK<#?bPw-gWXt@foy;E@GmEUckCCOKs8HKN3IdOcww*9=Jxx#MpIPVnGmE$K3%K zl|vorM+piadG>Wny#V2o)Sm0w&#KqWEUraP6&tY&Dj1sEU zydh!j|B>{|89tJQm9>9>+B58-hJ(s7IFsLl-ogbnGtXU~l526*(vl;G8+j63XM5s- zA^+ur?@d#e6;+g7Jb(uxq+i`fUVN_{hXv@c50o#SYm#Me!{s62`ln??fZ$krS;?2+ zn)|rIRl9Qbht>ftWbP(ta5lBGu+l|~~qE5PsEHV71cj06(WQe(!^N}15|niVqNF5e7x)AdMutu>mkLMfxY*^WkA3GlBGpkj8K>=u;a%+HewWc& zk>@aiR3AL*-QsKx)TMyn$KdZ2C{YVA!1LzSy`r|CQpi}xLZ4zSdoSDjzJFb~sc=-{ z0&oh3?YziV$XoUbuXMtMeDPnMU>}p?v{HS95Q;+Z!-w!ka_SIwfXRd9%%;MGbMNL) z_GsXb;4W%Q`#sW>tjc$VTEmx|BrVTrODDO|CgeP5X!&n4Ac%>UHJ(?aXcA%IJUN6o0_XEe!8Dc+$3xwyPzzqb}U=T8KSG> zyfkiI|EMg{Ra0xDemGet^HZ-!)sdEZwbz}RE4xD1Xb!Og@)IwHn)f@uQ@-QJ2HRU3 zOdcN31;zFCV`qw8^E>A>#qO1z{P^tCMx8>>#guWUHfYwKYshMTTi2`qgkm>|H; z)za;z(9sI?dlKo>fwN3PZ`MysNWstJFrM7$$t2`crzdw#!=ft__1r5}0}Lcu%Yp6g zDtSk09Zw$dl93wUwPP;n7E=7p-d9)A-1Y}A6Z|$s33JFl`2$Q&rp!kVgABb#F^}MRS=VT)i66D z+^kH;B{}HJ#7*On^tD%jZL)wbPDBQZs4zZki>Dw}0~D(gf9tOGtPZVz^%pgzNqBY^ zW+av-hdtSCWtMil4Vd9NT~^AM7`|xeAn;Y6m-QVv$FtQ#u~J;Gn1=AlF>sH5B{hq% zHR(bt*u4AfQ~Bz8*@t%dEaZ+1T%rP3C*!Lg*Bcsky$uo9Y?yhM9o@nES&`rNqfNLZ z(UTV^yh7H8?+wY*=Sh*gSlXex7!LXZ`V_q;1Vn<~|L*>3Z|YjktNn~5>MlDE#bx(= zmyt=Gbib*>*RE{uW%gi_Et2L(FCfWGln(O}l>;8sfSSa(g@57<^Cq}o6BGI?A~JWV zNilRbx_wf^y;4G>1#QGBOWGzS43sjYv51M(dLhMz{2R19H((DexT`)s6~7&L(f4^R z*=zPyY=~TND)j`rC`MKdrN9K<|Ki&?Q1NBp<)xF7Kake$1?)D8-h~HDbp|F zh-t7gj-K13z?BV_;Dhr`#TIw3fm6j)@JE!bP2%k0vO8_+X@lCc4LbVPa0Sm)i=CVj zuFGlEm0V{2^lhO>mEy$w)DCQyU#mm$cn}(WPw0l~g%@{~4-}wh_ojZ`FIwUvBNZ8k zL_MIE-pAyadcGxES)`buUgjNEEl{v7iOZ2cBq@L4cym$Y*xASqB!;>+@g`y_C}V}k z<+d2(dbX=AT3<`6W;69+sG$kKc0tM~x8M(6jrmUv0PIrr?Cd*!u3S*5^^FaC{Cnl- z6JY29P(I&Fw_LdquPCwnh{>GE=?;(|ZOAc9-T?qVL%MPltWC_mztRgFD?A9$C*KAM zY`|fw15DVrLFdnu$6S5FDG?FsfvSrduoVE>o(olMc*Am2+sB-#ocv~&*a_<C$CCJm-c9j(%x90adx&z0-3-7NxaOqTva=O?3Gs9rErN+%oe}AWA|Y z!NRelC_feU!XC89s8ccuZUuU4I9K>L-Y>M6w<{|zOKecJnv^|wt8`5< zndM#OtRD4@_`N<7jz~+hQ4pBbPpilLbqy79t`N1FNpUaz`+Pzk1>0}f(fgp0 zSGxBfli9lHWLF!Ku;>eFnO?i2g0pEFF~B-7Z;U5K*P$cwwcd|*d@g`CMYuxs2Li#E zZ#^3e%98~VPZ`+LHAdOmxvB4kO7kg|lV^`fOX<6BiL5=e|3N`v9j0~4dt34h;eCB-qEz2h zL56r0pKHJ6-eIX+m-s#%>zjU$)n>Fn`2sq{A%XH4HwEGqH!n&~tw_0+9@j?#}@RciM?WC7`4H8bInju90XYYh#De)7B^SZ)UZS;@Y6*ds+F3>=gJW_pH7F0u}){l%;3`H%Db*kt= zLgv3s6cmds;Xt%UEI$d#n$QY+`18%bXwKMdIdOG!H33*+U^_|^G zfGWlmpEGM>x4J**!s^6}?ZQw!N~6q&>CwQpW?nYogEz&sl{e$R&Q~#WwVa_Lw>~$l zhj6l?Eo*NO%B6RS;Q&9gzXvShC-S`j%XQsZuG%LYAcjm+OiQJqeMcRUiA-Q8V@GX7X(vlLwY^${jruQ>JFz8V$`#pTX}#N?MNg6>=O3$D)?;b}0%-glOp>32udF(JXJRxsbim2~OuSZ( zku~>topDsMM_s^(GdMOSUc>2tV$DqBHxR2dPE$soiyQEmYR!^iduSuCfaFeArU_Jk zj~kN!*4%4l;z6)`&AWT_LxkgG*zGt+vky{0chv7Sie^w$a~Ws6J7KExD?3yl7l&)x zaKD=MnCVE>6FRnQFTwp^49f1tKgpUYv*6A|hh8MPN3YQoA< z0_JW+gk=5dvzfbxbAvCFU-Z|CdoOoHuD&}WXj~hU=SVHi+_o~HpjVI6(j>4dj3g^Z zePtdkvlC?F-|Dn-p|lBwrJ~+6mj7tifC75Dwl}3a*((tIHppcR)xm*gYEd5$5t|tGaF*XxxjlU1NgIuuid6(uXp{-Ht`m!v^&Q zQ3kLbi3S103uPt;{caG^_{K{?*=~o??<{$iNDlRHZw=0l%XW4sP!k1cyN=8MQ7)0v z@v#k&WRlyIu9;~*Ft_LJTA|Oe9i4tD9vDlwdv$2 z19l?rLba3_IIg4t00JwY1v$2R)<;B9tFvzl9*YH1UtH045t;>&ZeDhVd~ogA0zf#w z@&d-04^Bq#yVbZ;a#wM$P3NbOm+D%dD35v8gDEZK+>ZsI(u>_W9iTd>i|W=yz7M*G zx`TWj>t#ABv|P-p_c}dl$PTMSuDfG!6Ro$GLB8QYnNDJ;mLT5^pAz%xgDKu!gKrDq z3EeV%pSOJDA)_p4!B@n*II|}x%NocThqcUekzR&y*PNj=jKFgx2m7&<3-$3)zg9tF zdmpo-c5k@y|3?{W4k4TFhv86$9Dr<>sVK%#2#D#$&nsqZvlIu?!TruO5e z(@<)1QJSnb?JZ6+ReTmY(lLjNh^N=fq~9?$P%bff@GE7ko{C=eyjQv8X;F`_K4<%A zy<1ro3POw(64(FUEWCXNO_hm??od~C<>H_wZ3eCb?qYmZ8K2FS*F(%-+;7%r+`x-DgKO zj+0})Z6mAEu72KnYY_&sti}JV0_4i$@6_P>y`q&xb^;1z11tK6Nw)itj`mLYc$#k2 zM9vh^kNl{tZK+B59+#XRRYu!`OOEF+CRU=G6jprcoW|5xE=O4CW^3@_))le3p z$_GwfwDo$O`;ugJm>pzVGL{=5_HBI~) zIE5F_H~#E{-=JUo*@Y6&TxdAjLtIT_H}WDDNvcz^lM73b>bnu*_<9t(&z-{cB=Uc6 zX4%@7W2o&48tF6x4stk#ZrUZkUI)o2SGELE2M77E9)Si$xDs27Y`b7;(kNbrC3)le zj*(L_nA&e;BbB$W|3&}Qo7elTO$Z?NxlesAh8V|Q+2)@sJHidq6ZNs6U5x|0zmZAa zprHYbx$PnLmdAP9F%l@4YVqE>S;F=2-J!Ry$ID6~80oBQw;HP#L|FZhZ5&S(H(R@* z`)Ap>-?t{q`*_*d_Jb%0Tg+`npOyYG8-yzB)3p%#H|OFDge@vn%D0FGiT0qInAb>k zD6FB|fdYkPY~T<4dkM{(lU;%r*j8){eKaS{tLkLUwEtkqH+Bf>+JtyXg1GpIXyjr_b{5v$gc4Z8`vR# zqS%FbErDz)zy<$-Ft-dKQ{HQ9yQ=;@@SKAMZJ)Fh!UoP*{sn(RO3JXA4Y}|qFdawh zD9EKN|A}!&<>Aa03lHx?ZlL`935%Cuw_z6iMA!?fGclPs{D19bcC!)Fu|f$Qb{ZYD zqtIp($zCTEQg?SJC!NhPssHvWX}%TN?Z2>gW`);-I{So!O9I;Wj~ZzjCU z6%ufE+<)XhK>7jbkn;yCTzvp?Mf`tQf3!{W1#Q>v312o|;1160@x{MK3*0Y#Y$s>- z*Smg&e(Lo9u)O}Hx3nYg?r!o!^9DaG-Tx0mH;H+RF|uJ){c_@piOKVnzlTJ;2M#g| z0jck%+s+s<>uEb5EQ;elec%E&4BSld*?YC|Q+wV-Y7jwB%t({t-QUke6R--K?>hdT z@9x|@c$72Suz(lRb6OtE*~ z!XV?JAp)?zKgRbdSD?%~2Sq!ue(je4)Nxo0 zv=XM_XR0u~v^&ge^TwUvMt-KT&Ao4$Fj5IX^yLuQ$)oD%#KyqpP~z)_Je9;&+x}R_ zQ*6L@$D>PMCs!VhD3Gg$pNg}gG6B*w`1Q)#?Eo|RJKPnhkLjnM zWlO7uaNaLp*g(=@F?ShUV-b^6y(fZ+Y8bQDm0l;@LxZST@=VmVA-1&O{MYXbgYP#3 zGQQI~R5;6-y7-Qb(#hRvo6rXEJ~Qk9AT!4s-Jt*;+Mf9)+J1qAv4gBFC6jQm6@bVe z7W2mAzHC|_hZF<=C3BSQEn&T>5x161ZSwW=-h-`mi)ADFfBEH$`_@alHsADSdD~o^ zKthG8E2{zPH@Sr6|Lz1(`eEG1T|apHqgt z-~2w{h_xexg@*+RS5qW@0pA}B;3gV!h={P~VREw5{1Pfd(Y}7>0M`i$o5x|&osuy3 zhdkiUH(zhRxk~qBAMwDE5C#@oqEZ#extD#)_f@#8rOR_+K_O@mzy{i3d)?FF@4q>o z01*Bh91>Rn1!t=`i6lWjyX?TcWwoK(mU|NAvU5l2MoZ|&&AoLB89+B{NScE z^1-Ci?OQnB1k#%hI&*D}W*?5gA;spZ=swvB)%=ogGTFZ9((RVdm@R_fiykSWzsPB_ zIVMA(Q0us&U255u@PY)wTb)9IH4&LoT>;ob=N(L^W9Mfuy8hZ32c7UN% zY>n?5mw~Z{%&J2xBPp^RLhQ3RI7-sg_W=H@yXDihgBt*{DG~z1*8ueU8}+-+T&xqy zv%#EihBheD3-|==3`sDw88Ea3bdltjPH>>YytLiNS3TQH1U7gyzzHnH&_vemePE7; zWsL37;Eah+T;Sx2%|CDO`tC*=9~izid-&1VG)Ge~^BeLuA0sKpID|InbbJq(q6fI= z_R%VsJ|qwI;nG_)V5WI#+*2pAm!rzY9t>fxdvugOxUZqH*o+Tb*XG22*YeEb?&1q{ zo}XXU1k4*aqq@B*Yxe<`_G<>bv>*L20`M<&qO#cj(FniX7{}+0H_mCt9|DYy_h` zjlX=mQvO(7z*~pXr`sN}Pe9;6g8$RnX}kjQLWg+!%c@dQB`*&y-;1^A-W*EaY55VT zN(5y;$L1H50Gn>d_vqUvA5wBDVUR{FTC8=*k~f5W0RyC> z?|Srkgz6Y1wz^+Ba>%3qYj{ti5%Nix@gfj1!`n`XJvnVbDBK`j$VkArI~?h zbV0n30=j5gO2((}#ue*)A^)Buu@Pb%9HRxS8`krFHSJq5jhAn8+yEfv;Isg6T8PpC zpl#9jv30|tu=w%UC17&V#+6%mR5(JwDIOdg6M*wHObl!qXV`BwyvJVXw&>C=TRv`( z_K7fUPhoD!(=u0|9@?IFTXpFT>-dRGV@U@-Uzj+$sP#D`dcuYRq4)|9zP+3};8^0#>j_Vj_*L{je>vkR)Id-m z@#hVfeiw+p{|t)BLpBN5r3a7y{Qb>)?GuqtT}>>2KHIn)Us3nfLzubXanXNzd>44! z{tlY&=W`-NHp8fN7=xz+MxtQoTA{UQpeQ!4-t>oq<_1T`4sf~KkQA0)uc=|IetsZB z?yLONHMSYZb?E-H_mKK)1uep{|9ErL2cdYSBFwTUDx50sW&6$aA43ay)g)?a0lp>x z4rtrFo@z}cK?Owmt$%(yjFChdGOBRkGlh*z6+o~2`8$jHQd~>HK~~(eUL0NH$fN%A z6=Rv6L26f`)zBj@GBT#%l*!G@G0+K%v%MazcJ(dP{EEH<($Xm8=OY|p>KND&A*J_% zPlAvC{4vbbiAUcq5h{1?v|yr?+)vKdk*$+}Is15T>nxWeMf6gK|m0(sjQwr0Jcx}e>~eBmn8n~t@$jd{ zyLc}03nXO`j>Mh=hfHr=jwvs1WevBe)~TCr(U&^1_ucaQ(@heTrkt1GEGY3mhweB$ z<5b58H?4I!?KznD@CcuF!v17jQ?lvppLc}yvTDPidm|4iT{3a}Nu&8cw(@Dqxjzti z*1Kcn=EW+7EQ+#RZW9`JQxkq#cFHzp)3buVVzooO2BSKkg9x AfdBvi literal 0 HcmV?d00001 diff --git a/docs/fendermint/subnet_activities.md b/docs/fendermint/subnet_activities.md new file mode 100644 index 000000000..cf2c6d038 --- /dev/null +++ b/docs/fendermint/subnet_activities.md @@ -0,0 +1,78 @@ +# Subnet activities roll up + +## Overview +This doc introduces the notion of child subnet rolls up its activities through bottom-up checkpoints to its parent. +This is a mechanism to synthesise and propagate selective information about the activity that occurred inside a subnet to the parent. +The definition of `Activity` can be application specific, but generally, it represents critical information about the child subnet that +is valuable and can be acted upon in the parent(if there is one). Some sample activities could be: +- The blocks mined by each validator +- The total number of blocks mined in between bottom up checkpoints. +- ... + +The parent can choose to either relay this information up the hierarchy, or consume the incoming report for some effect, such as: +- rewarding validators by minting a token +- rebalancing the validator set based on performance metrics +- tracking subnet state +- ... + +For each application, it is possible to define its own `Activity` struct to handle different scenarios, but IPC tracks +the blocks mined by each validator and total number of blocks mined in between bottom up checkpoints out of the box. + +## Overall flow +The key idea is that child subnet tracks these activities and submits `commitment` to the parent subnet through bottom up +checkpoints. In the parent to trigger downstream effects, one needs to provide proofs to interact with the activities in the child subnet. +As an example, consider validator rewarding. The commitment is a merkle root over the blocks mined by each validator. This root is carried +to the parent as part of the bottom up checkpoint. The validator can claim its reward in the parent subnet by submitting a +merkle proof. The merkle proof can be deduced from child subnet. + +The key components are shown below: +![Activity Rollup Key Components](diagrams/activity_rollup1.png) +- Activity tracker actor: A fvm actor that tracks and aggregates the blockchain's activities. It allows querying and purging of subnet activities. +- Bottom up checkpoint: The bottom up checkpoint contains the commitment to subnet activities. +- Diamond facets: The different diamond facets to handle the follow-ups in the parent. IPC ships with `ValidatorRewardFacet` by default, but one can choose not to deploy it if not needed. + +## Key data models +The overall activity struct submitted in the child subnet is FullActivityRollUp. It carries a set of aggregated reports on various aspects of the +activity that took place in the subnet between the previous checkpoint and the checkpoint this summary is committed into. If this is the +first checkpoint, the summary contains information about the subnet's activity since genesis. + +The first report we introduce is the consensus level report. This report contains: +- Aggregated block mining stats: + - Total active validators: the total number of unique validators that have mined within this period. + - Total number of blocks committed: the total number of blocks committed by all validators during this period. +- Validator data: Contains an array of addresses and blocks mined for each validator. One can add more fields to track more activities, such as uptime metrics . + +We emit the full activities roll up as a local event, `ActivityRollupRecorded`, in the subnet, for stakeholders (e.g. validators) to monitor and present +any relevant payloads up in the hierarchy to trigger downstream effects. + +Over time, `FullActivityRollUp` can bloat, e.g. if the validator count in a subnet grows. When combined with xnet messages, the checkpoint size can become large, +which has detrimental effects on gas costs at the parent, and could even potentially exceed message size limits. Therefore, instead of embedding +the payloads in the checkpoint, we use a compressed summary in the bottom up checkpoint: CompressedActivityRollup. This struct contains the distilled information +that is critical to the parent subnet. At the moment, it contains the consensus level summary for validator rewarding. + +## Activity tacking in child subnet +TODO + +## Roll up in action +Once the activities are tracked in the child subnet, fendermint will generate the compressed summary and attach it as part of the bottom up checkpoint. +The generation of the compressed summary for each report could be different and generally application specific. +Once a quorum is reached in the child subnet, the relayer will submit the bottom up checkpoint to the parent subnet actor. Since the compressed +summary is part of the bottom up checkpoint, the compressed summary is forwarded to the parent subnet. + +## Validator rewards +For IPC, the validator rewarding mechanism is shipped out of the box. When the bottom up checkpoint is constructed, fendermint will query all +validator mining details from the activity tracker. It aggregates the number of blocks mined for each validator and merklize over the array. +Note that the validators are sorted by their address and blocks mined for unique ordering. +Since the `FullActivityRollUp` contains all validators' mining activities, validator can filter the `ActivityRollupRecorded` events against +fendermint's eth rpc endpoints to obtain the full list of activities to derive the merkle proof. There is an `ipc-cli` command to help validators +claim the reward. + +The bottom up checkpoint submitted in the parent subnet will initialize a new reward distribution for that checkpoint height. When a validator claims +the reward, the subnet actor will verify the merkle proof for the pending checkpoint height, and call the ValidatorRewarder implementation if it +succeeds, marking the fragment as claimed in its state, so it cannot be double claimed. + +IPC `ValidatorRewarderFacet` tracks the claim history of each validator to avoid double claiming. However, the detailed reward distribution +logic is actually handled by a custom contract, namely `IValidatorRewarder`. When the subnet is created, the creator can specify the address +of the validator rewarder implementation and when eligible validator claims the reward, the subnet actor will pass the validator address and +blocks mined to the rewarder implementation. As an example, the rewarder implementation could mint erc20 based on the parameters sent from subnet +actor. \ No newline at end of file diff --git a/extras/axelar-token/foundry.toml b/extras/axelar-token/foundry.toml index bd8ca5d7e..e06f33cc0 100644 --- a/extras/axelar-token/foundry.toml +++ b/extras/axelar-token/foundry.toml @@ -5,4 +5,5 @@ libs = ["node_modules", "lib"] fs_permissions = [{ access = "read-write", path = "./out"}] remappings = [ "@consensus-shipyard/=node_modules/@consensus-shipyard/" -] \ No newline at end of file +] +allow_paths = ["../../contracts"] \ No newline at end of file diff --git a/extras/linked-token/foundry.toml b/extras/linked-token/foundry.toml index 57993af93..4409ba773 100644 --- a/extras/linked-token/foundry.toml +++ b/extras/linked-token/foundry.toml @@ -7,4 +7,4 @@ remappings = [ "@ipc/=node_modules/@consensus-shipyard/ipc-contracts/", ## this murky remapping is only needed transitively for testing; we should try to get rid of this. "murky/=node_modules/@consensus-shipyard/ipc-contracts/lib/murky/src/", -] +] \ No newline at end of file diff --git a/extras/linked-token/test/MultiSubnetTest.t.sol b/extras/linked-token/test/MultiSubnetTest.t.sol index 38143279f..26d7d529b 100644 --- a/extras/linked-token/test/MultiSubnetTest.t.sol +++ b/extras/linked-token/test/MultiSubnetTest.t.sol @@ -28,6 +28,7 @@ import {GatewayGetterFacet} from "@ipc/contracts/gateway/GatewayGetterFacet.sol" import {SubnetActorCheckpointingFacet} from "@ipc/contracts/subnet/SubnetActorCheckpointingFacet.sol"; import {CheckpointingFacet} from "@ipc/contracts/gateway/router/CheckpointingFacet.sol"; import {FvmAddressHelper} from "@ipc/contracts/lib/FvmAddressHelper.sol"; +import {Consensus, CompressedActivityRollup} from "@ipc/contracts/activities/Activity.sol"; import {IpcEnvelope, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality, IpcMsgKind, ResultMsg, CallMsg} from "@ipc/contracts/structs/CrossNet.sol"; import {SubnetIDHelper} from "@ipc/contracts/lib/SubnetIDHelper.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -383,7 +384,16 @@ contract MultiSubnetTest is IntegrationTestBase { blockHeight: batch.blockHeight, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: batch.msgs + msgs: batch.msgs, + activities: CompressedActivityRollup({ + consensus: Consensus.CompressedSummary({ + stats: Consensus.AggregatedStats({ + totalActiveValidators: 1, + totalNumBlocksCommitted: 1 + }), + dataRootCommitment: Consensus.MerkleHash.wrap(bytes32(0)) + }) + }) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/fendermint/actors/activity-tracker/Cargo.toml b/fendermint/actors/activity-tracker/Cargo.toml index 886262036..0c06eaa25 100644 --- a/fendermint/actors/activity-tracker/Cargo.toml +++ b/fendermint/actors/activity-tracker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fendermint_actor_activity_tracker" -description = "Tracks fendermint block mining activities" +description = "Tracks subnet activity and generates rollups to submit to the parent in checkpoints" license.workspace = true edition.workspace = true authors.workspace = true @@ -14,7 +14,6 @@ crate-type = ["cdylib", "lib"] [dependencies] anyhow = { workspace = true } cid = { workspace = true } -fil_actor_eam = { workspace = true } fil_actors_runtime = { workspace = true } fvm_ipld_blockstore = { workspace = true } fvm_ipld_encoding = { workspace = true } @@ -24,6 +23,7 @@ multihash = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } serde = { workspace = true } +serde_tuple = { workspace = true } hex-literal = { workspace = true } frc42_dispatch = { workspace = true } diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 791bc688a..f8f84faa9 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -1,21 +1,19 @@ -// Copyright 2021-2023 Protocol Labs +// Copyright 2021-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use fil_actors_runtime::actor_error; +use crate::state::ConsensusData; +pub use crate::state::State; +use crate::types::FullActivityRollup; use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fil_actors_runtime::runtime::{ActorCode, Runtime}; -use fil_actors_runtime::{actor_dispatch, ActorError}; -use fvm_ipld_encoding::tuple::*; -use fvm_shared::address::Address; -use fvm_shared::clock::ChainEpoch; +use fil_actors_runtime::{actor_dispatch, ActorError, EAM_ACTOR_ID}; +use fil_actors_runtime::{actor_error, DEFAULT_HAMT_CONFIG}; +use fvm_shared::address::{Address, Payload}; use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; -use serde::{Deserialize, Serialize}; - -pub use crate::state::State; -pub use crate::state::ValidatorSummary; mod state; +pub mod types; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(ActivityTrackerActor); @@ -24,64 +22,82 @@ pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity_tracker"; pub struct ActivityTrackerActor; -#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone)] -pub struct BlockedMinedParams { - pub validator: Address, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct GetActivitiesResult { - pub activities: Vec, - pub start_height: ChainEpoch, -} - #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - BlockMined = frc42_dispatch::method_hash!("BlockMined"), - GetActivities = frc42_dispatch::method_hash!("GetActivities"), - PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), + RecordBlockCommitted = frc42_dispatch::method_hash!("RecordBlockCommitted"), + CommitActivity = frc42_dispatch::method_hash!("CommitActivity"), + PendingActivity = frc42_dispatch::method_hash!("PendingActivity"), +} + +trait ActivityTracker { + /// Hook for the consensus layer to report that the validator committed a new block. + fn record_block_committed(rt: &impl Runtime, validator: Address) -> Result<(), ActorError>; + + /// Commits the pending activity into an activity rollup. + /// Currently, this constructs an activity rollup from the internal state, and then resets the internal state. + /// In the future, this might actually write the activity rollup to the gateway directly, instead of relying on the client to move it around. + /// Returns the activity rollup as a Solidity ABI-encoded type, in raw byte form. + fn commit_activity(rt: &impl Runtime) -> Result; + + /// Queries the activity that has been accumulated since the last commit, and is pending a flush. + fn pending_activity(rt: &impl Runtime) -> Result; } impl ActivityTrackerActor { pub fn constructor(rt: &impl Runtime) -> Result<(), ActorError> { let st = State::new(rt.store())?; rt.create(&st)?; - Ok(()) } +} - pub fn block_mined(rt: &impl Runtime, block: BlockedMinedParams) -> Result<(), ActorError> { +impl ActivityTracker for ActivityTrackerActor { + fn record_block_committed(rt: &impl Runtime, validator: Address) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + // Reject non-f410 addresses. + if !matches!(validator.payload(), Payload::Delegated(d) if d.namespace() == EAM_ACTOR_ID && d.subaddress().len() == 20) + { + return Err( + actor_error!(illegal_argument; "validator address must be a valid f410 address"), + ); + } + rt.transaction(|st: &mut State, rt| { - st.incr_validator_block_committed(rt, &block.validator) - })?; + let mut consensus = + ConsensusData::load(rt.store(), &st.consensus, DEFAULT_HAMT_CONFIG, "consensus")?; - Ok(()) + let mut v = consensus.get(&validator)?.cloned().unwrap_or_default(); + v.blocks_committed += 1; + consensus.set(&validator, v)?; + + st.consensus = consensus.flush()?; + + Ok(()) + }) } - pub fn purge_activities(rt: &impl Runtime) -> Result<(), ActorError> { + fn commit_activity(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + // Obtain the pending rollup from state. + let rollup = rt.state::()?.pending_activity_rollup(rt)?; + rt.transaction(|st: &mut State, rt| { - st.purge_validator_block_committed(rt)?; - st.reset_start_height(rt) + st.consensus = ConsensusData::flush_empty(rt.store(), DEFAULT_HAMT_CONFIG)?; + st.tracking_since = rt.curr_epoch(); + Ok(()) })?; - Ok(()) + Ok(rollup) } - pub fn get_activities(rt: &impl Runtime) -> Result { + fn pending_activity(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; - let state: State = rt.state()?; - let activities = state.validator_activities(rt)?; - Ok(GetActivitiesResult { - activities, - start_height: state.start_height, - }) + rt.state::()?.pending_activity_rollup(rt) } } @@ -94,8 +110,8 @@ impl ActorCode for ActivityTrackerActor { actor_dispatch! { Constructor => constructor, - BlockMined => block_mined, - GetActivities => get_activities, - PurgeActivities => purge_activities, + RecordBlockCommitted => record_block_committed, + CommitActivity => commit_activity, + PendingActivity => pending_activity, } } diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs index 7377b78b3..b7ae7b88b 100644 --- a/fendermint/actors/activity-tracker/src/state.rs +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use crate::types::{FullActivityRollup, ValidatorStats}; use cid::Cid; use fil_actors_runtime::runtime::Runtime; use fil_actors_runtime::{ActorError, Map2, DEFAULT_HAMT_CONFIG}; @@ -9,100 +10,45 @@ use fvm_shared::address::Address; use fvm_shared::clock::ChainEpoch; use serde::{Deserialize, Serialize}; -pub type BlockCommittedMap = Map2; -pub type BlockCommitted = u64; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ValidatorSummary { - pub validator: Address, - pub block_committed: BlockCommitted, - pub metadata: Vec, -} - #[derive(Deserialize, Serialize, Debug, Clone)] pub struct State { - pub start_height: ChainEpoch, - pub blocks_committed: Cid, // BlockCommittedMap + pub tracking_since: ChainEpoch, + pub consensus: Cid, // ConsensusData } +pub type ConsensusData = Map2; + impl State { pub fn new(store: &BS) -> Result { - let mut deployers_map = BlockCommittedMap::empty(store, DEFAULT_HAMT_CONFIG, "empty"); - Ok(State { - start_height: 0, - blocks_committed: deployers_map.flush()?, - }) - } - - pub fn reset_start_height(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { - self.start_height = rt.curr_epoch(); - Ok(()) - } - - pub fn purge_validator_block_committed(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { - let all_validators = self.validator_activities(rt)?; - let mut validators = BlockCommittedMap::load( - rt.store(), - &self.blocks_committed, - DEFAULT_HAMT_CONFIG, - "verifiers", - )?; - - for v in all_validators { - validators.delete(&v.validator)?; - } - - self.blocks_committed = validators.flush()?; - - Ok(()) - } - - pub fn incr_validator_block_committed( - &mut self, - rt: &impl Runtime, - validator: &Address, - ) -> Result<(), ActorError> { - let mut validators = BlockCommittedMap::load( - rt.store(), - &self.blocks_committed, - DEFAULT_HAMT_CONFIG, - "verifiers", - )?; - - let v = if let Some(v) = validators.get(validator)? { - *v + 1 - } else { - 1 + let state = State { + tracking_since: 0, + consensus: ConsensusData::flush_empty(store, DEFAULT_HAMT_CONFIG)?, }; - - validators.set(validator, v)?; - - self.blocks_committed = validators.flush()?; - - Ok(()) + Ok(state) } - pub fn validator_activities( + /// Returns the pending activity rollup. + pub fn pending_activity_rollup( &self, rt: &impl Runtime, - ) -> Result, ActorError> { - let mut result = vec![]; - - let validators = BlockCommittedMap::load( - rt.store(), - &self.blocks_committed, - DEFAULT_HAMT_CONFIG, - "verifiers", - )?; - validators.for_each(|k, v| { - result.push(ValidatorSummary { - validator: k, - block_committed: *v, - metadata: vec![], - }); + ) -> Result { + let consensus = { + let cid = &rt.state::()?.consensus; + ConsensusData::load(rt.store(), cid, DEFAULT_HAMT_CONFIG, "consensus") + }?; + + // Populate the rollup struct. + let mut rollup = FullActivityRollup::default(); + consensus.for_each(|validator_addr, validator_stats| { + rollup.consensus.stats.total_active_validators += 1; + rollup.consensus.stats.total_num_blocks_committed += validator_stats.blocks_committed; + rollup + .consensus + .data + .insert(validator_addr, validator_stats.clone()); Ok(()) })?; - Ok(result) + Ok(rollup) } } diff --git a/fendermint/actors/activity-tracker/src/types.rs b/fendermint/actors/activity-tracker/src/types.rs new file mode 100644 index 000000000..3ad297d4f --- /dev/null +++ b/fendermint/actors/activity-tracker/src/types.rs @@ -0,0 +1,28 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; +use fvm_shared::address::Address; +use std::collections::HashMap; + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct AggregatedStats { + pub total_active_validators: u64, + pub total_num_blocks_committed: u64, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct FullConsensusSummary { + pub stats: AggregatedStats, + pub data: HashMap, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct FullActivityRollup { + pub consensus: FullConsensusSummary, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct ValidatorStats { + pub blocks_committed: u64, +} diff --git a/fendermint/eth/api/Cargo.toml b/fendermint/eth/api/Cargo.toml index c1b675bb7..997bb4496 100644 --- a/fendermint/eth/api/Cargo.toml +++ b/fendermint/eth/api/Cargo.toml @@ -32,7 +32,7 @@ tokio = { workspace = true } tower-http = { workspace = true } fil_actors_evm_shared = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_encoding = { workspace = true } fendermint_crypto = { path = "../../crypto" } diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index 7e0af9213..01ba886f8 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -1016,6 +1016,7 @@ where // Filter by address. if !addrs.is_empty() && addrs.intersection(&emitters).next().is_none() { + height = height.increment(); continue; } diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 405b8c22f..60f82f8a7 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -13,7 +13,7 @@ anyhow = { workspace = true } cid = { workspace = true } ethers = { workspace = true } fvm = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_blockstore = { workspace = true } hex = { workspace = true } rand = { workspace = true } diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index be5248fb5..572df6af7 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -118,6 +118,11 @@ lazy_static! { name: "OwnershipFacet", abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(), }, + EthFacet { + name: "ValidatorRewardFacet", + abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), + }, + // ========== IF YOU WANT TO ADD FACET FOR SUBNET, APPEND HERE ========== // The registry has its own facets: // https://github.com/consensus-shipyard/ipc-solidity-actors/blob/b01a2dffe367745f55111a65536a3f6fea9165f5/scripts/deploy-registry.template.ts#L58-L67 EthFacet { @@ -129,10 +134,6 @@ lazy_static! { name: "SubnetGetterFacet", abi: ia::subnet_getter_facet::SUBNETGETTERFACET_ABI.to_owned(), }, - EthFacet { - name: "ValidatorRewardFacet", - abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), - }, ], }, ), @@ -546,7 +547,7 @@ pub mod subnet { let param_type = BottomUpCheckpoint::param_type(); // Captured value of `abi.encode` in Solidity. - let expected_abi: Bytes = "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000156b736f342ab34d9afe4234a92bdb190c35b2e8d822d9601b00b9d7089b190f0100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000abc8e314f58b4de5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007b11cf9ca8ccee13bb3d003c97af5c18434067a90000000000000000000000003d9019b8bf3bfd5e979ddc3b2761be54af867c470000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(); + let expected_abi: Bytes = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000156b736f342ab34d9afe4234a92bdb190c35b2e8d822d9601b00b9d7089b190f01000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000abc8e314f58b4de5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007b11cf9ca8ccee13bb3d003c97af5c18434067a90000000000000000000000003d9019b8bf3bfd5e979ddc3b2761be54af867c470000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(); // XXX: It doesn't work with `decode_whole`. let expected_tokens = diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 39c6f2e34..a82fae6e1 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -83,6 +83,7 @@ fendermint_testing = { path = "../../testing", features = ["golden"] } fvm = { workspace = true, features = ["arb", "testing"] } fendermint_vm_genesis = { path = "../genesis", features = ["arb"] } multihash = { workspace = true } +hex = { workspace = true } [features] default = [] diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs deleted file mode 100644 index 87ddad1cb..000000000 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::activities::{ActivityDetails, BlockMined, ValidatorActivityTracker}; -use crate::fvm::state::FvmExecState; -use crate::fvm::FvmMessage; -use anyhow::Context; -use fendermint_actor_activity_tracker::{GetActivitiesResult, ValidatorSummary}; -use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; -use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::system; -use fvm::executor::ApplyRet; -use fvm_ipld_blockstore::Blockstore; -use fvm_shared::clock::ChainEpoch; - -pub struct ActorActivityTracker<'a, DB: Blockstore + Clone + 'static> { - pub(crate) executor: &'a mut FvmExecState, - pub(crate) epoch: ChainEpoch, -} - -impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker - for ActorActivityTracker<'a, DB> -{ - type ValidatorSummaryDetail = ValidatorSummary; - - fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { - let params = fendermint_actor_activity_tracker::BlockedMinedParams { - validator: fvm_shared::address::Address::from(EthAddress::from(block.validator)), - }; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: ACTIVITY_TRACKER_ACTOR_ADDR, - sequence: self.epoch as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_activity_tracker::Method::BlockMined as u64, - params: fvm_ipld_encoding::RawBytes::serialize(params)?, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - self.apply_implicit_message(msg)?; - Ok(()) - } - - fn get_activities_summary( - &self, - ) -> anyhow::Result> { - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: ACTIVITY_TRACKER_ACTOR_ADDR, - sequence: self.epoch as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, - params: fvm_ipld_encoding::RawBytes::default(), - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let apply_ret = self.executor.call_state()?.call(msg)?; - let r = fvm_ipld_encoding::from_slice::( - &apply_ret.msg_receipt.return_data, - ) - .context("failed to parse validator activities")?; - Ok(ActivityDetails { - details: r.activities, - }) - } - - fn purge_activities(&mut self) -> anyhow::Result<()> { - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: ACTIVITY_TRACKER_ACTOR_ADDR, - sequence: self.epoch as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_activity_tracker::Method::PurgeActivities as u64, - params: fvm_ipld_encoding::RawBytes::default(), - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - self.apply_implicit_message(msg)?; - Ok(()) - } -} - -impl<'a, DB: Blockstore + Clone + 'static> ActorActivityTracker<'a, DB> { - fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { - let (apply_ret, _) = self.executor.execute_implicit(msg)?; - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to apply activity tracker messages: {}", err) - } else { - Ok(apply_ret) - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs deleted file mode 100644 index 1c509c228..000000000 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -//! Tracks the current blockchain block mining activities and propagates to the parent subnet if -//! needed. - -pub mod actor; -mod merkle; - -use crate::fvm::activities::merkle::MerkleProofGen; -use fendermint_actor_activity_tracker::ValidatorSummary; -use fendermint_crypto::PublicKey; -use ipc_api::checkpoint::ActivitySummary; -use std::fmt::Debug; - -pub struct BlockMined { - pub(crate) validator: PublicKey, -} - -#[derive(Debug, Clone)] -pub struct ActivityDetails { - pub details: Vec, -} - -/// Tracks the validator activities in the current blockchain -pub trait ValidatorActivityTracker { - type ValidatorSummaryDetail: Clone + Debug; - - /// Mark the validator has mined the target block. - fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()>; - - /// Get the validators activities summary since the checkpoint height - fn get_activities_summary( - &self, - ) -> anyhow::Result>; - - /// Purge the current validator activities summary - fn purge_activities(&mut self) -> anyhow::Result<()>; -} - -impl ActivityDetails { - pub fn commitment(&self) -> anyhow::Result { - let gen = MerkleProofGen::new(self.details.as_slice())?; - Ok(ActivitySummary { - total_active_validators: self.details.len() as u64, - commitment: gen.root().to_fixed_bytes().to_vec(), - }) - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activity/actor.rs b/fendermint/vm/interpreter/src/fvm/activity/actor.rs new file mode 100644 index 000000000..9948d0c53 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/activity/actor.rs @@ -0,0 +1,75 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::activity::{FullActivity, ValidatorActivityTracker}; +use crate::fvm::state::FvmExecState; +use crate::fvm::FvmMessage; +use anyhow::Context; +use fendermint_actor_activity_tracker::types::FullActivityRollup; +use fendermint_crypto::PublicKey; +use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; +use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_actor_interface::system; +use fvm::executor::ApplyRet; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::address::Address; + +pub struct ActorActivityTracker<'a, DB: Blockstore + Clone + 'static> { + pub(crate) executor: &'a mut FvmExecState, +} + +impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker + for ActorActivityTracker<'a, DB> +{ + fn record_block_committed(&mut self, validator: PublicKey) -> anyhow::Result<()> { + let address: Address = EthAddress::from(validator).into(); + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: 0, // irrelevant + gas_limit: i64::MAX as u64, // exclude this from gas restriction + method_num: fendermint_actor_activity_tracker::Method::RecordBlockCommitted as u64, + params: fvm_ipld_encoding::RawBytes::serialize(address)?, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + self.apply_implicit_message(msg)?; + Ok(()) + } + + fn commit_activity(&mut self) -> anyhow::Result { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: 0, // irrelevant + gas_limit: i64::MAX as u64, // exclude this from gas restriction + method_num: fendermint_actor_activity_tracker::Method::CommitActivity as u64, + params: fvm_ipld_encoding::RawBytes::default(), + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let (apply_ret, _) = self.executor.execute_implicit(msg)?; + let r = + fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse validator activities")?; + r.try_into() + } +} + +impl<'a, DB: Blockstore + Clone + 'static> ActorActivityTracker<'a, DB> { + fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { + let (apply_ret, _) = self.executor.execute_implicit(msg)?; + if let Some(err) = apply_ret.failure_info { + anyhow::bail!("failed to apply activity tracker messages: {}", err) + } else { + Ok(apply_ret) + } + } +} diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activity/merkle.rs similarity index 55% rename from fendermint/vm/interpreter/src/fvm/activities/merkle.rs rename to fendermint/vm/interpreter/src/fvm/activity/merkle.rs index 53f2e7c3a..7a7586bcc 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activity/merkle.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::Context; -use fendermint_actor_activity_tracker::ValidatorSummary; -use ipc_api::evm::payload_to_evm_address; +use ipc_actors_abis::checkpointing_facet::ValidatorData; use ipc_observability::lazy_static; use merkle_tree_rs::format::Raw; use merkle_tree_rs::standard::StandardMerkleTree; @@ -12,7 +11,7 @@ pub type Hash = ethers::types::H256; lazy_static!( /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned()]; ); /// The merkle tree based proof verification to interact with solidity contracts @@ -21,25 +20,16 @@ pub(crate) struct MerkleProofGen { } impl MerkleProofGen { + pub fn pack_validator(v: &ValidatorData) -> Vec { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + } + pub fn root(&self) -> Hash { self.tree.root() } -} -impl MerkleProofGen { - pub fn new(values: &[ValidatorSummary]) -> anyhow::Result { - let values = values - .iter() - .map(|t| { - payload_to_evm_address(t.validator.payload()).map(|addr| { - vec![ - format!("{addr:?}"), - t.block_committed.to_string(), - hex::encode(&t.metadata), - ] - }) - }) - .collect::>>()?; + pub fn new(values: &[ValidatorData]) -> anyhow::Result { + let values = values.iter().map(Self::pack_validator).collect::>(); let tree = StandardMerkleTree::of(&values, &VALIDATOR_SUMMARY_FIELDS) .context("failed to construct Merkle tree")?; diff --git a/fendermint/vm/interpreter/src/fvm/activity/mod.rs b/fendermint/vm/interpreter/src/fvm/activity/mod.rs new file mode 100644 index 000000000..f44173aaf --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/activity/mod.rs @@ -0,0 +1,156 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Tracks the current blockchain block mining activities and propagates to the parent subnet if +//! needed. + +pub mod actor; +mod merkle; + +use crate::fvm::activity::merkle::MerkleProofGen; +use fendermint_crypto::PublicKey; +use ipc_actors_abis::checkpointing_facet::{ + AggregatedStats, CompressedActivityRollup, CompressedSummary, FullActivityRollup, FullSummary, + ValidatorData, +}; +use ipc_api::evm::payload_to_evm_address; + +/// Wrapper for FullActivityRollup with some utility functions +pub struct FullActivity(FullActivityRollup); + +/// Tracks the validator activities in the current blockchain +pub trait ValidatorActivityTracker { + /// Mark the validator has mined the target block. + fn record_block_committed(&mut self, validator: PublicKey) -> anyhow::Result<()>; + + /// Get the validators activities summary since the checkpoint height + fn commit_activity(&mut self) -> anyhow::Result; +} + +impl TryFrom for FullActivity { + type Error = anyhow::Error; + + fn try_from( + value: fendermint_actor_activity_tracker::types::FullActivityRollup, + ) -> Result { + let f = FullActivityRollup { + consensus: FullSummary { + stats: AggregatedStats { + total_active_validators: value.consensus.stats.total_active_validators, + total_num_blocks_committed: value.consensus.stats.total_num_blocks_committed, + }, + data: value + .consensus + .data + .into_iter() + .map(|(addr, data)| { + Ok(ValidatorData { + validator: payload_to_evm_address(addr.payload())?, + blocks_committed: data.blocks_committed, + }) + }) + .collect::>>()?, + }, + }; + Ok(Self::new(f)) + } +} + +impl FullActivity { + pub fn new(mut full: FullActivityRollup) -> Self { + full.consensus.data.sort_by(|a, b| { + let cmp = a.validator.cmp(&b.validator); + if cmp.is_eq() { + // Address will be unique, do this just in case equal + a.blocks_committed.cmp(&b.blocks_committed) + } else { + cmp + } + }); + Self(full) + } + + pub fn compressed(&self) -> anyhow::Result { + let gen = MerkleProofGen::new(self.0.consensus.data.as_slice())?; + Ok(CompressedActivityRollup { + consensus: CompressedSummary { + stats: self.0.consensus.stats.clone(), + data_root_commitment: gen.root().to_fixed_bytes(), + }, + }) + } + + pub fn into_inner(self) -> FullActivityRollup { + self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::fvm::activity::FullActivity; + use ipc_actors_abis::checkpointing_facet::{ + AggregatedStats, FullActivityRollup, FullSummary, ValidatorData, + }; + use rand::prelude::SliceRandom; + use rand::thread_rng; + use std::str::FromStr; + + #[test] + fn test_commitment() { + let mut v = vec![ + ValidatorData { + validator: ethers::types::Address::from_str( + "0xB29C00299756135ec5d6A140CA54Ec77790a99d6", + ) + .unwrap(), + blocks_committed: 1, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x28345a43c2fBae4412f0AbadFa06Bd8BA3f58867", + ) + .unwrap(), + blocks_committed: 2, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x1A79385eAd0e873FE0C441C034636D3Edf7014cC", + ) + .unwrap(), + blocks_committed: 10, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x76B9d5a35C46B1fFEb37aadf929f1CA63a26A829", + ) + .unwrap(), + blocks_committed: 4, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x3c5cc76b07cb02a372e647887bD6780513659527", + ) + .unwrap(), + blocks_committed: 3, + }, + ]; + + for _ in 0..10 { + v.shuffle(&mut thread_rng()); + let full = FullActivityRollup { + consensus: FullSummary { + stats: AggregatedStats { + total_active_validators: 1, + total_num_blocks_committed: 2, + }, + data: v.clone(), + }, + }; + let details = FullActivity::new(full); + assert_eq!( + hex::encode(details.compressed().unwrap().consensus.data_root_commitment), + "5519955f33109df3338490473cb14458640efdccd4df05998c4c439738280ab0" + ); + } + } +} diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 4cb835c01..5aca2db83 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -1,42 +1,35 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::collections::HashMap; -use std::time::Duration; - +use super::observe::{ + CheckpointCreated, CheckpointFinalized, CheckpointSigned, CheckpointSignedRole, +}; +use super::state::ipc::tokens_to_burn; +use super::{ + broadcast::Broadcaster, + state::{ipc::GatewayCaller, FvmExecState}, + ValidatorContext, +}; +use crate::fvm::activity::ValidatorActivityTracker; +use crate::fvm::exec::BlockEndEvents; use anyhow::{anyhow, Context}; use ethers::abi::Tokenizable; -use tendermint::block::Height; -use tendermint_rpc::endpoint::commit; -use tendermint_rpc::{endpoint::validators, Client, Paging}; - -use fvm_ipld_blockstore::Blockstore; -use fvm_shared::{address::Address, chainid::ChainID}; - use fendermint_crypto::PublicKey; use fendermint_crypto::SecretKey; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::BottomUpCheckpoint; use fendermint_vm_genesis::{Power, Validator, ValidatorKey}; - -use ipc_api::evm::payload_to_evm_address; - +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::{address::Address, chainid::ChainID}; use ipc_actors_abis::checkpointing_facet as checkpoint; use ipc_actors_abis::gateway_getter_facet as getter; use ipc_api::staking::ConfigurationNumber; use ipc_observability::{emit, serde::HexEncodableBlockHash}; - -use super::observe::{ - CheckpointCreated, CheckpointFinalized, CheckpointSigned, CheckpointSignedRole, -}; -use super::state::ipc::tokens_to_burn; -use super::{ - broadcast::Broadcaster, - state::{ipc::GatewayCaller, FvmExecState}, - ValidatorContext, -}; -use crate::fvm::activities::ValidatorActivityTracker; -use crate::fvm::exec::BlockEndEvents; +use std::collections::HashMap; +use std::time::Duration; +use tendermint::block::Height; +use tendermint_rpc::endpoint::commit; +use tendermint_rpc::{endpoint::validators, Client, Paging}; /// Validator voting power snapshot. #[derive(Debug, Clone, PartialEq, Eq)] @@ -101,7 +94,7 @@ where let num_msgs = msgs.len(); - let activities = state.activities_tracker().get_activities_summary()?; + let full_activity_rollup = state.activities_tracker().commit_activity()?; // Construct checkpoint. let checkpoint = BottomUpCheckpoint { @@ -110,31 +103,21 @@ where block_hash, next_configuration_number, msgs, - activities: activities.commitment()?.try_into()?, + activities: full_activity_rollup.compressed()?, }; // Save the checkpoint in the ledger. // Pass in the current power table, because these are the validators who can sign this checkpoint. - let report = checkpoint::ActivityReport { - validators: activities - .details - .into_iter() - .map(|v| { - Ok(checkpoint::ValidatorActivityReport { - validator: payload_to_evm_address(v.validator.payload())?, - blocks_committed: v.block_committed, - metadata: ethers::types::Bytes::from(v.metadata), - }) - }) - .collect::>>()?, - }; let ret = gateway - .create_bu_ckpt_with_activities(state, checkpoint.clone(), &curr_power_table.0, report) + .create_bottom_up_checkpoint( + state, + checkpoint.clone(), + &curr_power_table.0, + full_activity_rollup.into_inner(), + ) .context("failed to store checkpoint")?; event_tracker.push((ret.apply_ret.events, ret.emitters)); - state.activities_tracker().purge_activities()?; - // Figure out the power updates if there was some change in the configuration. let power_updates = if next_configuration_number == 0 { PowerUpdates(Vec::new()) @@ -266,9 +249,22 @@ where block_hash: cp.block_hash, next_configuration_number: cp.next_configuration_number, msgs: convert_tokenizables(cp.msgs)?, - activities: checkpoint::ActivitySummary { - total_active_validators: cp.activities.total_active_validators, - commitment: cp.activities.commitment, + activities: checkpoint::CompressedActivityRollup { + consensus: checkpoint::CompressedSummary { + stats: checkpoint::AggregatedStats { + total_active_validators: cp + .activities + .consensus + .stats + .total_active_validators, + total_num_blocks_committed: cp + .activities + .consensus + .stats + .total_num_blocks_committed, + }, + data_root_commitment: cp.activities.consensus.data_root_commitment, + }, }, }; diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 00814b4fa..3006218a5 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -7,7 +7,7 @@ use super::{ state::FvmExecState, BlockGasLimit, FvmMessage, FvmMessageInterpreter, }; -use crate::fvm::activities::{BlockMined, ValidatorActivityTracker}; +use crate::fvm::activity::ValidatorActivityTracker; use crate::ExecInterpreter; use anyhow::Context; use async_trait::async_trait; @@ -204,9 +204,7 @@ where let mut block_end_events = BlockEndEvents::default(); if let Some(pubkey) = state.block_producer() { - state - .activities_tracker() - .track_block_mined(BlockMined { validator: pubkey })?; + state.activities_tracker().record_block_committed(pubkey)?; } let next_gas_market = state.finalize_gas_market()?; diff --git a/fendermint/vm/interpreter/src/fvm/externs.rs b/fendermint/vm/interpreter/src/fvm/externs.rs index 7f02b0611..f17e03f68 100644 --- a/fendermint/vm/interpreter/src/fvm/externs.rs +++ b/fendermint/vm/interpreter/src/fvm/externs.rs @@ -36,18 +36,6 @@ where } } -impl FendermintExterns -where - DB: Blockstore + 'static + Clone, -{ - pub fn read_only_clone(&self) -> FendermintExterns> { - FendermintExterns { - blockstore: ReadOnlyBlockstore::new(self.blockstore.clone()), - state_root: self.state_root, - } - } -} - impl Rand for FendermintExterns where DB: Blockstore + 'static, diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 2a4ce6432..85962ab4b 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -15,7 +15,7 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; -pub mod activities; +pub mod activity; pub(crate) mod gas; pub(crate) mod topdown; diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index f26f634bf..65b0a9438 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -1,13 +1,11 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use crate::fvm::activities::actor::ActorActivityTracker; +use crate::fvm::activity::actor::ActorActivityTracker; use crate::fvm::externs::FendermintExterns; use crate::fvm::gas::BlockGasTracker; -use crate::fvm::store::ReadOnlyBlockstore; use anyhow::Ok; use cid::Cid; use fendermint_actors_api::gas_market::Reading; @@ -16,7 +14,6 @@ use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_core::{chainid::HasChainID, Timestamp}; use fendermint_vm_encoding::IsHumanReadable; use fendermint_vm_genesis::PowerScale; -use fvm::engine::EnginePool; use fvm::{ call_manager::DefaultCallManager, engine::MultiEngine, @@ -118,8 +115,6 @@ where params: FvmUpdatableParams, /// Indicate whether the parameters have been updated. params_dirty: bool, - - executor_info: ExecutorInfo, } impl FvmExecState @@ -169,11 +164,6 @@ where power_scale: params.power_scale, }, params_dirty: false, - - executor_info: ExecutorInfo { - engine_pool: engine, - store: blockstore.clone(), - }, }) } @@ -303,10 +293,7 @@ where } pub fn activities_tracker(&mut self) -> ActorActivityTracker { - ActorActivityTracker { - epoch: self.block_height(), - executor: self, - } + ActorActivityTracker { executor: self } } /// Collect all the event emitters' delegated addresses, for those who have any. @@ -369,22 +356,6 @@ where f(&mut self.params); self.params_dirty = true; } - - pub fn call_state(&self) -> anyhow::Result> { - let externs = self.executor.externs().read_only_clone(); - let machine = DefaultMachine::new( - self.executor.context(), - ReadOnlyBlockstore::new(self.executor_info.store.clone()), - externs, - )?; - - Ok(FvmCallState { - executor: RefCell::new(DefaultExecutor::new( - self.executor_info.engine_pool.clone(), - machine, - )?), - }) - } } impl HasChainID for FvmExecState @@ -425,33 +396,3 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { }; (ret, Default::default()) } - -/// Tracks the metadata about the executor, so that it can be used to clone itself or create call state -struct ExecutorInfo { - engine_pool: EnginePool, - store: DB, -} - -type CallExecutor = DefaultExecutor< - DefaultKernel< - DefaultCallManager< - DefaultMachine, FendermintExterns>>, - >, - >, ->; - -/// A state we create for the calling the getters through fvm -pub struct FvmCallState -where - DB: Blockstore + Clone + 'static, -{ - executor: RefCell>, -} - -impl FvmCallState { - pub fn call(&self, msg: Message) -> anyhow::Result { - let mut inner = self.executor.borrow_mut(); - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - Ok(inner.execute_message(msg, ApplyKind::Implicit, raw_length)?) - } -} diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 7fb7143df..2eb04c752 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -123,28 +123,7 @@ impl GatewayCaller { state: &mut FvmExecState, checkpoint: checkpointing_facet::BottomUpCheckpoint, power_table: &[Validator], - ) -> anyhow::Result<()> { - // Construct a Merkle tree from the power table, which we can use to validate validator set membership - // when the signatures are submitted in transactions for accumulation. - let tree = - ValidatorMerkleTree::new(power_table).context("failed to create validator tree")?; - - let total_power = power_table.iter().fold(et::U256::zero(), |p, v| { - p.saturating_add(et::U256::from(v.power.0)) - }); - - self.checkpointing.call(state, |c| { - c.create_bottom_up_checkpoint(checkpoint, tree.root_hash().0, total_power) - }) - } - - /// Insert a new checkpoint at the period boundary. - pub fn create_bu_ckpt_with_activities( - &self, - state: &mut FvmExecState, - checkpoint: checkpointing_facet::BottomUpCheckpoint, - power_table: &[Validator], - activities: checkpointing_facet::ActivityReport, + activities: checkpointing_facet::FullActivityRollup, ) -> anyhow::Result { // Construct a Merkle tree from the power table, which we can use to validate validator set membership // when the signatures are submitted in transactions for accumulation. diff --git a/fendermint/vm/message/Cargo.toml b/fendermint/vm/message/Cargo.toml index 3816b9170..77d264fb2 100644 --- a/fendermint/vm/message/Cargo.toml +++ b/fendermint/vm/message/Cargo.toml @@ -22,7 +22,7 @@ quickcheck = { workspace = true, optional = true } rand = { workspace = true, optional = true } cid = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_encoding = { workspace = true } ipc-api = { workspace = true } diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 5edb1a7d8..e19691454 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -76,40 +76,52 @@ pub struct BottomUpMsgBatch { pub msgs: Vec, } -/// The commitments for the child subnet activities that should be submitted to the parent subnet -/// together with a bottom up checkpoint +/// Compressed representation of the activity summary that can be embedded in checkpoints to propagate up the hierarchy. #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct ValidatorSummary { - /// The checkpoint height in the child subnet - pub checkpoint_height: u64, - /// The validator address - pub validator: Address, - /// The number of blocks mined - pub blocks_committed: u64, - /// The extra metadata attached to the validator - pub metadata: Vec, +pub struct CompressedActivityRollup { + pub consensus: consensus::CompressedSummary, } -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct BatchClaimProofs { - pub subnet_id: SubnetID, - pub proofs: Vec, -} +/// Namespace for consensus-level activity summaries. +/// XYZ(raulk) move to activity module +pub mod consensus { + use fvm_shared::address::Address; + use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct ValidatorClaimProof { - pub summary: ValidatorSummary, - pub proof: Vec<[u8; 32]>, -} + /// Aggregated stats for consensus-level activity. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct AggregatedStats { + /// The total number of unique validators that have mined within this period. + pub total_active_validators: u64, + /// The total number of blocks committed by all validators during this period. + pub total_num_blocks_committed: u64, + } -#[serde_as] -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct ActivitySummary { - pub total_active_validators: u64, - /// The activity summary for validators - #[serde_as(as = "HumanReadable")] - pub commitment: Vec, - // TODO: add relayed activity commitment + /// The commitments for the child subnet activities that should be submitted to the parent subnet + /// together with a bottom up checkpoint + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct ValidatorData { + /// The validator address + pub validator: Address, + /// The number of blocks mined + pub blocks_committed: u64, + } + + // The full activity summary for consensus-level activity. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct FullSummary { + pub stats: AggregatedStats, + /// The breakdown of activity per validator. + pub data: Vec, + } + + /// The compresed representation of the activity summary for consensus-level activity suitable for embedding in a checkpoint. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct CompressedSummary { + pub stats: AggregatedStats, + /// The commitment for the validator details, so that we don't have to transmit them in full. + pub data_root_commitment: Vec, + } } #[serde_as] @@ -131,7 +143,7 @@ pub struct BottomUpCheckpoint { /// The list of messages for execution pub msgs: Vec, /// The activity commitment from child subnet to parent subnet - pub activities: ActivitySummary, + pub activity_rollup: CompressedActivityRollup, } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 7722a19d0..dd40829ea 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -4,8 +4,8 @@ //! Type conversion for IPC Agent struct with solidity contract struct use crate::address::IPCAddress; -use crate::checkpoint::{ActivitySummary, BatchClaimProofs, BottomUpCheckpoint}; -use crate::checkpoint::{BottomUpMsgBatch, ValidatorClaimProof}; +use crate::checkpoint::BottomUpMsgBatch; +use crate::checkpoint::{consensus, BottomUpCheckpoint, CompressedActivityRollup}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -122,14 +122,55 @@ macro_rules! cross_msg_types { /// The type conversion between different bottom up checkpoint definition in ethers and sdk macro_rules! bottom_up_checkpoint_conversion { ($module:ident) => { - impl TryFrom for $module::ActivitySummary { + impl TryFrom for $module::AggregatedStats { type Error = anyhow::Error; - fn try_from(c: ActivitySummary) -> Result { - Ok($module::ActivitySummary { + fn try_from(c: consensus::AggregatedStats) -> Result { + Ok($module::AggregatedStats { total_active_validators: c.total_active_validators, - commitment: c - .commitment + total_num_blocks_committed: c.total_num_blocks_committed, + }) + } + } + + impl TryFrom for $module::CompressedActivityRollup { + type Error = anyhow::Error; + + fn try_from(c: CompressedActivityRollup) -> Result { + Ok($module::CompressedActivityRollup { + consensus: c.consensus.try_into()?, + }) + } + } + + impl From<$module::CompressedActivityRollup> for CompressedActivityRollup { + fn from(value: $module::CompressedActivityRollup) -> Self { + CompressedActivityRollup { + consensus: consensus::CompressedSummary { + stats: consensus::AggregatedStats { + total_active_validators: value.consensus.stats.total_active_validators, + total_num_blocks_committed: value + .consensus + .stats + .total_num_blocks_committed, + }, + data_root_commitment: value.consensus.data_root_commitment.to_vec(), + }, + } + } + } + + impl TryFrom for $module::CompressedSummary { + type Error = anyhow::Error; + + fn try_from(c: consensus::CompressedSummary) -> Result { + Ok($module::CompressedSummary { + stats: c + .stats + .try_into() + .map_err(|_| anyhow!("cannot convert aggregated stats"))?, + data_root_commitment: c + .data_root_commitment .try_into() .map_err(|_| anyhow!("cannot convert bytes32"))?, }) @@ -150,7 +191,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map($module::IpcEnvelope::try_from) .collect::, _>>()?, - activities: checkpoint.activities.try_into()?, + activities: checkpoint.activity_rollup.try_into()?, }) } } @@ -169,10 +210,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map(IpcEnvelope::try_from) .collect::, _>>()?, - activities: ActivitySummary { - total_active_validators: value.activities.total_active_validators, - commitment: value.activities.commitment.to_vec(), - }, + activity_rollup: value.activities.into(), }) } } @@ -277,22 +315,6 @@ impl TryFrom for AssetKind { } } -impl TryFrom for validator_reward_facet::ValidatorClaimProof { - type Error = anyhow::Error; - - fn try_from(v: ValidatorClaimProof) -> Result { - Ok(Self { - proof: v.proof, - summary: validator_reward_facet::ValidatorSummary { - checkpoint_height: v.summary.checkpoint_height, - validator: payload_to_evm_address(v.summary.validator.payload())?, - blocks_committed: v.summary.blocks_committed, - metadata: ethers::types::Bytes::from(v.summary.metadata), - }, - }) - } -} - /// Convert the ipc SubnetID type to a vec of evm addresses. It extracts all the children addresses /// in the subnet id and turns them as a vec of evm addresses. pub fn subnet_id_to_evm_addresses( @@ -322,21 +344,6 @@ pub fn fil_to_eth_amount(amount: &TokenAmount) -> anyhow::Result { Ok(U256::from_dec_str(&str)?) } -impl TryFrom for validator_reward_facet::BatchClaimProofs { - type Error = anyhow::Error; - - fn try_from(v: BatchClaimProofs) -> Result { - Ok(Self { - subnet_id: validator_reward_facet::SubnetID::try_from(&v.subnet_id)?, - proofs: v - .proofs - .into_iter() - .map(validator_reward_facet::ValidatorClaimProof::try_from) - .collect::, _>>()?, - }) - } -} - impl TryFrom for top_down_finality_facet::StakingChange { type Error = anyhow::Error; diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs index 6cdb106ba..efc0522fa 100644 --- a/ipc/cli/src/commands/validator/batch_claim.rs +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -18,11 +18,8 @@ pub(crate) struct BatchClaimArgs { pub from: ChainEpoch, #[arg(long, help = "The checkpoint height to claim to")] pub to: ChainEpoch, - #[arg( - long, - help = "The source subnets that generated the reward, use ',' to separate subnets" - )] - pub reward_source_subnets: String, + #[arg(long, help = "The source subnet that generated the reward")] + pub reward_source_subnet: String, #[arg(long, help = "The subnet to claim reward from")] pub reward_claim_subnet: String, } @@ -38,18 +35,14 @@ impl CommandLineHandler for BatchClaim { let provider = get_ipc_provider(global)?; - let reward_source_subnets = arguments - .reward_source_subnets - .split(',') - .map(SubnetID::from_str) - .collect::, _>>()?; + let reward_source_subnet = SubnetID::from_str(&arguments.reward_source_subnet)?; let reward_claim_subnet = SubnetID::from_str(&arguments.reward_claim_subnet)?; let validator = Address::from_str(&arguments.validator)?; provider - .batch_claim( + .batch_subnet_claim( &reward_claim_subnet, - &reward_source_subnets, + &reward_source_subnet, arguments.from, arguments.to, &validator, diff --git a/ipc/cli/src/commands/validator/list.rs b/ipc/cli/src/commands/validator/list.rs index 7209de34d..f3b666e76 100644 --- a/ipc/cli/src/commands/validator/list.rs +++ b/ipc/cli/src/commands/validator/list.rs @@ -40,10 +40,9 @@ impl CommandLineHandler for ListActivities { .await?; println!("found total {} entries", r.len()); - for v in r { - println!("checkpoint height: {}", v.checkpoint_height); + for (checkpoint_height, v) in r { + println!(" checkpoint height: {}", checkpoint_height); println!(" addr: {}", v.validator); - println!(" metadata: {}", hex::encode(v.metadata)); println!(" locks_committed: {}", v.blocks_committed); } diff --git a/ipc/provider/Cargo.toml b/ipc/provider/Cargo.toml index 758f1306d..4a9c7bbe9 100644 --- a/ipc/provider/Cargo.toml +++ b/ipc/provider/Cargo.toml @@ -42,7 +42,7 @@ zeroize = { workspace = true } fil_actors_runtime = { workspace = true } fvm_ipld_encoding = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } merkle-tree-rs = { path = "../../ext/merkle-tree-rs" } diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index f6c7cb27c..aad51e6b4 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -9,9 +9,8 @@ use config::Config; use fvm_shared::{ address::Address, clock::ChainEpoch, crypto::signature::SignatureType, econ::TokenAmount, }; -use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpointBundle, QuorumReachedEvent, ValidatorSummary, -}; +use ipc_api::checkpoint::consensus::ValidatorData; +use ipc_api::checkpoint::{BottomUpCheckpointBundle, QuorumReachedEvent}; use ipc_api::evm::payload_to_evm_address; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; use ipc_api::subnet::{Asset, PermissionMode}; @@ -754,48 +753,34 @@ impl IpcProvider { validator: &Address, from: ChainEpoch, to: ChainEpoch, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let conn = self.get_connection(subnet)?; conn.manager() - .get_validator_activities(validator, from, to) + .query_validator_rewards(validator, from, to) .await } - pub async fn batch_claim( + pub async fn batch_subnet_claim( &self, reward_claim_subnet: &SubnetID, - reward_source_subnets: &[SubnetID], + reward_source_subnet: &SubnetID, // TODO(review): eventually support multiple source subnets from: ChainEpoch, to: ChainEpoch, validator: &Address, ) -> anyhow::Result<()> { - let mut batch_proofs = vec![]; - for source_subnet in reward_source_subnets { - let conn = self.get_connection(source_subnet)?; - - let proofs = conn - .manager() - .get_validator_claim_proofs(validator, from, to) - .await?; - if proofs.is_empty() { - return Err(anyhow!( - "address {} has no reward to claim", - validator.to_string() - )); - } + let conn = self.get_connection(reward_source_subnet)?; - batch_proofs.push(BatchClaimProofs { - subnet_id: source_subnet.clone(), - proofs, - }); - } + let claims = conn + .manager() + .query_reward_claims(validator, from, to) + .await?; let parent = reward_claim_subnet .parent() .ok_or_else(|| anyhow!("no parent found"))?; let conn = self.get_connection(&parent)?; conn.manager() - .batch_claim(validator, reward_claim_subnet, batch_proofs) + .batch_subnet_claim(validator, reward_claim_subnet, reward_source_subnet, claims) .await } } diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 503cdfbfa..43d68971a 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -36,15 +36,17 @@ use ethers::abi::Tokenizable; use ethers::contract::abigen; use ethers::prelude::k256::ecdsa::SigningKey; use ethers::prelude::{Signer, SignerMiddleware}; -use ethers::providers::{Authorization, Http, Middleware, Provider}; +use ethers::providers::{Authorization, Http, Provider}; use ethers::signers::{LocalWallet, Wallet}; -use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, I256, U256}; +use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, H256, I256, U256}; +use ethers::middleware::Middleware; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; +use ipc_actors_abis::validator_reward_facet::ValidatorClaim; use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, - ValidatorClaimProof, ValidatorSummary, + consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, + Signature, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo, ValidatorStakingInfo}; @@ -1284,124 +1286,128 @@ impl BottomUpCheckpointRelayer for EthSubnetManager { } lazy_static!( - /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + /// ABI types of the Merkle tree which contains validator addresses and their committed block count. + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned()]; ); #[async_trait] impl ValidatorRewarder for EthSubnetManager { - async fn get_validator_claim_proofs( + /// Query validator claims, indexed by checkpoint height, to batch claim rewards. + async fn query_reward_claims( &self, validator_addr: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result> { + ) -> Result> { let contract = checkpointing_facet::CheckpointingFacet::new( self.ipc_contract_info.gateway_addr, Arc::new(self.ipc_contract_info.provider.clone()), ); let ev = contract - .event::() + .event::() .from_block(from_checkpoint as u64) .to_block(to_checkpoint as u64) .address(ValueOrArray::Value(contract.address())); - let validator_evm_addr = payload_to_evm_address(validator_addr.payload())?; + let validator_eth_addr = payload_to_evm_address(validator_addr.payload())?; - let mut proofs = vec![]; + let mut claims = vec![]; for (event, meta) in query_with_meta(ev, contract.client()).await? { - tracing::debug!("found event at height: {}", meta.block_number); - - let mut activities = vec![]; - let mut maybe_validator = None; - for validator in event.report.validators { - let payload = vec![ - format!("{:?}", validator.validator), - validator.blocks_committed.to_string(), - hex::encode(validator.metadata.as_ref()), - ]; - - if validator.validator == validator_evm_addr { - let summary = ValidatorSummary { - checkpoint_height: event.checkpoint_height, - validator: *validator_addr, - blocks_committed: validator.blocks_committed, - metadata: validator.metadata.to_vec(), - }; - maybe_validator = Some((payload.clone(), summary)); - } - - activities.push(payload); - } - - let tree = StandardMerkleTree::::of(&activities, &VALIDATOR_SUMMARY_FIELDS) - .context("failed to construct Merkle tree")?; - - let Some((payload, summary)) = maybe_validator else { + tracing::debug!( + "found activity bundle published at height: {}", + meta.block_number + ); + + // Check if we have claims for this validator in this block. + let our_data = event + .rollup + .consensus + .data + .iter() + .find(|v| v.validator == validator_eth_addr); + + // If we don't, skip this block. + let Some(data) = our_data else { tracing::info!( - "target validator address has not activities in epoch {}", + "target validator address has no reward claims in epoch {}", meta.block_number ); continue; }; - let proof = tree.get_proof(LeafType::LeafBytes(payload))?; - proofs.push(ValidatorClaimProof { - summary, + let proof = gen_merkle_proof(&event.rollup.consensus.data, data)?; + + // Construct the claim and add it to the list. + let claim = ValidatorClaim { + // Even though it's the same struct but still need to do a mapping due to + // different crate from ethers-rs + data: validator_reward_facet::ValidatorData { + validator: data.validator, + blocks_committed: data.blocks_committed, + }, proof: proof.into_iter().map(|v| v.into()).collect(), - }); + }; + claims.push((event.checkpoint_height, claim)); } - Ok(proofs) + Ok(claims) } - /// Get the reward for specific validator in a subnet - async fn get_validator_activities( + /// Query validator rewards in the current subnet, without obtaining proofs. + async fn query_validator_rewards( &self, validator_addr: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result> { + ) -> Result> { let contract = checkpointing_facet::CheckpointingFacet::new( self.ipc_contract_info.gateway_addr, Arc::new(self.ipc_contract_info.provider.clone()), ); let ev = contract - .event::() + .event::() .from_block(from_checkpoint as u64) .to_block(to_checkpoint as u64) .address(ValueOrArray::Value(contract.address())); - let mut activities = vec![]; + let mut rewards = vec![]; let validator_eth_addr = payload_to_evm_address(validator_addr.payload())?; for (event, meta) in query_with_meta(ev, contract.client()).await? { - tracing::debug!("found event at height: {}", meta.block_number); - for validator in event.report.validators { - if validator.validator != validator_eth_addr { - continue; - } - - activities.push(ValidatorSummary { + tracing::debug!( + "found activity bundle published at height: {}", + meta.block_number + ); + + // Check if we have rewards for this validator in this block. + if let Some(data) = event + .rollup + .consensus + .data + .iter() + .find(|v| v.validator == validator_eth_addr) + { + // TODO type conversion. + let data = ValidatorData { validator: *validator_addr, - checkpoint_height: event.checkpoint_height, - blocks_committed: validator.blocks_committed, - metadata: validator.metadata.to_vec(), - }); + blocks_committed: data.blocks_committed, + }; + rewards.push((meta.block_number.as_u64(), data)); } } - Ok(activities) + Ok(rewards) } - /// Claim the reward in batch - async fn batch_claim( + /// Claim validator rewards in a batch for the specified subnet. + async fn batch_subnet_claim( &self, submitter: &Address, reward_claim_subnet: &SubnetID, - payloads: Vec, + reward_origin_subnet: &SubnetID, + claims: Vec<(u64, ValidatorClaim)>, ) -> Result<()> { let signer = Arc::new(self.get_signer(submitter)?); let contract = validator_reward_facet::ValidatorRewardFacet::new( @@ -1409,12 +1415,14 @@ impl ValidatorRewarder for EthSubnetManager { signer.clone(), ); - let p = payloads - .into_iter() - .map(validator_reward_facet::BatchClaimProofs::try_from) - .collect::>>()?; - let call = contract.batch_claim(p); - let call = call_with_premium_and_pending_block(signer, call).await?; + // separate the Vec of tuples claims into two Vecs of Height and Claim + let (heights, claims): (Vec, Vec) = claims.into_iter().unzip(); + + let call = { + let call = + contract.batch_subnet_claim(reward_origin_subnet.try_into()?, heights, claims); + call_with_premium_and_pending_block(signer, call).await? + }; call.send().await?; @@ -1422,6 +1430,63 @@ impl ValidatorRewarder for EthSubnetManager { } } +fn gen_merkle_proof( + validator_data: &[checkpointing_facet::ValidatorData], + validator: &checkpointing_facet::ValidatorData, +) -> anyhow::Result> { + // Utilty function to pack validator data into a vector of strings for proof generation. + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let tree = gen_merkle_tree(validator_data, pack_validator_data)?; + + let leaf = pack_validator_data(validator); + tree.get_proof(LeafType::LeafBytes(leaf)) +} + +fn gen_merkle_tree Vec>( + validator_data: &[checkpointing_facet::ValidatorData], + pack_validator_data: F, +) -> anyhow::Result> { + let leaves = order_validator_data(validator_data)? + .iter() + .map(pack_validator_data) + .collect::>(); + StandardMerkleTree::::of(&leaves, &VALIDATOR_SUMMARY_FIELDS) + .context("failed to construct Merkle tree") +} + +fn order_validator_data( + validator_data: &[checkpointing_facet::ValidatorData], +) -> anyhow::Result> { + let mut mapped = validator_data + .iter() + .map(|a| ethers_address_to_fil_address(&a.validator).map(|v| (v, a.blocks_committed))) + .collect::, _>>()?; + + mapped.sort_by(|a, b| { + let cmp = a.0.cmp(&b.0); + if cmp.is_eq() { + // Address will be unique, do this just in case equal + a.1.cmp(&b.1) + } else { + cmp + } + }); + + let back_to_eth = |(fvm_addr, blocks): (Address, u64)| { + payload_to_evm_address(fvm_addr.payload()).map(|v| checkpointing_facet::ValidatorData { + validator: v, + blocks_committed: blocks, + }) + }; + mapped + .into_iter() + .map(back_to_eth) + .collect::, _>>() +} + /// Takes a `FunctionCall` input and returns a new instance with an estimated optimal `gas_premium`. /// The function also uses the pending block number to help retrieve the latest nonce /// via `get_transaction_count` with the `pending` parameter. @@ -1648,8 +1713,11 @@ impl TryFrom for SubnetInfo { #[cfg(test)] mod tests { - use crate::manager::evm::manager::contract_address_from_subnet; + use crate::manager::evm::manager::{contract_address_from_subnet, gen_merkle_tree}; + use ethers::core::rand::prelude::SliceRandom; + use ethers::core::rand::{random, thread_rng}; use fvm_shared::address::Address; + use ipc_actors_abis::checkpointing_facet::{checkpointing_facet, ValidatorData}; use ipc_api::subnet_id::SubnetID; use std::str::FromStr; @@ -1664,4 +1732,91 @@ mod tests { "0x2e714a3c385ea88a09998ed74db265dae9853667" ); } + + /// test case that makes sure the commitment created for various addresses and blocks committed + /// are consistent + #[test] + fn test_validator_rewarder_claim_commitment() { + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let mut random_validator_data = vec![ + ValidatorData { + validator: ethers::types::Address::from_str( + "0xB29C00299756135ec5d6A140CA54Ec77790a99d6", + ) + .unwrap(), + blocks_committed: 1, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x1A79385eAd0e873FE0C441C034636D3Edf7014cC", + ) + .unwrap(), + blocks_committed: 10, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x28345a43c2fBae4412f0AbadFa06Bd8BA3f58867", + ) + .unwrap(), + blocks_committed: 2, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x3c5cc76b07cb02a372e647887bD6780513659527", + ) + .unwrap(), + blocks_committed: 3, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x76B9d5a35C46B1fFEb37aadf929f1CA63a26A829", + ) + .unwrap(), + blocks_committed: 4, + }, + ]; + random_validator_data.shuffle(&mut thread_rng()); + + let root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!( + hex::encode(root.0), + "5519955f33109df3338490473cb14458640efdccd4df05998c4c439738280ab0" + ); + } + + #[test] + fn test_validator_rewarder_claim_commitment_ii() { + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let mut random_validator_data = (0..100) + .map(|_| ValidatorData { + validator: ethers::types::Address::random(), + blocks_committed: random::(), + }) + .collect::>(); + + random_validator_data.shuffle(&mut thread_rng()); + let root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + + random_validator_data.shuffle(&mut thread_rng()); + let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!(new_root, root); + + random_validator_data.shuffle(&mut thread_rng()); + let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!(new_root, root); + } } diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index 62cbf983b..a52095949 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -7,9 +7,10 @@ use anyhow::Result; use async_trait::async_trait; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; +use ipc_actors_abis::validator_reward_facet::ValidatorClaim; use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, - ValidatorClaimProof, ValidatorSummary, + consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, + Signature, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; @@ -287,25 +288,28 @@ pub trait BottomUpCheckpointRelayer: Send + Sync { /// in the child subnet #[async_trait] pub trait ValidatorRewarder: Send + Sync { - /// Obtain the proofs needed for the validator to batch claim the rewards - async fn get_validator_claim_proofs( + /// Query validator claims, indexed by checkpoint height, to batch claim rewards. + async fn query_reward_claims( &self, validator_addr: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result>; - /// Get the reward for specific validator in the current subnet gateway - async fn get_validator_activities( + ) -> Result>; + + /// Query validator rewards in the current subnet, without obtaining proofs. + async fn query_validator_rewards( &self, validator: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result>; - /// Claim the reward in batches - async fn batch_claim( + ) -> Result>; + + /// Claim validator rewards in a batch for the specified subnet. + async fn batch_subnet_claim( &self, submitter: &Address, reward_claim_subnet: &SubnetID, - payloads: Vec, + reward_origin_subnet: &SubnetID, + claims: Vec<(u64, ValidatorClaim)>, ) -> Result<()>; } diff --git a/ipc/wallet/Cargo.toml b/ipc/wallet/Cargo.toml index 11cd5d946..02c02f692 100644 --- a/ipc/wallet/Cargo.toml +++ b/ipc/wallet/Cargo.toml @@ -15,7 +15,7 @@ base64 = { workspace = true } blake2b_simd = { workspace = true } bls-signatures = { version = "0.13.0", default-features = false, features = ["blst"] } ethers = { workspace = true, optional = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } hex = { workspace = true } libc = "0.2" libsecp256k1 = { workspace = true } From ff3baf734b85f72475a1490f3e55e062093b9188 Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Mon, 25 Nov 2024 19:42:14 +0800 Subject: [PATCH 100/111] remove stale code --- contracts/.env.real | 2 - fendermint/actors/gas_market/Cargo.toml | 34 --- fendermint/actors/gas_market/src/lib.rs | 371 ------------------------ 3 files changed, 407 deletions(-) delete mode 100644 contracts/.env.real delete mode 100644 fendermint/actors/gas_market/Cargo.toml delete mode 100644 fendermint/actors/gas_market/src/lib.rs diff --git a/contracts/.env.real b/contracts/.env.real deleted file mode 100644 index a740dbd47..000000000 --- a/contracts/.env.real +++ /dev/null @@ -1,2 +0,0 @@ -export PRIVATE_KEY=0xf6360e82b9ba04e381a8c9165338e1b308af4ce9fa88cbe6361dd328994f77f2 -export RPC_URL=https://api.calibration.node.glif.io/rpc/v1 \ No newline at end of file diff --git a/fendermint/actors/gas_market/Cargo.toml b/fendermint/actors/gas_market/Cargo.toml deleted file mode 100644 index a2ff9f0eb..000000000 --- a/fendermint/actors/gas_market/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "fendermint_actor_gas_market" -description = "Builtin transaction gas tracking actor for IPC" -license.workspace = true -edition.workspace = true -authors.workspace = true -version = "0.1.0" - -[lib] -## lib is necessary for integration tests -## cdylib is necessary for Wasm build -crate-type = ["cdylib", "lib"] - -[dependencies] -anyhow = { workspace = true } -cid = { workspace = true } -fil_actors_runtime = { workspace = true } -fvm_ipld_blockstore = { workspace = true } -fvm_ipld_encoding = { workspace = true } -fvm_shared = { workspace = true } -log = { workspace = true } -multihash = { workspace = true } -num-derive = { workspace = true } -num-traits = { workspace = true } -serde = { workspace = true } -hex-literal = { workspace = true } -frc42_dispatch = { workspace = true } - -[dev-dependencies] -fil_actors_evm_shared = { workspace = true } -fil_actors_runtime = { workspace = true, features = ["test_utils"] } - -[features] -fil-actor = ["fil_actors_runtime/fil-actor"] diff --git a/fendermint/actors/gas_market/src/lib.rs b/fendermint/actors/gas_market/src/lib.rs deleted file mode 100644 index d9b316d58..000000000 --- a/fendermint/actors/gas_market/src/lib.rs +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2021-2023 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use fil_actors_runtime::actor_error; -use fil_actors_runtime::runtime::{ActorCode, Runtime}; -use fil_actors_runtime::SYSTEM_ACTOR_ADDR; -use fil_actors_runtime::{actor_dispatch, ActorError}; -use fvm_ipld_encoding::tuple::*; -use fvm_shared::econ::TokenAmount; -use fvm_shared::METHOD_CONSTRUCTOR; -use num_derive::FromPrimitive; -use std::cmp::Ordering; - -#[cfg(feature = "fil-actor")] -fil_actors_runtime::wasm_trampoline!(EIP1559GasMarketActor); - -pub const IPC_GAS_MARKET_ACTOR_NAME: &str = "gas_market"; -pub type Gas = u64; -pub type SetConstants = EIP1559Constants; - -/// Constant params used by EIP1559 -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct EIP1559Constants { - pub block_gas_limit: Gas, - /// The minimal base fee when gas utilization is low - pub minimal_base_fee: TokenAmount, - /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - pub elasticity_multiplier: u64, - /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - pub base_fee_max_change_denominator: u64, -} - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct EIP1559GasState { - base_fee: TokenAmount, - constants: EIP1559Constants, -} - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct GasActorConstructorParams { - base_fee: TokenAmount, - constants: Option, -} - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct GasMarketReading { - pub block_gas_limit: Gas, - pub base_fee: TokenAmount, -} - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct BlockGasUtilization { - pub block_gas_used: Gas, -} - -#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] -pub struct BlockGasUtilizationRet { - pub base_fee: TokenAmount, -} - -pub struct EIP1559GasMarketActor {} - -#[derive(FromPrimitive)] -#[repr(u64)] -pub enum Method { - Constructor = METHOD_CONSTRUCTOR, - CurrentReading = frc42_dispatch::method_hash!("CurrentReading"), - GetConstants = frc42_dispatch::method_hash!("GetConstants"), - SetConstants = frc42_dispatch::method_hash!("SetConstants"), - UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), -} - -impl EIP1559GasMarketActor { - /// Creates the actor - pub fn constructor( - rt: &impl Runtime, - params: GasActorConstructorParams, - ) -> Result<(), ActorError> { - rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - - let st = EIP1559GasState::from(params); - rt.create(&st) - } - - fn set_constants(rt: &impl Runtime, constants: SetConstants) -> Result<(), ActorError> { - rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - - rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.constants = constants; - Ok(()) - })?; - - Ok(()) - } - - fn current_reading(rt: &impl Runtime) -> Result { - rt.validate_immediate_caller_accept_any()?; - - let st = rt.state::()?; - Ok(GasMarketReading { - block_gas_limit: st.constants.block_gas_limit, - base_fee: st.base_fee, - }) - } - - fn get_constants(rt: &impl Runtime) -> Result { - rt.validate_immediate_caller_accept_any()?; - - let st = rt.state::()?; - Ok(st.constants) - } - - fn update_utilization( - rt: &impl Runtime, - utilization: BlockGasUtilization, - ) -> Result { - rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - - rt.transaction(|st: &mut EIP1559GasState, _rt| { - st.base_fee = st.next_base_fee(utilization.block_gas_used); - Ok(BlockGasUtilizationRet { - base_fee: st.base_fee.clone(), - }) - }) - } -} - -impl Default for EIP1559Constants { - fn default() -> Self { - Self { - // Take from filecoin setting, fvm_shared::BLOCK_GAS_LIMIT - block_gas_limit: 10_000_000_000, - minimal_base_fee: TokenAmount::from_atto(100), - // Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - elasticity_multiplier: 2, - // Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - base_fee_max_change_denominator: 8, - } - } -} - -impl From for EIP1559GasState { - fn from(params: GasActorConstructorParams) -> Self { - Self { - base_fee: params.base_fee, - constants: params.constants.unwrap_or_default(), - } - } -} - -impl GasActorConstructorParams { - pub fn new(base_fee: TokenAmount) -> Self { - Self { - base_fee, - constants: None, - } - } - - pub fn with_constants(mut self, constants: EIP1559Constants) -> Self { - self.constants = Some(constants); - self - } -} - -impl EIP1559GasState { - fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { - let base_fee = self.base_fee.clone(); - let gas_target = self.constants.block_gas_limit / self.constants.elasticity_multiplier; - - match gas_used.cmp(&gas_target) { - Ordering::Equal => base_fee, - Ordering::Less => { - let base_fee_delta = base_fee.atto() * (gas_target - gas_used) - / gas_target - / self.constants.base_fee_max_change_denominator; - let base_fee_delta = TokenAmount::from_atto(base_fee_delta); - if base_fee_delta >= base_fee { - self.constants.minimal_base_fee.clone() - } else { - base_fee - base_fee_delta - } - } - Ordering::Greater => { - let gas_used_delta = gas_used - gas_target; - let delta = base_fee.atto() * gas_used_delta - / gas_target - / self.constants.base_fee_max_change_denominator; - base_fee + TokenAmount::from_atto(delta).max(TokenAmount::from_atto(1)) - } - } - } -} - -impl ActorCode for EIP1559GasMarketActor { - type Methods = Method; - - fn name() -> &'static str { - IPC_GAS_MARKET_ACTOR_NAME - } - - actor_dispatch! { - Constructor => constructor, - SetConstants => set_constants, - CurrentReading => current_reading, - GetConstants => get_constants, - UpdateUtilization => update_utilization, - } -} - -#[cfg(test)] -mod tests { - use crate::{ - BlockGasUtilization, EIP1559Constants, EIP1559GasMarketActor, EIP1559GasState, - GasActorConstructorParams, GasMarketReading, Method, - }; - use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; - use fil_actors_runtime::SYSTEM_ACTOR_ADDR; - use fvm_ipld_encoding::ipld_block::IpldBlock; - use fvm_shared::address::Address; - use fvm_shared::econ::TokenAmount; - use fvm_shared::error::ExitCode; - - pub fn construct_and_verify() -> MockRuntime { - let rt = MockRuntime { - receiver: Address::new_id(10), - ..Default::default() - }; - - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let result = rt - .call::( - Method::Constructor as u64, - IpldBlock::serialize_cbor(&GasActorConstructorParams { - base_fee: TokenAmount::from_atto(100), - constants: None, - }) - .unwrap(), - ) - .unwrap(); - expect_empty(result); - rt.verify(); - rt.reset(); - - rt - } - - #[test] - fn test_set_ok() { - let rt = construct_and_verify(); - - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let r = rt.call::( - Method::SetConstants as u64, - IpldBlock::serialize_cbor(&EIP1559Constants { - minimal_base_fee: Default::default(), - elasticity_multiplier: 0, - base_fee_max_change_denominator: 0, - block_gas_limit: 20, - }) - .unwrap(), - ); - assert!(r.is_ok()); - - let s = rt.get_state::(); - assert_eq!(s.constants.block_gas_limit, 20); - } - - #[test] - fn test_update_utilization_full_usage() { - let rt = construct_and_verify(); - - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let r = rt.call::( - Method::UpdateUtilization as u64, - IpldBlock::serialize_cbor(&BlockGasUtilization { - // full block usage - block_gas_used: 10_000_000_000, - }) - .unwrap(), - ); - assert!(r.is_ok()); - - rt.expect_validate_caller_any(); - let r = rt - .call::(Method::CurrentReading as u64, None) - .unwrap() - .unwrap(); - let reading = r.deserialize::().unwrap(); - assert_eq!(reading.base_fee, TokenAmount::from_atto(112)); - } - - #[test] - fn test_update_utilization_equal_usage() { - let rt = construct_and_verify(); - - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let r = rt.call::( - Method::UpdateUtilization as u64, - IpldBlock::serialize_cbor(&BlockGasUtilization { - // full block usage - block_gas_used: 5_000_000_000, - }) - .unwrap(), - ); - assert!(r.is_ok()); - - rt.expect_validate_caller_any(); - let r = rt - .call::(Method::CurrentReading as u64, None) - .unwrap() - .unwrap(); - let reading = r.deserialize::().unwrap(); - assert_eq!(reading.base_fee, TokenAmount::from_atto(100)); - } - - #[test] - fn test_update_utilization_under_usage() { - let rt = construct_and_verify(); - - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let r = rt.call::( - Method::UpdateUtilization as u64, - IpldBlock::serialize_cbor(&BlockGasUtilization { - // full block usage - block_gas_used: 100_000_000, - }) - .unwrap(), - ); - assert!(r.is_ok()); - - rt.expect_validate_caller_any(); - let r = rt - .call::(Method::CurrentReading as u64, None) - .unwrap() - .unwrap(); - let reading = r.deserialize::().unwrap(); - assert_eq!(reading.base_fee, TokenAmount::from_atto(88)); - } - - #[test] - fn test_not_allowed() { - let rt = construct_and_verify(); - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, Address::new_id(1000)); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - - let code = rt - .call::( - Method::SetConstants as u64, - IpldBlock::serialize_cbor(&EIP1559Constants { - minimal_base_fee: TokenAmount::from_atto(10000), - elasticity_multiplier: 0, - base_fee_max_change_denominator: 0, - block_gas_limit: 20, - }) - .unwrap(), - ) - .unwrap_err() - .exit_code(); - assert_eq!(code, ExitCode::USR_FORBIDDEN) - } -} From 0c3d067d855844d7abc48370edc7c1ebe142b23a Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 20:21:56 +0000 Subject: [PATCH 101/111] unroll nested implementation. --- .../vm/interpreter/src/fvm/activity/mod.rs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/fendermint/vm/interpreter/src/fvm/activity/mod.rs b/fendermint/vm/interpreter/src/fvm/activity/mod.rs index f44173aaf..d8519834f 100644 --- a/fendermint/vm/interpreter/src/fvm/activity/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activity/mod.rs @@ -33,24 +33,28 @@ impl TryFrom for F fn try_from( value: fendermint_actor_activity_tracker::types::FullActivityRollup, ) -> Result { + let stats = AggregatedStats { + total_active_validators: value.consensus.stats.total_active_validators, + total_num_blocks_committed: value.consensus.stats.total_num_blocks_committed, + }; + let data = value + .consensus + .data + .into_iter() + .map(|(addr, data)| { + let data = ValidatorData { + validator: payload_to_evm_address(addr.payload())?, + blocks_committed: data.blocks_committed, + }; + Ok(data) + }) + .collect::>>()?; + let consensus = FullSummary { + stats, + data, + }; let f = FullActivityRollup { - consensus: FullSummary { - stats: AggregatedStats { - total_active_validators: value.consensus.stats.total_active_validators, - total_num_blocks_committed: value.consensus.stats.total_num_blocks_committed, - }, - data: value - .consensus - .data - .into_iter() - .map(|(addr, data)| { - Ok(ValidatorData { - validator: payload_to_evm_address(addr.payload())?, - blocks_committed: data.blocks_committed, - }) - }) - .collect::>>()?, - }, + consensus, }; Ok(Self::new(f)) } From b223101723cdb12ca13ed89ceb0f4c22b3cc3f1e Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 20:22:06 +0000 Subject: [PATCH 102/111] lint Cargo.toml. --- ipc/wallet/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/wallet/Cargo.toml b/ipc/wallet/Cargo.toml index 02c02f692..984684d7c 100644 --- a/ipc/wallet/Cargo.toml +++ b/ipc/wallet/Cargo.toml @@ -15,7 +15,7 @@ base64 = { workspace = true } blake2b_simd = { workspace = true } bls-signatures = { version = "0.13.0", default-features = false, features = ["blst"] } ethers = { workspace = true, optional = true } -fvm_shared = { workspace = true, features = ["crypto"] } +fvm_shared = { workspace = true, features = ["crypto"] } hex = { workspace = true } libc = "0.2" libsecp256k1 = { workspace = true } From 0496330784a6e205c857910be27a766c980b6b95 Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 20:24:48 +0000 Subject: [PATCH 103/111] InvalidProof => InvalidActivityProof. --- contracts/contracts/activities/LibActivityMerkleVerifier.sol | 4 ++-- contracts/contracts/errors/IPCErrors.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 579e64d5f..42a737b34 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -import {InvalidProof} from "../errors/IPCErrors.sol"; +import {InvalidActivityProof} from "../errors/IPCErrors.sol"; import {Consensus} from "./Activity.sol"; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; @@ -22,7 +22,7 @@ library LibActivityMerkleVerifier { } bool valid = MerkleProof.verify({proof: proofBytes, root: commitment, leaf: leaf}); if (!valid) { - revert InvalidProof(); + revert InvalidActivityProof(); } } } diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index 2cdc7ba96..402eefdef 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -83,7 +83,7 @@ error ValidatorPowerChangeDenied(); error CommitmentAlreadyInitialized(); error SubnetNoTargetCommitment(); error ValidatorAlreadyClaimed(); -error InvalidProof(); +error InvalidActivityProof(); error NotOwner(); enum InvalidXnetMessageReason { From b0a9c04ca6ffc43dc6e7e605f764dd2a57ea60ea Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 20:37:26 +0000 Subject: [PATCH 104/111] updateRewarder => setRewarder. more idiomatic. --- contracts/contracts/SubnetActorDiamond.sol | 2 +- contracts/contracts/activities/ValidatorRewardFacet.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/SubnetActorDiamond.sol b/contracts/contracts/SubnetActorDiamond.sol index dc36e7ba8..7f3fb5087 100644 --- a/contracts/contracts/SubnetActorDiamond.sol +++ b/contracts/contracts/SubnetActorDiamond.sol @@ -107,7 +107,7 @@ contract SubnetActorDiamond { } if (params.validatorRewarder != address(0)) { - LibValidatorReward.updateRewarder(params.validatorRewarder); + LibValidatorReward.setRewarder(params.validatorRewarder); } } diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index 903ac8740..01aa6815f 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -157,7 +157,7 @@ library LibValidatorReward { return listDetails; } - function updateRewarder(address rewarder) internal { + function setRewarder(address rewarder) internal { ValidatorRewardStorage storage ds = facetStorage(); ds.validatorRewarder = rewarder; } From 2b9c3b469048367bd05865726af6db4cd2924b1b Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 20:38:58 +0000 Subject: [PATCH 105/111] rm unimplemented relayer logic; use NotAuthorized error. --- .../activities/ValidatorRewardFacet.sol | 24 +++++++------------ contracts/contracts/structs/CrossNet.sol | 8 ------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index 01aa6815f..1927a9f73 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -8,7 +8,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; import {LibDiamond} from "../lib/LibDiamond.sol"; -import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; +import {NotAuthorized, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; @@ -18,6 +18,8 @@ import {SubnetID} from "../structs/Subnet.sol"; /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. contract ValidatorRewardFacet is ReentrancyGuard, Pausable { + // Entrypoint for validators to batch claim rewards in the parent subnet, for a given subnet, + // against multiple checkpoints at once. Atomically succeeds or reverts. function batchSubnetClaim( SubnetID calldata subnet, uint64[] calldata checkpointHeights, @@ -33,7 +35,7 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { } } - /// Validators claim their reward for doing work in the child subnet + /// Entrypoint for validators to claim their reward for doing work in the child subnet. function claim( SubnetID calldata subnet, uint64 checkpointHeight, @@ -45,11 +47,6 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { // ======== Internal functions =========== - function handleRelay() internal pure { - // no-op for now - return; - } - function _claim( SubnetID calldata subnetId, uint64 checkpointHeight, @@ -58,15 +55,10 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { ) internal { ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight - // note: will never exist. - + // Note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight + // will never exist. if (msg.sender != detail.validator) { - revert NotValidator(msg.sender); - } - - if (s.validatorRewarder == address(0)) { - return handleRelay(); + revert NotAuthorized(msg.sender); } LibValidatorReward.handleDistribution(subnetId, checkpointHeight, detail, proof); @@ -83,7 +75,7 @@ struct RewardDistribution { /// Used by the SubnetActor to track the rewards for each validator struct ValidatorRewardStorage { - /// @notice The contract address for validator rewarder + /// @notice The contract address for the validator rewarder. address validatorRewarder; /// @notice Summaries look up pending to be processed. /// If the validator rewarder is non-zero, these denote summaries presentable at this level. diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 9d00cda8d..5f5d8ae1a 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -34,14 +34,6 @@ struct BottomUpCheckpoint { CompressedActivityRollup activities; } -struct RelayedSummary { - /// @dev The subnet IDs whose activity is being relayed. - SubnetID subnet; - /// @dev The commitment to the summary, so it can be presented later by the relayer. - /// A blake2b hash of the summary generated by abi.encode'ing the ActivitySummary and hashing it via the Eth precompile. - bytes32 commitment; -} - /// @notice A batch of bottom-up messages for execution. struct BottomUpMsgBatch { /// @dev Child subnet ID, for replay protection from other subnets where the exact same validators operate. From 3ad2c0f52fff1323fd32c20d1d3507bcc12094a3 Mon Sep 17 00:00:00 2001 From: raulk Date: Tue, 26 Nov 2024 23:49:03 +0000 Subject: [PATCH 106/111] chore: cleanup/refactor activity and rewards contracts. (#1214) --- contracts/binding/build.rs | 2 +- contracts/binding/src/lib.rs | 4 +- contracts/contracts/SubnetActorDiamond.sol | 4 +- contracts/contracts/SubnetRegistryDiamond.sol | 10 +- .../activities/IValidatorRewarder.sol | 30 --- .../activities/LibActivityMerkleVerifier.sol | 28 --- .../activities/ValidatorRewardFacet.sol | 237 ------------------ contracts/contracts/errors/IPCErrors.sol | 3 +- .../examples/ValidatorRewarderMap.sol | 6 +- .../gateway/router/CheckpointingFacet.sol | 43 +--- contracts/contracts/interfaces/IGateway.sol | 4 +- .../interfaces/IValidatorRewarder.sol | 20 ++ contracts/contracts/lib/LibActivity.sol | 173 +++++++++++++ contracts/contracts/lib/LibGateway.sol | 2 +- .../lib/LibSubnetRegistryStorage.sol | 2 +- .../{activities => structs}/Activity.sol | 0 contracts/contracts/structs/CrossNet.sol | 6 +- .../subnet/SubnetActorActivityFacet.sol | 60 +++++ .../subnet/SubnetActorCheckpointingFacet.sol | 9 +- .../subnetregistry/RegisterSubnetFacet.sol | 2 +- contracts/tasks/deploy-registry.ts | 6 +- contracts/tasks/gen-selector-library.ts | 10 +- contracts/test/IntegrationTestBase.sol | 18 +- contracts/test/helpers/ActivityHelper.sol | 13 +- contracts/test/helpers/MerkleTreeHelper.sol | 2 +- contracts/test/helpers/SelectorLibrary.sol | 4 +- .../test/helpers/SubnetActorFacetsHelper.sol | 8 +- .../test/integration/GatewayDiamond.t.sol | 88 ++++--- .../integration/GatewayDiamondToken.t.sol | 6 +- contracts/test/integration/MultiSubnet.t.sol | 20 +- .../test/integration/SubnetActorDiamond.t.sol | 79 +++--- .../test/integration/SubnetRegistry.t.sol | 8 +- .../invariants/SubnetRegistryInvariants.t.sol | 6 +- .../linked-token/test/MultiSubnetTest.t.sol | 7 +- .../contract-test/tests/staking/machine.rs | 2 +- fendermint/vm/actor_interface/src/ipc.rs | 11 +- .../vm/interpreter/src/fvm/checkpoint.rs | 12 +- fendermint/vm/interpreter/src/fvm/exec.rs | 2 +- .../vm/interpreter/src/fvm/state/exec.rs | 2 +- .../vm/interpreter/src/fvm/state/ipc.rs | 6 +- fendermint/vm/interpreter/src/genesis.rs | 6 +- ipc/api/src/evm.rs | 8 +- ipc/provider/src/manager/evm/manager.rs | 8 +- ipc/provider/src/manager/subnet.rs | 2 +- 44 files changed, 483 insertions(+), 496 deletions(-) delete mode 100644 contracts/contracts/activities/IValidatorRewarder.sol delete mode 100644 contracts/contracts/activities/LibActivityMerkleVerifier.sol delete mode 100644 contracts/contracts/activities/ValidatorRewardFacet.sol create mode 100644 contracts/contracts/interfaces/IValidatorRewarder.sol create mode 100644 contracts/contracts/lib/LibActivity.sol rename contracts/contracts/{activities => structs}/Activity.sol (100%) create mode 100644 contracts/contracts/subnet/SubnetActorActivityFacet.sol diff --git a/contracts/binding/build.rs b/contracts/binding/build.rs index 1c45e66a4..63de37f86 100644 --- a/contracts/binding/build.rs +++ b/contracts/binding/build.rs @@ -47,6 +47,7 @@ fn main() { "TopDownFinalityFacet", "XnetMessagingFacet", "GatewayMessengerFacet", + "SubnetActorActivityFacet", "SubnetActorCheckpointingFacet", "SubnetActorDiamond", "SubnetActorGetterFacet", @@ -60,7 +61,6 @@ fn main() { "LibStakingChangeLog", "LibGateway", "LibQuorum", - "ValidatorRewardFacet", ] { let module_name = camel_to_snake(contract_name); let input_path = diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index a617bead0..bbc78042b 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -30,6 +30,8 @@ pub mod ownership_facet; #[allow(clippy::all)] pub mod register_subnet_facet; #[allow(clippy::all)] +pub mod subnet_actor_activity_facet; +#[allow(clippy::all)] pub mod subnet_actor_checkpointing_facet; #[allow(clippy::all)] pub mod subnet_actor_diamond; @@ -48,8 +50,6 @@ pub mod subnet_registry_diamond; #[allow(clippy::all)] pub mod top_down_finality_facet; #[allow(clippy::all)] -pub mod validator_reward_facet; -#[allow(clippy::all)] pub mod xnet_messaging_facet; // The list of contracts need to convert FvmAddress to fvm_shared::Address diff --git a/contracts/contracts/SubnetActorDiamond.sol b/contracts/contracts/SubnetActorDiamond.sol index 7f3fb5087..cf4ac475b 100644 --- a/contracts/contracts/SubnetActorDiamond.sol +++ b/contracts/contracts/SubnetActorDiamond.sol @@ -15,7 +15,7 @@ import {SubnetIDHelper} from "./lib/SubnetIDHelper.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AssetHelper} from "./lib/AssetHelper.sol"; -import {LibValidatorReward} from "./activities/ValidatorRewardFacet.sol"; +import {LibActivity} from "./lib/LibActivity.sol"; error FunctionNotFound(bytes4 _functionSelector); @@ -107,7 +107,7 @@ contract SubnetActorDiamond { } if (params.validatorRewarder != address(0)) { - LibValidatorReward.setRewarder(params.validatorRewarder); + LibActivity.setRewarder(params.validatorRewarder); } } diff --git a/contracts/contracts/SubnetRegistryDiamond.sol b/contracts/contracts/SubnetRegistryDiamond.sol index 250dfffb6..871d67a06 100644 --- a/contracts/contracts/SubnetRegistryDiamond.sol +++ b/contracts/contracts/SubnetRegistryDiamond.sol @@ -25,7 +25,7 @@ contract SubnetRegistryDiamond { address diamondCutFacet; address diamondLoupeFacet; address ownershipFacet; - address validatorRewardFacet; + address activityFacet; bytes4[] subnetActorGetterSelectors; bytes4[] subnetActorManagerSelectors; bytes4[] subnetActorRewarderSelectors; @@ -34,7 +34,7 @@ contract SubnetRegistryDiamond { bytes4[] subnetActorDiamondCutSelectors; bytes4[] subnetActorDiamondLoupeSelectors; bytes4[] subnetActorOwnershipSelectors; - bytes4[] validatorRewardSelectors; + bytes4[] subnetActorActivitySelectors; SubnetCreationPrivileges creationPrivileges; } @@ -66,7 +66,7 @@ contract SubnetRegistryDiamond { if (params.ownershipFacet == address(0)) { revert FacetCannotBeZero(); } - if (params.validatorRewardFacet == address(0)) { + if (params.activityFacet == address(0)) { revert FacetCannotBeZero(); } @@ -88,7 +88,7 @@ contract SubnetRegistryDiamond { s.SUBNET_ACTOR_DIAMOND_CUT_FACET = params.diamondCutFacet; s.SUBNET_ACTOR_LOUPE_FACET = params.diamondLoupeFacet; s.SUBNET_ACTOR_OWNERSHIP_FACET = params.ownershipFacet; - s.VALIDATOR_REWARD_FACET = params.validatorRewardFacet; + s.VALIDATOR_REWARD_FACET = params.activityFacet; s.subnetActorGetterSelectors = params.subnetActorGetterSelectors; s.subnetActorManagerSelectors = params.subnetActorManagerSelectors; @@ -98,7 +98,7 @@ contract SubnetRegistryDiamond { s.subnetActorDiamondCutSelectors = params.subnetActorDiamondCutSelectors; s.subnetActorDiamondLoupeSelectors = params.subnetActorDiamondLoupeSelectors; s.subnetActorOwnershipSelectors = params.subnetActorOwnershipSelectors; - s.validatorRewardSelectors = params.validatorRewardSelectors; + s.subnetActorActivitySelectors = params.subnetActorActivitySelectors; s.creationPrivileges = params.creationPrivileges; } diff --git a/contracts/contracts/activities/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol deleted file mode 100644 index 052b233de..000000000 --- a/contracts/contracts/activities/IValidatorRewarder.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.23; - -import {SubnetID} from "../structs/Subnet.sol"; -import {Consensus} from "./Activity.sol"; - -/// @title ValidatorRewarder interface. -/// -/// @dev Implement this interface and supply the address of the implementation contract at subnet creation to process -/// subnet summaries at this level, and disburse rewards to validators based on their block production activity. -/// -/// This interface will be called by the subnet actor when a relayer presents a -interface IValidatorRewarder { - /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and - /// disburse any relevant rewards. - /// @dev This method should revert if the summary is invalid; this will cause the - function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) external; -} - -/// @title Validator reward setup interface -/// -/// @dev This is used to initialize a reward distribution -interface IValidatorRewardSetup { - function initDistribution( - SubnetID calldata subnetId, - uint64 checkpointHeight, - bytes32 commitment, - uint64 totalActiveValidators - ) external; -} diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol deleted file mode 100644 index 42a737b34..000000000 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.23; - -import {SubnetID} from "../structs/Subnet.sol"; -import {InvalidActivityProof} from "../errors/IPCErrors.sol"; -import {Consensus} from "./Activity.sol"; -import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; - -/// Verifies the proof to the commitment in subnet activity summary -library LibActivityMerkleVerifier { - function ensureValidProof( - bytes32 commitment, - Consensus.ValidatorData calldata detail, - Consensus.MerkleHash[] calldata proof - ) internal pure { - // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(detail.validator, detail.blocksCommitted)))); - // converting proof to bytes32[] - bytes32[] memory proofBytes = new bytes32[](proof.length); - for (uint256 i = 0; i < proof.length; i++) { - proofBytes[i] = Consensus.MerkleHash.unwrap(proof[i]); - } - bool valid = MerkleProof.verify({proof: proofBytes, root: commitment, leaf: leaf}); - if (!valid) { - revert InvalidActivityProof(); - } - } -} diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol deleted file mode 100644 index 1927a9f73..000000000 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.23; - -import {Consensus} from "./Activity.sol"; - -import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; -import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; -import {LibDiamond} from "../lib/LibDiamond.sol"; -import {NotAuthorized, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; -import {Pausable} from "../lib/LibPausable.sol"; -import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; -import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; -import {SubnetID} from "../structs/Subnet.sol"; - -/// The validator reward facet for the parent subnet, i.e. for validators in the child subnet -/// to claim their reward in the parent subnet, which should be the current subnet this facet -/// is deployed. -contract ValidatorRewardFacet is ReentrancyGuard, Pausable { - // Entrypoint for validators to batch claim rewards in the parent subnet, for a given subnet, - // against multiple checkpoints at once. Atomically succeeds or reverts. - function batchSubnetClaim( - SubnetID calldata subnet, - uint64[] calldata checkpointHeights, - Consensus.ValidatorClaim[] calldata claims - ) external nonReentrant whenNotPaused { - require(checkpointHeights.length == claims.length, "length mismatch"); - uint256 len = claims.length; - for (uint256 i = 0; i < len; ) { - _claim(subnet, checkpointHeights[i], claims[i].data, claims[i].proof); - unchecked { - i++; - } - } - } - - /// Entrypoint for validators to claim their reward for doing work in the child subnet. - function claim( - SubnetID calldata subnet, - uint64 checkpointHeight, - Consensus.ValidatorData calldata data, - Consensus.MerkleHash[] calldata proof - ) external nonReentrant whenNotPaused { - _claim(subnet, checkpointHeight, data, proof); - } - - // ======== Internal functions =========== - - function _claim( - SubnetID calldata subnetId, - uint64 checkpointHeight, - Consensus.ValidatorData calldata detail, - Consensus.MerkleHash[] calldata proof - ) internal { - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - - // Note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight - // will never exist. - if (msg.sender != detail.validator) { - revert NotAuthorized(msg.sender); - } - - LibValidatorReward.handleDistribution(subnetId, checkpointHeight, detail, proof); - } -} - -/// The activity summary commiment that is currently under reward distribution -struct RewardDistribution { - /// Total number of valdators to claim the distribution - uint64 totalValidators; - /// The list of validators that have claimed the reward - EnumerableSet.AddressSet claimed; -} - -/// Used by the SubnetActor to track the rewards for each validator -struct ValidatorRewardStorage { - /// @notice The contract address for the validator rewarder. - address validatorRewarder; - /// @notice Summaries look up pending to be processed. - /// If the validator rewarder is non-zero, these denote summaries presentable at this level. - /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. - /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment - mapping(bytes32 => EnumerableMap.Bytes32ToBytes32Map) commitments; - /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. - /// Only used if the validator rewarder is non-zero. - /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment - mapping(bytes32 => mapping(uint64 => RewardDistribution)) distributions; -} - -/// The payload for list commitments query -struct ListCommitmentDetail { - /// The child subnet checkpoint height - uint64 checkpointHeight; - /// The actual commiment of the activities - bytes32 commitment; -} - -library LibValidatorReward { - bytes32 private constant NAMESPACE = keccak256("validator.reward.parent"); - - using SubnetIDHelper for SubnetID; - using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; - using EnumerableSet for EnumerableSet.AddressSet; - - // =========== External library functions ============= - - function initNewDistribution( - SubnetID calldata subnetId, - uint64 checkpointHeight, - Consensus.MerkleHash commitment, - uint64 totalActiveValidators - ) internal { - ValidatorRewardStorage storage ds = facetStorage(); - - bytes32 subnetKey = subnetId.toHash(); - - if (ds.distributions[subnetKey][checkpointHeight].totalValidators != 0) { - revert CommitmentAlreadyInitialized(); - } - - ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), Consensus.MerkleHash.unwrap(commitment)); - ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; - } - - function listCommitments( - SubnetID calldata subnetId - ) internal view returns (ListCommitmentDetail[] memory listDetails) { - ValidatorRewardStorage storage ds = facetStorage(); - - bytes32 subnetKey = subnetId.toHash(); - - uint256 size = ds.commitments[subnetKey].length(); - listDetails = new ListCommitmentDetail[](size); - - for (uint256 i = 0; i < size; ) { - (bytes32 heightBytes32, bytes32 commitment) = ds.commitments[subnetKey].at(i); - - listDetails[i] = ListCommitmentDetail({ - checkpointHeight: uint64(uint256(heightBytes32)), - commitment: commitment - }); - - unchecked { - i++; - } - } - - return listDetails; - } - - function setRewarder(address rewarder) internal { - ValidatorRewardStorage storage ds = facetStorage(); - ds.validatorRewarder = rewarder; - } - - // ============ Internal library functions ============ - - function facetStorage() internal pure returns (ValidatorRewardStorage storage ds) { - bytes32 position = NAMESPACE; - assembly { - ds.slot := position - } - return ds; - } - - function handleDistribution( - SubnetID calldata subnetId, - uint64 checkpointHeight, - Consensus.ValidatorData calldata detail, - Consensus.MerkleHash[] calldata proof - ) internal { - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - - bytes32 subnetKey = subnetId.toHash(); - - bytes32 commitment = ensureValidCommitment(s, subnetKey, checkpointHeight); - LibActivityMerkleVerifier.ensureValidProof(commitment, detail, proof); - - validatorTryClaim(s, subnetKey, checkpointHeight, detail.validator); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, detail); - } - - function ensureValidCommitment( - ValidatorRewardStorage storage ds, - bytes32 subnetKey, - uint64 checkpointHeight - ) internal view returns (bytes32) { - (bool exists, bytes32 commitment) = ds.commitments[subnetKey].tryGet(bytes32(uint256(checkpointHeight))); - if (!exists) { - revert SubnetNoTargetCommitment(); - } - - // Note: ideally we should check the commitment actually exists, but we dont have to as - // Note: the code will ensure if commitments contains the commitment, - // Note: the commitment will have distribution - // if (ds.distributions[commitment].checkpointHeight == 0) { - // revert CommitmentNotFound(); - // } - - return commitment; - } - - /// Validator tries to claim the reward. The validator can only claim the reward if the validator - /// has not claimed before - function validatorTryClaim( - ValidatorRewardStorage storage ds, - bytes32 subnetKey, - uint64 checkpointHeight, - address validator - ) internal { - if (ds.distributions[subnetKey][checkpointHeight].claimed.contains(validator)) { - revert ValidatorAlreadyClaimed(); - } - - ds.distributions[subnetKey][checkpointHeight].claimed.add(validator); - } - - /// Try to remove the distribution in the target subnet when ALL VALIDATORS HAVE CLAIMED. - function tryPurgeDistribution( - ValidatorRewardStorage storage ds, - SubnetID calldata subnetId, - uint64 checkpointHeight - ) internal { - bytes32 subnetKey = subnetId.toHash(); - - uint256 total = uint256(ds.distributions[subnetKey][checkpointHeight].totalValidators); - uint256 claimed = ds.distributions[subnetKey][checkpointHeight].claimed.length(); - - if (claimed < total) { - return; - } - - delete ds.commitments[subnetKey]; - delete ds.distributions[subnetKey][checkpointHeight]; - } -} diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index 402eefdef..7d73a8df4 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -80,8 +80,7 @@ error InvalidFederationPayload(); error DuplicatedGenesisValidator(); error NotEnoughGenesisValidators(); error ValidatorPowerChangeDenied(); -error CommitmentAlreadyInitialized(); -error SubnetNoTargetCommitment(); +error MissingActivityCommitment(); error ValidatorAlreadyClaimed(); error InvalidActivityProof(); error NotOwner(); diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 8f0ffd7d8..08022b698 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; -import {Consensus} from "../activities/Activity.sol"; +import {IValidatorRewarder} from "../interfaces/IValidatorRewarder.sol"; +import {Consensus} from "../structs/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; @@ -20,7 +20,7 @@ contract ValidatorRewarderMap is IValidatorRewarder, Ownable { subnetId = id; } - function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) external { + function notifyValidClaim(SubnetID calldata id, Consensus.ValidatorData calldata detail) external { require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); address actor = id.route[id.route.length - 1]; diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index fafbecc0c..54e2b1e52 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -17,7 +17,7 @@ import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {ActivityRollupRecorded, FullActivityRollup} from "../../activities/Activity.sol"; +import {ActivityRollupRecorded, FullActivityRollup} from "../../structs/Activity.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -45,54 +45,21 @@ contract CheckpointingFacet is GatewayActorModifiers { execBottomUpMsgs(checkpoint.msgs, subnet); } - /// @notice creates a new bottom-up checkpoint with activity report - /// @param checkpoint - a bottom-up checkpoint - /// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight - /// @param membershipWeight - the total weight of the membership - /// @param fullSummary - the full validators' activities summary - function createBUChptWithActivities( - BottomUpCheckpoint calldata checkpoint, - bytes32 membershipRootHash, - uint256 membershipWeight, - FullActivityRollup calldata fullSummary - ) external systemActorOnly { - if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { - revert CheckpointAlreadyExists(); - } - - LibQuorum.createQuorumInfo({ - self: s.checkpointQuorumMap, - objHeight: checkpoint.blockHeight, - objHash: keccak256(abi.encode(checkpoint)), - membershipRootHash: membershipRootHash, - membershipWeight: membershipWeight, - majorityPercentage: s.majorityPercentage - }); - - LibGateway.storeBottomUpCheckpoint(checkpoint); - - emit ActivityRollupRecorded(uint64(checkpoint.blockHeight), fullSummary); - } - /// @notice creates a new bottom-up checkpoint /// @param checkpoint - a bottom-up checkpoint /// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight /// @param membershipWeight - the total weight of the membership + /// @param activity - the full activity rollup function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, bytes32 membershipRootHash, - uint256 membershipWeight + uint256 membershipWeight, + FullActivityRollup calldata activity ) external systemActorOnly { if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { revert CheckpointAlreadyExists(); } - // TODO(rewarder): step 1. call fvm ActivityTrackerActor::get_summary to generate the summary - // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 - // TODO: (if there is more time, should wrap param checkpoint with another data structure) - // TODO(rewarder): step 3. call fvm ActivityTrackerActor::purge_activities to purge the activities - // TODO(rewarder): step 4. emit validator details as event - LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, objHeight: checkpoint.blockHeight, @@ -103,6 +70,8 @@ contract CheckpointingFacet is GatewayActorModifiers { }); LibGateway.storeBottomUpCheckpoint(checkpoint); + + emit ActivityRollupRecorded(uint64(checkpoint.blockHeight), activity); } /// @notice Set a new checkpoint retention height and garbage collect all checkpoints in range [`retentionHeight`, `newRetentionHeight`) diff --git a/contracts/contracts/interfaces/IGateway.sol b/contracts/contracts/interfaces/IGateway.sol index 9d820dff6..1c807ea81 100644 --- a/contracts/contracts/interfaces/IGateway.sol +++ b/contracts/contracts/interfaces/IGateway.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; import {BottomUpCheckpoint, BottomUpMsgBatch, IpcEnvelope, ParentFinality} from "../structs/CrossNet.sol"; +import {FullActivityRollup} from "../structs/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; import {FvmAddress} from "../structs/FvmAddress.sol"; @@ -71,6 +72,7 @@ interface IGateway { function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, bytes32 membershipRootHash, - uint256 membershipWeight + uint256 membershipWeight, + FullActivityRollup calldata activity ) external; } diff --git a/contracts/contracts/interfaces/IValidatorRewarder.sol b/contracts/contracts/interfaces/IValidatorRewarder.sol new file mode 100644 index 000000000..44a9e2cec --- /dev/null +++ b/contracts/contracts/interfaces/IValidatorRewarder.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; +import {Consensus} from "../structs/Activity.sol"; + +/// @title ValidatorRewarder interface. +/// +/// @dev Implement this interface and supply the address of the implementation contract at subnet creation to process +/// consensus activity summaries at this level, and disburse rewards to validators based on their block production +/// activities inside the subnet. +/// +/// This interface will be called by the subnet actor when a validator presents a _valid_ proof of consensus activity, +/// via the SubnetActivityActivityFacet#claim method. +interface IValidatorRewarder { + /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and + /// disburse any relevant rewards. + /// @dev This method should revert if the summary is invalid; this will cause the + function notifyValidClaim(SubnetID calldata id, Consensus.ValidatorData calldata validatedData) external; +} diff --git a/contracts/contracts/lib/LibActivity.sol b/contracts/contracts/lib/LibActivity.sol new file mode 100644 index 000000000..4e72dd50c --- /dev/null +++ b/contracts/contracts/lib/LibActivity.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {IValidatorRewarder} from "../interfaces/IValidatorRewarder.sol"; +import {Consensus, CompressedActivityRollup} from "../structs/Activity.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {SubnetID} from "../structs/Subnet.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {InvalidActivityProof, MissingActivityCommitment, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; + +/// Library to handle activity rollups. +library LibActivity { + bytes32 private constant CONSENSUS_NAMESPACE = keccak256("activity.consensus"); + + using SubnetIDHelper for SubnetID; + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + + // Newtype for extra safety. + type SubnetKey is bytes32; + + /// An object to track consensus-related activity submissions from subnets. + struct ConsensusTracker { + /// An enumeration of checkpoint heights that are pending. + EnumerableSet.Bytes32Set pendingHeights; + /// The pending summaries for each checkpoint height, including information about the validators that have already claimed rewards. + mapping(uint64 => ConsensusPendingAtHeight) pending; + } + + struct ConsensusPendingAtHeight { + /// The original compressed summary submitted for this height. + /// We store the summary in full, or we relay it upwards if there is no validator rewarder configured. + Consensus.CompressedSummary summary; + /// Tracks validators have already claimed rewards for this height. + EnumerableSet.AddressSet claimed; + } + + /// Used by the SubnetActor to track the rewards for each validator + struct ConsensusStorage { + /// @notice The contract address for the validator rewarder. + address validatorRewarder; + /// @notice Tracks pending summaries to be processed. + /// If the validator rewarder is non-zero, these denote summaries presentable at this level. + /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. + /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment + mapping(SubnetKey => ConsensusTracker) tracker; + } + + /// Return type for the list commitments view method. + struct ListPendingReturnEntry { + uint64 height; + Consensus.CompressedSummary summary; + address[] claimed; + } + + function ensureValidProof( + Consensus.MerkleHash commitment, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof + ) internal pure { + // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(detail.validator, detail.blocksCommitted)))); + // converting proof to bytes32[] + bytes32[] memory proofBytes = new bytes32[](proof.length); + for (uint256 i = 0; i < proof.length; i++) { + proofBytes[i] = Consensus.MerkleHash.unwrap(proof[i]); + } + bool valid = MerkleProof.verify({proof: proofBytes, root: Consensus.MerkleHash.unwrap(commitment), leaf: leaf}); + if (!valid) { + revert InvalidActivityProof(); + } + } + + // =========== External library functions ============= + + function recordActivityRollup( + SubnetID calldata subnet, + uint64 checkpointHeight, + CompressedActivityRollup calldata activity + ) internal { + ConsensusStorage storage s = consensusStorage(); + + SubnetKey subnetKey = SubnetKey.wrap(subnet.toHash()); + + ConsensusTracker storage tracker = s.tracker[subnetKey]; + bool added = tracker.pendingHeights.add(bytes32(uint256(checkpointHeight))); + require(added, "duplicate checkpoint height"); + + ConsensusPendingAtHeight storage pending = tracker.pending[checkpointHeight]; + pending.summary = activity.consensus; + } + + /// A view accessor to query the pending consensus summaries for a given subnet. + function listPendingConsensus( + SubnetID calldata subnet + ) internal view returns (ListPendingReturnEntry[] memory result) { + ConsensusStorage storage s = consensusStorage(); + + SubnetKey subnetKey = SubnetKey.wrap(subnet.toHash()); + + uint256 size = s.tracker[subnetKey].pendingHeights.length(); + result = new ListPendingReturnEntry[](size); + + // Ok to not optimize with unchecked increments, since we expect this to be used off-chain only, for introspection. + for (uint256 i = 0; i < size; i++) { + ConsensusTracker storage tracker = s.tracker[subnetKey]; + bytes32[] memory heights = tracker.pendingHeights.values(); + + for (uint256 j = 0; j < heights.length; j++) { + uint64 height = uint64(uint256(heights[j]) << 192 >> 192); + ConsensusPendingAtHeight storage pending = tracker.pending[height]; + result[i] = ListPendingReturnEntry({ + height: height, + summary: pending.summary, + claimed: pending.claimed.values() + }); + } + } + + return result; + } + + function processConsensusClaim( + SubnetID calldata subnet, + uint64 checkpointHeight, + Consensus.ValidatorData calldata data, + Consensus.MerkleHash[] calldata proof + ) internal { + ConsensusStorage storage s = consensusStorage(); + + SubnetKey subnetKey = SubnetKey.wrap(subnet.toHash()); + + // Check the validity of the proof. + ConsensusPendingAtHeight storage pending = s.tracker[subnetKey].pending[checkpointHeight]; + Consensus.MerkleHash commitment = pending.summary.dataRootCommitment; + if (Consensus.MerkleHash.unwrap(commitment) == bytes32(0)) { + revert MissingActivityCommitment(); + } + ensureValidProof(commitment, data, proof); + + // Mark the validator as claimed. + bool added = pending.claimed.add(data.validator); + if (!added) { + revert ValidatorAlreadyClaimed(); + } + + // Notify the validator rewarder of a valid claim. + IValidatorRewarder(s.validatorRewarder).notifyValidClaim(subnet, data); + + // Prune state for this height if all validators have claimed. + if (pending.claimed.length() == pending.summary.stats.totalActiveValidators) { + ConsensusTracker storage tracker = s.tracker[subnetKey]; + tracker.pendingHeights.remove(bytes32(uint256(checkpointHeight))); + delete tracker.pending[checkpointHeight]; + } + } + + function setRewarder(address rewarder) internal { + ConsensusStorage storage ds = consensusStorage(); + ds.validatorRewarder = rewarder; + } + + // ============ Internal library functions ============ + + function consensusStorage() internal pure returns (ConsensusStorage storage ds) { + bytes32 position = CONSENSUS_NAMESPACE; + assembly { + ds.slot := position + } + return ds; + } +} diff --git a/contracts/contracts/lib/LibGateway.sol b/contracts/contracts/lib/LibGateway.sol index eb96ea800..e48c0dcee 100644 --- a/contracts/contracts/lib/LibGateway.sol +++ b/contracts/contracts/lib/LibGateway.sol @@ -82,7 +82,7 @@ library LibGateway { b.subnetID = checkpoint.subnetID; b.nextConfigurationNumber = checkpoint.nextConfigurationNumber; b.blockHeight = checkpoint.blockHeight; - b.activities = checkpoint.activities; + b.activity = checkpoint.activity; uint256 msgLength = checkpoint.msgs.length; for (uint256 i; i < msgLength; ) { diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index c40381513..665a144fb 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -44,7 +44,7 @@ struct SubnetRegistryActorStorage { /// The subnet actor ownership facet functions selectors bytes4[] subnetActorOwnershipSelectors; /// The validator reward facet functions selectors - bytes4[] validatorRewardSelectors; + bytes4[] subnetActorActivitySelectors; /// @notice Mapping that tracks the deployed subnet actors per user. /// Key is the hash of Subnet ID, values are addresses. /// mapping owner => nonce => subnet diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/structs/Activity.sol similarity index 100% rename from contracts/contracts/activities/Activity.sol rename to contracts/contracts/structs/Activity.sol diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 5f5d8ae1a..1f5962346 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {CompressedActivityRollup} from "../activities/Activity.sol"; +import {CompressedActivityRollup} from "../structs/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -30,8 +30,8 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; - /// @dev The activity summary from child subnet to parent subnet - CompressedActivityRollup activities; + /// @dev The activity rollup from child subnet to parent subnet. + CompressedActivityRollup activity; } /// @notice A batch of bottom-up messages for execution. diff --git a/contracts/contracts/subnet/SubnetActorActivityFacet.sol b/contracts/contracts/subnet/SubnetActorActivityFacet.sol new file mode 100644 index 000000000..49cd64b71 --- /dev/null +++ b/contracts/contracts/subnet/SubnetActorActivityFacet.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {Consensus} from "../structs/Activity.sol"; +import {LibActivity} from "../lib/LibActivity.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {NotAuthorized} from "../errors/IPCErrors.sol"; +import {Pausable} from "../lib/LibPausable.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {SubnetID} from "../structs/Subnet.sol"; + +/// The validator reward facet for the parent subnet, i.e. for validators in the child subnet +/// to claim their reward in the parent subnet, which should be the current subnet this facet +/// is deployed. +contract SubnetActorActivityFacet is ReentrancyGuard, Pausable { + // Entrypoint for validators to batch claim rewards in the parent subnet, for a given subnet, + // against multiple checkpoints at once. Atomically succeeds or reverts. + function batchSubnetClaim( + SubnetID calldata subnet, + uint64[] calldata checkpointHeights, + Consensus.ValidatorClaim[] calldata claims + ) external nonReentrant whenNotPaused { + require(checkpointHeights.length == claims.length, "length mismatch"); + uint256 len = claims.length; + for (uint256 i = 0; i < len; ) { + _claim(subnet, checkpointHeights[i], claims[i].data, claims[i].proof); + unchecked { + i++; + } + } + } + + /// Entrypoint for validators to claim their reward for doing work in the child subnet. + function claim( + SubnetID calldata subnet, + uint64 checkpointHeight, + Consensus.ValidatorData calldata data, + Consensus.MerkleHash[] calldata proof + ) external nonReentrant whenNotPaused { + _claim(subnet, checkpointHeight, data, proof); + } + + // ======== Internal functions =========== + + function _claim( + SubnetID calldata subnetId, + uint64 checkpointHeight, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof + ) internal { + // Note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight + // will never exist. + if (msg.sender != detail.validator) { + revert NotAuthorized(msg.sender); + } + + LibActivity.processConsensusClaim(subnetId, checkpointHeight, detail, proof); + } +} diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index ad540d61f..fd72786f9 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -13,7 +13,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {LibGateway} from "../lib/LibGateway.sol"; -import {LibValidatorReward} from "../activities/ValidatorRewardFacet.sol"; +import {LibActivity} from "../lib/LibActivity.sol"; contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using EnumerableSet for EnumerableSet.AddressSet; @@ -50,12 +50,7 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); - LibValidatorReward.initNewDistribution( - checkpoint.subnetID, - uint64(checkpoint.blockHeight), - checkpoint.activities.consensus.dataRootCommitment, - checkpoint.activities.consensus.stats.totalActiveValidators - ); + LibActivity.recordActivityRollup(checkpoint.subnetID, uint64(checkpoint.blockHeight), checkpoint.activity); // confirming the changes in membership in the child LibStaking.confirmChange(checkpoint.nextConfigurationNumber); diff --git a/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol b/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol index dea2c5eb7..ed9cb4f25 100644 --- a/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol +++ b/contracts/contracts/subnetregistry/RegisterSubnetFacet.sol @@ -83,7 +83,7 @@ contract RegisterSubnetFacet is ReentrancyGuard { diamondCut[8] = IDiamond.FacetCut({ facetAddress: s.VALIDATOR_REWARD_FACET, action: IDiamond.FacetCutAction.Add, - functionSelectors: s.validatorRewardSelectors + functionSelectors: s.subnetActorActivitySelectors }); // slither-disable-next-line reentrancy-benign diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index 50c0491e4..14a6ac1f7 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -45,7 +45,7 @@ task('deploy-registry') { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, { - name: 'ValidatorRewardFacet', + name: 'SubnetActorActivityFacet', libraries: ['SubnetIDHelper'], }, ) @@ -92,7 +92,7 @@ task('deploy-registry') diamondCutFacet: subnetActorFacets.addresses['DiamondCutFacet'], diamondLoupeFacet: subnetActorFacets.addresses['DiamondLoupeFacet'], ownershipFacet: subnetActorFacets.addresses['OwnershipFacet'], - validatorRewardFacet: subnetActorFacets.addresses['ValidatorRewardFacet'], + activityFacet: subnetActorFacets.addresses['SubnetActorActivityFacet'], subnetActorGetterSelectors: selectors(subnetActorFacets.contracts['SubnetActorGetterFacet']), subnetActorManagerSelectors: selectors(subnetActorFacets.contracts['SubnetActorManagerFacet']), @@ -102,7 +102,7 @@ task('deploy-registry') subnetActorDiamondCutSelectors: selectors(subnetActorFacets.contracts['DiamondCutFacet']), subnetActorDiamondLoupeSelectors: selectors(subnetActorFacets.contracts['DiamondLoupeFacet']), subnetActorOwnershipSelectors: selectors(subnetActorFacets.contracts['OwnershipFacet']), - validatorRewardSelectors: selectors(subnetActorFacets.contracts['ValidatorRewardFacet']), + subnetActorActivitySelectors: selectors(subnetActorFacets.contracts['SubnetActorActivityFacet']), creationPrivileges: Number(mode), } diff --git a/contracts/tasks/gen-selector-library.ts b/contracts/tasks/gen-selector-library.ts index 512060afb..a55db8ddb 100644 --- a/contracts/tasks/gen-selector-library.ts +++ b/contracts/tasks/gen-selector-library.ts @@ -6,14 +6,18 @@ import { selectors } from './lib' task('gen-selector-library', 'Generates a Solidity library with contract selectors for tests').setAction( async (_args, hre: HardhatRuntimeEnvironment) => { - await hre.run('compile') - // ridiculously, this appears to be the only way to get hardhat to compile a specific subtree // we are only updating the in-memory representation of the config, so it won't write this value out to disk // be careful if you compose this task with other tasks in larger scripts! + console.log('compiling mocks...') + const oldSources = hre.config.paths.sources hre.config.paths.sources = './test/mocks' await hre.run('compile') + hre.config.paths.sources = oldSources + console.log('compiling contracts...') + await hre.run('compile') + const contracts: string[] = [ 'OwnershipFacet', 'DiamondCutFacet', @@ -32,7 +36,7 @@ task('gen-selector-library', 'Generates a Solidity library with contract selecto 'RegisterSubnetFacet', 'SubnetGetterFacet', 'SubnetActorMock', - 'ValidatorRewardFacet', + 'SubnetActorActivityFacet', ] const resolveSelectors = async (contractName: string) => { diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 066c2e72e..19013b3d7 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,9 +46,9 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {FullActivityRollup, CompressedActivityRollup, Consensus} from "../contracts/activities/Activity.sol"; +import {FullActivityRollup, CompressedActivityRollup, Consensus} from "../contracts/structs/Activity.sol"; import {ValidatorRewarderMap} from "../contracts/examples/ValidatorRewarderMap.sol"; -import {ValidatorRewardFacet} from "../contracts/activities/ValidatorRewardFacet.sol"; +import {SubnetActorActivityFacet} from "../contracts/subnet/SubnetActorActivityFacet.sol"; struct TestSubnetDefinition { GatewayDiamond gateway; @@ -167,7 +167,7 @@ contract TestSubnetActor is Test, TestParams { bytes4[] saCutterSelectors; bytes4[] saLouperSelectors; bytes4[] saOwnershipSelectors; - bytes4[] validatorRewardSelectors; + bytes4[] saActivitySelectors; SubnetActorDiamond saDiamond; SubnetActorMock saMock; @@ -182,7 +182,7 @@ contract TestSubnetActor is Test, TestParams { saCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); saLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); saOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); - validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); + saActivitySelectors = SelectorLibrary.resolveSelectors("SubnetActorActivityFacet"); } function defaultSubnetActorParamsWith( @@ -499,7 +499,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, DiamondLoupeFacet louper = new DiamondLoupeFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); OwnershipFacet ownership = new OwnershipFacet(); - ValidatorRewardFacet validatorReward = new ValidatorRewardFacet(); + SubnetActorActivityFacet activity = new SubnetActorActivityFacet(); IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](9); @@ -569,9 +569,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, diamondCut[8] = ( IDiamond.FacetCut({ - facetAddress: address(validatorReward), + facetAddress: address(activity), action: IDiamond.FacetCutAction.Add, - functionSelectors: validatorRewardSelectors + functionSelectors: saActivitySelectors }) ); SubnetActorDiamond diamond = new SubnetActorDiamond(diamondCut, params, address(this)); @@ -934,7 +934,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: CompressedActivityRollup({ + activity: CompressedActivityRollup({ consensus: Consensus.CompressedSummary({ stats: Consensus.AggregatedStats({ totalActiveValidators: uint64(validators.length), @@ -977,7 +977,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: activities + activity: activities }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/helpers/ActivityHelper.sol b/contracts/test/helpers/ActivityHelper.sol index 037db293f..104fa5b15 100644 --- a/contracts/test/helpers/ActivityHelper.sol +++ b/contracts/test/helpers/ActivityHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {Consensus, CompressedActivityRollup} from "../../contracts/activities/Activity.sol"; +import {Consensus, CompressedActivityRollup, FullActivityRollup} from "../../contracts/structs/Activity.sol"; library ActivityHelper { function newCompressedActivityRollup( @@ -45,4 +45,15 @@ library ActivityHelper { return wrapped; } + + function dummyActivityRollup() internal pure returns (FullActivityRollup memory rollup) { + Consensus.ValidatorData[] memory data = new Consensus.ValidatorData[](0); + rollup = FullActivityRollup({ + consensus: Consensus.FullSummary({ + stats: Consensus.AggregatedStats({totalActiveValidators: 0, totalNumBlocksCommitted: 0}), + data: data + }) + }); + return rollup; + } } diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index f3b911eaf..c2789fb62 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -32,7 +32,7 @@ library MerkleTreeHelper { return (root, proofs); } - function createMerkleProofsForActivities( + function createMerkleProofsForConsensusActivity( address[] memory addrs, uint64[] memory blocksMined ) internal returns (bytes32, bytes32[][] memory) { diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 9dfe76e4c..ae80a4482 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a0316672000000000000000000000000000000000000000000000000000000002ea952910000000000000000000000000000000000000000000000000000000036bfdf6700000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000a031667200000000000000000000000000000000000000000000000000000000d8b4602500000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -122,7 +122,7 @@ library SelectorLibrary { (bytes4[]) ); } - if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorActivityFacet"))) { return abi.decode( hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000202eca6eb00000000000000000000000000000000000000000000000000000000f9d3434c00000000000000000000000000000000000000000000000000000000", diff --git a/contracts/test/helpers/SubnetActorFacetsHelper.sol b/contracts/test/helpers/SubnetActorFacetsHelper.sol index fcea95a4f..595b0ba93 100644 --- a/contracts/test/helpers/SubnetActorFacetsHelper.sol +++ b/contracts/test/helpers/SubnetActorFacetsHelper.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.23; import {SubnetActorManagerFacet} from "../../contracts/subnet/SubnetActorManagerFacet.sol"; import {SubnetActorPauseFacet} from "../../contracts/subnet/SubnetActorPauseFacet.sol"; import {SubnetActorCheckpointingFacet} from "../../contracts/subnet/SubnetActorCheckpointingFacet.sol"; +import {SubnetActorActivityFacet} from "../../contracts/subnet/SubnetActorActivityFacet.sol"; import {SubnetActorRewardFacet} from "../../contracts/subnet/SubnetActorRewardFacet.sol"; import {SubnetActorGetterFacet} from "../../contracts/subnet/SubnetActorGetterFacet.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; -import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; library SubnetActorFacetsHelper { function manager(address sa) internal pure returns (SubnetActorManagerFacet) { @@ -47,10 +47,8 @@ library SubnetActorFacetsHelper { return facet; } - // - - function validatorReward(SubnetActorDiamond sa) internal pure returns (ValidatorRewardFacet) { - ValidatorRewardFacet facet = ValidatorRewardFacet(address(sa)); + function activity(SubnetActorDiamond sa) internal pure returns (SubnetActorActivityFacet) { + SubnetActorActivityFacet facet = SubnetActorActivityFacet(address(sa)); return facet; } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index 69e53166a..6a96a6c3b 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/structs/Activity.sol"; import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { @@ -1071,7 +1071,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1080,13 +1080,18 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // failed to create a checkpoint with zero membership weight vm.startPrank(FilAddress.SYSTEM_ACTOR); vm.expectRevert(ZeroMembershipWeight.selector); - gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 0); + gatewayDiamond.checkpointer().createBottomUpCheckpoint( + checkpoint, + membershipRoot, + 0, + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); // failed create a processed checkpoint @@ -1095,7 +1100,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( old, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1104,7 +1110,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1122,7 +1129,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1130,7 +1137,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1146,7 +1154,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1168,7 +1176,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1215,7 +1223,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1236,7 +1244,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1245,7 +1253,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1253,12 +1261,14 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint1, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint2, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1310,7 +1320,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1318,7 +1328,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1372,7 +1383,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1380,7 +1391,8 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT gatewayDiamond.checkpointer().createBottomUpCheckpoint( checkpoint, membershipRoot, - weights[0] + weights[1] + weights[2] + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() ); vm.stopPrank(); @@ -1456,12 +1468,17 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint vm.startPrank(FilAddress.SYSTEM_ACTOR); - gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); + gatewayDiamond.checkpointer().createBottomUpCheckpoint( + checkpoint, + membershipRoot, + 10, + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); uint8 v; @@ -1491,12 +1508,17 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint vm.startPrank(FilAddress.SYSTEM_ACTOR); - gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); + gatewayDiamond.checkpointer().createBottomUpCheckpoint( + checkpoint, + membershipRoot, + 10, + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); uint8 v; @@ -1536,12 +1558,17 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint vm.startPrank(FilAddress.SYSTEM_ACTOR); - gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); + gatewayDiamond.checkpointer().createBottomUpCheckpoint( + checkpoint, + membershipRoot, + 10, + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); uint8 v; @@ -1585,10 +1612,15 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); - gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); + gatewayDiamond.checkpointer().createBottomUpCheckpoint( + checkpoint, + membershipRoot, + 10, + ActivityHelper.dummyActivityRollup() + ); } vm.stopPrank(); @@ -1649,7 +1681,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 3b239b599..b1d01eb35 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,7 +33,7 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/structs/Activity.sol"; import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { @@ -167,7 +167,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(address(saDiamond)); @@ -226,7 +226,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 1bf25a3f0..53654136d 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,7 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/structs/Activity.sol"; import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { @@ -1352,11 +1352,16 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); - checkpointer.createBottomUpCheckpoint(checkpoint, membershipRoot, weights[0] + weights[1] + weights[2]); + checkpointer.createBottomUpCheckpoint( + checkpoint, + membershipRoot, + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); return checkpoint; @@ -1382,11 +1387,16 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); - checkpointer.createBottomUpCheckpoint(checkpoint, membershipRoot, weights[0] + weights[1] + weights[2]); + checkpointer.createBottomUpCheckpoint( + checkpoint, + membershipRoot, + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() + ); vm.stopPrank(); return checkpoint; diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index ecfd54c18..5ead392fb 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/structs/Activity.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; import {ActivityHelper} from "../helpers/ActivityHelper.sol"; @@ -695,7 +695,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -704,7 +704,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -805,7 +805,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -814,7 +814,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -843,7 +843,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))); + checkpoint.activity = ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -900,7 +900,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -913,7 +913,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -925,7 +925,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -937,7 +937,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -948,7 +948,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -962,7 +962,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -976,7 +976,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -990,7 +990,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -1001,7 +1001,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1015,7 +1015,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1057,7 +1057,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -1101,7 +1101,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); hash = keccak256(abi.encode(checkpoint)); @@ -2334,7 +2334,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } // ============== Test Activities =============== - function testGatewayDiamond_ValidatorClaimMiningReward_Works() public { + function testSubnetActor_ValidatorClaimMiningReward_Works() public { gatewayAddress = address(gatewayDiamond); Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)}); @@ -2370,45 +2370,52 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blocksMined[0] = 1; blocksMined[1] = 2; - (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( + (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForConsensusActivity( addrs, blocksMined ); confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot)); + uint64 bottomUpCheckPeriod = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); - saDiamond.validatorReward().claim( + saDiamond.activity().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + bottomUpCheckPeriod, Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), ActivityHelper.wrapBytes32Array(proofs[0]) ); vm.startPrank(addrs[1]); vm.deal(addrs[1], 1 ether); - saDiamond.validatorReward().claim( + saDiamond.activity().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + bottomUpCheckPeriod, Consensus.ValidatorData({validator: addrs[1], blocksCommitted: blocksMined[1]}), ActivityHelper.wrapBytes32Array(proofs[1]) ); + // These validators have no claims; they were inactive, so the pending activity should've been removed + // and as a result, the claim should fail. + vm.startPrank(addrs[2]); vm.deal(addrs[2], 1 ether); - saDiamond.validatorReward().claim( + vm.expectRevert(MissingActivityCommitment.selector); + saDiamond.activity().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + bottomUpCheckPeriod, Consensus.ValidatorData({validator: addrs[2], blocksCommitted: blocksMined[2]}), ActivityHelper.wrapBytes32Array(proofs[2]) ); vm.startPrank(addrs[3]); vm.deal(addrs[3], 1 ether); - saDiamond.validatorReward().claim( + vm.expectRevert(MissingActivityCommitment.selector); + saDiamond.activity().claim( subnetId, - uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + bottomUpCheckPeriod, Consensus.ValidatorData({validator: addrs[3], blocksCommitted: blocksMined[3]}), ActivityHelper.wrapBytes32Array(proofs[3]) ); @@ -2420,7 +2427,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { assert(m.blocksCommitted(addrs[3]) == 0); } - function testGatewayDiamond_ValidatorBatchClaimMiningReward_Works() public { + function testSubnetActor_ValidatorBatchClaimMiningReward_Works() public { ValidatorRewarderMap m = new ValidatorRewarderMap(); { gatewayAddress = address(gatewayDiamond); @@ -2460,12 +2467,12 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blocksMined[0] = 1; blocksMined[1] = 2; - (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( + (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForConsensusActivity( addrs, blocksMined ); - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( + (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForConsensusActivity( addrs, blocksMined ); @@ -2491,13 +2498,13 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { proof: ActivityHelper.wrapBytes32Array(proofs2[0]) }); - saDiamond.validatorReward().batchSubnetClaim(subnetId, checkpointHeights, claimProofs); + saDiamond.activity().batchSubnetClaim(subnetId, checkpointHeights, claimProofs); // check assert(m.blocksCommitted(addrs[0]) == 2); } - function testGatewayDiamond_ValidatorBatchClaimMiningReward_NoDoubleClaim() public { + function testSubnetActor_ValidatorBatchClaimMiningReward_NoDoubleClaim() public { ValidatorRewarderMap m = new ValidatorRewarderMap(); { gatewayAddress = address(gatewayDiamond); @@ -2537,11 +2544,11 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blocksMined[0] = 1; blocksMined[1] = 2; - (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( + (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForConsensusActivity( addrs, blocksMined ); - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( + (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForConsensusActivity( addrs, blocksMined ); @@ -2568,7 +2575,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { }); vm.expectRevert(ValidatorAlreadyClaimed.selector); - saDiamond.validatorReward().batchSubnetClaim(subnetId, heights, claimProofs); + saDiamond.activity().batchSubnetClaim(subnetId, heights, claimProofs); } // ----------------------------------------------------------------------------------------------------------------- diff --git a/contracts/test/integration/SubnetRegistry.t.sol b/contracts/test/integration/SubnetRegistry.t.sol index 68e99f550..3703c3fbe 100644 --- a/contracts/test/integration/SubnetRegistry.t.sol +++ b/contracts/test/integration/SubnetRegistry.t.sol @@ -18,6 +18,7 @@ import {SubnetActorPauseFacet} from "../../contracts/subnet/SubnetActorPauseFace import {SubnetActorCheckpointingFacet} from "../../contracts/subnet/SubnetActorCheckpointingFacet.sol"; import {SubnetActorRewardFacet} from "../../contracts/subnet/SubnetActorRewardFacet.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; +import {SubnetActorActivityFacet} from "../../contracts/subnet/SubnetActorActivityFacet.sol"; import {SubnetID, PermissionMode, SubnetCreationPrivileges} from "../../contracts/structs/Subnet.sol"; import {SubnetRegistryDiamond} from "../../contracts/SubnetRegistryDiamond.sol"; @@ -29,7 +30,6 @@ import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; import {AssetHelper} from "../../contracts/lib/AssetHelper.sol"; import {RegistryFacetsHelper} from "../helpers/RegistryFacetsHelper.sol"; import {DiamondFacetsHelper} from "../helpers/DiamondFacetsHelper.sol"; -import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -68,7 +68,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); - params.validatorRewardFacet = address(new ValidatorRewardFacet()); + params.activityFacet = address(new SubnetActorActivityFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -78,7 +78,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); - params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); + params.subnetActorActivitySelectors = SelectorLibrary.resolveSelectors("SubnetActorActivityFacet"); params.creationPrivileges = SubnetCreationPrivileges.Unrestricted; @@ -174,7 +174,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { new SubnetRegistryDiamond(diamondCut, params); params.ownershipFacet = address(8); - params.validatorRewardFacet = address(9); + params.activityFacet = address(9); new SubnetRegistryDiamond(diamondCut, params); } diff --git a/contracts/test/invariants/SubnetRegistryInvariants.t.sol b/contracts/test/invariants/SubnetRegistryInvariants.t.sol index e26c32a2e..b6858f132 100644 --- a/contracts/test/invariants/SubnetRegistryInvariants.t.sol +++ b/contracts/test/invariants/SubnetRegistryInvariants.t.sol @@ -18,7 +18,7 @@ import {SubnetGetterFacet} from "../../contracts/subnetregistry/SubnetGetterFace import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; -import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; +import {SubnetActorActivityFacet} from "../../contracts/subnet/SubnetActorActivityFacet.sol"; import {IntegrationTestBase, TestRegistry} from "../IntegrationTestBase.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -52,7 +52,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); - params.validatorRewardFacet = address(new ValidatorRewardFacet()); + params.activityFacet = address(new SubnetActorActivityFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -62,7 +62,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); - params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); + params.subnetActorActivitySelectors = SelectorLibrary.resolveSelectors("SubnetActorActivityFacet"); registryDiamond = createSubnetRegistry(params); registryHandler = new SubnetRegistryHandler(registryDiamond); diff --git a/extras/linked-token/test/MultiSubnetTest.t.sol b/extras/linked-token/test/MultiSubnetTest.t.sol index 26d7d529b..8b421d7e3 100644 --- a/extras/linked-token/test/MultiSubnetTest.t.sol +++ b/extras/linked-token/test/MultiSubnetTest.t.sol @@ -9,6 +9,7 @@ import {TestUtils} from "@ipc/test/helpers/TestUtils.sol"; import {MerkleTreeHelper} from "@ipc/test/helpers/MerkleTreeHelper.sol"; import {GatewayFacetsHelper} from "@ipc/test/helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "@ipc/test/helpers/SubnetActorFacetsHelper.sol"; +import {ActivityHelper} from "@ipc/test/helpers/ActivityHelper.sol"; import {LinkedTokenController} from "../contracts/LinkedTokenController.sol"; import {LinkedTokenReplica} from "../contracts/LinkedTokenReplica.sol"; @@ -28,7 +29,7 @@ import {GatewayGetterFacet} from "@ipc/contracts/gateway/GatewayGetterFacet.sol" import {SubnetActorCheckpointingFacet} from "@ipc/contracts/subnet/SubnetActorCheckpointingFacet.sol"; import {CheckpointingFacet} from "@ipc/contracts/gateway/router/CheckpointingFacet.sol"; import {FvmAddressHelper} from "@ipc/contracts/lib/FvmAddressHelper.sol"; -import {Consensus, CompressedActivityRollup} from "@ipc/contracts/activities/Activity.sol"; +import {Consensus, CompressedActivityRollup} from "@ipc/contracts/structs/Activity.sol"; import {IpcEnvelope, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality, IpcMsgKind, ResultMsg, CallMsg} from "@ipc/contracts/structs/CrossNet.sol"; import {SubnetIDHelper} from "@ipc/contracts/lib/SubnetIDHelper.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -385,7 +386,7 @@ contract MultiSubnetTest is IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: CompressedActivityRollup({ + activity: CompressedActivityRollup({ consensus: Consensus.CompressedSummary({ stats: Consensus.AggregatedStats({ totalActiveValidators: 1, @@ -397,7 +398,7 @@ contract MultiSubnetTest is IntegrationTestBase { }); vm.startPrank(FilAddress.SYSTEM_ACTOR); - checkpointer.createBottomUpCheckpoint(checkpoint, membershipRoot, weights[0] + weights[1] + weights[2]); + checkpointer.createBottomUpCheckpoint(checkpoint, membershipRoot, weights[0] + weights[1] + weights[2], ActivityHelper.dummyActivityRollup()); vm.stopPrank(); return checkpoint; diff --git a/fendermint/testing/contract-test/tests/staking/machine.rs b/fendermint/testing/contract-test/tests/staking/machine.rs index 4926475f5..b950dd339 100644 --- a/fendermint/testing/contract-test/tests/staking/machine.rs +++ b/fendermint/testing/contract-test/tests/staking/machine.rs @@ -286,7 +286,7 @@ impl StateMachine for StakingMachine { block_hash: *block_hash, next_configuration_number: *next_configuration_number, msgs: Vec::new(), - activities: Default::default(), + activity: Default::default(), }; let checkpoint_hash = checkpoint.clone().abi_hash(); diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index 572df6af7..d3f4c18d7 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -119,8 +119,9 @@ lazy_static! { abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(), }, EthFacet { - name: "ValidatorRewardFacet", - abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), + name: "SubnetActorActivityFacet", + abi: ia::subnet_actor_activity_facet::SUBNETACTORACTIVITYFACET_ABI + .to_owned(), }, // ========== IF YOU WANT TO ADD FACET FOR SUBNET, APPEND HERE ========== // The registry has its own facets: @@ -475,7 +476,7 @@ pub mod registry { pub diamond_cut_facet: Address, pub diamond_loupe_facet: Address, pub ownership_facet: Address, - pub validator_reward_facet: Address, + pub activity_facet: Address, pub subnet_getter_selectors: Vec, pub subnet_manager_selectors: Vec, pub subnet_rewarder_selectors: Vec, @@ -484,7 +485,7 @@ pub mod registry { pub subnet_actor_diamond_cut_selectors: Vec, pub subnet_actor_diamond_loupe_selectors: Vec, pub subnet_actor_ownership_selectors: Vec, - pub validator_reward_selectors: Vec, + pub subnet_actor_activity_selectors: Vec, pub creation_privileges: u8, // 0 = Unrestricted, 1 = Owner. } } @@ -541,7 +542,7 @@ pub mod subnet { ], next_configuration_number: 1, msgs: vec![], - activities: Default::default(), + activity: Default::default(), }; let param_type = BottomUpCheckpoint::param_type(); diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 5aca2db83..6f077de65 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -94,7 +94,7 @@ where let num_msgs = msgs.len(); - let full_activity_rollup = state.activities_tracker().commit_activity()?; + let full_activity_rollup = state.activity_tracker().commit_activity()?; // Construct checkpoint. let checkpoint = BottomUpCheckpoint { @@ -103,7 +103,7 @@ where block_hash, next_configuration_number, msgs, - activities: full_activity_rollup.compressed()?, + activity: full_activity_rollup.compressed()?, }; // Save the checkpoint in the ledger. @@ -249,21 +249,21 @@ where block_hash: cp.block_hash, next_configuration_number: cp.next_configuration_number, msgs: convert_tokenizables(cp.msgs)?, - activities: checkpoint::CompressedActivityRollup { + activity: checkpoint::CompressedActivityRollup { consensus: checkpoint::CompressedSummary { stats: checkpoint::AggregatedStats { total_active_validators: cp - .activities + .activity .consensus .stats .total_active_validators, total_num_blocks_committed: cp - .activities + .activity .consensus .stats .total_num_blocks_committed, }, - data_root_commitment: cp.activities.consensus.data_root_commitment, + data_root_commitment: cp.activity.consensus.data_root_commitment, }, }, }; diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 3006218a5..803d54bc4 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -204,7 +204,7 @@ where let mut block_end_events = BlockEndEvents::default(); if let Some(pubkey) = state.block_producer() { - state.activities_tracker().record_block_committed(pubkey)?; + state.activity_tracker().record_block_committed(pubkey)?; } let next_gas_market = state.finalize_gas_market()?; diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 65b0a9438..bcd6570b8 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -292,7 +292,7 @@ where self.executor.context().network.chain_id } - pub fn activities_tracker(&mut self) -> ActorActivityTracker { + pub fn activity_tracker(&mut self) -> ActorActivityTracker { ActorActivityTracker { executor: self } } diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 2eb04c752..484ef1d52 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -123,7 +123,7 @@ impl GatewayCaller { state: &mut FvmExecState, checkpoint: checkpointing_facet::BottomUpCheckpoint, power_table: &[Validator], - activities: checkpointing_facet::FullActivityRollup, + activity: checkpointing_facet::FullActivityRollup, ) -> anyhow::Result { // Construct a Merkle tree from the power table, which we can use to validate validator set membership // when the signatures are submitted in transactions for accumulation. @@ -137,11 +137,11 @@ impl GatewayCaller { Ok(self .checkpointing .call_with_return(state, |c| { - c.create_bu_chpt_with_activities( + c.create_bottom_up_checkpoint( checkpoint, tree.root_hash().0, total_power, - activities, + activity, ) })? .into_return()) diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index c7458de3f..ce610e0bf 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -594,7 +594,7 @@ fn deploy_contracts( let diamond_loupe_facet = facets.remove(0); let diamond_cut_facet = facets.remove(0); let ownership_facet = facets.remove(0); - let validator_reward_facet = facets.remove(0); + let activity_facet = facets.remove(0); debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); @@ -608,7 +608,7 @@ fn deploy_contracts( diamond_cut_facet: diamond_cut_facet.facet_address, diamond_loupe_facet: diamond_loupe_facet.facet_address, ownership_facet: ownership_facet.facet_address, - validator_reward_facet: validator_reward_facet.facet_address, + activity_facet: activity_facet.facet_address, subnet_getter_selectors: getter_facet.function_selectors, subnet_manager_selectors: manager_facet.function_selectors, subnet_rewarder_selectors: rewarder_facet.function_selectors, @@ -617,7 +617,7 @@ fn deploy_contracts( subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, subnet_actor_ownership_selectors: ownership_facet.function_selectors, - validator_reward_selectors: validator_reward_facet.function_selectors, + subnet_actor_activity_selectors: activity_facet.function_selectors, creation_privileges: 0, }; diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index dd40829ea..0e15ed8e5 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -20,7 +20,7 @@ use fvm_shared::econ::TokenAmount; use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, - subnet_actor_getter_facet, top_down_finality_facet, validator_reward_facet, + subnet_actor_getter_facet, top_down_finality_facet, subnet_actor_activity_facet, xnet_messaging_facet, }; @@ -191,7 +191,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map($module::IpcEnvelope::try_from) .collect::, _>>()?, - activities: checkpoint.activity_rollup.try_into()?, + activity: checkpoint.activity_rollup.try_into()?, }) } } @@ -210,7 +210,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map(IpcEnvelope::try_from) .collect::, _>>()?, - activity_rollup: value.activities.into(), + activity_rollup: value.activity.into(), }) } } @@ -284,7 +284,7 @@ base_type_conversion!(subnet_actor_checkpointing_facet); base_type_conversion!(gateway_getter_facet); base_type_conversion!(gateway_messenger_facet); base_type_conversion!(lib_gateway); -base_type_conversion!(validator_reward_facet); +base_type_conversion!(subnet_actor_activity_facet); base_type_conversion!(checkpointing_facet); cross_msg_types!(gateway_getter_facet); diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 43d68971a..b5f2342ac 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -11,7 +11,7 @@ use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, lib_quorum, lib_staking_change_log, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_getter_facet, subnet_actor_manager_facet, - subnet_actor_reward_facet, validator_reward_facet, + subnet_actor_reward_facet, subnet_actor_activity_facet, }; use ipc_api::evm::{fil_to_eth_amount, payload_to_evm_address, subnet_id_to_evm_addresses}; use ipc_api::validator::from_contract_validators; @@ -43,7 +43,7 @@ use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, H256, I256 use ethers::middleware::Middleware; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; -use ipc_actors_abis::validator_reward_facet::ValidatorClaim; +use ipc_actors_abis::subnet_actor_activity_facet::ValidatorClaim; use ipc_api::checkpoint::{ consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, @@ -1342,7 +1342,7 @@ impl ValidatorRewarder for EthSubnetManager { let claim = ValidatorClaim { // Even though it's the same struct but still need to do a mapping due to // different crate from ethers-rs - data: validator_reward_facet::ValidatorData { + data: subnet_actor_activity_facet::ValidatorData { validator: data.validator, blocks_committed: data.blocks_committed, }, @@ -1410,7 +1410,7 @@ impl ValidatorRewarder for EthSubnetManager { claims: Vec<(u64, ValidatorClaim)>, ) -> Result<()> { let signer = Arc::new(self.get_signer(submitter)?); - let contract = validator_reward_facet::ValidatorRewardFacet::new( + let contract = subnet_actor_activity_facet::SubnetActorActivityFacet::new( contract_address_from_subnet(reward_claim_subnet)?, signer.clone(), ); diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index a52095949..6e31b4552 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -7,7 +7,7 @@ use anyhow::Result; use async_trait::async_trait; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; -use ipc_actors_abis::validator_reward_facet::ValidatorClaim; +use ipc_actors_abis::subnet_actor_activity_facet::ValidatorClaim; use ipc_api::checkpoint::{ consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, From 03e3ebdca9859bfcf9ea8a1fa69109fde451d6ed Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 27 Nov 2024 13:09:56 +0800 Subject: [PATCH 107/111] clean up --- Cargo.lock | 1 + .../contracts/lib/LibGatewayActorStorage.sol | 17 -------- .../lib/LibSubnetRegistryStorage.sol | 4 +- .../subnet/SubnetActorCheckpointingFacet.sol | 4 -- contracts/test/helpers/ActivityHelper.sol | 22 ++++------ .../vm/interpreter/src/fvm/activity/actor.rs | 16 +------- .../vm/interpreter/src/fvm/activity/merkle.rs | 38 ----------------- .../vm/interpreter/src/fvm/activity/mod.rs | 19 ++++----- .../vm/interpreter/src/fvm/state/exec.rs | 10 +++++ .../vm/interpreter/src/fvm/state/ipc.rs | 7 +--- ipc/api/Cargo.toml | 1 + ipc/api/src/checkpoint.rs | 2 + ipc/api/src/evm.rs | 6 +-- ipc/api/src/lib.rs | 1 + ipc/api/src/merkle.rs | 41 +++++++++++++++++++ ipc/cli/src/commands/checkpoint/relayer.rs | 7 ---- ipc/provider/src/manager/evm/manager.rs | 27 ++++-------- 17 files changed, 87 insertions(+), 136 deletions(-) delete mode 100644 fendermint/vm/interpreter/src/fvm/activity/merkle.rs create mode 100644 ipc/api/src/merkle.rs diff --git a/Cargo.lock b/Cargo.lock index 0c7a060ff..889fddf1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5146,6 +5146,7 @@ dependencies = [ "ipc_actors_abis", "lazy_static", "log", + "merkle-tree-rs", "num-traits", "num_enum", "serde", diff --git a/contracts/contracts/lib/LibGatewayActorStorage.sol b/contracts/contracts/lib/LibGatewayActorStorage.sol index 8e98f78f3..4cb63536e 100644 --- a/contracts/contracts/lib/LibGatewayActorStorage.sol +++ b/contracts/contracts/lib/LibGatewayActorStorage.sol @@ -102,20 +102,3 @@ contract GatewayActorModifiers { _; } } - -contract SystemContract { - using FilAddress for address; - using FilAddress for address payable; - using AccountHelper for address; - - function _systemActorOnly() private view { - if (!msg.sender.isSystemActor()) { - revert NotSystemActor(); - } - } - - modifier systemActorOnly() { - _systemActorOnly(); - _; - } -} \ No newline at end of file diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index 665a144fb..4d3a380aa 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -11,7 +11,7 @@ struct SubnetRegistryActorStorage { address SUBNET_ACTOR_GETTER_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_MANAGER_FACET; - /// TODO: this should be removed as it's for collateral withdraw only, not rewarder + /// TODO: see https://github.com/consensus-shipyard/ipc/issues/1217 // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_REWARD_FACET; // solhint-disable-next-line var-name-mixedcase @@ -31,7 +31,7 @@ struct SubnetRegistryActorStorage { /// The subnet actor manager facet functions selectors bytes4[] subnetActorManagerSelectors; /// The subnet actor reward facet functions selectors - /// TODO: this should be removed as it's for collateral withdraw only, not rewarder + /// TODO: see https://github.com/consensus-shipyard/ipc/issues/1217 bytes4[] subnetActorRewarderSelectors; /// The subnet actor checkpointing facet functions selectors bytes4[] subnetActorCheckpointerSelectors; diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index fd72786f9..160ab9e1d 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -43,10 +43,6 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, s.lastBottomUpCheckpointHeight = checkpoint.blockHeight; - // TODO(rewarder): if we have a non-zero validator rewarder at this level, queue the commitment for processing in storage (add to pending and presentable summaries). - // If we have a zero validator rewarder at this level, and we are the L1, discard the incoming commitments. - // If we have a zero validator rewarder at this level, and we are not the L1, relay the commitments upwards (add to pending summaries). - // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); diff --git a/contracts/test/helpers/ActivityHelper.sol b/contracts/test/helpers/ActivityHelper.sol index 104fa5b15..d4bda988c 100644 --- a/contracts/test/helpers/ActivityHelper.sol +++ b/contracts/test/helpers/ActivityHelper.sol @@ -9,25 +9,17 @@ library ActivityHelper { uint64 totalNumBlocksCommitted, bytes32 detailsRootCommitment ) internal pure returns (CompressedActivityRollup memory compressed) { - Consensus.CompressedSummary memory summary = newCompressedSummary( - totalActiveValidators, - totalNumBlocksCommitted, - detailsRootCommitment - ); + Consensus.CompressedSummary memory summary = Consensus.CompressedSummary({ + stats: Consensus.AggregatedStats({ + totalActiveValidators: totalActiveValidators, + totalNumBlocksCommitted: totalNumBlocksCommitted + }), + dataRootCommitment: Consensus.MerkleHash.wrap(detailsRootCommitment) + }); compressed.consensus = summary; return compressed; } - function newCompressedSummary( - uint64 totalActiveValidators, - uint64 totalNumBlocksCommitted, - bytes32 detailsRootCommitment - ) internal pure returns (Consensus.CompressedSummary memory summary) { - summary.stats.totalActiveValidators = totalActiveValidators; - summary.stats.totalNumBlocksCommitted = totalNumBlocksCommitted; - summary.dataRootCommitment = Consensus.MerkleHash.wrap(detailsRootCommitment); - } - function wrapBytes32Array(bytes32[] memory data) internal pure returns (Consensus.MerkleHash[] memory wrapped) { uint256 length = data.length; diff --git a/fendermint/vm/interpreter/src/fvm/activity/actor.rs b/fendermint/vm/interpreter/src/fvm/activity/actor.rs index 9948d0c53..289cd534c 100644 --- a/fendermint/vm/interpreter/src/fvm/activity/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/activity/actor.rs @@ -10,7 +10,6 @@ use fendermint_crypto::PublicKey; use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::system; -use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; use fvm_shared::address::Address; @@ -37,7 +36,7 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker gas_premium: Default::default(), }; - self.apply_implicit_message(msg)?; + self.executor.execute_implicit_ok(msg)?; Ok(()) } @@ -55,21 +54,10 @@ impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker gas_premium: Default::default(), }; - let (apply_ret, _) = self.executor.execute_implicit(msg)?; + let (apply_ret, _) = self.executor.execute_implicit_ok(msg)?; let r = fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) .context("failed to parse validator activities")?; r.try_into() } } - -impl<'a, DB: Blockstore + Clone + 'static> ActorActivityTracker<'a, DB> { - fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { - let (apply_ret, _) = self.executor.execute_implicit(msg)?; - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to apply activity tracker messages: {}", err) - } else { - Ok(apply_ret) - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activity/merkle.rs b/fendermint/vm/interpreter/src/fvm/activity/merkle.rs deleted file mode 100644 index 7a7586bcc..000000000 --- a/fendermint/vm/interpreter/src/fvm/activity/merkle.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use anyhow::Context; -use ipc_actors_abis::checkpointing_facet::ValidatorData; -use ipc_observability::lazy_static; -use merkle_tree_rs::format::Raw; -use merkle_tree_rs::standard::StandardMerkleTree; - -pub type Hash = ethers::types::H256; - -lazy_static!( - /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned()]; -); - -/// The merkle tree based proof verification to interact with solidity contracts -pub(crate) struct MerkleProofGen { - tree: StandardMerkleTree, -} - -impl MerkleProofGen { - pub fn pack_validator(v: &ValidatorData) -> Vec { - vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] - } - - pub fn root(&self) -> Hash { - self.tree.root() - } - - pub fn new(values: &[ValidatorData]) -> anyhow::Result { - let values = values.iter().map(Self::pack_validator).collect::>(); - - let tree = StandardMerkleTree::of(&values, &VALIDATOR_SUMMARY_FIELDS) - .context("failed to construct Merkle tree")?; - Ok(MerkleProofGen { tree }) - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activity/mod.rs b/fendermint/vm/interpreter/src/fvm/activity/mod.rs index d8519834f..0aa4113c8 100644 --- a/fendermint/vm/interpreter/src/fvm/activity/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activity/mod.rs @@ -5,15 +5,15 @@ //! needed. pub mod actor; -mod merkle; -use crate::fvm::activity::merkle::MerkleProofGen; use fendermint_crypto::PublicKey; use ipc_actors_abis::checkpointing_facet::{ AggregatedStats, CompressedActivityRollup, CompressedSummary, FullActivityRollup, FullSummary, ValidatorData, }; +use ipc_api::checkpoint::VALIDATOR_REWARD_FIELDS; use ipc_api::evm::payload_to_evm_address; +use ipc_api::merkle::MerkleGen; /// Wrapper for FullActivityRollup with some utility functions pub struct FullActivity(FullActivityRollup); @@ -49,13 +49,8 @@ impl TryFrom for F Ok(data) }) .collect::>>()?; - let consensus = FullSummary { - stats, - data, - }; - let f = FullActivityRollup { - consensus, - }; + let consensus = FullSummary { stats, data }; + let f = FullActivityRollup { consensus }; Ok(Self::new(f)) } } @@ -75,7 +70,11 @@ impl FullActivity { } pub fn compressed(&self) -> anyhow::Result { - let gen = MerkleProofGen::new(self.0.consensus.data.as_slice())?; + let gen = MerkleGen::new( + |v| vec![format!("{:?}", v.validator), v.blocks_committed.to_string()], + self.0.consensus.data.as_slice(), + &VALIDATOR_REWARD_FIELDS, + )?; Ok(CompressedActivityRollup { consensus: CompressedSummary { stats: self.0.consensus.stats.clone(), diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index bcd6570b8..d849328d7 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -196,6 +196,16 @@ where self.execute_message(msg, ApplyKind::Implicit) } + /// Execute message implicitly but ensures the execution is successful and returns only the ApplyRet. + pub fn execute_implicit_ok(&mut self, msg: Message) -> ExecResult { + let r = self.execute_implicit(msg)?; + if let Some(err) = &r.0.failure_info { + anyhow::bail!("failed to apply message: {}", err) + } else { + Ok(r) + } + } + /// Execute message explicitly. pub fn execute_explicit(&mut self, msg: Message) -> ExecResult { self.execute_message(msg, ApplyKind::Explicit) diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 484ef1d52..8fd153832 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -137,12 +137,7 @@ impl GatewayCaller { Ok(self .checkpointing .call_with_return(state, |c| { - c.create_bottom_up_checkpoint( - checkpoint, - tree.root_hash().0, - total_power, - activity, - ) + c.create_bottom_up_checkpoint(checkpoint, tree.root_hash().0, total_power, activity) })? .into_return()) } diff --git a/ipc/api/Cargo.toml b/ipc/api/Cargo.toml index 9cbcdceb1..df2c6bacb 100644 --- a/ipc/api/Cargo.toml +++ b/ipc/api/Cargo.toml @@ -31,6 +31,7 @@ serde_with = { workspace = true, features = ["hex"] } ipc_actors_abis = { workspace = true } ipc-types = { workspace = true } +merkle-tree-rs = { path = "../../ext/merkle-tree-rs" } [dev-dependencies] serde_json = { workspace = true } diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index e19691454..145e90a86 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -25,6 +25,8 @@ lazy_static! { // for storing the cid of an inaccessible HAMT. pub static ref CHECKPOINT_GENESIS_CID: Cid = Cid::new_v1(DAG_CBOR, Code::Blake2b256.digest("genesis".as_bytes())); + /// ABI types of the Merkle tree which contains validator addresses and their voting power. + pub static ref VALIDATOR_REWARD_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned()]; } pub type Signature = Vec; diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 0e15ed8e5..d19a853a3 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -19,9 +19,9 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, - lib_gateway, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, - subnet_actor_getter_facet, top_down_finality_facet, subnet_actor_activity_facet, - xnet_messaging_facet, + lib_gateway, register_subnet_facet, subnet_actor_activity_facet, + subnet_actor_checkpointing_facet, subnet_actor_diamond, subnet_actor_getter_facet, + top_down_finality_facet, xnet_messaging_facet, }; /// The type conversion for IPC structs to evm solidity contracts. We need this convenient macro because diff --git a/ipc/api/src/lib.rs b/ipc/api/src/lib.rs index dd9336935..9dd45bef3 100644 --- a/ipc/api/src/lib.rs +++ b/ipc/api/src/lib.rs @@ -19,6 +19,7 @@ pub mod subnet_id; pub mod validator; pub mod evm; +pub mod merkle; pub mod staking; /// Converts an ethers::U256 TokenAmount into a FIL amount. diff --git a/ipc/api/src/merkle.rs b/ipc/api/src/merkle.rs new file mode 100644 index 000000000..c631db02a --- /dev/null +++ b/ipc/api/src/merkle.rs @@ -0,0 +1,41 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +//! This is a util handles the merkle tree root and proof generation. + +use anyhow::Context; +use merkle_tree_rs::format::Raw; +use merkle_tree_rs::standard::{LeafType, StandardMerkleTree}; +use std::marker::PhantomData; + +pub type Hash = ethers::types::H256; + +pub struct MerkleGen { + /// The function that converts the payload `E` to vec string that feeds into the inner merkle tree + f_gen: F, + tree: StandardMerkleTree, + _p: PhantomData, +} + +impl Vec, E> MerkleGen { + pub fn root(&self) -> Hash { + self.tree.root() + } + + pub fn new(f: F, values: &[E], fields: &[S]) -> anyhow::Result { + let values = values.iter().map(&f).collect::>(); + + let tree = + StandardMerkleTree::of(&values, fields).context("failed to construct Merkle tree")?; + Ok(Self { + f_gen: f, + tree, + _p: Default::default(), + }) + } + + pub fn get_proof(&self, data: &E) -> anyhow::Result> { + let leaf = (self.f_gen)(data); + self.tree.get_proof(LeafType::LeafBytes(leaf)) + } +} diff --git a/ipc/cli/src/commands/checkpoint/relayer.rs b/ipc/cli/src/commands/checkpoint/relayer.rs index 2db1122a8..12a121880 100644 --- a/ipc/cli/src/commands/checkpoint/relayer.rs +++ b/ipc/cli/src/commands/checkpoint/relayer.rs @@ -32,13 +32,6 @@ impl CommandLineHandler for BottomUpRelayer { async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { log::debug!("start bottom up relayer with args: {:?}", arguments); - // TODO(rewards): enable the relayer to watch multiple subnets at once. - - // TODO(rewards): add a new flag --process-summaries to activate processing summaries on all subnets. - // Enabling this mode makes the relayer watch for ActivitySummaryCommitted events, and stores the summaries in a database. - // It then tracks which summaries have been committed to the root (right now we only support submitting to the L1), to chase - // after those and present them via SubnetActor#submitSummary in order to trigger reward payout. - // Prometheus metrics match &arguments.metrics_address { Some(addr) => { diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 9d43995c0..3a62b8780 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -10,8 +10,8 @@ use ethers_contract::{ContractError, EthLogDecode, LogMeta}; use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, lib_quorum, lib_staking_change_log, register_subnet_facet, - subnet_actor_checkpointing_facet, subnet_actor_getter_facet, subnet_actor_manager_facet, - subnet_actor_reward_facet, subnet_actor_activity_facet, + subnet_actor_activity_facet, subnet_actor_checkpointing_facet, subnet_actor_getter_facet, + subnet_actor_manager_facet, subnet_actor_reward_facet, }; use ipc_api::evm::{fil_to_eth_amount, payload_to_evm_address, subnet_id_to_evm_addresses}; use ipc_api::validator::from_contract_validators; @@ -48,16 +48,15 @@ use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_actors_abis::subnet_actor_activity_facet::ValidatorClaim; use ipc_api::checkpoint::{ consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, - Signature, + Signature, VALIDATOR_REWARD_FIELDS, }; use ipc_api::cross::IpcEnvelope; +use ipc_api::merkle::MerkleGen; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo, ValidatorStakingInfo}; use ipc_api::subnet::ConstructParams; use ipc_api::subnet_id::SubnetID; use ipc_observability::lazy_static; use ipc_wallet::{EthKeyAddress, EvmKeyStore, PersistentKeyStore}; -use merkle_tree_rs::format::Raw; -use merkle_tree_rs::standard::{LeafType, StandardMerkleTree}; use num_traits::ToPrimitive; use std::result; @@ -1439,22 +1438,10 @@ fn gen_merkle_proof( vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] }; - let tree = gen_merkle_tree(validator_data, pack_validator_data)?; + let leaves = order_validator_data(validator_data)?; + let tree = MerkleGen::new(pack_validator_data, &leaves, &VALIDATOR_REWARD_FIELDS)?; - let leaf = pack_validator_data(validator); - tree.get_proof(LeafType::LeafBytes(leaf)) -} - -fn gen_merkle_tree Vec>( - validator_data: &[checkpointing_facet::ValidatorData], - pack_validator_data: F, -) -> anyhow::Result> { - let leaves = order_validator_data(validator_data)? - .iter() - .map(pack_validator_data) - .collect::>(); - StandardMerkleTree::::of(&leaves, &VALIDATOR_SUMMARY_FIELDS) - .context("failed to construct Merkle tree") + tree.get_proof(validator) } fn order_validator_data( From 1d281bbf1277b279e7e59247fcae91bce133d1dc Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Wed, 27 Nov 2024 13:24:49 +0800 Subject: [PATCH 108/111] clippy --- ipc/provider/src/manager/evm/manager.rs | 44 +++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 3a62b8780..6aff4d5aa 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -1592,11 +1592,13 @@ impl TryFrom for SubnetInfo { #[cfg(test)] mod tests { - use crate::manager::evm::manager::{contract_address_from_subnet, gen_merkle_tree}; + use crate::manager::evm::manager::contract_address_from_subnet; use ethers::core::rand::prelude::SliceRandom; use ethers::core::rand::{random, thread_rng}; use fvm_shared::address::Address; use ipc_actors_abis::checkpointing_facet::{checkpointing_facet, ValidatorData}; + use ipc_api::checkpoint::VALIDATOR_REWARD_FIELDS; + use ipc_api::merkle::MerkleGen; use ipc_api::subnet_id::SubnetID; use std::str::FromStr; @@ -1659,9 +1661,13 @@ mod tests { ]; random_validator_data.shuffle(&mut thread_rng()); - let root = gen_merkle_tree(&random_validator_data, pack_validator_data) - .unwrap() - .root(); + let root = MerkleGen::new( + pack_validator_data, + &random_validator_data, + &VALIDATOR_REWARD_FIELDS, + ) + .unwrap() + .root(); assert_eq!( hex::encode(root.0), "5519955f33109df3338490473cb14458640efdccd4df05998c4c439738280ab0" @@ -1682,20 +1688,32 @@ mod tests { .collect::>(); random_validator_data.shuffle(&mut thread_rng()); - let root = gen_merkle_tree(&random_validator_data, pack_validator_data) - .unwrap() - .root(); + let root = MerkleGen::new( + pack_validator_data, + &random_validator_data, + &VALIDATOR_REWARD_FIELDS, + ) + .unwrap() + .root(); random_validator_data.shuffle(&mut thread_rng()); - let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) - .unwrap() - .root(); + let new_root = MerkleGen::new( + pack_validator_data, + &random_validator_data, + &VALIDATOR_REWARD_FIELDS, + ) + .unwrap() + .root(); assert_eq!(new_root, root); random_validator_data.shuffle(&mut thread_rng()); - let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) - .unwrap() - .root(); + let new_root = MerkleGen::new( + pack_validator_data, + &random_validator_data, + &VALIDATOR_REWARD_FIELDS, + ) + .unwrap() + .root(); assert_eq!(new_root, root); } } From 01c0d213b46cefe1b79a1b71f22d2aaac770af58 Mon Sep 17 00:00:00 2001 From: raulk Date: Wed, 27 Nov 2024 15:40:32 +0000 Subject: [PATCH 109/111] add checkpoint height to IValidatorRewarder#notifyValidClaim. --- .../contracts/examples/ValidatorRewarderMap.sol | 8 ++++++-- .../contracts/interfaces/IValidatorRewarder.sol | 12 ++++++++---- contracts/contracts/lib/LibActivity.sol | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 08022b698..61fce27cd 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -20,12 +20,16 @@ contract ValidatorRewarderMap is IValidatorRewarder, Ownable { subnetId = id; } - function notifyValidClaim(SubnetID calldata id, Consensus.ValidatorData calldata detail) external { + function notifyValidClaim( + SubnetID calldata id, + uint64 checkpointHeight, + Consensus.ValidatorData calldata data + ) external { require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); address actor = id.route[id.route.length - 1]; require(actor == msg.sender, "not from subnet"); - blocksCommitted[detail.validator] += detail.blocksCommitted; + blocksCommitted[data.validator] += data.blocksCommitted; } } diff --git a/contracts/contracts/interfaces/IValidatorRewarder.sol b/contracts/contracts/interfaces/IValidatorRewarder.sol index 44a9e2cec..55a7877ea 100644 --- a/contracts/contracts/interfaces/IValidatorRewarder.sol +++ b/contracts/contracts/interfaces/IValidatorRewarder.sol @@ -13,8 +13,12 @@ import {Consensus} from "../structs/Activity.sol"; /// This interface will be called by the subnet actor when a validator presents a _valid_ proof of consensus activity, /// via the SubnetActivityActivityFacet#claim method. interface IValidatorRewarder { - /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and - /// disburse any relevant rewards. - /// @dev This method should revert if the summary is invalid; this will cause the - function notifyValidClaim(SubnetID calldata id, Consensus.ValidatorData calldata validatedData) external; + /// Called by the subnet actor when a validator presents a _valid_ proof of consensus activity, via + /// SubnetActorActivityFacet#claim() or its batch equivalents. + /// @dev This method should revert if the summary is invalid; this will cause the claim submission to be rejected. + function notifyValidClaim( + SubnetID calldata id, + uint64 checkpointHeight, + Consensus.ValidatorData calldata validatedData + ) external; } diff --git a/contracts/contracts/lib/LibActivity.sol b/contracts/contracts/lib/LibActivity.sol index 4e72dd50c..ac2c736c3 100644 --- a/contracts/contracts/lib/LibActivity.sol +++ b/contracts/contracts/lib/LibActivity.sol @@ -146,7 +146,7 @@ library LibActivity { } // Notify the validator rewarder of a valid claim. - IValidatorRewarder(s.validatorRewarder).notifyValidClaim(subnet, data); + IValidatorRewarder(s.validatorRewarder).notifyValidClaim(subnet, checkpointHeight, data); // Prune state for this height if all validators have claimed. if (pending.claimed.length() == pending.summary.stats.totalActiveValidators) { From 06116c4954764f9cf0ac0987fa9e12190f9e479e Mon Sep 17 00:00:00 2001 From: raulk Date: Wed, 27 Nov 2024 15:51:15 +0000 Subject: [PATCH 110/111] temporarily remove docs from this branch to work on them. --- docs/fendermint/diagrams/activity_rollup1.png | Bin 119471 -> 0 bytes docs/fendermint/subnet_activities.md | 78 ------------------ 2 files changed, 78 deletions(-) delete mode 100644 docs/fendermint/diagrams/activity_rollup1.png delete mode 100644 docs/fendermint/subnet_activities.md diff --git a/docs/fendermint/diagrams/activity_rollup1.png b/docs/fendermint/diagrams/activity_rollup1.png deleted file mode 100644 index 6a2dc98cb771c9d6f72de8816e959a08b9c2d21a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119471 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

CY8Mp1Eup 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 AfdBvi diff --git a/docs/fendermint/subnet_activities.md b/docs/fendermint/subnet_activities.md deleted file mode 100644 index cf2c6d038..000000000 --- a/docs/fendermint/subnet_activities.md +++ /dev/null @@ -1,78 +0,0 @@ -# Subnet activities roll up - -## Overview -This doc introduces the notion of child subnet rolls up its activities through bottom-up checkpoints to its parent. -This is a mechanism to synthesise and propagate selective information about the activity that occurred inside a subnet to the parent. -The definition of `Activity` can be application specific, but generally, it represents critical information about the child subnet that -is valuable and can be acted upon in the parent(if there is one). Some sample activities could be: -- The blocks mined by each validator -- The total number of blocks mined in between bottom up checkpoints. -- ... - -The parent can choose to either relay this information up the hierarchy, or consume the incoming report for some effect, such as: -- rewarding validators by minting a token -- rebalancing the validator set based on performance metrics -- tracking subnet state -- ... - -For each application, it is possible to define its own `Activity` struct to handle different scenarios, but IPC tracks -the blocks mined by each validator and total number of blocks mined in between bottom up checkpoints out of the box. - -## Overall flow -The key idea is that child subnet tracks these activities and submits `commitment` to the parent subnet through bottom up -checkpoints. In the parent to trigger downstream effects, one needs to provide proofs to interact with the activities in the child subnet. -As an example, consider validator rewarding. The commitment is a merkle root over the blocks mined by each validator. This root is carried -to the parent as part of the bottom up checkpoint. The validator can claim its reward in the parent subnet by submitting a -merkle proof. The merkle proof can be deduced from child subnet. - -The key components are shown below: -![Activity Rollup Key Components](diagrams/activity_rollup1.png) -- Activity tracker actor: A fvm actor that tracks and aggregates the blockchain's activities. It allows querying and purging of subnet activities. -- Bottom up checkpoint: The bottom up checkpoint contains the commitment to subnet activities. -- Diamond facets: The different diamond facets to handle the follow-ups in the parent. IPC ships with `ValidatorRewardFacet` by default, but one can choose not to deploy it if not needed. - -## Key data models -The overall activity struct submitted in the child subnet is FullActivityRollUp. It carries a set of aggregated reports on various aspects of the -activity that took place in the subnet between the previous checkpoint and the checkpoint this summary is committed into. If this is the -first checkpoint, the summary contains information about the subnet's activity since genesis. - -The first report we introduce is the consensus level report. This report contains: -- Aggregated block mining stats: - - Total active validators: the total number of unique validators that have mined within this period. - - Total number of blocks committed: the total number of blocks committed by all validators during this period. -- Validator data: Contains an array of addresses and blocks mined for each validator. One can add more fields to track more activities, such as uptime metrics . - -We emit the full activities roll up as a local event, `ActivityRollupRecorded`, in the subnet, for stakeholders (e.g. validators) to monitor and present -any relevant payloads up in the hierarchy to trigger downstream effects. - -Over time, `FullActivityRollUp` can bloat, e.g. if the validator count in a subnet grows. When combined with xnet messages, the checkpoint size can become large, -which has detrimental effects on gas costs at the parent, and could even potentially exceed message size limits. Therefore, instead of embedding -the payloads in the checkpoint, we use a compressed summary in the bottom up checkpoint: CompressedActivityRollup. This struct contains the distilled information -that is critical to the parent subnet. At the moment, it contains the consensus level summary for validator rewarding. - -## Activity tacking in child subnet -TODO - -## Roll up in action -Once the activities are tracked in the child subnet, fendermint will generate the compressed summary and attach it as part of the bottom up checkpoint. -The generation of the compressed summary for each report could be different and generally application specific. -Once a quorum is reached in the child subnet, the relayer will submit the bottom up checkpoint to the parent subnet actor. Since the compressed -summary is part of the bottom up checkpoint, the compressed summary is forwarded to the parent subnet. - -## Validator rewards -For IPC, the validator rewarding mechanism is shipped out of the box. When the bottom up checkpoint is constructed, fendermint will query all -validator mining details from the activity tracker. It aggregates the number of blocks mined for each validator and merklize over the array. -Note that the validators are sorted by their address and blocks mined for unique ordering. -Since the `FullActivityRollUp` contains all validators' mining activities, validator can filter the `ActivityRollupRecorded` events against -fendermint's eth rpc endpoints to obtain the full list of activities to derive the merkle proof. There is an `ipc-cli` command to help validators -claim the reward. - -The bottom up checkpoint submitted in the parent subnet will initialize a new reward distribution for that checkpoint height. When a validator claims -the reward, the subnet actor will verify the merkle proof for the pending checkpoint height, and call the ValidatorRewarder implementation if it -succeeds, marking the fragment as claimed in its state, so it cannot be double claimed. - -IPC `ValidatorRewarderFacet` tracks the claim history of each validator to avoid double claiming. However, the detailed reward distribution -logic is actually handled by a custom contract, namely `IValidatorRewarder`. When the subnet is created, the creator can specify the address -of the validator rewarder implementation and when eligible validator claims the reward, the subnet actor will pass the validator address and -blocks mined to the rewarder implementation. As an example, the rewarder implementation could mint erc20 based on the parameters sent from subnet -actor. \ No newline at end of file From dd95a47c81699a555b03dcbb5aa175f662bca7a2 Mon Sep 17 00:00:00 2001 From: raulk Date: Wed, 27 Nov 2024 16:08:36 +0000 Subject: [PATCH 111/111] fix lint. --- contracts/contracts/examples/ValidatorRewarderMap.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 61fce27cd..98d1384f8 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -20,11 +20,7 @@ contract ValidatorRewarderMap is IValidatorRewarder, Ownable { subnetId = id; } - function notifyValidClaim( - SubnetID calldata id, - uint64 checkpointHeight, - Consensus.ValidatorData calldata data - ) external { + function notifyValidClaim(SubnetID calldata id, uint64, Consensus.ValidatorData calldata data) external { require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); address actor = id.route[id.route.length - 1];