Skip to content
Draft
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 @@ -47,6 +47,8 @@

- [#6975](https://github.com/ChainSafe/forest/issues/6975): Fixed `Filecoin.MpoolSelect` to not remove the messages from the live pool, only simulate the head change.

- [#7156](https://github.com/ChainSafe/forest/pull/7156): Fixed the block-level bloom is now computed from the block's logs.

## Forest v0.33.6 "Ebb"

Non-mandatory release for all node operators. It fixes a critical memory leak in `v0.33.5`. (Earlier releases are not affected)
Expand Down
2 changes: 0 additions & 2 deletions scripts/tests/api_compare/filter-list
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@

!Filecoin.StateGetNetworkParams
!Filecoin.EthEstimateGas
!Filecoin.EthGetBlockByHash
!Filecoin.EthGetBlockByNumber
3 changes: 0 additions & 3 deletions scripts/tests/api_compare/filter-list-gateway
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,3 @@
!Filecoin.StateGetNetworkParams
# https://github.com/filecoin-project/lotus/pull/13644
!Filecoin.EthEstimateGas
# https://github.com/filecoin-project/lotus/pull/13618
!Filecoin.EthGetBlockByHash
!Filecoin.EthGetBlockByNumber
86 changes: 82 additions & 4 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ impl Block {
full_transactions.push(tx);
}

let logs_bloom =
compute_block_logs_bloom(state_manager, &tipset, &executed_messages).await?;

Ok(Arc::new(Block {
hash: block_hash,
number: block_number,
Expand All @@ -563,6 +566,7 @@ impl Block {
.into(),
gas_used: EthUint64(gas_used),
transactions: Transactions::Full(full_transactions),
logs_bloom,
..Block::new(has_transactions, tipset.len())
}))
})
Expand Down Expand Up @@ -1372,10 +1376,7 @@ async fn new_eth_tx_receipt(

let mut bloom = Bloom::default();
for log in tx_receipt.logs.iter() {
for topic in log.topics.iter() {
bloom.accrue(topic.0.as_bytes());
}
bloom.accrue(log.address.0.as_bytes());
accrue_eth_log(&mut bloom, &log.address, &log.topics);
}
tx_receipt.logs_bloom = bloom.into();

Expand Down Expand Up @@ -3237,6 +3238,43 @@ fn eth_filter_logs_from_events(
Ok(logs)
}

/// Accrues a single Ethereum log's address and topics into `bloom` using the standard `M3:2048` scheme.
fn accrue_eth_log(bloom: &mut Bloom, address: &EthAddress, topics: &[EthHash]) {
for topic in topics {
bloom.accrue(topic.0.as_bytes());
}
bloom.accrue(address.0.as_bytes());
}

async fn compute_block_logs_bloom(
state_manager: &StateManager,
tipset: &Tipset,
executed_messages: &[ExecutedMessage],
) -> Result<Bloom> {
let mut events = vec![];
EthEventHandler::collect_events_from_messages(
state_manager,
tipset,
executed_messages,
None::<&EthFilterSpec>,
SkipEvent::OnUnresolvedAddress,
&mut events,
)
.await?;

let mut bloom = Bloom::default();
for event in &events {
let Some((_data, topics)) = eth_log_from_event(&event.entries) else {
continue;
};
let Ok(address) = EthAddress::from_filecoin_address(&event.emitter_addr) else {
continue;
};
accrue_eth_log(&mut bloom, &address, &topics);
}
Ok(bloom)
}

fn eth_filter_result_from_events(
ctx: &Ctx,
events: &[CollectedEvent],
Expand Down Expand Up @@ -4547,6 +4585,46 @@ mod test {
assert!(eth_log_from_event(&entries).is_none());
}

#[test]
fn test_accrue_eth_log_and_block_bloom_decomposition() {
let empty = Bloom::default();
let full = Bloom(ethereum_types::Bloom(FULL_BLOOM));

// No logs yields the all-zeros bloom — the "definitely no events here" case
// indexers rely on.
assert_eq!(empty.0.0, EMPTY_BLOOM);

let addr_a = EthAddress(ethereum_types::H160::from_slice(&[0x11; ADDRESS_LENGTH]));
let topic_a = EthHash(ethereum_types::H256::from_slice(&[0x22; EVM_WORD_LENGTH]));
let addr_b = EthAddress(ethereum_types::H160::from_slice(&[0x33; ADDRESS_LENGTH]));
let topic_b = EthHash(ethereum_types::H256::from_slice(&[0x44; EVM_WORD_LENGTH]));

// A real log sets some bits, but not all of them.
let mut bloom_a = empty.clone();
accrue_eth_log(&mut bloom_a, &addr_a, std::slice::from_ref(&topic_a));
assert_ne!(bloom_a, empty);
assert_ne!(bloom_a, full);

let mut bloom_b = empty.clone();
accrue_eth_log(&mut bloom_b, &addr_b, std::slice::from_ref(&topic_b));

// The block bloom (both logs) equals the bitwise OR of the two individual
// (receipt) blooms.
let mut combined = bloom_a.clone();
accrue_eth_log(&mut combined, &addr_b, std::slice::from_ref(&topic_b));

let mut expected = bloom_a.0.0;
for (out, b) in expected.iter_mut().zip(bloom_b.0.0.iter()) {
*out |= *b;
}
assert_eq!(combined.0.0, expected);

// Accruing the same log twice equals accruing it once.
let mut twice = bloom_a.clone();
accrue_eth_log(&mut twice, &addr_a, std::slice::from_ref(&topic_a));
assert_eq!(twice, bloom_a);
}

#[test]
fn test_from_bytes_valid() {
let zero_bytes = [0u8; 32];
Expand Down
25 changes: 22 additions & 3 deletions src/rpc/methods/eth/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,31 @@ impl EthEventHandler {
skip_event: SkipEvent,
collected_events: &mut Vec<CollectedEvent>,
) -> anyhow::Result<()> {
let msg_cid_filter = spec.and_then(|s| s.msg_cid_filter()).copied();
let height = tipset.epoch();
let tipset_key = tipset.key();
let ExecutedTipset {
executed_messages, ..
} = state_manager.load_executed_tipset_for_rpc(tipset).await?;
Self::collect_events_from_messages(
state_manager,
tipset,
&executed_messages,
spec,
skip_event,
collected_events,
)
.await
}

pub async fn collect_events_from_messages(
state_manager: &StateManager,
tipset: &Tipset,
executed_messages: &[ExecutedMessage],
spec: Option<&impl Matcher>,
skip_event: SkipEvent,
collected_events: &mut Vec<CollectedEvent>,
) -> anyhow::Result<()> {
Comment on lines +358 to +365

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add doc comment for the new public function.

The new collect_events_from_messages function is public but lacks documentation. As per coding guidelines, all public functions should have doc comments explaining their purpose, parameters, and behavior.

📝 Suggested documentation
+    /// Collects events from executed messages for a given tipset.
+    ///
+    /// This is a lower-level helper that operates on already-loaded executed messages,
+    /// enabling reuse from both RPC event collection and block bloom computation.
+    ///
+    /// # Parameters
+    /// - `state_manager`: State manager for address resolution
+    /// - `tipset`: The tipset containing the executed messages
+    /// - `executed_messages`: Pre-loaded executed messages from the tipset
+    /// - `spec`: Optional filter spec to match specific events
+    /// - `skip_event`: Strategy for handling unresolved addresses
+    /// - `collected_events`: Output buffer for matching events
     pub async fn collect_events_from_messages(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn collect_events_from_messages(
state_manager: &StateManager,
tipset: &Tipset,
executed_messages: &[ExecutedMessage],
spec: Option<&impl Matcher>,
skip_event: SkipEvent,
collected_events: &mut Vec<CollectedEvent>,
) -> anyhow::Result<()> {
/// Collects events from executed messages for a given tipset.
///
/// This is a lower-level helper that operates on already-loaded executed messages,
/// enabling reuse from both RPC event collection and block bloom computation.
///
/// # Parameters
/// - `state_manager`: State manager for address resolution
/// - `tipset`: The tipset containing the executed messages
/// - `executed_messages`: Pre-loaded executed messages from the tipset
/// - `spec`: Optional filter spec to match specific events
/// - `skip_event`: Strategy for handling unresolved addresses
/// - `collected_events`: Output buffer for matching events
pub async fn collect_events_from_messages(
state_manager: &StateManager,
tipset: &Tipset,
executed_messages: &[ExecutedMessage],
spec: Option<&impl Matcher>,
skip_event: SkipEvent,
collected_events: &mut Vec<CollectedEvent>,
) -> anyhow::Result<()> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rpc/methods/eth/filter/mod.rs` around lines 358 - 365, Add a doc comment
above the public function collect_events_from_messages that succinctly describes
its purpose (collect events from executed messages in a tipset), its parameters
(state_manager: &StateManager, tipset: &Tipset, executed_messages:
&[ExecutedMessage], spec: Option<&impl Matcher>, skip_event: SkipEvent,
collected_events: &mut Vec<CollectedEvent>), its behavior (filters/matches
events using spec, skips events per skip_event, appends found CollectedEvent
items into collected_events), and its return value (anyhow::Result<()>
indicating success or error); make sure the doc mentions that collected_events
is mutated in-place and any notable side effects or error conditions the caller
should expect.

Source: Coding guidelines

let msg_cid_filter = spec.and_then(|s| s.msg_cid_filter()).copied();
let height = tipset.epoch();
let tipset_key = tipset.key();
let mut resolved_id_addrs = HashMap::default();
let mut event_count = 0;
for (
Expand Down
6 changes: 3 additions & 3 deletions src/tool/subcommands/api_cmd/test_snapshots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ filecoin_ethgetbalance_v2_latest_1770291948779489.rpcsnap.json.zst
filecoin_ethgetbalance_v2_pending_1770291949065157.rpcsnap.json.zst
filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst
filecoin_ethgetbalance_v2_unknown_addr_1770287845559744.rpcsnap.json.zst
filecoin_ethgetblockbyhash_1781166100013912.rpcsnap.json.zst
filecoin_ethgetblockbynumber_1737446676696328.rpcsnap.json.zst
filecoin_ethgetblockbynumber_v2_1781166100024105.rpcsnap.json.zst
filecoin_ethgetblockbyhash_1781886021508619.rpcsnap.json.zst
filecoin_ethgetblockbynumber_1781887507408523.rpcsnap.json.zst
filecoin_ethgetblockbynumber_v2_1781887507413486.rpcsnap.json.zst
filecoin_ethgetblockreceipts_1781166100049979.rpcsnap.json.zst
filecoin_ethgetblockreceipts_v2_1781166100038659.rpcsnap.json.zst
filecoin_ethgetblockreceiptslimited_1781166100061549.rpcsnap.json.zst
Expand Down
Loading