Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/forest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ jobs:
forest-dev tests wallet
forest-dev devnet eth-gas
forest-dev devnet eth-skip-sender
forest-dev devnet state-call
timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}
- name: Dump docker logs
if: always()
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@

- [#7702](https://github.com/ChainSafe/forest/issues/7702): `eth_estimateGas` matches Lotus v1.38.0 and reth. The estimate never exceeds the call's `gas` or, with `gasPrice` or `maxFeePerGas` set, the gas the sender can pay for after the value; without a price the sender's balance does not limit the gas. A revert that more gas fixes is searched past, and failures use the Ethereum codes: `-32000` when the cap is below the message inclusion cost or the sender affords no gas, `-32003` when the call runs out of gas within the cap or, with a price, the value exceeds the balance, `-32602` when both prices are set. See [lotus#13911](https://github.com/filecoin-project/lotus/pull/13911).

- [#7754](https://github.com/ChainSafe/forest/pull/7754): `Filecoin.StateCall` runs the message at its own `GasLimit` instead of an effectively unlimited one. A `GasLimit` of `0` selects the node's cap, the block gas limit unless the new `FOREST_STATE_CALL_GAS_LIMIT` is set, and a larger `GasLimit` is rejected. Matches Lotus v1.38.0 ([lotus#13910](https://github.com/filecoin-project/lotus/pull/13910)).

### Removed

### Fixed

- [#7733](https://github.com/ChainSafe/forest/pull/7733): Fixed `Filecoin.GetActorEventsRaw` ignoring the maximum filter height range. The height range is now correctly validated, and a filter without `fromHeight` returns no events.
- [#7754](https://github.com/ChainSafe/forest/pull/7754): `Filecoin.StateCall`, `Filecoin.StateReplay` and `Filecoin.StateCompute` report `unknown error (RetCode=<code>)` for a failed message the FVM attaches no failure info to, such as one that runs out of gas, instead of an empty error. Matches Lotus.

## Forest v0.38.0 "Ayanant"

Expand Down
12 changes: 12 additions & 0 deletions docs/docs/users/reference/cli.md

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

1 change: 1 addition & 0 deletions docs/docs/users/reference/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,4 @@ generate_markdown_section "forest-dev" "tests mpool"
generate_markdown_section "forest-dev" "devnet"
generate_markdown_section "forest-dev" "devnet eth-gas"
generate_markdown_section "forest-dev" "devnet eth-skip-sender"
generate_markdown_section "forest-dev" "devnet state-call"
1 change: 1 addition & 0 deletions docs/docs/users/reference/env_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ process.
| `FOREST_RPC_MAX_CONNECTIONS` | positive integer | 1000 | 42 | Maximum number of allowed connections for the RPC server |
| `FOREST_RPC_MAX_BATCH_LEN` | positive integer | 100 | 500 | Maximum number of entries in a single JSON-RPC batch request. Larger batches are rejected with error `-32010`. `0` or invalid values fall back to the default |
| `FOREST_RPC_REPLAY_CONCURRENCY` | positive integer | half the available CPUs | 4 | Maximum concurrent tipset replays triggered by RPC methods (`StateReplay`, `trace_*`, `debug_trace*`). Each replay is a full VM execution of a tipset; bounding them keeps replay-heavy clients from starving the node |
| `FOREST_STATE_CALL_GAS_LIMIT` | positive integer | 10000000000 (the block gas limit) | 20000000000 | Caps the `GasLimit` of `Filecoin.StateCall` messages: a message above the cap is rejected, and one with `GasLimit` `0` runs at the cap |
| `FOREST_RPC_COMPRESS_MIN_BODY_SIZE` | integer in `[-1, 65535]` (bytes) | -1 (disabled) | 2048 (or `-1` to disable) | Disabled by default. When set to a non-negative value, gzip-compresses responses whose body is at least that many bytes; smaller responses are sent uncompressed. Values above 65535 are clamped to 65535. Set to a negative value (e.g. `-1`) to disable compression entirely |
| `FOREST_MAX_CONCURRENT_REQUEST_RESPONSE_STREAMS_PER_PEER` | positive integer | 10 | 10 | the maximum concurrent streams per peer for request-response-based p2p protocols |
| `FOREST_BLOCK_DELAY_SECS` | positive integer | Depends on the network | 30 | Duration of each tipset epoch |
Expand Down
11 changes: 6 additions & 5 deletions scripts/devnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ Under the hood this sources `test_harness.sh`, which reads the admin token and
the funded genesis key from the running `forest` container, exports
`FULLNODE_API_INFO` (Forest RPC on port 3456) and `FOREST_TEST_PRELOADED_ADDRESS`,
then runs `forest-dev tests mpool`, `forest-dev tests wallet`,
`forest-dev devnet eth-gas` and `forest-dev devnet eth-skip-sender`.
`forest-dev devnet eth-gas`, `forest-dev devnet eth-skip-sender` and
`forest-dev devnet state-call`.

The `devnet` suites (unlike `tests`) need both nodes reachable: `eth-gas`
compares Forest against Lotus, and `eth-skip-sender` deploys contracts and
submits transactions, so they run pre-flight checks and fail early if the
devnet or harness environment is missing.
The `devnet` suites (unlike `tests`) need both nodes reachable: `eth-gas` and
`state-call` compare Forest against Lotus, and `eth-skip-sender` deploys
contracts and submits transactions, so they run pre-flight checks and fail
early if the devnet or harness environment is missing.

## Local devnet development

Expand Down
1 change: 1 addition & 0 deletions scripts/devnet/run_integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ forest-dev tests mpool
forest-dev tests wallet
forest-dev devnet eth-gas
forest-dev devnet eth-skip-sender
forest-dev devnet state-call
2 changes: 1 addition & 1 deletion scripts/tests/api_compare/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Note: this should be a `fat` image so that it contains the pre-downloaded filecoin proof parameters
FOREST_IMAGE=ghcr.io/chainsafe/forest:edge-fat
LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.37.0-calibnet
LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.38.0-calibnet
FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters
LOTUS_RPC_PORT=1234
LOTUS_VIA_GATEWAY_RPC_PORT=4568
Expand Down
3 changes: 3 additions & 0 deletions src/dev/subcommands/devnet_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

mod eth_gas;
mod eth_skip_sender;
mod state_call;

use crate::dev::subcommands::tests_cmd::helpers::{docker, forest_client, lotus_client};
use crate::rpc::prelude::*;
Expand All @@ -18,6 +19,7 @@ use anyhow::{Context as _, ensure};
pub enum DevnetCommand {
EthGas(eth_gas::EthGasTestCommand),
EthSkipSender(eth_skip_sender::EthSkipSenderTestCommand),
StateCall(state_call::StateCallTestCommand),
}

impl DevnetCommand {
Expand All @@ -26,6 +28,7 @@ impl DevnetCommand {
match self {
Self::EthGas(cmd) => cmd.run().await,
Self::EthSkipSender(cmd) => cmd.run().await,
Self::StateCall(cmd) => cmd.run().await,
}
}
}
Expand Down
39 changes: 3 additions & 36 deletions src/dev/subcommands/devnet_cmd/eth_gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ use crate::rpc::eth::{
use crate::rpc::prelude::*;
use crate::rpc::types::ApiTipsetKey;
use crate::shim::econ::{BLOCK_GAS_LIMIT, TokenAmount};
use crate::utils::encoding::keccak_256;
use anyhow::{Context as _, bail, ensure};
use jsonrpsee::types::ErrorObjectOwned;
use jsonrpsee::types::error::INVALID_PARAMS_CODE;
use libtest_mimic::{Arguments, Failed, Trial};
use libtest_mimic::{Arguments, Trial};
use tokio::sync::OnceCell;

/// `NestedGas`, whose `recurse(uint256)` calls itself that many times.
Expand All @@ -46,8 +45,6 @@ const ALWAYS_REVERTS_REASON: &str = "always reverts";
const CONTROL_DEPTH: u64 = 0;
/// Deep enough that the penalty is ~1.9x, well clear of the crossover measured around 40-60.
const NESTED_DEPTH: u64 = 100;
/// Pays for the on-chain submit and lets the priced estimates afford the nested call.
const SENDER_FUND_AMT: &str = "10 FIL";
/// Enough for the sender to exist, far too little to pay for the nested call's gas.
const POOR_SENDER_FUND_AMT: &str = "1 nanoFIL";

Expand All @@ -66,12 +63,6 @@ impl EthGasTestCommand {
}

fn tests() -> Vec<Trial> {
fn trial(name: &'static str, body: fn() -> anyhow::Result<()>) -> Trial {
Trial::test(name, move || {
body().map_err(|e| Failed::from(format!("{e:?}")))
})
}

vec![
trial("eth_estimate_gas_agrees_without_nesting", || {
block_on(estimate_agrees(CONTROL_DEPTH))
Expand Down Expand Up @@ -111,14 +102,6 @@ fn tests() -> Vec<Trial> {
]
}

/// The 4-byte Ethereum function selector: first 4 bytes of `keccak256(signature)`.
fn selector(signature: &str) -> Vec<u8> {
keccak_256(signature.as_bytes())
.get(..4)
.expect("keccak256 is 32 bytes")
.to_vec()
}

/// ABI calldata for `recurse(uint256)`: the selector followed by `depth` as a 32-byte word.
fn recurse_calldata(depth: u64) -> Vec<u8> {
let mut out = selector(RECURSE_SIGNATURE);
Expand All @@ -131,25 +114,9 @@ async fn contract() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| async {
let deploy = forest_evm_deploy_hex(&sender().await?.cli, NESTED_GAS_HEX)?;
let f4 = parse_f4_from_evm_deploy(&deploy)?;
eprintln!("deployed NestedGas at {f4}");
poll_until_actor_on("forest", f4, forest_client).await?;
poll_until_actor_on("lotus", f4, lotus_client).await?;
let contract = deploy_contract("NestedGas", NESTED_GAS_HEX).await?;
poll_until_next_epoch().await?;
EthAddress::from_filecoin_address(&f4)
})
.await
}

/// The funded sender, created once per process and known to both nodes and both wallets.
async fn sender() -> anyhow::Result<&'static Wallet> {
static SENDER: OnceCell<Wallet> = OnceCell::const_new();
SENDER
.get_or_try_init(|| async {
let wallet = new_funded(SENDER_FUND_AMT).await?;
import_lotus_wallet_into_forest(&wallet.cli)?;
Ok(wallet)
Ok(contract)
})
.await
}
Expand Down
75 changes: 12 additions & 63 deletions src/dev/subcommands/devnet_cmd/eth_skip_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ use crate::rpc::prelude::*;
use crate::shim::address::Address;
use crate::shim::econ::TokenAmount;
use crate::shim::state_tree::ActorState;
use crate::utils::encoding::{hex, keccak_256};
use crate::utils::encoding::hex;
use anyhow::{Context as _, ensure};
use libtest_mimic::{Arguments, Failed, Trial};
use libtest_mimic::{Arguments, Trial};
use std::str::FromStr as _;
use tokio::sync::OnceCell;

Expand All @@ -52,7 +52,6 @@ const FAIL_REVERT_EMPTY: &str = "failRevertEmpty()";
const FAIL_CUSTOM: &str = "failCustom()";

const NESTED_DEPTH: u64 = 100;
const DEPLOYER_FUND_AMT: &str = "10 FIL";
const ROUND_TRIP_FUND_AMT: &str = "1 FIL";
const RECURSIVE_FUND_AMT: &str = "10 FIL";
const EOA_FUND_AMT: &str = "10 FIL";
Expand Down Expand Up @@ -84,12 +83,6 @@ impl EthSkipSenderTestCommand {
}

fn tests() -> Vec<Trial> {
fn trial(name: &'static str, body: fn() -> anyhow::Result<()>) -> Trial {
Trial::test(name, move || {
body().map_err(|e| Failed::from(format!("{e:?}")))
})
}

vec![
trial("evm_deploy_and_call", || block_on(evm_deploy_and_call())),
trial("round_trip_from_unfunded", || {
Expand Down Expand Up @@ -134,25 +127,6 @@ fn tests() -> Vec<Trial> {
]
}

fn selector(signature: &str) -> Vec<u8> {
keccak_256(signature.as_bytes())
.get(..4)
.expect("keccak256 is 32 bytes")
.to_vec()
}

fn abi_address_word(addr: EthAddress) -> Vec<u8> {
let mut word = vec![0u8; 12];
word.extend_from_slice(addr.0.as_bytes());
word
}

fn calldata(sig: &str, extra: &[u8]) -> Vec<u8> {
let mut out = selector(sig);
out.extend_from_slice(extra);
out
}

fn send_coin_calldata(to: EthAddress, amount: u64) -> Vec<u8> {
let mut extra = abi_address_word(to);
extra.extend_from_slice(&ethereum_types::U256::from(amount).to_big_endian());
Expand Down Expand Up @@ -201,41 +175,16 @@ fn latest() -> BlockNumberOrHash {
BlockNumberOrHash::PredefinedBlock(Predefined::Latest)
}

/// Dedicated delegated wallet used to deploy and to credit `SimpleCoin`.
/// Not the genesis/miner key: that wallet races with the miner on nonce.
async fn deployer() -> anyhow::Result<&'static Address> {
static DEPLOYER: OnceCell<Address> = OnceCell::const_new();
DEPLOYER
.get_or_try_init(|| async {
let addr = lotus_exec(&["wallet", "new", "delegated"])?;
let funded = fund_on_chain(&addr, DEPLOYER_FUND_AMT).await?;
import_lotus_wallet_into_forest(&addr)?;
Ok(funded)
})
.await
}

async fn deploy_hex(label: &str, bytecode: &str) -> anyhow::Result<EthAddress> {
let deployer = deployer().await?;
let from = deployer.to_string();
let deploy = forest_evm_deploy_hex(&from, bytecode)?;
let f4 = parse_f4_from_evm_deploy(&deploy)?;
eprintln!("deployed {label} at {f4}");
poll_until_actor(f4).await?;
poll_until_actor_on("lotus", f4, lotus_client).await?;
EthAddress::from_filecoin_address(&f4)
}

async fn simple_coin() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| deploy_hex("SimpleCoin", SIMPLE_COIN_HEX))
.get_or_try_init(|| deploy_contract("SimpleCoin", SIMPLE_COIN_HEX))
.await
}

async fn evm_deploy_and_call() -> anyhow::Result<()> {
let coin = simple_coin().await?;
let from = EthAddress::from_filecoin_address(deployer().await?)?;
let from = sender().await?.eth;
let from_hex = hex::encode_prefixed(from.0.as_bytes());
let to_hex = hex::encode_prefixed(coin.0.as_bytes());
let data = hex::encode_prefixed(get_balance_calldata(from));
Expand All @@ -260,21 +209,21 @@ async fn evm_deploy_and_call() -> anyhow::Result<()> {
async fn contract_b() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| deploy_hex("ContractB", CONTRACT_B_HEX))
.get_or_try_init(|| deploy_contract("ContractB", CONTRACT_B_HEX))
.await
}

async fn nested_gas() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| deploy_hex("NestedGas", NESTED_GAS_HEX))
.get_or_try_init(|| deploy_contract("NestedGas", NESTED_GAS_HEX))
.await
}

async fn errors_contract() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| deploy_hex("Errors", ERRORS_HEX))
.get_or_try_init(|| deploy_contract("Errors", ERRORS_HEX))
.await
}

Expand Down Expand Up @@ -309,7 +258,7 @@ async fn linked_contracts() -> anyhow::Result<&'static (EthAddress, EthAddress)>
LINKED
.get_or_try_init(|| async {
let b = contract_b().await?;
let a = deploy_hex("ContractA", CONTRACT_A_HEX).await?;
let a = deploy_contract("ContractA", CONTRACT_A_HEX).await?;
invoke(a, &set_contract_b_calldata(*b)).await?;
Ok((a, *b))
})
Expand All @@ -321,9 +270,9 @@ async fn poll_until_actor(addr: Address) -> anyhow::Result<ActorState> {
}

async fn invoke(to: EthAddress, calldata: &[u8]) -> anyhow::Result<()> {
let from = deployer().await?.to_string();
let from = &sender().await?.cli;
let to_fil = to.to_filecoin_address()?.to_string();
forest_evm_invoke(&from, &to_fil, &hex::encode(calldata))?;
forest_evm_invoke(from, &to_fil, &hex::encode(calldata))?;
Ok(())
}

Expand Down Expand Up @@ -1021,7 +970,7 @@ async fn call_sender_identity() -> anyhow::Result<()> {

async fn skip_sender_state_isolation() -> anyhow::Result<()> {
let forest = forest_client()?;
let to = EthAddress::from_filecoin_address(deployer().await?)?;
let to = sender().await?.eth;
let from = non_existent(0x03)?;

let first = eth_call(&forest, from, to, Vec::new(), latest()).await?;
Expand Down Expand Up @@ -1056,7 +1005,7 @@ async fn skip_sender_state_isolation() -> anyhow::Result<()> {

async fn skip_sender_historical_call() -> anyhow::Result<()> {
let forest = forest_client()?;
let to = EthAddress::from_filecoin_address(deployer().await?)?;
let to = sender().await?.eth;
let from = non_existent(0x03)?;
let head = forest
.call(EthBlockNumber::request(())?)
Expand Down
Loading
Loading