Skip to content

Commit

Permalink
feat: customizable gas markets (with EIP-1559 default), base fee osci…
Browse files Browse the repository at this point in the history
…llation, premium distribution (#1173)

Co-authored-by: cryptoAtwill <willes.lau@protocol.ai>
raulk and cryptoAtwill authored Oct 17, 2024
1 parent 9b4df00 commit 702d49b
Showing 33 changed files with 1,533 additions and 153 deletions.
112 changes: 83 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -34,7 +34,10 @@ members = [
"fendermint/tracing",
"fendermint/vm/*",
"fendermint/actors",
"fendermint/actors/api",
"fendermint/actors/chainmetadata",
"fendermint/actors/eam",
"fendermint/actors/gas_market/eip1559",
]

[workspace.package]
@@ -179,6 +182,7 @@ ipc_ipld_resolver = { path = "ipld/resolver" }
ipc-types = { path = "ipc/types" }
ipc-observability = { path = "ipc/observability" }
ipc_actors_abis = { path = "contracts/binding" }
fendermint_actors_api = { path = "fendermint/actors/api" }

# Vendored for cross-compilation, see https://github.com/cross-rs/cross/wiki/Recipes#openssl
# Make sure every top level build target actually imports this dependency, and don't end up
8 changes: 5 additions & 3 deletions fendermint/actors/Cargo.toml
Original file line number Diff line number Diff line change
@@ -6,9 +6,8 @@ edition.workspace = true
license.workspace = true

[target.'cfg(target_arch = "wasm32")'.dependencies]
fendermint_actor_chainmetadata = { path = "chainmetadata", features = [
"fil-actor",
] }
fendermint_actor_chainmetadata = { path = "chainmetadata", features = ["fil-actor"] }
fendermint_actor_gas_market_eip1559 = { path = "gas_market/eip1559", features = ["fil-actor"] }
fendermint_actor_eam = { path = "eam", features = ["fil-actor"] }

[dependencies]
@@ -18,8 +17,11 @@ fvm_ipld_blockstore = { workspace = true }
fvm_ipld_encoding = { workspace = true }
fendermint_actor_chainmetadata = { path = "chainmetadata" }
fendermint_actor_eam = { path = "eam" }
fendermint_actor_gas_market_eip1559 = { path = "gas_market/eip1559" }

[build-dependencies]
anyhow = { workspace = true }
fil_actors_runtime = { workspace = true, features = ["test_utils"] }
fil_actor_bundler = "6.1.0"
num-traits = { workspace = true }
toml = "0.8.19"
34 changes: 34 additions & 0 deletions fendermint/actors/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "fendermint_actors_api"
description = "API and interface types for pluggable actors."
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"]
47 changes: 47 additions & 0 deletions fendermint/actors/api/src/gas_market.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use fil_actors_runtime::runtime::Runtime;
use fil_actors_runtime::ActorError;
use fvm_ipld_encoding::tuple::*;
use fvm_shared::econ::TokenAmount;
use num_derive::FromPrimitive;

pub type Gas = u64;

/// A reading of the current gas market state for use by consensus.
#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)]
pub struct Reading {
/// The current gas limit for the block.
pub block_gas_limit: Gas,
/// The current base fee for the block.
pub base_fee: TokenAmount,
}

/// The current utilization for the client to report to the gas market.
#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)]
pub struct Utilization {
/// The gas used by the current block, at the end of the block. To be invoked as an implicit
/// message, so that gas metering for this message is disabled.
pub block_gas_used: Gas,
}

#[derive(FromPrimitive)]
#[repr(u64)]
pub enum Method {
CurrentReading = frc42_dispatch::method_hash!("CurrentReading"),
UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"),
}

/// The trait to be implemented by a gas market actor, provided here for convenience,
/// using the standard Runtime libraries. Ready to be implemented as-is by an actor.
pub trait GasMarket {
/// Returns the current gas market reading.
fn current_reading(rt: &impl Runtime) -> Result<Reading, ActorError>;

/// Updates the current utilization in the gas market, returning the reading after the update.
fn update_utilization(
rt: &impl Runtime,
utilization: Utilization,
) -> Result<Reading, ActorError>;
}
4 changes: 4 additions & 0 deletions fendermint/actors/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod gas_market;
53 changes: 43 additions & 10 deletions fendermint/actors/build.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use anyhow::anyhow;
use fil_actor_bundler::Bundler;
use std::error::Error;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::thread;
use toml::Value;

fn parse_dependencies_for_wasm32() -> anyhow::Result<Vec<(String, String)>> {
let manifest = std::fs::read_to_string("Cargo.toml")?;
let document = manifest.parse::<Value>()?;

let dependencies = document
.get("target")
.and_then(|t| t.get(r#"cfg(target_arch = "wasm32")"#))
.and_then(|t| t.get("dependencies"))
.and_then(Value::as_table)
.ok_or_else(|| anyhow!("could not find wasm32 dependencies"))?;

let mut ret = Vec::with_capacity(dependencies.len());
for (name, details) in dependencies.iter() {
let path = details
.get("path")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("could not find path for a wasm32 dependency"))?;
ret.push((name.clone(), path.to_string()));
}

const ACTORS: &[&str] = &["chainmetadata", "eam"];
Ok(ret)
}

const FILES_TO_WATCH: &[&str] = &["Cargo.toml", "src"];

@@ -27,18 +50,20 @@ fn main() -> Result<(), Box<dyn Error>> {
Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR unset"))
.join("Cargo.toml");

for file in [FILES_TO_WATCH, ACTORS].concat() {
let actors = parse_dependencies_for_wasm32()?;
let actor_files = actors
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>();

for file in [FILES_TO_WATCH, actor_files.as_slice()].concat() {
println!("cargo:rerun-if-changed={}", file);
}

// Cargo build command for all test_actors at once.
let mut cmd = Command::new(cargo);
cmd.arg("build")
.args(
ACTORS
.iter()
.map(|pkg| "-p=fendermint_actor_".to_owned() + pkg),
)
.args(actors.iter().map(|(pkg, _)| "-p=".to_owned() + pkg))
.arg("--target=wasm32-unknown-unknown")
.arg("--profile=wasm")
.arg("--features=fil-actor")
@@ -88,17 +113,25 @@ fn main() -> Result<(), Box<dyn Error>> {

let dst = Path::new("output/custom_actors_bundle.car");
let mut bundler = Bundler::new(dst);
for (&pkg, id) in ACTORS.iter().zip(1u32..) {
for (pkg, id) in actors.iter().map(|(pkg, _)| pkg).zip(1u32..) {
let bytecode_path = Path::new(&out_dir)
.join("wasm32-unknown-unknown/wasm")
.join(format!("fendermint_actor_{}.wasm", pkg));
.join(format!("{}.wasm", pkg));

// This actor version doesn't force synthetic CIDs; it uses genuine
// content-addressed CIDs.
let forced_cid = None;

let actor_name = pkg
.to_owned()
.strip_prefix("fendermint_actor_")
.ok_or_else(|| {
format!("expected fendermint_actor_ prefix in actor package name; got: {pkg}")
})?
.to_owned();

let cid = bundler
.add_from_file(id, pkg.to_owned(), forced_cid, &bytecode_path)
.add_from_file(id, actor_name, forced_cid, &bytecode_path)
.unwrap_or_else(|err| {
panic!(
"failed to add file {:?} to bundle for actor {}: {}",
35 changes: 35 additions & 0 deletions fendermint/actors/gas_market/eip1559/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "fendermint_actor_gas_market_eip1559"
description = "EIP-1559 gas market actor for IPC. Singleton actor to be deployed at ID 98."
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 }
fendermint_actors_api = { 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"]
344 changes: 344 additions & 0 deletions fendermint/actors/gas_market/eip1559/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
// Copyright 2021-2023 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use fendermint_actors_api::gas_market::Gas;
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!(Actor);

pub const ACTOR_NAME: &str = "gas_market_eip1559";

pub type SetConstants = Constants;

#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)]
pub struct State {
pub base_fee: TokenAmount,
pub constants: Constants,
}

/// Constant params used by EIP-1559.
#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)]
pub struct Constants {
pub block_gas_limit: Gas,
/// The minimal base fee floor 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 ConstructorParams {
initial_base_fee: TokenAmount,
constants: Constants,
}

pub struct Actor {}

#[derive(FromPrimitive)]
#[repr(u64)]
pub enum Method {
Constructor = METHOD_CONSTRUCTOR,
GetConstants = frc42_dispatch::method_hash!("GetConstants"),
SetConstants = frc42_dispatch::method_hash!("SetConstants"),

// Standard methods.
CurrentReading = fendermint_actors_api::gas_market::Method::CurrentReading as u64,
UpdateUtilization = fendermint_actors_api::gas_market::Method::UpdateUtilization as u64,
}

impl Actor {
/// Creates the actor
pub fn constructor(rt: &impl Runtime, params: ConstructorParams) -> Result<(), ActorError> {
rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;

let st = State {
base_fee: params.initial_base_fee,
constants: params.constants,
};

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 State, _rt| {
st.constants = constants;
Ok(())
})?;

Ok(())
}

fn get_constants(rt: &impl Runtime) -> Result<Constants, ActorError> {
rt.validate_immediate_caller_accept_any()?;
rt.state::<State>().map(|s| s.constants)
}
}

impl fendermint_actors_api::gas_market::GasMarket for Actor {
fn current_reading(
rt: &impl Runtime,
) -> Result<fendermint_actors_api::gas_market::Reading, ActorError> {
rt.validate_immediate_caller_accept_any()?;

let st = rt.state::<State>()?;
Ok(fendermint_actors_api::gas_market::Reading {
block_gas_limit: st.constants.block_gas_limit,
base_fee: st.base_fee,
})
}

fn update_utilization(
rt: &impl Runtime,
utilization: fendermint_actors_api::gas_market::Utilization,
) -> Result<fendermint_actors_api::gas_market::Reading, ActorError> {
rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;

rt.transaction(|st: &mut State, _rt| {
st.base_fee = st.next_base_fee(utilization.block_gas_used);
Ok(fendermint_actors_api::gas_market::Reading {
block_gas_limit: st.constants.block_gas_limit,
base_fee: st.base_fee.clone(),
})
})
}
}

impl Default for Constants {
fn default() -> Self {
Self {
// Matching the Filecoin block gas limit. Note that IPC consensus != Filecoin Expected Consensus,
// TODO
block_gas_limit: 10_000_000_000,
// Matching Filecoin's minimal base fee.
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 State {
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))
}
}
}
}

// This import is necessary so that the actor_dispatch macro can find the methods on the GasMarket
// trait, implemented by Self.
use fendermint_actors_api::gas_market::GasMarket;

impl ActorCode for Actor {
type Methods = Method;

fn name() -> &'static str {
ACTOR_NAME
}

actor_dispatch! {
Constructor => constructor,
SetConstants => set_constants,
GetConstants => get_constants,

CurrentReading => current_reading,
UpdateUtilization => update_utilization,
}
}

#[cfg(test)]
mod tests {
use crate::{Actor, Constants, ConstructorParams, Method, State};
use fendermint_actors_api::gas_market::{Reading, Utilization};
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::<Actor>(
Method::Constructor as u64,
IpldBlock::serialize_cbor(&ConstructorParams {
initial_base_fee: TokenAmount::from_atto(100),
constants: Constants::default(),
})
.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::<Actor>(
Method::SetConstants as u64,
IpldBlock::serialize_cbor(&Constants {
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::<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::<Actor>(
Method::UpdateUtilization as u64,
IpldBlock::serialize_cbor(&Utilization {
// full block usage
block_gas_used: 10_000_000_000,
})
.unwrap(),
);
assert!(r.is_ok());

rt.expect_validate_caller_any();
let r = rt
.call::<Actor>(Method::CurrentReading as u64, None)
.unwrap()
.unwrap();
let reading = r.deserialize::<Reading>().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::<Actor>(
Method::UpdateUtilization as u64,
IpldBlock::serialize_cbor(&Utilization {
// full block usage
block_gas_used: 5_000_000_000,
})
.unwrap(),
);
assert!(r.is_ok());

rt.expect_validate_caller_any();
let r = rt
.call::<Actor>(Method::CurrentReading as u64, None)
.unwrap()
.unwrap();
let reading = r.deserialize::<Reading>().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::<Actor>(
Method::UpdateUtilization as u64,
IpldBlock::serialize_cbor(&Utilization {
// full block usage
block_gas_used: 100_000_000,
})
.unwrap(),
);
assert!(r.is_ok());

rt.expect_validate_caller_any();
let r = rt
.call::<Actor>(Method::CurrentReading as u64, None)
.unwrap()
.unwrap();
let reading = r.deserialize::<Reading>().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::<Actor>(
Method::SetConstants as u64,
IpldBlock::serialize_cbor(&Constants {
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)
}
}
7 changes: 6 additions & 1 deletion fendermint/actors/src/manifest.rs
Original file line number Diff line number Diff line change
@@ -4,12 +4,17 @@ use anyhow::{anyhow, Context};
use cid::Cid;
use fendermint_actor_chainmetadata::CHAINMETADATA_ACTOR_NAME;
use fendermint_actor_eam::IPC_EAM_ACTOR_NAME;
use fendermint_actor_gas_market_eip1559::ACTOR_NAME as GAS_MARKET_EIP1559_ACTOR_NAME;
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::CborStore;
use std::collections::HashMap;

// array of required actors
pub const REQUIRED_ACTORS: &[&str] = &[CHAINMETADATA_ACTOR_NAME, IPC_EAM_ACTOR_NAME];
pub const REQUIRED_ACTORS: &[&str] = &[
CHAINMETADATA_ACTOR_NAME,
IPC_EAM_ACTOR_NAME,
GAS_MARKET_EIP1559_ACTOR_NAME,
];

/// A mapping of internal actor CIDs to their respective types.
pub struct Manifest {
1 change: 1 addition & 0 deletions fendermint/app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ fendermint_rocksdb = { path = "../rocksdb" }
fendermint_rpc = { path = "../rpc" }
fendermint_storage = { path = "../storage" }
fendermint_tracing = { path = "../tracing" }
fendermint_actor_gas_market_eip1559 = { path = "../actors/gas_market/eip1559" }
fendermint_vm_actor_interface = { path = "../vm/actor_interface" }
fendermint_vm_core = { path = "../vm/core" }
fendermint_vm_encoding = { path = "../vm/encoding" }
129 changes: 93 additions & 36 deletions fendermint/app/src/app.rs
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ use fendermint_vm_interpreter::fvm::state::{
FvmUpdatableParams,
};
use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore;
use fendermint_vm_interpreter::fvm::{FvmApplyRet, PowerUpdates};
use fendermint_vm_interpreter::fvm::{BlockGasLimit, FvmApplyRet, PowerUpdates};
use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState};
use fendermint_vm_interpreter::signed::InvalidSignature;
use fendermint_vm_interpreter::{
@@ -41,12 +41,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};
@@ -115,10 +117,11 @@ pub struct AppConfig<S: KVStore> {

/// Handle ABCI requests.
#[derive(Clone)]
pub struct App<DB, SS, S, I>
pub struct App<DB, SS, S, I, C>
where
SS: Blockstore + Clone + 'static,
S: KVStore,
C: Client,
{
/// Database backing all key-value operations.
db: Arc<DB>,
@@ -159,9 +162,13 @@ where
///
/// Zero means unlimited.
state_hist_size: u64,
/// Tracks the validator
validators: ValidatorTracker<C>,
/// The cometbft client
client: C,
}

impl<DB, SS, S, I> App<DB, SS, S, I>
impl<DB, SS, S, I, C> App<DB, SS, S, I, C>
where
S: KVStore
+ Codec<AppState>
@@ -170,6 +177,7 @@ where
+ Codec<FvmStateParams>,
DB: KVWritable<S> + KVReadable<S> + Clone + 'static,
SS: Blockstore + Clone + 'static,
C: Client + Clone,
{
pub fn new(
config: AppConfig<S>,
@@ -178,6 +186,7 @@ where
interpreter: I,
chain_env: ChainEnv,
snapshots: Option<SnapshotClient>,
client: C,
) -> Result<Self> {
let app = Self {
db: Arc::new(db),
@@ -192,13 +201,15 @@ where
snapshots,
exec_state: Arc::new(tokio::sync::Mutex::new(None)),
check_state: Arc::new(tokio::sync::Mutex::new(None)),
validators: ValidatorTracker::new(client.clone()),
client,
};
app.init_committed_state()?;
Ok(app)
}
}

impl<DB, SS, S, I> App<DB, SS, S, I>
impl<DB, SS, S, I, C> App<DB, SS, S, I, C>
where
S: KVStore
+ Codec<AppState>
@@ -207,6 +218,7 @@ where
+ Codec<FvmStateParams>,
DB: KVWritable<S> + KVReadable<S> + 'static + Clone,
SS: Blockstore + 'static + Clone,
C: Client,
{
/// Get an owned clone of the state store.
fn state_store_clone(&self) -> SS {
@@ -312,34 +324,35 @@ where
Ok(ret)
}

/// Get a read only fvm execution state. This is useful to perform query commands targeting
/// the latest state.
pub fn new_read_only_exec_state(
/// Get a read-only view from the current FVM execution state, optionally passing a new BlockContext.
/// This is useful to perform query commands targeting the latest state. Mutations from transactions
/// will not be persisted.
pub fn read_only_view(
&self,
height: Option<BlockHeight>,
) -> Result<Option<FvmExecState<ReadOnlyBlockstore<Arc<SS>>>>> {
let maybe_app_state = self.get_committed_state()?;
let app_state = match self.get_committed_state()? {
Some(app_state) => app_state,
None => return Ok(None),
};

Ok(if let Some(app_state) = maybe_app_state {
let block_height = app_state.block_height;
let state_params = app_state.state_params;
let block_height = height.unwrap_or(app_state.block_height);
let state_params = app_state.state_params;

// wait for block production
if !Self::can_query_state(block_height, &state_params) {
return Ok(None);
}
// wait for block production
if !Self::can_query_state(block_height, &state_params) {
return Ok(None);
}

let exec_state = FvmExecState::new(
ReadOnlyBlockstore::new(self.state_store.clone()),
self.multi_engine.as_ref(),
block_height as ChainEpoch,
state_params,
)
.context("error creating execution state")?;
let exec_state = FvmExecState::new(
ReadOnlyBlockstore::new(self.state_store.clone()),
self.multi_engine.as_ref(),
block_height as ChainEpoch,
state_params,
)
.context("error creating execution state")?;

Some(exec_state)
} else {
None
})
Ok(Some(exec_state))
}

/// Look up a past state at a particular height Tendermint Core is looking for.
@@ -392,7 +405,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<DB, SS, S, I> Application for App<DB, SS, S, I>
impl<DB, SS, S, I, C> Application for App<DB, SS, S, I, C>
where
S: KVStore
+ Codec<AppState>
@@ -402,13 +415,16 @@ where
S::Namespace: Sync + Send,
DB: KVWritable<S> + KVReadable<S> + Clone + Send + Sync + 'static,
SS: Blockstore + Clone + Send + Sync + 'static,
I: ProposalInterpreter<State = ChainEnv, Message = Vec<u8>>,
I: ProposalInterpreter<
State = (ChainEnv, FvmExecState<ReadOnlyBlockstore<Arc<SS>>>),
Message = Vec<u8>,
>,
I: ExecInterpreter<
State = (ChainEnv, FvmExecState<SS>),
Message = Vec<u8>,
BeginOutput = FvmApplyRet,
DeliverOutput = BytesMessageApplyRes,
EndOutput = PowerUpdates,
EndOutput = (PowerUpdates, BlockGasLimit),
>,
I: CheckInterpreter<
State = FvmExecState<ReadOnlyBlockstore<SS>>,
@@ -420,6 +436,7 @@ where
Query = BytesMessageQuery,
Output = BytesMessageQueryRes,
>,
C: Client + Sync + Clone,
{
/// Provide information about the ABCI application.
async fn info(&self, _request: request::Info) -> AbciResult<response::Info> {
@@ -610,13 +627,19 @@ where
);
let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect();

let state = self
.read_only_view(Some(request.height.value()))?
.ok_or_else(|| anyhow!("exec state should be present"))?;

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 {
@@ -643,9 +666,13 @@ where
let size_txs = txs.iter().map(|tx| tx.len()).sum::<usize>();
let num_txs = txs.len();

let state = self
.read_only_view(Some(request.height.value()))?
.ok_or_else(|| anyhow!("exec state should be present"))?;

let accept = self
.interpreter
.process(self.chain_env.clone(), txs)
.process((self.chain_env.clone(), state), txs)
.await
.context("failed to process proposal")?;

@@ -704,10 +731,15 @@ 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_block_producer(validator);

tracing::debug!("initialized exec state");

@@ -761,15 +793,40 @@ where
async fn end_block(&self, request: request::EndBlock) -> AbciResult<response::EndBlock> {
tracing::debug!(height = request.height, "end block");

// TODO: Return events from epoch transitions.
let ret = self
// End the interpreter for this block.
let (power_updates, new_block_gas_limit) = self
.modify_exec_state(|s| self.interpreter.end(s))
.await
.context("end failed")?;

let r = to_end_block(ret)?;
// Convert the incoming power updates to Tendermint validator updates.
let validator_updates =
to_validator_updates(power_updates.0).context("failed to convert validator updates")?;

// If the block gas limit has changed, we need to update the consensus layer so it can
// pack subsequent blocks against the new limit.
let consensus_param_updates = {
let mut consensus_params = self
.client
.consensus_params(tendermint::block::Height::try_from(request.height)?)
.await?
.consensus_params;

if consensus_params.block.max_gas != new_block_gas_limit as i64 {
consensus_params.block.max_gas = new_block_gas_limit as i64;
Some(consensus_params)
} else {
None
}
};

Ok(r)
let ret = response::EndBlock {
validator_updates,
consensus_param_updates,
events: Vec::new(), // TODO: Return events from epoch transitions.
};

Ok(ret)
}

/// Commit the current state at the current height.
3 changes: 2 additions & 1 deletion fendermint/app/src/cmd/run.rs
Original file line number Diff line number Diff line change
@@ -295,7 +295,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,
@@ -311,6 +311,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 {
16 changes: 10 additions & 6 deletions fendermint/app/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -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<DB, SS, S, I>
pub struct AppParentFinalityQuery<DB, SS, S, I, C>
where
SS: Blockstore + Clone + 'static,
S: KVStore,
C: Client,
{
/// The app to get state
app: App<DB, SS, S, I>,
app: App<DB, SS, S, I, C>,
gateway_caller: GatewayCaller<ReadOnlyBlockstore<Arc<SS>>>,
}

impl<DB, SS, S, I> AppParentFinalityQuery<DB, SS, S, I>
impl<DB, SS, S, I, C> AppParentFinalityQuery<DB, SS, S, I, C>
where
S: KVStore
+ Codec<AppState>
@@ -43,8 +45,9 @@ where
+ Codec<FvmStateParams>,
DB: KVWritable<S> + KVReadable<S> + 'static + Clone,
SS: Blockstore + 'static + Clone,
C: Client,
{
pub fn new(app: App<DB, SS, S, I>) -> Self {
pub fn new(app: App<DB, SS, S, I, C>) -> Self {
Self {
app,
gateway_caller: GatewayCaller::default(),
@@ -55,14 +58,14 @@ where
where
F: FnOnce(FvmExecState<ReadOnlyBlockstore<Arc<SS>>>) -> anyhow::Result<T>,
{
match self.app.new_read_only_exec_state()? {
match self.app.read_only_view(None)? {
Some(s) => f(s).map(Some),
None => Ok(None),
}
}
}

impl<DB, SS, S, I> ParentFinalityStateQuery for AppParentFinalityQuery<DB, SS, S, I>
impl<DB, SS, S, I, C> ParentFinalityStateQuery for AppParentFinalityQuery<DB, SS, S, I, C>
where
S: KVStore
+ Codec<AppState>
@@ -71,6 +74,7 @@ where
+ Codec<FvmStateParams>,
DB: KVWritable<S> + KVReadable<S> + 'static + Clone,
SS: Blockstore + 'static + Clone,
C: Client,
{
fn get_latest_committed_finality(&self) -> anyhow::Result<Option<IPCParentFinality>> {
self.with_exec_state(|mut exec_state| {
1 change: 1 addition & 0 deletions fendermint/app/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
16 changes: 3 additions & 13 deletions fendermint/app/src/tmconv.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ 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 +148,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<response::EndBlock> {
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);
@@ -320,6 +308,8 @@ pub fn to_query(ret: FvmQueryRet, block_height: BlockHeight) -> anyhow::Result<r
}

/// Project Genesis validators to Tendermint.
/// 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<Validator<Power>>,
) -> anyhow::Result<Vec<tendermint::validator::Update>> {
69 changes: 69 additions & 0 deletions fendermint/app/src/validators.rs
Original file line number Diff line number Diff line change
@@ -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<C> {
client: C,
public_keys: Arc<RwLock<HashMap<tendermint::account::Id, PublicKey>>>,
}

impl<C: Client> ValidatorTracker<C> {
pub fn new(client: C) -> Self {
Self {
client,
public_keys: Arc::new(RwLock::new(HashMap::new())),
}
}
}

impl<C: Client + Sync> ValidatorTracker<C> {
/// 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<PublicKey> {
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<PublicKey> {
let keys = self.public_keys.read().unwrap();
keys.get(id).copied()
}
}
3 changes: 3 additions & 0 deletions fendermint/testing/contract-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ byteorder = { workspace = true }
ipc-api = { workspace = true }
ipc_actors_abis = { workspace = true }

fendermint_actors_api = { workspace = true }
fendermint_testing = { path = "..", features = ["smt", "arb"] }
fendermint_crypto = { path = "../../crypto" }
fendermint_vm_actor_interface = { path = "../../vm/actor_interface" }
@@ -44,3 +45,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_eip1559 = { path = "../../actors/gas_market/eip1559" }
28 changes: 23 additions & 5 deletions fendermint/testing/contract-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@
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_crypto::PublicKey;
use fendermint_vm_genesis::Genesis;
use fendermint_vm_interpreter::fvm::{BlockGasLimit, PowerUpdates};
use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput};
use fendermint_vm_interpreter::{
fvm::{
@@ -66,7 +67,7 @@ where
Message = FvmMessage,
BeginOutput = FvmApplyRet,
DeliverOutput = FvmApplyRet,
EndOutput = PowerUpdates,
EndOutput = (PowerUpdates, BlockGasLimit),
>,
{
pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result<Self> {
@@ -96,7 +97,7 @@ where
}

/// Take the execution state, update it, put it back, return the output.
async fn modify_exec_state<T, F, R>(&self, f: F) -> anyhow::Result<T>
pub async fn modify_exec_state<T, F, R>(&self, f: F) -> anyhow::Result<T>
where
F: FnOnce(FvmExecState<MemoryBlockstore>) -> R,
R: Future<Output = Result<(FvmExecState<MemoryBlockstore>, T)>>,
@@ -124,7 +125,7 @@ where
guard.take().expect("exec state empty")
}

pub async fn begin_block(&self, block_height: ChainEpoch) -> Result<()> {
pub async fn begin_block(&self, block_height: ChainEpoch, producer: PublicKey) -> Result<()> {
let mut block_hash: [u8; 32] = [0; 32];
let _ = block_hash.as_mut().write_i64::<BigEndian>(block_height);

@@ -134,7 +135,8 @@ where

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_block_hash(block_hash)
.with_block_producer(producer);

self.put_exec_state(state).await;

@@ -146,6 +148,22 @@ where
Ok(())
}

pub async fn execute_msgs(&self, msgs: Vec<FvmMessage>) -> 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;
}
Ok((s, ()))
})
.await
.context("execute msgs failed")
}

pub async fn end_block(&self, _block_height: ChainEpoch) -> Result<()> {
let _ret = self
.modify_exec_state(|s| self.interpreter.end(s))
327 changes: 327 additions & 0 deletions fendermint/testing/contract-test/tests/gas_market.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

mod staking;

use async_trait::async_trait;
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_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::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::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;

lazy_static! {
static ref ADDR: Address =
Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap();
static ref ADDR2: Address =
Address::new_secp256k1(&rand_secret_key().public_key().serialize()).unwrap();
}
const CHAIN_NAME: &str = "mychain";
type I = FvmMessageInterpreter<MemoryBlockstore, NeverCallClient>;

// returns a seeded secret key which is guaranteed to be the same every time
fn rand_secret_key() -> SecretKey {
SecretKey::random(&mut StdRng::seed_from_u64(123))
}

/// Creates a default tester with validator public key
async fn default_tester() -> (Tester<I>, PublicKey) {
tester_with_upgrader(UpgradeScheduler::new()).await
}

/// Creates a default tester with validator public key
async fn tester_with_upgrader(
upgrade_scheduler: UpgradeScheduler<MemoryBlockstore>,
) -> (Tester<I>, PublicKey) {
let validator = rand_secret_key().public_key();

let interpreter: FvmMessageInterpreter<MemoryBlockstore, _> =
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 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,
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::<Vec<Message>>();

let producer = rand_secret_key().public_key();

// block 1: set the gas constants
let height = 1;
tester.begin_block(height, producer).await.unwrap();
tester
.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, producer).await.unwrap();
let before_reading = tester
.modify_exec_state(|mut state| async {
let reading = state.read_gas_market()?;
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, producer).await.unwrap();
let post_full_block_reading = tester
.modify_exec_state(|mut state| async {
let reading = state.read_gas_market()?;
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, producer).await.unwrap();
let post_empty_block_reading = tester
.modify_exec_state(|mut state| async {
let reading = state.read_gas_market()?;
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,
to: *ADDR2,
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::<Vec<Message>>();

let proposer = rand_secret_key().public_key();

// iterate over all the upgrades
let height = 1;
tester.begin_block(height, proposer).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"
)
}

#[tokio::test]
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.
upgrader
.add(
Upgrade::new(CHAIN_NAME, 2, Some(1), move |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(())
})
})
.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();
let reading = tester
.modify_exec_state(|mut state| async {
let reading = state.read_gas_market()?;
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"
);
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();
let reading = tester
.modify_exec_state(|mut state| async {
let reading = state.read_gas_market()?;
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}"
);
}

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: 0,
value: 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(),
}
}

#[derive(Clone)]
struct NeverCallClient;

#[async_trait]
impl Client for NeverCallClient {
async fn perform<R>(&self, _request: R) -> Result<R::Output, tendermint_rpc::Error>
where
R: tendermint_rpc::SimpleRequest,
{
todo!()
}
}
4 changes: 3 additions & 1 deletion fendermint/testing/contract-test/tests/run_upgrades.rs
Original file line number Diff line number Diff line change
@@ -219,9 +219,11 @@ async fn test_applying_upgrades() {
// check that the app version is 0
assert_eq!(tester.state_params().app_version, 0);

let producer = my_secret_key().public_key();

// iterate over all the upgrades
for block_height in 1..=3 {
tester.begin_block(block_height).await.unwrap();
tester.begin_block(block_height, producer).await.unwrap();
tester.end_block(block_height).await.unwrap();
tester.commit().await.unwrap();

4 changes: 4 additions & 0 deletions fendermint/vm/actor_interface/src/gas_market.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

define_id!(GAS_MARKET { id: 98 });
1 change: 1 addition & 0 deletions fendermint/vm/actor_interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ pub mod diamond;
pub mod eam;
pub mod ethaccount;
pub mod evm;
pub mod gas_market;
pub mod init;
pub mod ipc;
pub mod multisig;
3 changes: 3 additions & 0 deletions fendermint/vm/interpreter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fendermint_actors_api = { workspace = true }
fendermint_vm_actor_interface = { path = "../actor_interface" }
fendermint_vm_core = { path = "../core" }
fendermint_vm_event = { path = "../event" }
@@ -23,6 +24,7 @@ fendermint_rpc = { path = "../../rpc" }
fendermint_tracing = { path = "../../tracing" }
fendermint_actors = { path = "../../actors" }
fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" }
fendermint_actor_gas_market_eip1559 = { path = "../../actors/gas_market/eip1559" }
fendermint_actor_eam = { workspace = true }
fendermint_testing = { path = "../../testing", optional = true }
ipc_actors_abis = { workspace = true }
@@ -73,6 +75,7 @@ quickcheck_macros = { workspace = true }
tempfile = { workspace = true }

fendermint_vm_interpreter = { path = ".", features = ["arb"] }
fendermint_vm_message = { path = "../message", features = ["arb"] }
fendermint_testing = { path = "../../testing", features = ["golden"] }
fvm = { workspace = true, features = ["arb", "testing"] }
fendermint_vm_genesis = { path = "../genesis", features = ["arb"] }
81 changes: 66 additions & 15 deletions fendermint/vm/interpreter/src/chain.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT
use crate::fvm::state::ipc::GatewayCaller;
use crate::fvm::{topdown, FvmApplyRet, PowerUpdates};
use crate::fvm::store::ReadOnlyBlockstore;
use crate::fvm::{topdown, BlockGasLimit, FvmApplyRet, PowerUpdates};
use crate::selector::{GasLimitSelector, MessageSelector};
use crate::{
fvm::state::FvmExecState,
fvm::FvmMessage,
signed::{SignedMessageApplyRes, SignedMessageCheckRes, SyntheticMessage, VerifiableMessage},
CheckInterpreter, ExecInterpreter, ProposalInterpreter, QueryInterpreter,
};
use anyhow::{bail, Context};
use anyhow::{anyhow, bail, Context};
use async_stm::atomically;
use async_trait::async_trait;
use fendermint_tracing::emit;
@@ -102,7 +104,7 @@ where
DB: Blockstore + Clone + 'static + Send + Sync,
I: Sync + Send,
{
type State = ChainEnv;
type State = (ChainEnv, FvmExecState<ReadOnlyBlockstore<Arc<DB>>>);
type Message = ChainMessage;

/// Check whether there are any "ready" messages in the IPLD resolution mempool which can be appended to the proposal.
@@ -111,11 +113,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<Self::Message>,
) -> anyhow::Result<Vec<Self::Message>> {
msgs = messages_selection(msgs, &state)?;

// 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 +128,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 +184,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<Self::Message>) -> anyhow::Result<bool> {
async fn process(
&self,
(chain_env, state): Self::State,
msgs: Vec<Self::Message>,
) -> anyhow::Result<bool> {
let mut block_gas_usage = 0;

for msg in msgs {
match msg {
ChainMessage::Ipc(IpcMessage::BottomUpExec(msg)) => {
@@ -187,7 +202,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 +221,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.block_gas_tracker().available())
}
}

@@ -226,7 +246,7 @@ where
Message = VerifiableMessage,
DeliverOutput = SignedMessageApplyRes,
State = FvmExecState<DB>,
EndOutput = PowerUpdates,
EndOutput = (PowerUpdates, BlockGasLimit),
>,
{
// The state consists of the resolver pool, which this interpreter needs, and the rest of the
@@ -368,7 +388,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
.block_producer()
.map(|id| hex::encode(id.serialize_compressed()));
let proposer_ref = proposer.as_deref();

atomically(|| {
@@ -412,9 +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.is_empty() {
if !out.0 .0.is_empty() {
let power_updates = out
.0
.0
.iter()
.map(|v| {
let vk = ValidatorKey::from(v.public_key.0);
@@ -531,3 +554,31 @@ fn relayed_bottom_up_ckpt_to_fvm(

Ok(msg)
}

/// Selects messages to be executed. Currently, this is a static function whose main purpose is to
/// coordinate various selectors. However, it does not have formal semantics for doing so, e.g.
/// do we daisy-chain selectors, do we parallelize, how do we treat rejections and acceptances?
/// It hasn't been well thought out yet. When we refactor the whole *Interpreter stack, we will
/// revisit this and make the selection function properly pluggable.
fn messages_selection<DB: Blockstore + Clone + 'static>(
msgs: Vec<ChainMessage>,
state: &FvmExecState<DB>,
) -> anyhow::Result<Vec<ChainMessage>> {
let mut user_msgs = msgs
.into_iter()
.map(|msg| match msg {
ChainMessage::Signed(inner) => Ok(inner),
ChainMessage::Ipc(_) => Err(anyhow!("should not have ipc messages in user proposals")),
})
.collect::<anyhow::Result<Vec<_>>>()?;

// Currently only one selector, we can potentially extend to more selectors
// This selector enforces that the total cumulative gas limit of all messages is less than the
// currently active block gas limit.
let selectors = vec![GasLimitSelector {}];
for s in selectors {
user_msgs = s.select_messages(state, user_msgs)
}

Ok(user_msgs.into_iter().map(ChainMessage::Signed).collect())
}
17 changes: 13 additions & 4 deletions fendermint/vm/interpreter/src/fvm/exec.rs
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ use super::{
checkpoint::{self, PowerUpdates},
observe::{CheckpointFinalized, MsgExec, MsgExecPurpose},
state::FvmExecState,
FvmMessage, FvmMessageInterpreter,
BlockGasLimit, FvmMessage, FvmMessageInterpreter,
};

/// The return value extended with some things from the message that
@@ -45,10 +45,10 @@ where
type Message = FvmMessage;
type BeginOutput = FvmApplyRet;
type DeliverOutput = FvmApplyRet;
/// Return validator power updates.
/// 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;
type EndOutput = (PowerUpdates, BlockGasLimit);

async fn begin(
&self,
@@ -157,6 +157,12 @@ where

(apply_ret, emitters, latency)
} else {
if let Err(err) = state.block_gas_tracker().ensure_sufficient_gas(&msg) {
// This is panic-worthy, but we suppress it to avoid liveness issues.
// Consider maybe record as evidence for the validator slashing?
tracing::warn!("insufficient block gas; continuing to avoid halt, but this should've not happened: {}", err);
}

let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone()));
let (apply_ret, emitters) = execution_result?;

@@ -186,6 +192,8 @@ where
}

async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> {
let next_gas_market = state.finalize_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)
.inspect_err(|e| {
@@ -244,6 +252,7 @@ where
PowerUpdates::default()
};

Ok((state, updates))
let ret = (updates, next_gas_market.block_gas_limit);
Ok((state, ret))
}
}
164 changes: 164 additions & 0 deletions fendermint/vm/interpreter/src/fvm/gas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::fvm::FvmMessage;
use anyhow::{bail, Context};

use fendermint_actors_api::gas_market::{Gas, Reading, Utilization};
use fendermint_vm_actor_interface::gas_market::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::econ::TokenAmount;
use fvm_shared::METHOD_SEND;
use num_traits::Zero;

#[derive(Debug, Clone)]
pub struct BlockGasTracker {
/// The current base fee.
base_fee: TokenAmount,
/// The current block gas limit.
block_gas_limit: Gas,
/// The cumulative gas premiums claimable by the block producer.
cumul_gas_premium: TokenAmount,
/// The accumulated gas usage throughout the block.
cumul_gas_used: Gas,
}

impl BlockGasTracker {
pub fn create<E: Executor>(executor: &mut E) -> anyhow::Result<BlockGasTracker> {
let mut ret = Self {
base_fee: Zero::zero(),
block_gas_limit: Zero::zero(),
cumul_gas_premium: Zero::zero(),
cumul_gas_used: Zero::zero(),
};

let reading = Self::read_gas_market(executor)?;

ret.base_fee = reading.base_fee;
ret.block_gas_limit = reading.block_gas_limit;

Ok(ret)
}

pub fn available(&self) -> Gas {
self.block_gas_limit.saturating_sub(self.cumul_gas_used)
}

pub fn ensure_sufficient_gas(&self, msg: &FvmMessage) -> anyhow::Result<()> {
let available_gas = self.available();
if msg.gas_limit > available_gas {
bail!("message gas limit exceed available block gas limit; consensus engine may be misbehaving; txn gas limit: {}, block gas available: {}",
msg.gas_limit,
available_gas
);
}
Ok(())
}

pub fn record_utilization(&mut self, ret: &ApplyRet) {
self.cumul_gas_premium += ret.miner_tip.clone();
self.cumul_gas_used = self.cumul_gas_used.saturating_add(ret.msg_receipt.gas_used);

// sanity check, should not happen; only trace if it does so we can debug later.
if self.cumul_gas_used >= self.block_gas_limit {
tracing::warn!("out of block gas; cumulative gas used exceeds block gas limit!");
}
}

pub fn finalize<E: Executor>(
&self,
executor: &mut E,
premium_recipient: Option<Address>,
) -> anyhow::Result<Reading> {
if let Some(premium_recipient) = premium_recipient {
self.distribute_premiums(executor, premium_recipient)?
}
self.commit_utilization(executor)
}

pub fn read_gas_market<E: Executor>(executor: &mut E) -> anyhow::Result<Reading> {
let msg = FvmMessage {
from: system::SYSTEM_ACTOR_ADDR,
to: GAS_MARKET_ACTOR_ADDR,
sequence: 0, // irrelevant for implicit executions.
gas_limit: i64::MAX as u64,
method_num: fendermint_actors_api::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 = Self::apply_implicit_message(executor, msg)?;

if let Some(err) = apply_ret.failure_info {
bail!("failed to acquire gas market reading: {}", err);
}

fvm_ipld_encoding::from_slice::<Reading>(&apply_ret.msg_receipt.return_data)
.context("failed to parse gas market reading")
}

fn commit_utilization<E: Executor>(&self, executor: &mut E) -> anyhow::Result<Reading> {
let params = fvm_ipld_encoding::RawBytes::serialize(Utilization {
block_gas_used: self.cumul_gas_used,
})?;

let msg = FvmMessage {
from: system::SYSTEM_ACTOR_ADDR,
to: GAS_MARKET_ACTOR_ADDR,
sequence: 0, // irrelevant for implicit executions.
gas_limit: i64::MAX as u64,
method_num: fendermint_actors_api::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(executor, msg)?;
fvm_ipld_encoding::from_slice::<Reading>(&apply_ret.msg_receipt.return_data)
.context("failed to parse gas utilization result")
}

fn distribute_premiums<E: Executor>(
&self,
executor: &mut E,
premium_recipient: Address,
) -> anyhow::Result<()> {
if self.cumul_gas_premium.is_zero() {
return Ok(());
}

let msg = FvmMessage {
from: reward::REWARD_ACTOR_ADDR,
to: premium_recipient,
sequence: 0, // irrelevant for implicit executions.
gas_limit: i64::MAX as u64,
method_num: METHOD_SEND,
params: fvm_ipld_encoding::RawBytes::default(),
value: self.cumul_gas_premium.clone(),
version: Default::default(),
gas_fee_cap: Default::default(),
gas_premium: Default::default(),
};
Self::apply_implicit_message(executor, msg)?;

Ok(())
}

fn apply_implicit_message<E: Executor>(
executor: &mut E,
msg: FvmMessage,
) -> anyhow::Result<ApplyRet> {
let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, 0)?;
if let Some(err) = apply_ret.failure_info {
bail!("failed to apply message: {}", err)
}
Ok(apply_ret)
}
}
4 changes: 4 additions & 0 deletions fendermint/vm/interpreter/src/fvm/mod.rs
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@ pub mod upgrades;

#[cfg(any(test, feature = "bundle"))]
pub mod bundle;

pub(crate) mod gas;
pub(crate) mod topdown;

pub use check::FvmCheckRet;
@@ -29,6 +31,8 @@ pub use self::broadcast::Broadcaster;
use self::{state::ipc::GatewayCaller, upgrades::UpgradeScheduler};

pub type FvmMessage = fvm_shared::message::Message;
pub type BaseFee = fvm_shared::econ::TokenAmount;
pub type BlockGasLimit = u64;

#[derive(Clone)]
pub struct ValidatorContext<C> {
91 changes: 66 additions & 25 deletions fendermint/vm/interpreter/src/fvm/state/exec.rs
Original file line number Diff line number Diff line change
@@ -3,8 +3,15 @@

use std::collections::{HashMap, HashSet};

use crate::fvm::externs::FendermintExterns;
use crate::fvm::gas::BlockGasTracker;
use anyhow::Ok;
use cid::Cid;
use fendermint_actors_api::gas_market::Reading;
use fendermint_crypto::PublicKey;
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::{
call_manager::DefaultCallManager,
@@ -23,15 +30,8 @@ use fvm_shared::{
use serde::{Deserialize, Serialize};
use serde_with::serde_as;

use crate::fvm::externs::FendermintExterns;
use fendermint_vm_core::{chainid::HasChainID, Timestamp};
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<ActorID, Address>;

/// The result of the message application bundled with any delegated addresses of event emitters.
@@ -99,18 +99,19 @@ where
executor: DefaultExecutor<
DefaultKernel<DefaultCallManager<DefaultMachine<DB, FendermintExterns<DB>>>>,
>,

/// Hash of the block currently being executed. For queries and checks this is empty.
///
/// The main motivation to add it here was to make it easier to pass in data to the
/// execution interpreter without having to add yet another piece to track at the app level.
block_hash: Option<BlockHash>,

/// ID of the validator who created this block. For queries and checks this is empty.
validator_id: Option<ValidatorId>,
/// Public key of the validator who created this block. For queries, checks, and proposal
/// validations this is None.
block_producer: Option<PublicKey>,
/// 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).
block_gas_tracker: BlockGasTracker,
/// State of parameters that are outside the control of the FVM but can change and need to be persisted.
params: FvmUpdatableParams,

/// Indicate whether the parameters have been updated.
params_dirty: bool,
}
@@ -146,12 +147,15 @@ 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 block_gas_tracker = BlockGasTracker::create(&mut executor)?;

Ok(Self {
executor,
block_hash: None,
validator_id: None,
block_producer: None,
block_gas_tracker,
params: FvmUpdatableParams {
app_version: params.app_version,
base_fee: params.base_fee,
@@ -169,11 +173,23 @@ 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_block_producer(mut self, pubkey: PublicKey) -> Self {
self.block_producer = Some(pubkey);
self
}

pub fn block_gas_tracker(&self) -> &BlockGasTracker {
&self.block_gas_tracker
}

pub fn block_gas_tracker_mut(&mut self) -> &mut BlockGasTracker {
&mut self.block_gas_tracker
}

pub fn read_gas_market(&mut self) -> anyhow::Result<Reading> {
BlockGasTracker::read_gas_market(&mut self.executor)
}

/// Execute message implicitly.
pub fn execute_implicit(&mut self, msg: Message) -> ExecResult {
self.execute_message(msg, ApplyKind::Implicit)
@@ -193,9 +209,27 @@ where
let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?;
let ret = self.executor.execute_message(msg, kind, raw_length)?;
let addrs = self.emitter_delegated_addresses(&ret)?;

// Record the utilization of this message if the apply type was Explicit.
if kind == ApplyKind::Explicit {
self.block_gas_tracker.record_utilization(&ret);
}

Ok((ret, addrs))
}

/// Execute a function with the internal executor and return an arbitrary result.
pub fn execute_with_executor<F, R>(&mut self, exec_func: F) -> anyhow::Result<R>
where
F: FnOnce(
&mut DefaultExecutor<
DefaultKernel<DefaultCallManager<DefaultMachine<DB, FendermintExterns<DB>>>>,
>,
) -> anyhow::Result<R>,
{
exec_func(&mut self.executor)
}

/// Commit the state. It must not fail, but we're returning a result so that error
/// handling can be done in the application root.
///
@@ -218,9 +252,9 @@ where
self.block_hash
}

/// Identity of the block creator, if we are indeed executing any blocks.
pub fn validator_id(&self) -> Option<ValidatorId> {
self.validator_id
/// Identity of the block producer, if we are indeed executing any blocks.
pub fn block_producer(&self) -> Option<PublicKey> {
self.block_producer
}

/// The timestamp of the currently executing block.
@@ -286,12 +320,19 @@ where
self.update_params(|p| f(&mut p.app_version))
}

/// Update the application version.
pub fn update_base_fee<F>(&mut self, f: F)
where
F: FnOnce(&mut TokenAmount),
{
self.update_params(|p| f(&mut p.base_fee))
/// Finalizes updates to the gas market based on the transactions processed by this instance.
/// Returns the new base fee for the next height.
pub fn finalize_gas_market(&mut self) -> anyhow::Result<Reading> {
let premium_recipient = match self.block_producer {
Some(pubkey) => Some(Address::from(EthAddress::new_secp256k1(
&pubkey.serialize(),
)?)),
None => None,
};

self.block_gas_tracker
.finalize(&mut self.executor, premium_recipient)
.inspect(|reading| self.update_params(|p| p.base_fee = reading.base_fee.clone()))
}

/// Update the circulating supply, effective from the next block.
7 changes: 4 additions & 3 deletions fendermint/vm/interpreter/src/fvm/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod fevm;
pub mod ipc;
pub mod snapshot;

mod check;
mod exec;
pub mod fevm;
mod genesis;
pub mod ipc;
mod query;
pub mod snapshot;

use std::sync::Arc;

24 changes: 23 additions & 1 deletion fendermint/vm/interpreter/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -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, 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};
@@ -430,6 +430,28 @@ impl GenesisBuilder {
)
.context("failed to replace built in eam actor")?;

// Currently hardcoded for now, once genesis V2 is implemented, should be taken
// from genesis parameters.
//
// Default initial base fee equals minimum base fee in Filecoin.
let initial_base_fee = TokenAmount::from_atto(100);
// We construct the actor state here for simplicity, but for better decoupling we should
// be invoking the constructor instead.
let gas_market_state = fendermint_actor_gas_market_eip1559::State {
base_fee: initial_base_fee,
// If you need to customize the gas market constants, you can do so here.
constants: fendermint_actor_gas_market_eip1559::Constants::default(),
};
state
.create_custom_actor(
fendermint_actor_gas_market_eip1559::ACTOR_NAME,
gas_market::GAS_MARKET_ACTOR_ID,
&gas_market_state,
TokenAmount::zero(),
None,
)
.context("failed to create default eip1559 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.
1 change: 1 addition & 0 deletions fendermint/vm/interpreter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ pub mod signed;

#[cfg(feature = "arb")]
mod arb;
mod selector;

/// Prepare and process transaction proposals.
#[async_trait]
44 changes: 44 additions & 0 deletions fendermint/vm/interpreter/src/selector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

//! Gas related message selection
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<DB: Blockstore + Clone + 'static>(
&self,
state: &FvmExecState<DB>,
msgs: Vec<SignedMessage>,
) -> Vec<SignedMessage>;
}

pub(crate) struct GasLimitSelector;

impl MessageSelector for GasLimitSelector {
fn select_messages<DB: Blockstore + Clone + 'static>(
&self,
state: &FvmExecState<DB>,
mut msgs: Vec<SignedMessage>,
) -> Vec<SignedMessage> {
let total_gas_limit = state.block_gas_tracker().available();

// 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;
msgs.into_iter()
.take_while(|msg| {
let gas_limit = msg.message.gas_limit;
let accepted = total_gas_limit_consumed + gas_limit <= total_gas_limit;
if accepted {
total_gas_limit_consumed += gas_limit;
}
accepted
})
.collect()
}
}

0 comments on commit 702d49b

Please sign in to comment.