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 @@ -512,6 +512,7 @@ jobs:
forest-dev devnet eth-gas
forest-dev devnet eth-skip-sender
forest-dev devnet state-call
forest-dev devnet eth-pending-tx
timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}
- name: Dump docker logs
if: always()
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,22 @@

- [#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)).
- [#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 ([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.

- [#7761](https://github.com/ChainSafe/forest/pull/7761): `eth_getLogs`, `eth_getFilterLogs`, `eth_getFilterChanges`, `eth_getTransactionReceipt`, `eth_getBlockReceipts` and their `Limited` variants return an error instead of partial logs when an event's message cannot be read. Matches Lotus ([lotus#13926](https://github.com/filecoin-project/lotus/pull/13926)).

- [#7761](https://github.com/ChainSafe/forest/pull/7761): `eth_getTransactionByHash` and `eth_getTransactionByHashLimited` return a pending Ethereum transaction received from another node instead of `null` until it is mined. Matches Lotus ([lotus#13677](https://github.com/filecoin-project/lotus/pull/13677)).

- [#7761](https://github.com/ChainSafe/forest/pull/7761): `eth_getTransactionByHash` and `eth_getTransactionByHashLimited` report the `hash` and `from` of a pending transaction instead of zeros.

## Forest v0.38.0 "Ayanant"

Mandatory release for mainnet node operators. It includes support for the NV29 _Solstice_ network upgrade for mainnet, which is set to activate at epoch `6470279` (2026-10-19T12:59:30Z). Note the breaking change below: JSON-RPC batch requests are now limited to 100 entries by default. There are also other changes and improvements; see below.
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 @@ -219,3 +219,4 @@ 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"
generate_markdown_section "forest-dev" "devnet eth-pending-tx"
9 changes: 5 additions & 4 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`, `forest-dev devnet eth-skip-sender` and
`forest-dev devnet state-call`.
`forest-dev devnet eth-gas`, `forest-dev devnet eth-skip-sender`,
`forest-dev devnet state-call` and `forest-dev devnet eth-pending-tx`.

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
`state-call` compare Forest against Lotus, `eth-skip-sender` deploys
contracts and submits transactions, and `eth-pending-tx` submits a transaction
through Lotus and looks it up on Forest, 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 @@ -23,3 +23,4 @@ forest-dev tests wallet
forest-dev devnet eth-gas
forest-dev devnet eth-skip-sender
forest-dev devnet state-call
forest-dev devnet eth-pending-tx
3 changes: 3 additions & 0 deletions src/dev/subcommands/devnet_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! that environment is missing, rather than letting a suite surface it as an opaque mid-run error.

mod eth_gas;
mod eth_pending_tx;
mod eth_skip_sender;
mod state_call;

Expand All @@ -18,6 +19,7 @@ use anyhow::{Context as _, ensure};
#[derive(Debug, clap::Subcommand)]
pub enum DevnetCommand {
EthGas(eth_gas::EthGasTestCommand),
EthPendingTx(eth_pending_tx::EthPendingTxTestCommand),
EthSkipSender(eth_skip_sender::EthSkipSenderTestCommand),
StateCall(state_call::StateCallTestCommand),
}
Expand All @@ -27,6 +29,7 @@ impl DevnetCommand {
preflight().await.context("devnet pre-flight failed")?;
match self {
Self::EthGas(cmd) => cmd.run().await,
Self::EthPendingTx(cmd) => cmd.run().await,
Self::EthSkipSender(cmd) => cmd.run().await,
Self::StateCall(cmd) => cmd.run().await,
}
Expand Down
105 changes: 105 additions & 0 deletions src/dev/subcommands/devnet_cmd/eth_pending_tx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

//! `eth_getTransactionByHash` pending transaction tests on the docker devnet.

use crate::dev::subcommands::tests_cmd::helpers::*;
use crate::rpc::eth::types::EthHash;
use crate::rpc::prelude::*;
use anyhow::{Context as _, ensure};
use cid::Cid;
use libtest_mimic::{Arguments, Trial};
use serde_json::{Value, json};
use std::str::FromStr as _;

/// `eth_getTransactionByHash` pending transaction tests
#[derive(Debug, clap::Args)]
pub struct EthPendingTxTestCommand {}

impl EthPendingTxTestCommand {
pub async fn run(self) -> anyhow::Result<()> {
let args = Arguments {
test_threads: Some(1),
..Default::default()
};
libtest_mimic::run(&args, tests()).exit();
}
}

fn tests() -> Vec<Trial> {
vec![trial("pending_tx_from_lotus_is_found_by_its_hash", || {
block_on(pending_tx_from_lotus_is_found_by_its_hash())
})]
}

/// A transaction submitted through Lotus, so without a hash mapping on Forest, is found by its
/// Ethereum hash while it is still pending.
async fn pending_tx_from_lotus_is_found_by_its_hash() -> anyhow::Result<()> {
let (forest, lotus) = (forest_client()?, lotus_client()?);
let sender = sender().await?;

// A nonce gap keeps the transaction pending until the gap is filled, so the lookup cannot
// race the miner.
let gap_nonce = mpool_nonce(&sender.cli)? + 1;
// Lotus's mpool reads state at its own head, which trails the chain head the sender was
// funded on.
let gapped = poll("lotus to accept the gapped send", || async {
match lotus_send_to_self(&sender.cli, Some(gap_nonce)) {
Ok(cid) => Ok(Some(cid)),
Err(e) if is_unknown_actor_error(&e) => Ok(None),
Err(e) => Err(e),
}
})
.await?;
poll_until_pending_nonce(&sender.cli, gap_nonce).await?;
let hash = lotus
.call(EthGetTransactionHashByCid::request((gapped,))?)
.await?
.context("lotus does not know the transaction's hash")?;

let tx = forest
.call(EthGetTransactionByHash::request((hash,))?)
.await?
.context("forest does not return the pending transaction by its hash")?;
// Lotus's answer for the same transaction; its block fields are `null` while pending, so only
// the fields that identify the transaction are compared.
let lotus_tx = lotus
.call(EthGetTransactionByHash::request((hash,))?.map_ty::<Value>())
.await?;
for (field, forest_value) in [
("hash", json!(tx.hash)),
("from", json!(tx.from)),
("nonce", json!(tx.nonce)),
] {
ensure!(
lotus_tx.get(field) == Some(&forest_value),
"{field}: forest {forest_value}, lotus {lotus_tx}"
);
}
ensure!(
tx.block_hash == EthHash::default(),
"reported as mined while pending: {tx:?}"
);

// Filling the gap gets both mined, so nothing stays pending after the trial.
lotus_send_to_self(&sender.cli, None)?;
poll_until_message_executed(&forest, gapped).await?;
Ok(())
}

/// Sends 0 FIL from `from` to itself through Lotus at `nonce`, or at the next nonce, and returns
/// the message CID.
fn lotus_send_to_self(from: &str, nonce: Option<u64>) -> anyhow::Result<Cid> {
let nonce = nonce.map(|n| n.to_string());
// `--force-send` skips the check that the nonce is the next one.
let mut args = vec!["--force-send", "send", "--from", from];
if let Some(nonce) = &nonce {
args.extend(["--nonce", nonce]);
}
args.extend([from, "0"]);
let out = lotus_exec(&args)?;
// The CID follows the `Sending message ...` lines.
let cid = out.lines().last().context("empty `lotus send` output")?;
Cid::from_str(cid.trim())
.with_context(|| format!("parsing the cid in `lotus send` output: {out}"))
}
16 changes: 9 additions & 7 deletions src/dev/subcommands/tests_cmd/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,19 @@ pub async fn get_actor(client: &Client, addr: Address) -> anyhow::Result<Option<
.await
{
Ok(actor) => Ok(actor),
Err(e)
if ["actor not found", "resolution lookup failed"]
.iter()
.any(|s| format!("{e:#}").contains(s)) =>
{
Ok(None)
}
Err(e) if is_unknown_actor_error(&e) => Ok(None),
Err(e) => Err(anyhow::anyhow!("{e:#}")),
}
}

/// Whether a node answered that it does not have the actor (yet).
pub fn is_unknown_actor_error(e: &impl std::fmt::Display) -> bool {
let text = format!("{e:#}");
["actor not found", "resolution lookup failed"]
.iter()
.any(|s| text.contains(s))
}

/// Poll until `node`'s state has an actor at `addr` (the Lotus node trails Forest by a block on the
/// forest-produced devnet, so a Forest-funded sender must be awaited before any `lotus --from`).
pub async fn poll_until_actor_on(
Expand Down
Loading
Loading