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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

- [#7073](https://github.com/ChainSafe/forest/pull/7073) and [#7077](https://github.com/ChainSafe/forest/pull/7077): Replaced the underlying cache engine across the node. The eviction policy is no longer strict LRU — it is now CLOCK-PRO via [`quick_cache`](https://crates.io/crates/quick_cache), which is scan-resistant and typically gives higher hit rates on chain workloads. Refactored internal cache metrics to include `hits` and `misses` for all automatically. The old metrics `lru_cache_hit_total` and `lru_cache_miss_total` are deprecated in favor of `cache_{name}_hits` and `cache_{name}_misses`. Details can be found in https://forest-docs.pages.dev/reference/metrics

- [#7116](https://github.com/ChainSafe/forest/pull/7116): Disable state computation for Ethereum RPC methods by default. It could be explictly enabled by setting environment variable `FOREST_ETH_RPC_COMPUTE_STATE_ON_INDEX_MISS=1`
Comment thread
hanabi1224 marked this conversation as resolved.

### Added

- [#6012](https://github.com/ChainSafe/forest/issues/6012): Stricter validation of address arguments in `forest-wallet` subcommands.
Expand Down
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 @@ -69,6 +69,7 @@ process.
| `FOREST_MAX_CONCURRENT_INBOUND_CHAIN_EXCHANGE_REQUESTS` | positive integer | 32 | 32 | Maximum number of inbound chain exchange requests Forest will service concurrently. Excess requests are rejected with a `GoAway` response |
| `FOREST_MAX_CONCURRENT_INBOUND_CHAIN_EXCHANGE_REQUESTS_PER_PEER` | positive integer | 4 | 4 | Per-peer cap on concurrent inbound chain exchange requests. Excess requests from a single peer are rejected with a `GoAway` response |
| `FOREST_MAX_OUTBOUND_CHAIN_EXCHANGE_RESPONSE_BYTES` | positive integer (bytes) | 10485760 (10 MiB) | 10485760 | Cap on the encoded byte size of a chain exchange response Forest serves to peers. Building stops as soon as the running encoded size would exceed this cap and the response is returned with `PartialResponse` status |
| `FOREST_ETH_RPC_COMPUTE_STATE_ON_INDEX_MISS` | 1 or true | false | 1 | Allows Ethereum RPC methods to compute state trees on index miss |

### `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT`

Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/api_compare/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ services:
- |
set -euxo pipefail
forest-tool api serve $(ls /data/*.car.zst | tail -n 1) \
--height=-50 --index-backfill-epochs 50 --port ${FOREST_OFFLINE_RPC_PORT} --save-token /data/forest-token-offline
--height=-50 --index-backfill-epochs 200 --port ${FOREST_OFFLINE_RPC_PORT} --save-token /data/forest-token-offline
healthcheck:
test: ["CMD", "forest-cli", "chain", "head"]
interval: 10s
Expand Down
6 changes: 6 additions & 0 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,12 @@ fn maybe_prefill_rpc_caches(
async fn prefill_rpc_caches_for_tipset(state_manager: StateManager, tsk: TipsetKey) {
match state_manager.chain_index().load_required_tipset(&tsk) {
Ok(ts) => {
{
// First, compute state for the ts as it's disallowed for RPC methods by default
if let Err(e) = state_manager.load_executed_tipset(&ts).await {
warn!("failed to load executed tipset for cache warmup: {e:#}");
}
}
for tx_info in [crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash] {
if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
&state_manager,
Expand Down
9 changes: 6 additions & 3 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ impl Block {
state_root,
executed_messages,
..
} = state_manager.load_executed_tipset(&tipset).await?;
} = state_manager.load_executed_tipset_for_rpc(&tipset).await?;
let has_transactions = !executed_messages.is_empty();
let state_tree = state_manager.get_state_tree(&state_root)?;

Expand Down Expand Up @@ -1528,7 +1528,10 @@ async fn get_block_receipts(
state_root,
executed_messages,
..
} = ctx.state_manager.load_executed_tipset(&ts_ref).await?;
} = ctx
.state_manager
.load_executed_tipset_for_rpc(&ts_ref)
.await?;

// Load the state tree
let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
Expand Down Expand Up @@ -2032,7 +2035,7 @@ async fn eth_fee_history(
let base_fee = &ts.block_headers().first().parent_base_fee;
let ExecutedTipset {
executed_messages, ..
} = ctx.state_manager.load_executed_tipset(&ts).await?;
} = ctx.state_manager.load_executed_tipset_for_rpc(&ts).await?;
let mut tx_gas_rewards = Vec::with_capacity(executed_messages.len());
for ExecutedMessage {
message, receipt, ..
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/methods/eth/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ impl EthEventHandler {
let tipset_key = tipset.key();
let ExecutedTipset {
executed_messages, ..
} = state_manager.load_executed_tipset(tipset).await?;
} = state_manager.load_executed_tipset_for_rpc(tipset).await?;
let mut resolved_id_addrs = HashMap::default();
let mut event_count = 0;
for (
Expand Down
57 changes: 56 additions & 1 deletion src/state_manager/state_computation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use anyhow::{bail, ensure};
use fil_actors_shared::fvm_ipld_amt::{Amt, Amtv0};
use tracing::{error, info, instrument};

enum StateRecomputePolicy {
Allowed,
Disallowed,
}

impl StateManager {
/// Load the state of a tipset, including state root, message receipts
pub async fn load_tipset_state(&self, ts: &Tipset) -> anyhow::Result<TipsetState> {
Expand All @@ -27,8 +32,42 @@ impl StateManager {
}
}

/// Load an executed tipset for RPC methods, with state computation unless explicitly enabled.
pub async fn load_executed_tipset_for_rpc(
&self,
ts: &Tipset,
) -> anyhow::Result<ExecutedTipset> {
crate::def_is_env_truthy!(
Comment thread
hanabi1224 marked this conversation as resolved.
enable_state_computation,
"FOREST_ETH_RPC_COMPUTE_STATE_ON_INDEX_MISS"
);
let policy = if enable_state_computation() {
StateRecomputePolicy::Allowed
} else {
StateRecomputePolicy::Disallowed
};

// https://github.com/ChainSafe/forest/issues/7118
Comment thread
hanabi1224 marked this conversation as resolved.
#[cfg(test)]
let policy = {
_ = policy;
StateRecomputePolicy::Allowed
};

self.load_executed_tipset_with_cache(ts, policy).await
}

/// Load an executed tipset, including state root, message receipts and events with caching.
pub async fn load_executed_tipset(&self, ts: &Tipset) -> anyhow::Result<ExecutedTipset> {
self.load_executed_tipset_with_cache(ts, StateRecomputePolicy::Allowed)
.await
}

async fn load_executed_tipset_with_cache(
&self,
ts: &Tipset,
policy: StateRecomputePolicy,
) -> anyhow::Result<ExecutedTipset> {
// validate the existence of state trees for post-chain-head-epoch tipsets in case chain head is reset(e.g. manually or via GC).
if ts.epoch() >= self.heaviest_tipset().epoch()
&& let Some(cached) = self.cache.get(ts.key())
Expand All @@ -42,7 +81,7 @@ impl StateManager {
self.cache
.get_or_insert_async(ts.key(), async move {
let receipt_ts = self.chain_store().load_child_tipset(ts)?;
self.load_executed_tipset_inner(ts, receipt_ts.as_ref())
self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
.await
})
.await
Expand All @@ -53,13 +92,23 @@ impl StateManager {
msg_ts: &Tipset,
// when `msg_ts` is the current head, `receipt_ts` is `None`
receipt_ts: Option<&Tipset>,
policy: StateRecomputePolicy,
) -> anyhow::Result<ExecutedTipset> {
let state_compute_disallow_error = || {
format!(
"failed to load tipset state output and recomputation is disallowed, epoch={}, key={}",
msg_ts.epoch(),
msg_ts.key()
)
};

if let Some(receipt_ts) = receipt_ts {
anyhow::ensure!(
msg_ts.key() == receipt_ts.parents(),
"message tipset should be the parent of message receipt tipset"
);
}
let allow_state_compute = matches!(policy, StateRecomputePolicy::Allowed);
let mut recomputed = false;
let (state_root, receipt_root, receipts) = match receipt_ts.and_then(|ts| {
let receipt_root = *ts.parent_message_receipts();
Expand All @@ -69,6 +118,9 @@ impl StateManager {
}) {
Some((state_root, receipt_root, receipts)) => (state_root, receipt_root, receipts),
None => {
if !allow_state_compute {
anyhow::bail!(state_compute_disallow_error());
}
let state_output = self
.compute_tipset_state(msg_ts.shallow_clone(), NO_CALLBACK, VMTrace::NotTraced)
.await?;
Expand All @@ -95,6 +147,9 @@ impl StateManager {
Ok(events) => events,
Err(e) if recomputed => return Err(e),
Err(_) => {
if !allow_state_compute {
anyhow::bail!(state_compute_disallow_error());
}
Comment thread
hanabi1224 marked this conversation as resolved.
self.compute_tipset_state(
msg_ts.shallow_clone(),
NO_CALLBACK,
Expand Down
Loading