diff --git a/.github/workflows/auto-deploy-contracts.yaml b/.github/workflows/auto-deploy-contracts.yaml index 2403137bc..26228a549 100644 --- a/.github/workflows/auto-deploy-contracts.yaml +++ b/.github/workflows/auto-deploy-contracts.yaml @@ -1,4 +1,4 @@ -name: Auto deploy IPC contracts when changed +name: Auto-deploy IPC contracts to Calibrationnet when changed on: workflow_dispatch: @@ -8,6 +8,9 @@ on: paths: - contracts/** +permissions: + contents: write + env: GIT_USERNAME: github-actions[bot] GIT_EMAIL: ipc+github-actions[bot]@users.noreply.github.com @@ -20,72 +23,79 @@ concurrency: jobs: deploy-contracts: runs-on: ubuntu-latest + env: - RPC_URL: https://calibration.filfox.info/rpc/v1 + RPC_URL: https://calibration.filfox.io/rpc/v1 PRIVATE_KEY: ${{ secrets.CONTRACTS_DEPLOYER_PRIVATE_KEY }} - steps: - - name: Checkout cd/contracts branch - uses: actions/checkout@v4 - with: - ref: cd/contracts - submodules: recursive - fetch-depth: 0 - token: ${{ secrets.WORKFLOW_PAT_JIE }} - - name: (Dry run) Try merge from main branch to see if there's any conflicts that can't be resolved itself + steps: + - name: Configure git run: | - git show HEAD git config --global user.name "$GIT_USERNAME" git config --global user.email "$GIT_EMAIL" - git checkout main - git pull --rebase origin main - git checkout cd/contracts - git merge main --no-edit --allow-unrelated-histories - - name: Checkout the branch that triggered this run + - name: Check out the branch that triggered this run uses: actions/checkout@v4 with: - # TODO(jie): After switch to workflow_dispatch only, we should use ref_name. - # head_ref only works for workflow triggered by pull requests. - # ref: ${{ github.ref_name }} - ref: ${{ github.head_ref }} + ref: ${{ github.ref_name }} submodules: recursive + fetch-depth: 0 + + - uses: pnpm/action-setup@v2 - - name: Setup node and npm + - name: Set up node.js uses: actions/setup-node@v4 with: - node-version: 20 - cache: 'npm' - cache-dependency-path: 'pnpm-lock.yaml' + node-version: '21' + cache: 'pnpm' - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: Deploy IPC contracts to calibration net + - name: Restore cache + id: cache-restore + uses: actions/cache/restore@v4 + with: + ## Hardhat is intelligent enough to perform incremental compilation. But GitHub Actions caches are immutable. + ## Since we can't have a rolling cache, we create a new cache for each run, but use restore-keys to load the + ## most recently created cache. + ## Reference: https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache + key: ${{ runner.os }}-contracts-artifacts-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-contracts-artifacts- + path: | + contracts/out + contracts/deployments + contracts/artifacts + + - name: Deploy IPC contracts to Calibrationnet id: deploy_contracts + env: + REGISTRY_CREATION_PRIVILEGES: 'unrestricted' run: | cd contracts - npm install --save hardhat - output=$(make deploy-stack NETWORK=calibrationnet) - echo "deploy_output<> $GITHUB_OUTPUT - echo "$output" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + pnpm install + make deploy-stack NETWORK=calibrationnet - - name: Parse deploy output - run: | - deploy_output='${{ steps.deploy_contracts.outputs.deploy_output }}' - echo "$deploy_output" - deployed_gateway_address=$(echo "$deploy_output" | grep '"Gateway"' | awk -F'"' '{print $4}') - deployed_registry_address=$(echo "$deploy_output" | grep '"SubnetRegistry"' | awk -F'"' '{print $4}') - echo "gateway_address=$deployed_gateway_address" >> $GITHUB_ENV - echo "registry_address=$deployed_registry_address" >> $GITHUB_ENV - echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_ENV - - - name: Review deployed addresses + - name: Save cache + id: cache-save + uses: actions/cache/save@v4 + if: always() && steps.cache-restore.outputs.cache-hit != 'true' + with: + key: ${{ runner.os }}-contracts-artifacts-${{ github.run_id }} + path: | + contracts/out + contracts/deployments + contracts/artifacts + + - name: Populate output run: | - echo "commit_hash: $commit_hash" - echo "gateway_address: $gateway_address" - echo "registry_address: $registry_address" + cd contracts + jq -n --arg commit "$(git rev-parse HEAD)" \ + --arg gateway_addr "$(jq -r '.address' deployments/calibrationnet/GatewayDiamond.json)" \ + --arg registry_addr "$(jq -r '.address' deployments/calibrationnet/SubnetRegistryDiamond.json)" \ + '{"commit":$commit, "gateway_addr":$gateway_addr, "registry_addr":$registry_addr}' > /tmp/output.json + cat /tmp/output.json - name: Switch code repo to cd/contracts branch uses: actions/checkout@v4 @@ -93,33 +103,11 @@ jobs: ref: cd/contracts submodules: recursive fetch-depth: 0 - token: ${{ secrets.WORKFLOW_PAT_JIE }} - - name: Merge from main branch and update cd/contracts branch - run: | - git config --global user.name "$GIT_USERNAME" - git config --global user.email "$GIT_EMAIL" - git checkout main - git pull --rebase origin main - git checkout cd/contracts - git merge main --no-edit --allow-unrelated-histories - git push -f origin cd/contracts - - - name: Write deployed address to output file + - name: Commit and push deployment info run: | mkdir -p deployments - json_str='{"commit":"'$commit_hash'","gateway_addr":"'$gateway_address'","registry_addr":"'$registry_address'"}' - jq -n "$json_str" > deployments/r314159.json - cat deployments/r314159.json - - - name: Commit output file and push it to remote repo - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Update contract address - branch: cd/contracts - file_pattern: deployments/r314159.json - commit_user_name: ${{env.GIT_USERNAME}} - commit_user_email: ${{env.GIT_EMAIL}} - push_options: '--force' - skip_dirty_check: true - create_branch: true + cp /tmp/output.json deployments/r314159.json + git add deployments/r314159.json + git commit -m "Contracts deployed @ ${{ github.sha }}" + git push origin cd/contracts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9125aee52..cbf239d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## [axon-r08] - 2024-12-31 + +### 🚀 Features + +- *(node)* Configurable chain id (#1230) +- *(cli)* Add `list-validators` command (#1221) +- *(node)* Txn prioritization based on gas parameters (#1185) +- *(node)* Support legacy transactions (#1235) + +### 🐛 Bug Fixes + +- Patch missing ipc messages in eth get_logs (#1226) +- Prevent panic on chain replay (#1197) +- Use current exec state when querying validator table (#1234) +- Contracts auto-deploy GitHub Actions workflow (#1238) + ## [axon-r07] - 2024-12-02 ### 🚀 Features diff --git a/Cargo.lock b/Cargo.lock index 3bf6b8d8d..f584133e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2893,6 +2893,7 @@ dependencies = [ "cid", "fendermint_abci", "fendermint_actor_gas_market_eip1559", + "fendermint_actors_api", "fendermint_app_options", "fendermint_app_settings", "fendermint_crypto", diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index 27bf1db70..9cb9ed118 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -42,6 +42,7 @@ tracing-subscriber = { workspace = true } literally = { workspace = true } fendermint_abci = { path = "../abci" } +fendermint_actors_api = { path = "../actors/api" } fendermint_app_options = { path = "./options" } fendermint_app_settings = { path = "./settings" } fendermint_crypto = { path = "../crypto" } diff --git a/fendermint/app/options/src/genesis.rs b/fendermint/app/options/src/genesis.rs index 04cf9b070..f2133a049 100644 --- a/fendermint/app/options/src/genesis.rs +++ b/fendermint/app/options/src/genesis.rs @@ -176,7 +176,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: Option, + 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/app.rs b/fendermint/app/src/app.rs index 2b6178fcb..427c70641 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -3,12 +3,22 @@ use std::future::Future; use std::sync::Arc; +use crate::observe::{ + BlockCommitted, BlockProposalEvaluated, BlockProposalReceived, BlockProposalSent, Message, + MpoolReceived, +}; +use crate::validators::ValidatorCache; +use crate::AppExitCode; +use crate::BlockHeight; +use crate::{tmconv::*, VERSION}; use anyhow::{anyhow, Context, Result}; use async_stm::{atomically, atomically_or_err}; use async_trait::async_trait; use cid::Cid; use fendermint_abci::util::take_until_max_size; use fendermint_abci::{AbciResult, Application}; +use fendermint_actors_api::gas_market::Reading; +use fendermint_crypto::PublicKey; use fendermint_storage::{ Codec, Encode, KVCollection, KVRead, KVReadable, KVStore, KVWritable, KVWrite, }; @@ -41,18 +51,9 @@ use num_traits::Zero; use serde::{Deserialize, Serialize}; use tendermint::abci::request::CheckTxKind; use tendermint::abci::{request, response}; -use tendermint_rpc::Client; +use tendermint::consensus::params::Params as TendermintConsensusParams; 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}; - #[derive(Serialize)] #[repr(u8)] pub enum AppStoreKey { @@ -117,11 +118,10 @@ 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, @@ -162,13 +162,11 @@ where /// /// Zero means unlimited. state_hist_size: u64, - /// Tracks the validator - validators: ValidatorTracker, - /// The cometbft client - client: C, + /// Caches the validators. + validators_cache: Arc>>, } -impl App +impl App where S: KVStore + Codec @@ -177,7 +175,6 @@ where + Codec, DB: KVWritable + KVReadable + Clone + 'static, SS: Blockstore + Clone + 'static, - C: Client + Clone, { pub fn new( config: AppConfig, @@ -186,7 +183,6 @@ where interpreter: I, chain_env: ChainEnv, snapshots: Option, - client: C, ) -> Result { let app = Self { db: Arc::new(db), @@ -201,15 +197,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.clone()), - client, + validators_cache: Arc::new(tokio::sync::Mutex::new(None)), }; app.init_committed_state()?; Ok(app) } } -impl App +impl App where S: KVStore + Codec @@ -218,7 +213,6 @@ 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 { @@ -244,6 +238,7 @@ where chain_id: 0, power_scale: 0, app_version: 0, + consensus_params: None, }, }; self.set_committed_state(state)?; @@ -294,6 +289,31 @@ where .context("commit failed") } + /// Diff our current consensus params with new values, and return Some with the final params + /// if they differ (and therefore a consensus layer update is necessary). + fn maybe_update_app_state( + &self, + gas_market: &Reading, + ) -> Result> { + let mut state = self.committed_state()?; + let current = state + .state_params + .consensus_params + .ok_or_else(|| anyhow!("no current consensus params in state"))?; + + if current.block.max_gas == gas_market.block_gas_limit as i64 { + return Ok(None); // No update necessary. + } + + // Proceeding with update. + let mut updated = current; + updated.block.max_gas = gas_market.block_gas_limit as i64; + state.state_params.consensus_params = Some(updated.clone()); + self.set_committed_state(state)?; + + Ok(Some(updated)) + } + /// Put the execution state during block execution. Has to be empty. async fn put_exec_state(&self, state: FvmExecState) { let mut guard = self.exec_state.lock().await; @@ -397,6 +417,40 @@ where _ => Err(anyhow!("invalid app state json")), } } + + /// Replaces the current validators cache with a new one. + async fn refresh_validators_cache(&self) -> Result<()> { + // TODO: This should be read only state, but we can't use the read-only view here + // because it hasn't been committed to state store yet. + self.modify_exec_state(|mut s| async { + let mut cache = self.validators_cache.lock().await; + *cache = Some(ValidatorCache::new_from_state(&mut s.1)?); + Ok((s, ())) + }) + .await?; + + Ok(()) + } + + /// Retrieves a validator from the cache, initializing it if necessary. + async fn get_validator_from_cache(&self, id: &tendermint::account::Id) -> Result { + let mut cache = self.validators_cache.lock().await; + + // If cache is not initialized, update it from the state + if cache.is_none() { + let mut state = self + .read_only_view(None)? + .ok_or_else(|| anyhow!("exec state should be present"))?; + + *cache = Some(ValidatorCache::new_from_state(&mut state)?); + } + + // Retrieve the validator from the cache + cache + .as_ref() + .context("Validator cache is not available")? + .get_validator(id) + } } // NOTE: The `Application` interface doesn't allow failures at the moment. The protobuf @@ -405,7 +459,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 @@ -436,7 +490,6 @@ where Query = BytesMessageQuery, Output = BytesMessageQueryRes, >, - C: Client + Sync + Clone, { /// Provide information about the ABCI application. async fn info(&self, _request: request::Info) -> AbciResult { @@ -463,7 +516,11 @@ 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, mut state_params) = + read_genesis_car(genesis_bytes, &self.state_store).await?; + + state_params.consensus_params = Some(request.consensus_params); + let validators = to_validator_updates(validators).context("failed to convert validators")?; @@ -488,7 +545,7 @@ where }; let response = response::InitChain { - consensus_params: None, + consensus_params: None, // not updating the proposed consensus params validators, app_hash: app_state.app_hash(), }; @@ -589,9 +646,6 @@ where .await .context("error running check")?; - // Update the check state. - *guard = Some(state); - let mut mpool_received_trace = MpoolReceived::default(); let response = match result { @@ -601,11 +655,16 @@ where Ok(Err(InvalidSignature(d))) => invalid_check_tx(AppError::InvalidSignature, d), Ok(Ok(ret)) => { mpool_received_trace.message = Some(Message::from(&ret.message)); - to_check_tx(ret) + + let priority = state.txn_priority_calculator().priority(&ret.message); + to_check_tx(ret, priority) } }, }; + // Update the check state. + *guard = Some(state); + mpool_received_trace.accept = response.code.is_ok(); if !mpool_received_trace.accept { mpool_received_trace.reason = Some(format!("{:?} - {}", response.code, response.info)); @@ -732,8 +791,7 @@ where state_params.timestamp = to_timestamp(request.header.time); let validator = self - .validators - .get_validator(&request.header.proposer_address, block_height) + .get_validator_from_cache(&request.header.proposer_address) .await?; let state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) @@ -796,7 +854,7 @@ where // End the interpreter for this block. let EndBlockOutput { power_updates, - block_gas_limit: new_block_gas_limit, + gas_market, events, } = self .modify_exec_state(|s| self.interpreter.end(s)) @@ -807,22 +865,15 @@ where 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 - } - }; + // Replace the validator cache if the validator set has changed. + if !validator_updates.is_empty() { + self.refresh_validators_cache().await?; + } + + // Maybe update the app state with the new block gas limit. + let consensus_param_updates = self + .maybe_update_app_state(&gas_market) + .context("failed to update block gas limit")?; let ret = response::EndBlock { validator_updates, diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index 65de4d2d5..4e9a5e8a9 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -305,16 +305,13 @@ fn set_ipc_gateway(genesis_file: &PathBuf, args: &GenesisIpcGatewayArgs) -> anyh async fn seal_genesis(genesis_file: &PathBuf, args: &SealGenesisArgs) -> anyhow::Result<()> { let genesis_params = read_genesis(genesis_file)?; - let mut builder = GenesisBuilder::new( + let builder = GenesisBuilder::new( args.builtin_actors_path.clone(), args.custom_actors_path.clone(), + args.artifacts_path.clone(), genesis_params, ); - 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 } diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index 9ddb9cf16..a412b39a4 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -298,7 +298,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, @@ -314,7 +314,6 @@ 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 8f187a787..eb5c22130 100644 --- a/fendermint/app/src/ipc.rs +++ b/fendermint/app/src/ipc.rs @@ -15,7 +15,6 @@ 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)] @@ -25,18 +24,17 @@ 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 @@ -45,9 +43,8 @@ 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(), @@ -65,7 +62,7 @@ where } } -impl ParentFinalityStateQuery for AppParentFinalityQuery +impl ParentFinalityStateQuery for AppParentFinalityQuery where S: KVStore + Codec @@ -74,7 +71,6 @@ 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/tmconv.rs b/fendermint/app/src/tmconv.rs index 029270e47..407c9f4f9 100644 --- a/fendermint/app/src/tmconv.rs +++ b/fendermint/app/src/tmconv.rs @@ -125,7 +125,7 @@ pub fn to_deliver_tx( } } -pub fn to_check_tx(ret: FvmCheckRet) -> response::CheckTx { +pub fn to_check_tx(ret: FvmCheckRet, priority: i64) -> response::CheckTx { // Putting the message `log` because only `log` appears in the `tx_sync` JSON-RPC response. let message = ret .info @@ -144,6 +144,7 @@ pub fn to_check_tx(ret: FvmCheckRet) -> response::CheckTx { data, gas_wanted: ret.gas_limit.try_into().unwrap_or(i64::MAX), sender: ret.sender.to_string(), + priority, ..Default::default() } } diff --git a/fendermint/app/src/validators.rs b/fendermint/app/src/validators.rs index 5870b7160..3987d4437 100644 --- a/fendermint/app/src/validators.rs +++ b/fendermint/app/src/validators.rs @@ -1,69 +1,49 @@ // 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 anyhow::{anyhow, Ok, Result}; use fendermint_crypto::PublicKey; -use fvm_shared::clock::ChainEpoch; +use fendermint_vm_interpreter::fvm::state::ipc::GatewayCaller; +use fendermint_vm_interpreter::fvm::state::FvmExecState; 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)?; +use tendermint::account::Id as TendermintId; +use tendermint::PublicKey as TendermintPubKey; - if *id == validator.address { - pubkey = Some(key); - } +use fvm_ipld_blockstore::Blockstore; - new_validators.insert(validator.address, key); - } - - *self.public_keys.write().unwrap() = new_validators; +#[derive(Clone)] +// Tracks the validator ID from Tendermint to their corresponding public key. +pub(crate) struct ValidatorCache { + map: HashMap, +} - // cannot find the validator, this should not have happened usually - pubkey.ok_or_else(|| anyhow!("{} not validator", id)) +impl ValidatorCache { + pub fn new_from_state(state: &mut FvmExecState) -> Result + where + SS: Blockstore + Clone + 'static, + { + let gateway = GatewayCaller::default(); + let (_, validators) = gateway.current_power_table(state)?; + + let map = validators + .iter() + .map(|v| { + let tendermint_pub_key: TendermintPubKey = + TendermintPubKey::try_from(v.public_key.clone())?; + let id = TendermintId::from(tendermint_pub_key); + let key = *v.public_key.public_key(); + Ok((id, key)) + }) + .collect::, _>>()?; + + Ok(Self { map }) } - fn get_from_cache(&self, id: &tendermint::account::Id) -> Option { - let keys = self.public_keys.read().unwrap(); - keys.get(id).copied() + pub fn get_validator(&self, id: &tendermint::account::Id) -> Result { + self.map + .get(id) + .cloned() + .ok_or_else(|| anyhow!("validator not found")) } } diff --git a/fendermint/eth/api/examples/ethers.rs b/fendermint/eth/api/examples/ethers.rs index 565102dde..42f0d8298 100644 --- a/fendermint/eth/api/examples/ethers.rs +++ b/fendermint/eth/api/examples/ethers.rs @@ -41,8 +41,8 @@ use ethers_core::{ abi::Abi, types::{ transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes, - Eip1559TransactionRequest, Filter, Log, SyncingStatus, TransactionReceipt, TxHash, H256, - U256, U64, + Eip1559TransactionRequest, Filter, Log, SyncingStatus, TransactionReceipt, + TransactionRequest, TxHash, H256, U256, U64, }, }; use tracing::Level; @@ -706,6 +706,17 @@ where })?; } + // Check legacy transactions + // Set up the transaction details for a legacy transaction + let tx = TransactionRequest::new() + .to(to.eth_addr) // Replace with recipient address + .value(U256::from(1u64)); // Gas limit for standard ETH transfer + + // Send the transaction + let pending_tx = mw.send_transaction(tx, None).await?; + let receipt = pending_tx.await?.unwrap(); + tracing::info!("legacy transaction sent: {:?}", receipt.transaction_hash); + Ok(()) } diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index 5db68de70..536f3e295 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -20,6 +20,7 @@ use fendermint_vm_actor_interface::evm; use fendermint_vm_message::chain::ChainMessage; use fendermint_vm_message::query::FvmQueryHeight; use fendermint_vm_message::signed::SignedMessage; +use fil_actors_evm_shared::uints; use futures::FutureExt; use fvm_ipld_encoding::RawBytes; use fvm_shared::address::Address; @@ -36,9 +37,7 @@ use tendermint_rpc::{ Client, }; -use fil_actors_evm_shared::uints; - -use crate::conv::from_eth::{self, to_fvm_message}; +use crate::conv::from_eth::{self, derive_origin_kind, to_fvm_message}; use crate::conv::from_tm::{self, msg_hash, to_chain_message, to_cumulative, to_eth_block_zero}; use crate::error::{error_with_revert, OutOfSequence}; use crate::filters::{matches_topics, FilterId, FilterKind, FilterRecords}; @@ -46,7 +45,7 @@ use crate::{ conv::{ from_eth::to_fvm_address, from_fvm::to_eth_tokens, - from_tm::{to_eth_receipt, to_eth_transaction}, + from_tm::{to_eth_receipt, to_eth_transaction_response}, }, error, JsonRpcData, JsonRpcResult, }; @@ -427,7 +426,8 @@ where C: Client + Sync + Send, { // Check in the pending cache first. - if let Some(tx) = data.tx_cache.get(&tx_hash) { + if let Some((tx, sig)) = data.tx_cache.get(&tx_hash) { + let tx = from_eth::to_eth_transaction_response(&tx, sig)?; Ok(Some(tx)) } else if let Some(res) = data.tx_by_hash(tx_hash).await? { let msg = to_chain_message(&res.tx)?; @@ -439,8 +439,11 @@ where .state_params(FvmQueryHeight::Height(header.header.height.value())) .await?; let chain_id = ChainID::from(sp.value.chain_id); + let hash = msg_hash(&res.tx_result.events, &res.tx); - let mut tx = to_eth_transaction(msg, chain_id, hash)?; + let mut tx = to_eth_transaction_response(msg, chain_id)?; + debug_assert_eq!(hash, tx.hash); + tx.transaction_index = Some(et::U64::from(res.index)); tx.block_hash = Some(et::H256::from_slice(header.header.hash().as_bytes())); tx.block_number = Some(et::U64::from(res.height.value())); @@ -612,6 +615,14 @@ pub async fn get_uncle_by_block_number_and_index( Ok(None) } +fn normalize_signature(sig: &mut et::Signature) -> JsonRpcResult<()> { + sig.v = sig + .recovery_id() + .context("cannot normalize eth signature")? + .to_byte() as u64; + Ok(()) +} + /// Creates new message call transaction or a contract creation for signed transactions. pub async fn send_raw_transaction( data: JsonRpcData, @@ -621,23 +632,23 @@ where C: Client + Sync + Send, { let rlp = rlp::Rlp::new(tx.as_ref()); - let (tx, sig): (TypedTransaction, et::Signature) = TypedTransaction::decode_signed(&rlp) + let (tx, mut sig): (TypedTransaction, et::Signature) = TypedTransaction::decode_signed(&rlp) .context("failed to decode RLP as signed TypedTransaction")?; + // for legacy eip155 transactions, the chain id is encoded in it. The `v` most likely will not + // be normalized, normalize to ensure consistent txn hash calculation. + normalize_signature(&mut sig)?; + let sighash = tx.sighash(); - let msghash = et::TxHash::from(ethers_core::utils::keccak256(rlp.as_raw())); + let msghash = tx.hash(&sig); tracing::debug!(?sighash, eth_hash = ?msghash, ?tx, "received raw transaction"); - if let Some(tx) = tx.as_eip1559_ref() { - let tx = from_eth::to_eth_transaction(tx.clone(), sig, msghash); - data.tx_cache.insert(msghash, tx); - } - - let msg = to_fvm_message(tx, false)?; + let msg = to_fvm_message(tx.clone())?; let sender = msg.from; let nonce = msg.sequence; let msg = SignedMessage { + origin_kind: derive_origin_kind(&tx)?, message: msg, signature: Signature::new_secp256k1(sig.to_vec()), }; @@ -648,6 +659,8 @@ where // but not the execution results - those will have to be polled with get_transaction_receipt. let res: tx_sync::Response = data.tm().broadcast_tx_sync(bz).await?; if res.code.is_ok() { + data.tx_cache.insert(msghash, (tx, sig)); + // The following hash would be okay for ethers-rs,and we could use it to look up the TX with Tendermint, // but ethers.js would reject it because it doesn't match what Ethereum would use. // Ok(et::TxHash::from_slice(res.hash.as_bytes())) @@ -671,6 +684,8 @@ where tracing::debug!(eth_hash = ?msghash, expected = oos.expected, got = oos.got, is_admissible, "out-of-sequence transaction received"); if is_admissible { + data.tx_cache.insert(msghash, (tx, sig)); + data.tx_buffer.insert(sender, nonce, msg); return Ok(msghash); } @@ -688,7 +703,7 @@ pub async fn call( where C: Client + Sync + Send, { - let msg = to_fvm_message(tx.into(), true)?; + let msg = to_fvm_message(tx.into())?; let is_create = msg.to == EAM_ACTOR_ADDR; let height = data.query_height(block_id).await?; let response = data.client.call(msg, height).await?; @@ -737,7 +752,7 @@ where EstimateGasParams::Two((tx, block_id)) => (tx, block_id), }; - let msg = to_fvm_message(tx.into(), true).context("failed to convert to FVM message")?; + let msg = to_fvm_message(tx.into()).context("failed to convert to FVM message")?; let height = data .query_height(block_id) diff --git a/fendermint/eth/api/src/conv/from_eth.rs b/fendermint/eth/api/src/conv/from_eth.rs index 40ce51ce9..df670763f 100644 --- a/fendermint/eth/api/src/conv/from_eth.rs +++ b/fendermint/eth/api/src/conv/from_eth.rs @@ -5,66 +5,128 @@ use ethers_core::types as et; use ethers_core::types::transaction::eip2718::TypedTransaction; +use ethers_core::types::{Eip1559TransactionRequest, TransactionRequest}; pub use fendermint_vm_message::conv::from_eth::*; +use fendermint_vm_message::signed::OriginKind; use fvm_shared::{error::ExitCode, message::Message}; -use crate::{error, JsonRpcResult}; +use crate::error::error_with_revert; +use crate::JsonRpcResult; -pub fn to_fvm_message(tx: TypedTransaction, accept_legacy: bool) -> JsonRpcResult { +fn handle_typed_txn< + R, + F1: Fn(&TransactionRequest) -> JsonRpcResult, + F2: Fn(&Eip1559TransactionRequest) -> JsonRpcResult, +>( + tx: &TypedTransaction, + handle_legacy: F1, + handle_eip1559: F2, +) -> JsonRpcResult { match tx { - TypedTransaction::Eip1559(ref tx) => { - Ok(fendermint_vm_message::conv::from_eth::to_fvm_message(tx)?) - } - TypedTransaction::Legacy(_) if accept_legacy => { - // legacy transactions are only accepted for gas estimation purposes - // (when accept_legacy is explicitly set) - // eth_sendRawTransaction should fail for legacy transactions. - // For this purpose it os OK to not set `max_fee_per_gas` and - // `max_priority_fee_per_gas`. Legacy transactions don't include - // that information - Ok(fendermint_vm_message::conv::from_eth::to_fvm_message( - &tx.into(), - )?) - } - TypedTransaction::Legacy(_) | TypedTransaction::Eip2930(_) => error( + TypedTransaction::Legacy(ref t) => handle_legacy(t), + TypedTransaction::Eip1559(ref t) => handle_eip1559(t), + _ => error_with_revert( ExitCode::USR_ILLEGAL_ARGUMENT, - "unexpected transaction type", + "txn type not supported", + None::>, ), } } +pub fn derive_origin_kind(tx: &TypedTransaction) -> JsonRpcResult { + handle_typed_txn( + tx, + |_| Ok(OriginKind::EthereumLegacy), + |_| Ok(OriginKind::EthereumEIP1559), + ) +} + +pub fn to_fvm_message(tx: TypedTransaction) -> JsonRpcResult { + handle_typed_txn( + &tx, + |r| Ok(fvm_message_from_legacy(r)?), + |r| Ok(fvm_message_from_eip1559(r)?), + ) +} + /// Turn a request into the DTO returned by the API. -pub fn to_eth_transaction( - tx: et::Eip1559TransactionRequest, +pub fn to_eth_transaction_response( + tx: &TypedTransaction, sig: et::Signature, - hash: et::TxHash, -) -> et::Transaction { - et::Transaction { - hash, - nonce: tx.nonce.unwrap_or_default(), - block_hash: None, - block_number: None, - transaction_index: None, - from: tx.from.unwrap_or_default(), - to: tx.to.and_then(|to| to.as_address().cloned()), - value: tx.value.unwrap_or_default(), - gas: tx.gas.unwrap_or_default(), - max_fee_per_gas: tx.max_fee_per_gas, - max_priority_fee_per_gas: tx.max_priority_fee_per_gas, - // Strictly speaking a "Type 2" transaction should not need to set this, but we do because Blockscout - // has a database constraint that if a transaction is included in a block this can't be null. - gas_price: Some( - tx.max_fee_per_gas.unwrap_or_default() - + tx.max_priority_fee_per_gas.unwrap_or_default(), - ), - input: tx.data.unwrap_or_default(), - chain_id: tx.chain_id.map(|x| et::U256::from(x.as_u64())), - v: et::U64::from(sig.v), - r: sig.r, - s: sig.s, - transaction_type: Some(2u64.into()), - access_list: Some(tx.access_list), - other: Default::default(), +) -> JsonRpcResult { + macro_rules! essential_txn_response { + ($tx: expr, $hash: expr) => {{ + let mut r = et::Transaction::default(); + + r.nonce = $tx.nonce.unwrap_or_default(); + r.hash = $hash; + r.from = $tx.from.unwrap_or_default(); + r.to = $tx.to.clone().and_then(|to| to.as_address().cloned()); + r.value = $tx.value.unwrap_or_default(); + r.gas = $tx.gas.unwrap_or_default(); + r.input = $tx.data.clone().unwrap_or_default(); + r.chain_id = $tx.chain_id.map(|x| et::U256::from(x.as_u64())); + r.v = et::U64::from(sig.v); + r.r = sig.r; + r.s = sig.s; + + r + }}; + } + + let hash = tx.hash(&sig); + + handle_typed_txn( + tx, + |tx| { + let mut r = essential_txn_response!(tx, hash); + r.gas_price = tx.gas_price; + r.transaction_type = Some(0u64.into()); + Ok(r) + }, + |tx| { + let mut r = essential_txn_response!(tx, hash); + r.max_fee_per_gas = tx.max_fee_per_gas; + r.max_priority_fee_per_gas = tx.max_priority_fee_per_gas; + // Strictly speaking a "Type 2" transaction should not need to set this, but we do because Blockscout + // has a database constraint that if a transaction is included in a block this can't be null. + r.gas_price = Some( + tx.max_fee_per_gas.unwrap_or_default() + + tx.max_priority_fee_per_gas.unwrap_or_default(), + ); + r.transaction_type = Some(2u64.into()); + r.access_list = Some(tx.access_list.clone()); + Ok(r) + }, + ) +} + +#[cfg(test)] +mod tests { + use crate::conv::from_eth::to_fvm_message; + use ethers_core::types::transaction::eip2718::TypedTransaction; + use ethers_core::types::Signature; + use ethers_core::utils::rlp; + use fendermint_vm_message::signed::{OriginKind, SignedMessage}; + use fvm_shared::chainid::ChainID; + + #[test] + fn test_legacy_transaction() { + let raw_tx = "f8ac821dac850df8475800830186a09465292eeadf1426cd2df1c4793a3d7519f253913b80b844a9059cbb000000000000000000000000cd50511c4e355f2bc3c084d854253cc17b2230bf00000000000000000000000000000000000000000000148a616ad7f95aa0000025a0a4f3a70a01cfb3969c4a12510ebccd7d08250a4d34181123bebae3f865392643a063116147193f2badc611fa20dfa1c339bca299f50e470353ee4f676bc236479d"; + let raw_tx = hex::decode(raw_tx).unwrap(); + + let rlp = rlp::Rlp::new(raw_tx.as_ref()); + let (tx, sig): (TypedTransaction, Signature) = + TypedTransaction::decode_signed(&rlp).unwrap(); + + let msg = to_fvm_message(tx).unwrap(); + + let signed_msg = SignedMessage { + origin_kind: OriginKind::EthereumLegacy, + message: msg, + signature: fvm_shared::crypto::signature::Signature::new_secp256k1(sig.to_vec()), + }; + assert!(signed_msg.verify(&ChainID::from(1)).is_ok()); } } diff --git a/fendermint/eth/api/src/conv/from_tm.rs b/fendermint/eth/api/src/conv/from_tm.rs index 064ea50fa..72197e557 100644 --- a/fendermint/eth/api/src/conv/from_tm.rs +++ b/fendermint/eth/api/src/conv/from_tm.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use anyhow::{anyhow, Context}; use ethers_core::types::{self as et}; use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_message::conv::from_fvm::to_eth_transaction_request; +use fendermint_vm_message::conv::from_fvm::to_eth_typed_transaction; use fendermint_vm_message::{chain::ChainMessage, signed::SignedMessage}; use fvm_shared::address::Address; use fvm_shared::bigint::Zero; @@ -160,9 +160,11 @@ pub fn to_eth_block( if let ChainMessage::Signed(msg) = msg { let hash = msg_hash(&result.events, data); - let mut tx = to_eth_transaction(msg, chain_id, hash) + let mut tx = to_eth_transaction_response(msg, chain_id) .context("failed to convert to eth transaction")?; + debug_assert_eq!(hash, tx.hash); + tx.transaction_index = Some(et::U64::from(idx)); tx.block_hash = Some(et::H256::from_slice(block.header.hash().as_bytes())); tx.block_number = Some(et::U64::from(block.header.height.value())); @@ -205,20 +207,19 @@ pub fn to_eth_block( Ok(block) } -pub fn to_eth_transaction( +pub fn to_eth_transaction_response( msg: SignedMessage, chain_id: ChainID, - hash: et::TxHash, ) -> anyhow::Result { // Based on https://github.com/filecoin-project/lotus/blob/6cc506f5cf751215be6badc94a960251c6453202/node/impl/full/eth.go#L2048 let sig = to_eth_signature(msg.signature(), true).context("failed to convert to eth signature")?; // Recover the original request; this method has better tests. - let tx = to_eth_transaction_request(&msg.message, &chain_id) + let tx = to_eth_typed_transaction(msg.origin_kind, &msg.message, &chain_id) .context("failed to convert to tx request")?; - let tx = from_eth::to_eth_transaction(tx, sig, hash); + let tx = from_eth::to_eth_transaction_response(&tx, sig)?; Ok(tx) } diff --git a/fendermint/eth/api/src/mpool.rs b/fendermint/eth/api/src/mpool.rs index 6d651210f..da40507bd 100644 --- a/fendermint/eth/api/src/mpool.rs +++ b/fendermint/eth/api/src/mpool.rs @@ -4,6 +4,7 @@ use std::{collections::BTreeMap, time::Duration}; use ethers_core::types as et; +use ethers_core::types::transaction::eip2718::TypedTransaction; use fendermint_rpc::{ client::TendermintClient, message::SignedMessageFactory, FendermintClient, QueryClient, }; @@ -21,11 +22,12 @@ use crate::{cache::Cache, state::Nonce, HybridClient}; const RETRY_SLEEP_SECS: u64 = 5; +pub type SignedTransaction = (TypedTransaction, et::Signature); /// Cache submitted transactions by their Ethereum hash, because the CometBFT /// API would not be able to find them until they are delivered to the application /// and indexed by their domain hash, which some tools interpret as the transaction /// being dropped from the mempool. -pub type TransactionCache = Cache; +pub type TransactionCache = Cache; /// Buffer out-of-order messages until they can be sent to the chain. #[derive(Clone)] diff --git a/fendermint/eth/api/src/state.rs b/fendermint/eth/api/src/state.rs index 641a587c5..00374aa19 100644 --- a/fendermint/eth/api/src/state.rs +++ b/fendermint/eth/api/src/state.rs @@ -40,7 +40,9 @@ use crate::handlers::ws::MethodNotification; use crate::mpool::{TransactionBuffer, TransactionCache}; use crate::GasOpt; use crate::{ - conv::from_tm::{map_rpc_block_txs, to_chain_message, to_eth_block, to_eth_transaction}, + conv::from_tm::{ + map_rpc_block_txs, to_chain_message, to_eth_block, to_eth_transaction_response, + }, error, JsonRpcResult, }; @@ -357,8 +359,10 @@ where return error(ExitCode::USR_ILLEGAL_ARGUMENT, "incompatible transaction"); }; - let mut tx = to_eth_transaction(msg, chain_id, hash) + let mut tx = to_eth_transaction_response(msg, chain_id) .context("failed to convert to eth transaction")?; + debug_assert_eq!(tx.hash, hash); + tx.transaction_index = Some(index); tx.block_hash = Some(et::H256::from_slice(block.header.hash().as_bytes())); tx.block_number = Some(et::U64::from(block.header.height.value())); diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index 8d6e3f9cf..071ae6677 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -33,13 +33,13 @@ pub async fn create_test_exec_state( )> { let bundle_path = bundle_path(); let custom_actors_bundle_path = custom_actors_bundle_path(); - let maybe_contract_path = genesis.ipc.as_ref().map(|_| contracts_path()); + let artifacts_path = contracts_path(); let (state, out) = create_test_genesis_state( bundle_path, custom_actors_bundle_path, + artifacts_path, genesis, - maybe_contract_path, ) .await?; let store = state.store().clone(); @@ -85,6 +85,7 @@ where chain_id: out.chain_id.into(), power_scale: out.power_scale, app_version: 0, + consensus_params: None, }; Ok(Self { diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index 6e04c0a59..b01e19e62 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -256,6 +256,27 @@ pub mod ipc { pub majority_percentage: u8, pub active_validators_limit: u16, } + + impl Default for GatewayParams { + fn default() -> Self { + // Default values are taken from here contracts/tasks/deploy-gateway.ts. + Self { + subnet_id: SubnetID::default(), + bottom_up_check_period: 10, + majority_percentage: 66, + active_validators_limit: 100, + } + } + } + + impl GatewayParams { + pub fn new(subnet_id: SubnetID) -> Self { + Self { + subnet_id, + ..Default::default() + } + } + } } #[cfg(test)] diff --git a/fendermint/vm/interpreter/src/arb.rs b/fendermint/vm/interpreter/src/arb.rs index 991e403cb..4ae411946 100644 --- a/fendermint/vm/interpreter/src/arb.rs +++ b/fendermint/vm/interpreter/src/arb.rs @@ -21,6 +21,7 @@ impl Arbitrary for FvmStateParams { .into(), power_scale: *g.choose(&[-1, 0, 3]).unwrap(), app_version: *g.choose(&[0, 1, 2]).unwrap(), + consensus_params: None, } } } diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 6f077de65..cbe1cf5a9 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -331,7 +331,7 @@ pub fn emit_trace_if_check_checkpoint_finalized( where DB: Blockstore + Clone, { - if !gateway.enabled(state)? { + if !gateway.is_anchored(state)? { return Ok(()); } @@ -372,10 +372,6 @@ fn should_create_checkpoint( where DB: Blockstore + Clone, { - if !gateway.enabled(state)? { - return Ok(None); - } - let id = gateway.subnet_id(state)?; let is_root = id.route.is_empty(); diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 803d54bc4..e123e106a 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -5,12 +5,13 @@ use super::{ checkpoint::{self, PowerUpdates}, observe::{CheckpointFinalized, MsgExec, MsgExecPurpose}, state::FvmExecState, - BlockGasLimit, FvmMessage, FvmMessageInterpreter, + FvmMessage, FvmMessageInterpreter, }; use crate::fvm::activity::ValidatorActivityTracker; use crate::ExecInterpreter; use anyhow::Context; use async_trait::async_trait; +use fendermint_actors_api::gas_market::Reading; use fendermint_vm_actor_interface::{chainmetadata, cron, system}; use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; @@ -39,7 +40,7 @@ pub struct FvmApplyRet { pub struct EndBlockOutput { pub power_updates: PowerUpdates, - pub block_gas_limit: BlockGasLimit, + pub gas_market: Reading, /// The end block events to be recorded pub events: BlockEndEvents, } @@ -269,7 +270,7 @@ where let ret = EndBlockOutput { power_updates: updates, - block_gas_limit: next_gas_market.block_gas_limit, + gas_market: next_gas_market, events: block_end_events, }; Ok((state, ret)) diff --git a/fendermint/vm/interpreter/src/fvm/gas.rs b/fendermint/vm/interpreter/src/fvm/gas.rs index a0c5289b4..6b59f42d6 100644 --- a/fendermint/vm/interpreter/src/fvm/gas.rs +++ b/fendermint/vm/interpreter/src/fvm/gas.rs @@ -26,6 +26,10 @@ pub struct BlockGasTracker { } impl BlockGasTracker { + pub fn base_fee(&self) -> &TokenAmount { + &self.base_fee + } + pub fn create(executor: &mut E) -> anyhow::Result { let mut ret = Self { base_fee: Zero::zero(), diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index d849328d7..4d2c3985b 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -6,6 +6,7 @@ use std::collections::{HashMap, HashSet}; use crate::fvm::activity::actor::ActorActivityTracker; use crate::fvm::externs::FendermintExterns; use crate::fvm::gas::BlockGasTracker; +use crate::fvm::state::priority::TxnPriorityCalculator; use anyhow::Ok; use cid::Cid; use fendermint_actors_api::gas_market::Reading; @@ -30,6 +31,8 @@ use fvm_shared::{ }; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use std::fmt; +use tendermint::consensus::params::Params as TendermintConsensusParams; pub type BlockHash = [u8; 32]; @@ -40,7 +43,7 @@ pub type ExecResult = anyhow::Result<(ApplyRet, ActorAddressMap)>; /// Parts of the state which evolve during the lifetime of the chain. #[serde_as] -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct FvmStateParams { /// Root CID of the actor state map. #[serde_as(as = "IsHumanReadable")] @@ -66,6 +69,38 @@ pub struct FvmStateParams { /// The application protocol version. #[serde(default)] pub app_version: u64, + /// Tendermint consensus params. + pub consensus_params: Option, +} + +/// Custom implementation of Debug to exclude `consensus_params` from the debug output +/// if it is `None`. This ensures consistency between the debug output and JSON/CBOR +/// serialization, which omits `None` values for `consensus_params`. See: fendermint/vm/interpreter/tests/golden.rs. +/// +/// This implementation is temporary and should be removed once `consensus_params` is +/// no longer part of `FvmStateParams`. +/// +/// @TODO: Remove this implementation when `consensus_params` is deprecated. +impl fmt::Debug for FvmStateParams { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ds = f.debug_struct("FvmStateParams"); + + ds.field("state_root", &self.state_root) + .field("timestamp", &self.timestamp) + .field("network_version", &self.network_version) + .field("base_fee", &self.base_fee) + .field("circ_supply", &self.circ_supply) + .field("chain_id", &self.chain_id) + .field("power_scale", &self.power_scale) + .field("app_version", &self.app_version); + + // Only include `consensus_params` in the debug output if it is `Some`. + if let Some(ref params) = self.consensus_params { + ds.field("consensus_params", params); + } + + ds.finish() + } } /// Parts of the state which can be updated by message execution, apart from the actor state. @@ -115,6 +150,8 @@ where params: FvmUpdatableParams, /// Indicate whether the parameters have been updated. params_dirty: bool, + + txn_priority: TxnPriorityCalculator, } impl FvmExecState @@ -151,6 +188,7 @@ where let mut executor = DefaultExecutor::new(engine.clone(), machine)?; let block_gas_tracker = BlockGasTracker::create(&mut executor)?; + let base_fee = block_gas_tracker.base_fee().clone(); Ok(Self { executor, @@ -164,6 +202,7 @@ where power_scale: params.power_scale, }, params_dirty: false, + txn_priority: TxnPriorityCalculator::new(base_fee), }) } @@ -278,6 +317,10 @@ where self.params.power_scale } + pub fn txn_priority_calculator(&self) -> &TxnPriorityCalculator { + &self.txn_priority + } + pub fn app_version(&self) -> u64 { self.params.app_version } diff --git a/fendermint/vm/interpreter/src/fvm/state/genesis.rs b/fendermint/vm/interpreter/src/fvm/state/genesis.rs index 7959bf47e..24ff25e09 100644 --- a/fendermint/vm/interpreter/src/fvm/state/genesis.rs +++ b/fendermint/vm/interpreter/src/fvm/state/genesis.rs @@ -154,6 +154,7 @@ where chain_id, power_scale, app_version: 0, + consensus_params: None, }; let exec_state = diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 8fd153832..9a91429b3 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -79,16 +79,8 @@ impl GatewayCaller { } impl GatewayCaller { - /// Check that IPC is configured in this deployment. - pub fn enabled(&self, state: &mut FvmExecState) -> anyhow::Result { - match state.state_tree_mut().get_actor(GATEWAY_ACTOR_ID)? { - None => Ok(false), - Some(a) => Ok(!state.builtin_actors().is_placeholder_actor(&a.code)), - } - } - /// Return true if the current subnet is the root subnet. - pub fn is_root(&self, state: &mut FvmExecState) -> anyhow::Result { + pub fn is_anchored(&self, state: &mut FvmExecState) -> anyhow::Result { self.subnet_id(state).map(|id| id.route.is_empty()) } diff --git a/fendermint/vm/interpreter/src/fvm/state/mod.rs b/fendermint/vm/interpreter/src/fvm/state/mod.rs index ab3d9e7f6..ba601f0a2 100644 --- a/fendermint/vm/interpreter/src/fvm/state/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/state/mod.rs @@ -8,6 +8,7 @@ pub mod snapshot; mod check; mod exec; mod genesis; +mod priority; mod query; use std::sync::Arc; diff --git a/fendermint/vm/interpreter/src/fvm/state/priority.rs b/fendermint/vm/interpreter/src/fvm/state/priority.rs new file mode 100644 index 000000000..f17799f68 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/state/priority.rs @@ -0,0 +1,80 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::FvmMessage; +use fvm_shared::econ::TokenAmount; +use num_traits::ToPrimitive; + +/// The transaction priority calculator. The priority calculated is used to determine the ordering +/// in the mempool. +pub struct TxnPriorityCalculator { + base_fee: TokenAmount, +} + +impl TxnPriorityCalculator { + pub fn new(base_fee: TokenAmount) -> Self { + Self { base_fee } + } + + pub fn priority(&self, msg: &FvmMessage) -> i64 { + if msg.gas_fee_cap < self.base_fee { + return i64::MIN; + } + + let effective_premium = msg + .gas_premium + .clone() + .min(&msg.gas_fee_cap - &self.base_fee); + effective_premium.atto().to_i64().unwrap_or(i64::MAX) + } +} + +#[cfg(test)] +mod tests { + use crate::fvm::state::priority::TxnPriorityCalculator; + use crate::fvm::FvmMessage; + use fvm_shared::address::Address; + use fvm_shared::bigint::BigInt; + use fvm_shared::econ::TokenAmount; + + fn create_msg(fee_cap: TokenAmount, premium: TokenAmount) -> FvmMessage { + FvmMessage { + version: 0, + from: Address::new_id(10), + to: Address::new_id(12), + sequence: 0, + value: Default::default(), + method_num: 0, + params: Default::default(), + gas_limit: 0, + gas_fee_cap: fee_cap, + gas_premium: premium, + } + } + + #[test] + fn priority_calculation() { + let cal = TxnPriorityCalculator::new(TokenAmount::from_atto(30)); + + let msg = create_msg(TokenAmount::from_atto(1), TokenAmount::from_atto(20)); + assert_eq!(cal.priority(&msg), i64::MIN); + + let msg = create_msg(TokenAmount::from_atto(10), TokenAmount::from_atto(20)); + assert_eq!(cal.priority(&msg), i64::MIN); + + let msg = create_msg(TokenAmount::from_atto(35), TokenAmount::from_atto(20)); + assert_eq!(cal.priority(&msg), 5); + + let msg = create_msg(TokenAmount::from_atto(50), TokenAmount::from_atto(20)); + assert_eq!(cal.priority(&msg), 20); + + let msg = create_msg(TokenAmount::from_atto(50), TokenAmount::from_atto(10)); + assert_eq!(cal.priority(&msg), 10); + + let msg = create_msg( + TokenAmount::from_atto(BigInt::from(i128::MAX)), + TokenAmount::from_atto(BigInt::from(i128::MAX)), + ); + assert_eq!(cal.priority(&msg), i64::MAX); + } +} diff --git a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs index 7f70c5cbc..a375a7c96 100644 --- a/fendermint/vm/interpreter/src/fvm/state/snapshot.rs +++ b/fendermint/vm/interpreter/src/fvm/state/snapshot.rs @@ -375,6 +375,7 @@ mod tests { chain_id: 1024, power_scale: 0, app_version: 0, + consensus_params: None, }; let block_height = 2048; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 156f45d03..c156d199a 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -38,7 +38,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 fendermint_vm_genesis::ipc::{GatewayParams, IpcParams}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use tokio_stream::StreamExt; @@ -63,6 +63,7 @@ impl GenesisMetadata { chain_id: out.chain_id.into(), power_scale: out.power_scale, app_version: 0, + consensus_params: None, }; GenesisMetadata { @@ -158,7 +159,7 @@ pub struct GenesisOutput { pub struct GenesisBuilder { /// Hardhat like util to deploy ipc contracts - hardhat: Option, + hardhat: Hardhat, /// The built in actors bundle path builtin_actors_path: PathBuf, /// The custom actors bundle path @@ -172,21 +173,17 @@ impl GenesisBuilder { pub fn new( builtin_actors_path: PathBuf, custom_actors_path: PathBuf, + artifacts_path: PathBuf, genesis_params: Genesis, ) -> Self { Self { - hardhat: None, + hardhat: Hardhat::new(artifacts_path), builtin_actors_path, custom_actors_path, genesis_params, } } - 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<()> { @@ -256,19 +253,6 @@ impl GenesisBuilder { .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, @@ -304,10 +288,7 @@ impl GenesisBuilder { // STAGE 0: Declare the built-in EVM contracts we'll have to deploy. // 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())); + let (all_ipc_contracts, ipc_entrypoints) = self.collect_contracts()?; // STAGE 1: First we initialize native built-in actors. // System actor @@ -500,52 +481,61 @@ impl GenesisBuilder { ) .context("failed to init exec state")?; - 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, - genesis.validators, - next_id, - state, - ipc_params, - hardhat, - )?; - } + // STAGE 4: Deploy the IPC system contracts. + + let config = DeployConfig { + ipc_params: genesis.ipc.as_ref(), + chain_id: out.chain_id, + hardhat: &self.hardhat, + }; + + deploy_contracts( + all_ipc_contracts, + &ipc_entrypoints, + genesis.validators, + next_id, + state, + config, + )?; Ok(out) } -} -fn collect_contracts( - hardhat: &Hardhat, -) -> anyhow::Result<(Vec, EthContractMap)> { - let mut all_contracts = Vec::new(); - let mut top_level_contracts = EthContractMap::default(); + fn collect_contracts(&self) -> anyhow::Result<(Vec, EthContractMap)> { + let mut all_contracts = Vec::new(); + let mut top_level_contracts = EthContractMap::default(); - top_level_contracts.extend(IPC_CONTRACTS.clone()); + 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")?; + 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())); - Ok((eth_libs, top_level_contracts)) + // 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)) + } +} + +// Configuration for deploying IPC contracts. +// This is to circumvent the arguments limit of the deploy_contracts function. +struct DeployConfig<'a> { + ipc_params: Option<&'a IpcParams>, + chain_id: ChainID, + hardhat: &'a Hardhat, } fn deploy_contracts( @@ -554,10 +544,10 @@ fn deploy_contracts( validators: Vec>, mut next_id: u64, state: &mut FvmGenesisState, - ipc_params: &IpcParams, - hardhat: &Hardhat, + config: DeployConfig, ) -> anyhow::Result<()> { - let mut deployer = ContractDeployer::::new(hardhat, top_level_contracts); + let mut deployer = + ContractDeployer::::new(config.hardhat, top_level_contracts); // Deploy Ethereum libraries. for (lib_src, lib_name) in ipc_contracts { @@ -567,8 +557,15 @@ fn deploy_contracts( // IPC Gateway actor. let gateway_addr = { use ipc::gateway::ConstructorParameters; + use ipc_api::subnet_id::SubnetID; + + let ipc_params = if let Some(p) = config.ipc_params { + p.gateway.clone() + } else { + GatewayParams::new(SubnetID::new(config.chain_id.into(), vec![])) + }; - let params = ConstructorParameters::new(ipc_params.gateway.clone(), validators) + let params = ConstructorParameters::new(ipc_params, validators) .context("failed to create gateway constructor")?; let facets = deployer @@ -781,13 +778,15 @@ fn circ_supply(g: &Genesis) -> TokenAmount { pub async fn create_test_genesis_state( bundle_path: PathBuf, custom_actors_bundle_path: PathBuf, + ipc_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 builder = GenesisBuilder::new( + bundle_path, + custom_actors_bundle_path, + ipc_path, + genesis_params, + ); let mut state = builder.init_state().await?; let out = builder.populate_state(&mut state, builder.genesis_params.clone())?; diff --git a/fendermint/vm/message/golden/chain/signed.cbor b/fendermint/vm/message/golden/chain/signed.cbor index b63ef0707..70641def9 100644 --- a/fendermint/vm/message/golden/chain/signed.cbor +++ b/fendermint/vm/message/golden/chain/signed.cbor @@ -1 +1 @@ -a1665369676e6564828a1bac71e8e42b55008e582b04fff3efb881a6b19dce0182c731f1fd04945bfa8590630037f89320d034798000ffe20060fed5dcf722f2582404d2fcadaf839b9a85b7010026d4c9c66adab0dbffef73b22e3600c470c6eab2108da4851b7c238774425960f851005d314720bfe239411e0e66ef5c0598c51b6dfbf507e3470990510078e0e7b84f368d02db6b275fb2ef9ac15100e10bb794c65ea994bae618a298dd55bc1b9c7edd0516dd41094749bf8441dd1bab42024f \ No newline at end of file +a1665369676e6564836346766d8a1b4b8532f818ddaaf5550204d85d9156850dc6ce4b8a6b0101375f700a8278583904a4acb98481c882f28301ff0ae7e7c01e572b5282675f00ef067ce0a48c36fc3268a4abb000520c72ec076b4dc560014a2469bd27da777aa81b56c5d56baf7b2a1e510094ef316ac3b680473f7c04ed868770da1b6ea904e6e307bff05100347196edaa73fe5af0a455fcbbb0ed2d510024eaf6f65e84e2391e12a07a6f6253421ba8f37909642d99b746a923e1b3dc1a4402d83601 \ No newline at end of file diff --git a/fendermint/vm/message/golden/chain/signed.txt b/fendermint/vm/message/golden/chain/signed.txt index 0ee1f3227..e3310313b 100644 --- a/fendermint/vm/message/golden/chain/signed.txt +++ b/fendermint/vm/message/golden/chain/signed.txt @@ -1 +1 @@ -Signed(SignedMessage { message: Message { version: 12425968913569087630, from: Address("f413189469736534769234faatnjsognlnlbw7755z3elrwadchbrxkwiii3jefwb4lkty"), to: Address("f414860406730799184383fqldtd4p5askfx6ufsbrqan7ysmqnandzqaap7yqamd7nlxhxelzh4lcyw4"), sequence: 8945142218287046904, value: TokenAmount(123874068799833566946.114438504305236165), method_num: 11276693530910474505, params: RawBytes { 49bf8441dd1bab }, gas_limit: 7925197383515179408, gas_fee_cap: TokenAmount(160675133829617315974.277524819398335169), gas_premium: TokenAmount(299137137785167946669.271275665499182524) }, signature: Signature { sig_type: BLS, bytes: [79] } }) \ No newline at end of file +Signed(SignedMessage { origin_kind: Fvm, message: Message { version: 5441811765897571061, from: Address("f49503732383930537508f74fopz6adzlswuucm5pqb3ygptqkjdbw7qzgrjflwaafedds5qdwwtofmaauujdjxut5u532vdejzs63"), to: Address("f2atmf3ekwqug4ntslrjvqcajxl5yavatyuuaj5ba"), sequence: 6252638316156103198, value: TokenAmount(197967704622183385628.38811607395306313), method_num: 12174207298954959287, params: RawBytes { a923e1b3dc1a }, gas_limit: 7973910004934098928, gas_fee_cap: TokenAmount(69709646517097803841.751553548689141037), gas_premium: TokenAmount(49072214305296835349.416608417645220674) }, signature: Signature { sig_type: BLS, bytes: [216, 54, 1] } }) \ No newline at end of file diff --git a/fendermint/vm/message/src/conv/from_eth.rs b/fendermint/vm/message/src/conv/from_eth.rs index 264107e5c..20fa7a790 100644 --- a/fendermint/vm/message/src/conv/from_eth.rs +++ b/fendermint/vm/message/src/conv/from_eth.rs @@ -3,7 +3,9 @@ //! Helper methods to convert between Ethereum and FVM data formats. -use ethers_core::types::{Eip1559TransactionRequest, NameOrAddress, H160, U256}; +use ethers_core::types::{ + Eip1559TransactionRequest, NameOrAddress, TransactionRequest, H160, U256, +}; use fendermint_vm_actor_interface::{ eam::{self, EthAddress}, evm, @@ -16,8 +18,60 @@ use fvm_shared::{ message::Message, }; +fn handle_to_address(to: &Option) -> anyhow::Result<(u64, Address)> { + // FIP-55 says that we should use `InvokeContract` for transfers instead of `METHOD_SEND`, + // because if we are sending to some Ethereum actor by ID using `METHOD_SEND`, they will + // get the tokens but the contract might not provide any way of retrieving them. + // The `Account` actor has been modified to accept any method call, so it will not fail + // even if it receives tokens using `InvokeContract`. + Ok(match to { + None => (eam::Method::CreateExternal as u64, eam::EAM_ACTOR_ADDR), + Some(NameOrAddress::Address(to)) => { + let to = to_fvm_address(*to); + (evm::Method::InvokeContract as u64, to) + } + Some(NameOrAddress::Name(_)) => { + anyhow::bail!("Turning name to address would require ENS which is not supported.") + } + }) +} + +pub fn fvm_message_from_legacy(tx: &TransactionRequest) -> anyhow::Result { + let (method_num, to) = handle_to_address(&tx.to)?; + + // The `from` of the transaction is inferred from the signature. + // As long as the client and the server use the same hashing scheme, this should be usable as a delegated address. + // If none, use the 0x00..00 null ethereum address, which in the node will be replaced with the SYSTEM_ACTOR_ADDR; + // This is similar to https://github.com/filecoin-project/lotus/blob/master/node/impl/full/eth_utils.go#L124 + let from = to_fvm_address(tx.from.unwrap_or_default()); + + // Wrap calldata in IPLD byte format. + let calldata = tx.data.clone().unwrap_or_default().to_vec(); + let params = RawBytes::serialize(BytesSer(&calldata))?; + + let gas_price = tx.gas_price.unwrap_or_default(); + + let msg = Message { + version: 0, + from, + to, + sequence: tx.nonce.unwrap_or_default().as_u64(), + value: to_fvm_tokens(&tx.value.unwrap_or_default()), + method_num, + params, + gas_limit: tx + .gas + .map(|gas| gas.min(U256::from(u64::MAX)).as_u64()) + .unwrap_or_default(), + gas_fee_cap: to_fvm_tokens(&gas_price), + gas_premium: to_fvm_tokens(&gas_price), + }; + + Ok(msg) +} + // https://github.com/filecoin-project/lotus/blob/594c52b96537a8c8728389b446482a2d7ea5617c/chain/types/ethtypes/eth_transactions.go#L152 -pub fn to_fvm_message(tx: &Eip1559TransactionRequest) -> anyhow::Result { +pub fn fvm_message_from_eip1559(tx: &Eip1559TransactionRequest) -> anyhow::Result { // FIP-55 says that we should use `InvokeContract` for transfers instead of `METHOD_SEND`, // because if we are sending to some Ethereum actor by ID using `METHOD_SEND`, they will // get the tokens but the contract might not provide any way of retrieving them. @@ -85,8 +139,9 @@ mod tests { use fvm_shared::{chainid::ChainID, crypto::signature::Signature}; use quickcheck_macros::quickcheck; + use crate::signed::OriginKind; use crate::{ - conv::{from_eth::to_fvm_message, from_fvm::to_eth_tokens}, + conv::{from_eth::fvm_message_from_eip1559, from_fvm::to_eth_tokens}, signed::{DomainHash, SignedMessage}, }; @@ -120,7 +175,9 @@ mod tests { let chain_id: ChainID = tx0.chain_id().unwrap().as_u64().into(); let msg = SignedMessage { - message: to_fvm_message(tx0.as_eip1559_ref().unwrap()).expect("to_fvm_message"), + origin_kind: OriginKind::EthereumEIP1559, + message: fvm_message_from_eip1559(tx0.as_eip1559_ref().unwrap()) + .expect("to_fvm_message"), signature: Signature::new_secp256k1(sig.to_vec()), }; diff --git a/fendermint/vm/message/src/conv/from_fvm.rs b/fendermint/vm/message/src/conv/from_fvm.rs index 2bd813adc..79fd6c0d4 100644 --- a/fendermint/vm/message/src/conv/from_fvm.rs +++ b/fendermint/vm/message/src/conv/from_fvm.rs @@ -5,10 +5,11 @@ use std::str::FromStr; +use crate::signed::OriginKind; use anyhow::anyhow; use anyhow::bail; use ethers_core::types as et; -use fendermint_crypto::{RecoveryId, Signature}; +use ethers_core::types::transaction::eip2718::TypedTransaction; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::eam::EAM_ACTOR_ID; use fvm_ipld_encoding::BytesDe; @@ -17,7 +18,6 @@ use fvm_shared::bigint::BigInt; use fvm_shared::chainid::ChainID; use fvm_shared::crypto::signature::Signature as FvmSignature; use fvm_shared::crypto::signature::SignatureType; -use fvm_shared::crypto::signature::SECP_SIG_LEN; use fvm_shared::message::Message; use fvm_shared::{address::Payload, econ::TokenAmount}; use lazy_static::lazy_static; @@ -50,24 +50,6 @@ pub fn to_eth_address(addr: &Address) -> anyhow::Result> { } } -fn parse_secp256k1(sig: &[u8]) -> anyhow::Result<(RecoveryId, Signature)> { - if sig.len() != SECP_SIG_LEN { - return Err(anyhow!("unexpected Secp256k1 length: {}", sig.len())); - } - - // generate types to recover key from - let rec_id = RecoveryId::parse(sig[64])?; - - // Signature value without recovery byte - let mut s = [0u8; 64]; - s.clone_from_slice(&sig[..64]); - - // generate Signature - let sig = Signature::parse_standard(&s)?; - - Ok((rec_id, sig)) -} - /// Convert an FVM signature, which is a normal Secp256k1 signature, to an Ethereum one, /// where the `v` is optionally shifted by 27 to make it compatible with Solidity. /// @@ -75,26 +57,83 @@ fn parse_secp256k1(sig: &[u8]) -> anyhow::Result<(RecoveryId, Signature)> { /// /// Ethers normalizes Ethereum signatures during conversion to RLP. pub fn to_eth_signature(sig: &FvmSignature, normalized: bool) -> anyhow::Result { - let (v, sig) = match sig.sig_type { - SignatureType::Secp256k1 => parse_secp256k1(&sig.bytes)?, + let mut sig = match sig.sig_type { + SignatureType::Secp256k1 => et::Signature::try_from(sig.bytes.as_slice())?, other => return Err(anyhow!("unexpected signature type: {other:?}")), }; // By adding 27 to the recovery ID we make this compatible with Ethereum, // so that we can verify such signatures in Solidity with e.g. openzeppelin ECDSA.sol - let shift = if normalized { 0 } else { 27 }; - - let sig = et::Signature { - v: et::U64::from(v.serialize() + shift).as_u64(), - r: et::U256::from_big_endian(sig.r.b32().as_ref()), - s: et::U256::from_big_endian(sig.s.b32().as_ref()), + if !normalized { + sig.v += 27 }; Ok(sig) } -/// Turn an FVM `Message` back into an Ethereum transaction request. -pub fn to_eth_transaction_request( +pub fn to_eth_typed_transaction( + origin_kind: OriginKind, + message: &Message, + chain_id: &ChainID, +) -> anyhow::Result { + match origin_kind { + OriginKind::Fvm => Err(anyhow!("fvm message not allowed")), + OriginKind::EthereumLegacy => Ok(TypedTransaction::Legacy(to_eth_legacy_request( + message, chain_id, + )?)), + OriginKind::EthereumEIP1559 => Ok(TypedTransaction::Eip1559(to_eth_eip1559_request( + message, chain_id, + )?)), + } +} + +/// Turn an FVM `Message` back into an Ethereum legacy transaction request. +pub fn to_eth_legacy_request( + msg: &Message, + chain_id: &ChainID, +) -> anyhow::Result { + let chain_id: u64 = (*chain_id).into(); + + let Message { + from, + to, + sequence, + value, + params, + gas_limit, + gas_fee_cap, + .. + } = msg; + + let mut tx = et::TransactionRequest::new() + .from(to_eth_address(from)?.unwrap_or_default()) + .nonce(*sequence) + .gas(*gas_limit) + .gas_price(to_eth_tokens(gas_fee_cap)?) + // ethers sometimes interprets chain id == 1 as None for mainnet. + // but most likely chain id will not be 1 in our use case, set it anyways + .chain_id(chain_id); + + let data = fvm_ipld_encoding::from_slice::(params).map(|bz| bz.0)?; + // ethers seems to parse empty bytes as None instead of Some(Bytes(0x)) + if !data.is_empty() { + tx = tx.data(et::Bytes::from(data)); + } + + tx.to = to_eth_address(to)?.map(et::NameOrAddress::Address); + + // NOTE: It's impossible to tell if the original Ethereum transaction sent None or Some(0). + // The ethers deployer sends None, so let's assume that's the useful behavour to match. + // Luckily the RLP encoding at some point seems to resolve them to the same thing. + if !value.is_zero() { + tx.value = Some(to_eth_tokens(value)?); + } + + Ok(tx) +} + +/// Turn an FVM `Message` back into an Ethereum eip1559 transaction request. +pub fn to_eth_eip1559_request( msg: &Message, chain_id: &ChainID, ) -> anyhow::Result { @@ -146,18 +185,18 @@ pub mod tests { use ethers_core::{k256::ecdsa::SigningKey, types::transaction::eip2718::TypedTransaction}; use fendermint_crypto::SecretKey; use fendermint_testing::arb::ArbTokenAmount; - use fendermint_vm_message::signed::SignedMessage; + use fendermint_vm_message::signed::{OriginKind, SignedMessage}; use fvm_shared::crypto::signature::Signature; use fvm_shared::{bigint::BigInt, chainid::ChainID, econ::TokenAmount}; use quickcheck_macros::quickcheck; use rand::{rngs::StdRng, SeedableRng}; use crate::conv::{ - from_eth::to_fvm_message, + from_eth::fvm_message_from_eip1559, tests::{EthMessage, KeyPair}, }; - use super::{to_eth_signature, to_eth_tokens, to_eth_transaction_request}; + use super::{to_eth_eip1559_request, to_eth_signature, to_eth_tokens}; #[quickcheck] fn prop_to_eth_tokens(tokens: ArbTokenAmount) -> bool { @@ -210,9 +249,9 @@ pub mod tests { fn prop_to_and_from_eth_transaction(msg: EthMessage, chain_id: u64) { let chain_id = ChainID::from(chain_id); let msg0 = msg.0; - let tx = to_eth_transaction_request(&msg0, &chain_id) - .expect("to_eth_transaction_request failed"); - let msg1 = to_fvm_message(&tx).expect("to_fvm_message failed"); + let tx = + to_eth_eip1559_request(&msg0, &chain_id).expect("to_eth_transaction_request failed"); + let msg1 = fvm_message_from_eip1559(&tx).expect("to_fvm_message failed"); assert_eq!(msg1, msg0) } @@ -225,7 +264,7 @@ pub mod tests { let chain_id = ChainID::from(chain_id); let msg0 = msg.0; - let tx: TypedTransaction = to_eth_transaction_request(&msg0, &chain_id) + let tx: TypedTransaction = to_eth_eip1559_request(&msg0, &chain_id) .expect("to_eth_transaction_request failed") .into(); @@ -242,9 +281,10 @@ pub mod tests { .expect("failed to decode RLP as signed TypedTransaction"); let tx1 = tx1.as_eip1559_ref().expect("not an eip1559 transaction"); - let msg1 = to_fvm_message(tx1).expect("to_fvm_message failed"); + let msg1 = fvm_message_from_eip1559(tx1).expect("to_fvm_message failed"); let signed = SignedMessage { + origin_kind: OriginKind::EthereumEIP1559, message: msg1, signature: Signature::new_secp256k1(sig.to_vec()), }; diff --git a/fendermint/vm/message/src/signed.rs b/fendermint/vm/message/src/signed.rs index 78af1f655..b324c5676 100644 --- a/fendermint/vm/message/src/signed.rs +++ b/fendermint/vm/message/src/signed.rs @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::anyhow; -use cid::multihash::MultihashDigest; use cid::Cid; use ethers_core::types as et; use ethers_core::types::transaction::eip2718::TypedTransaction; @@ -16,6 +15,7 @@ use fvm_shared::chainid::ChainID; use fvm_shared::crypto::signature::ops::recover_secp_public_key; use fvm_shared::crypto::signature::{Signature, SignatureType, SECP_SIG_LEN}; use fvm_shared::message::Message; +use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -61,30 +61,65 @@ pub enum DomainHash { /// and because the `Message` is already serialized as a tuple. #[derive(PartialEq, Clone, Debug, Serialize_tuple, Deserialize_tuple, Hash, Eq)] pub struct SignedMessage { + pub origin_kind: OriginKind, pub message: Message, pub signature: Signature, } +/// The original type of the message determines which fields of the message that should be used to +/// to valid the signature. +#[repr(u8)] +#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize, Hash, Eq)] +pub enum OriginKind { + Fvm = 0, + EthereumLegacy = 1, + EthereumEIP1559 = 2, +} + +impl From for OriginKind { + fn from(value: u8) -> Self { + match value { + 0 => Self::Fvm, + 1 => Self::EthereumLegacy, + _ => Self::EthereumEIP1559, + } + } +} + impl SignedMessage { /// Generate a new signed message from fields. /// /// The signature will not be verified. - pub fn new_unchecked(message: Message, signature: Signature) -> SignedMessage { - SignedMessage { message, signature } + pub fn new_unchecked( + origin_kind: OriginKind, + message: Message, + signature: Signature, + ) -> SignedMessage { + SignedMessage { + origin_kind, + message, + signature, + } } - /// Create a signed message. + /// Create a signed message. Note that for evm, only EIP1559 txn is supported. pub fn new_secp256k1( message: Message, sk: &SecretKey, chain_id: &ChainID, ) -> Result { - let signature = match Self::signable(&message, chain_id)? { - Signable::Ethereum((hash, _)) => sign_eth(sk, hash), - Signable::Regular(data) => sign_regular(sk, &data), - Signable::RegularFromEth((data, _)) => sign_regular(sk, &data), + let (signature, origin_kind) = match Self::signable(&message, chain_id)? { + Signable::Ethereum((hash, _)) => (sign_eth(sk, hash), OriginKind::EthereumEIP1559), + Signable::Regular(data) => (sign_regular(sk, &data), OriginKind::Fvm), + Signable::RegularFromEth((data, _)) => { + (sign_regular(sk, &data), OriginKind::EthereumEIP1559) + } }; - Ok(Self { message, signature }) + Ok(Self { + origin_kind, + message, + signature, + }) } /// Calculate the CID of an FVM message. @@ -115,7 +150,7 @@ impl SignedMessage { // which should allow messages from ethereum accounts to go to any other type of account, e.g. custom Wasm actors. match maybe_eth_address(&message.from) { Some(addr) if is_eth_addr_compat(&message.to) => { - let tx: TypedTransaction = from_fvm::to_eth_transaction_request(message, chain_id) + let tx: TypedTransaction = from_fvm::to_eth_eip1559_request(message, chain_id) .map_err(SignedMessageError::Ethereum)? .into(); @@ -136,46 +171,109 @@ impl SignedMessage { /// Verify that the message CID was signed by the `from` address. pub fn verify_signature( + origin_kind: OriginKind, message: &Message, signature: &Signature, chain_id: &ChainID, ) -> Result<(), SignedMessageError> { - match Self::signable(message, chain_id)? { - Signable::Ethereum((hash, from)) => { - // If the sender is ethereum, recover the public key from the signature (which verifies it), - // then turn it into an `EthAddress` and verify it matches the `from` of the message. - let sig = from_fvm::to_eth_signature(signature, true) - .map_err(SignedMessageError::Ethereum)?; - - let rec = sig - .recover(hash) - .map_err(|e| SignedMessageError::Ethereum(anyhow!(e)))?; - - if rec == from { - verify_eth_method(message) - } else { - Err(SignedMessageError::InvalidSignature(format!("the Ethereum delegated address did not match the one recovered from the signature (sighash = {:?})", hash))) - } - } - Signable::Regular(data) => { - // This works when `from` corresponds to the signature type. - signature - .verify(&data, &message.from) - .map_err(SignedMessageError::InvalidSignature) - } - Signable::RegularFromEth((data, from)) => { - let rec = recover_secp256k1(signature, &data) - .map_err(SignedMessageError::InvalidSignature)?; + match origin_kind { + OriginKind::Fvm => Self::verify_fvm_signature(message, chain_id, signature), + OriginKind::EthereumLegacy => Self::verify_ethereum_signature( + message, + chain_id, + signature, + |message, chain_id| { + let tx = from_fvm::to_eth_legacy_request(message, chain_id) + .map_err(SignedMessageError::Ethereum)?; + Ok(TypedTransaction::Legacy(tx)) + }, + ), + OriginKind::EthereumEIP1559 => Self::verify_ethereum_signature( + message, + chain_id, + signature, + |message, chain_id| { + Ok(from_fvm::to_eth_eip1559_request(message, chain_id) + .map_err(SignedMessageError::Ethereum)? + .into()) + }, + ), + } + } + + fn verify_ethereum_signature anyhow::Result>( + message: &Message, + chain_id: &ChainID, + signature: &Signature, + to_eth_txn: F, + ) -> anyhow::Result<(), SignedMessageError> { + // This is in contrast to https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0055.md#delegated-signature-type + // which introduces a `SignatureType::Delegated`, in which case the signature check should be done by the recipient actor. + // + // However, that isn't implemented, and adding that type would mean copying the entire `Signature` type into Fendermint, + // similarly to how Forest did it https://github.com/ChainSafe/forest/blob/b3c5efe6cc81607da945227bb41c60cec47909c3/utils/forest_shim/src/crypto.rs#L166 + // + // Instead of special casing on the signature type, we are special casing on the sender, + // which should be okay because the CLI only uses `f1` addresses and the Ethereum API only uses `f410` addresses, + // so at least for now they are easy to tell apart: any `f410` address is coming from Ethereum API and must have + // been signed according to the Ethereum scheme, and it could not have been signed by an `f1` address, it doesn't + // work with regular accounts. + // + // We detect the case where the recipient is not an ethereum address. If that is the case then use regular signing rules, + // which should allow messages from ethereum accounts to go to any other type of account, e.g. custom Wasm actors. + let Some(from) = maybe_eth_address(&message.from) else { + return Err(SignedMessageError::Ethereum(anyhow!( + "sender not ethereum address" + ))); + }; - let rec_addr = EthAddress::from(rec); + if !is_eth_addr_compat(&message.to) { + let mut data = Self::cid(message)?.to_bytes(); + data.extend(chain_id_bytes(chain_id).iter()); - if rec_addr.0 == from.0 { - Ok(()) - } else { - Err(SignedMessageError::InvalidSignature("the Ethereum delegated address did not match the one recovered from the signature".to_string())) - } - } + let rec = recover_secp256k1(signature, &data) + .map_err(SignedMessageError::InvalidSignature)?; + + let rec_addr = EthAddress::from(rec); + + return if rec_addr.0 == from.0 { + Ok(()) + } else { + Err(SignedMessageError::InvalidSignature("the Ethereum delegated address did not match the one recovered from the signature".to_string())) + }; + } + + let hash = to_eth_txn(message, chain_id) + .map_err(SignedMessageError::Ethereum)? + .sighash(); + + // If the sender is ethereum, recover the public key from the signature (which verifies it), + // then turn it into an `EthAddress` and verify it matches the `from` of the message. + let sig = + from_fvm::to_eth_signature(signature, true).map_err(SignedMessageError::Ethereum)?; + + let rec = sig + .recover(hash) + .map_err(|e| SignedMessageError::Ethereum(anyhow!(e)))?; + + if rec != from { + return Err(SignedMessageError::InvalidSignature(format!("the Ethereum delegated address did not match the one recovered from the signature (sighash = {:?})", hash))); } + verify_eth_method(message) + } + + fn verify_fvm_signature( + message: &Message, + chain_id: &ChainID, + signature: &Signature, + ) -> anyhow::Result<(), SignedMessageError> { + let mut data = Self::cid(message)?.to_bytes(); + data.extend(chain_id_bytes(chain_id).iter()); + + // This works when `from` corresponds to the signature type. + signature + .verify(&data, &message.from) + .map_err(SignedMessageError::InvalidSignature) } /// Calculate an optional hash that ecosystem tools expect. @@ -184,20 +282,13 @@ impl SignedMessage { chain_id: &ChainID, ) -> Result, SignedMessageError> { if is_eth_addr_deleg(&self.message.from) && is_eth_addr_compat(&self.message.to) { - let tx: TypedTransaction = - from_fvm::to_eth_transaction_request(self.message(), chain_id) - .map_err(SignedMessageError::Ethereum)? - .into(); + let tx = from_fvm::to_eth_typed_transaction(self.origin_kind, self.message(), chain_id) + .map_err(SignedMessageError::Ethereum)?; let sig = from_fvm::to_eth_signature(self.signature(), true) .map_err(SignedMessageError::Ethereum)?; - let rlp = tx.rlp_signed(&sig); - - let hash = cid::multihash::Code::Keccak256.digest(&rlp); - let hash = hash.digest().try_into().expect("Keccak256 is 32 bytes"); - - Ok(Some(DomainHash::Eth(hash))) + Ok(Some(DomainHash::Eth(tx.hash(&sig).0))) } else { // Use the default transaction ID. Ok(None) @@ -206,7 +297,7 @@ impl SignedMessage { /// Verifies that the from address of the message generated the signature. pub fn verify(&self, chain_id: &ChainID) -> Result<(), SignedMessageError> { - Self::verify_signature(&self.message, &self.signature, chain_id) + Self::verify_signature(self.origin_kind, &self.message, &self.signature, chain_id) } /// Returns reference to the unsigned message. @@ -351,6 +442,7 @@ fn recover_secp256k1(signature: &Signature, data: &[u8]) -> Result Self { Self { + origin_kind: OriginKind::from(u8::arbitrary(g) % 3), message: ArbMessage::arbitrary(g).0, signature: Signature::arbitrary(g), } diff --git a/fendermint/vm/snapshot/src/manager.rs b/fendermint/vm/snapshot/src/manager.rs index 4a6f490f8..2a749ed90 100644 --- a/fendermint/vm/snapshot/src/manager.rs +++ b/fendermint/vm/snapshot/src/manager.rs @@ -441,12 +441,11 @@ mod tests { let mut g = quickcheck::Gen::new(5); let genesis = Genesis::arbitrary(&mut g); - let maybe_contract_path = genesis.ipc.as_ref().map(|_| contracts_path()); let (state, out) = create_test_genesis_state( bundle_path(), custom_actors_bundle_path(), + contracts_path(), genesis, - maybe_contract_path, ) .await .expect("cannot create genesis state"); @@ -467,6 +466,7 @@ mod tests { chain_id: out.chain_id.into(), power_scale: out.power_scale, app_version: 0, + consensus_params: None, }; (state_params, store) diff --git a/fendermint/vm/snapshot/src/manifest.rs b/fendermint/vm/snapshot/src/manifest.rs index aea771718..42ffbbc51 100644 --- a/fendermint/vm/snapshot/src/manifest.rs +++ b/fendermint/vm/snapshot/src/manifest.rs @@ -190,6 +190,7 @@ mod arb { .into(), power_scale: *g.choose(&[-1, 0, 3]).unwrap(), app_version: 0, + consensus_params: None, }, version: Arbitrary::arbitrary(g), }