You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This was AI found and seems correct, but do your own checks. Regardless of that, a test will be useful.
Summary
Forest re-encodes every message in a block to CBOR at least three times while validating it, and
throws each encoding away. The root cause is that Cid::from_cbor_blake2b256 materialises the whole
encoding into a Vec<u8> purely to feed a hasher, and neither Message nor SignedMessage caches
its CID.
No consensus risk in fixing this: the changes below alter only how the bytes are produced, never
what they are.
Where the encodings happen
src/utils/cid/mod.rs, the root of the problem:
fnfrom_cbor_blake2b256<S: serde::ser::Serialize>(obj:&S) -> Result<Cid,Error>{let bytes = fvm_ipld_encoding::to_vec(obj)?;// full Vec allocationOk(Self::from_cbor_encoded_raw_bytes_blake2b256(&bytes))// hash it, drop it}
Message::cid() (src/shim/message.rs:188) and SignedMessage::cid()
(src/message/signed_message.rs:83) both route through it. Neither memoises.
Per BLS message, inside one check_block_messages call
(src/chain_sync/tipset_syncer.rs):
#
Site
Fate of the bytes
1
tipset_syncer.rs:397 — m.cid().to_bytes(), for the BLS aggregate check
hashed, dropped
2
tipset_syncer.rs:424 — to_vec(msg)?.len(), for price_list.on_chain_message
Delegated (EIP-155/1559) signatures cost two more: src/shim/crypto.rs:147 compares msg.message().cid() == filecoin_msg.cid(), building both sides from scratch.
It is worse across a block's lifetime
compute_msg_root is reached from three independent places on the same data, with no memoisation
between them:
A block that arrives over gossip, gets persisted, then gets fully validated re-encodes every message
it contains three additional times.
Proposed work
1. Stream into the hasher instead of buffering (highest value/effort ratio)
Change Cid::from_cbor_blake2b256 in src/utils/cid/mod.rs to serialise directly into the Blake2b
digest via fvm_ipld_encoding::to_writer and a small io::Write adapter around the hasher.
Removes one heap allocation per call, sized to the encoded object.
No API change, no call-site churn.
Blast radius is much wider than block validation: 17 non-test call sites, plus every Message::cid() / SignedMessage::cid() in the mempool, chain store, and eth-mapping paths.
Keep from_cbor_encoded_raw_bytes_blake2b256 as-is — it has callers that already hold the bytes.
2. Count bytes without allocating
to_vec(x)?.len() appears at:
src/chain_sync/tipset_syncer.rs:424
src/interpreter/vm.rs:433 and :463 (these run per message per epoch for every FVM
version, not just the pre-nv18 path — the most valuable of the set)
A counting io::Write sink passed to fvm_ipld_encoding::to_writer makes all of them
allocation-free. Consider exposing it as crate::utils::encoding::encoded_len(&impl Serialize).
3. Cache the CID on the message (separate, larger change)
The deeper fix, and the one that actually removes the 3×. CachingBlockHeader already does exactly
this for headers with a OnceCell; Message has no equivalent.
Bigger blast radius — Message is widely used and derives Clone/Hash/PartialEq, so a cache
field needs care (skip it in comparisons and hashing, and in the serde impls). Worth doing only
after 1 and 2 are measured. Note the subtlety already documented at src/message/signed_message.rs:75-78: SignedMessage::cid() is not Cid::from_cbor_blake2b256(signed_msg) for BLS messages — it delegates to the inner message. Any
cache has to preserve that.
Measuring
check_block_messages runs on validated sync, not on snapshot import, so the headline win is live
sync and forest-tool validation runs — not import throughput. Measure before claiming
otherwise.
Suggested: forest-tool benchmark against a calibnet snapshot, plus a targeted criterion bench over compute_msg_root on a realistic block (mainnet blocks carry up to BLOCK_MESSAGE_LIMIT = 10000
messages; typical tipsets are in the hundreds to low thousands).
Correctness is easy to pin down: CIDs and roots must be byte-identical before and after. Existing
coverage in src/chain_sync/validation.rs:372,386 (compute_msg_root) and the src/message/signed_message.rs CID tests should catch a regression, and both changes are
mechanical enough that a differential test over arbitrary Message values is cheap to add.
Suggested split
PR 1: items 1 and 2. Mechanical, independently benchmarkable, no consensus risk.
PR 2: item 3, only if 1 and 2 leave a measurable gap.
Note
This was AI found and seems correct, but do your own checks. Regardless of that, a test will be useful.
Summary
Forest re-encodes every message in a block to CBOR at least three times while validating it, and
throws each encoding away. The root cause is that
Cid::from_cbor_blake2b256materialises the wholeencoding into a
Vec<u8>purely to feed a hasher, and neitherMessagenorSignedMessagecachesits CID.
No consensus risk in fixing this: the changes below alter only how the bytes are produced, never
what they are.
Where the encodings happen
src/utils/cid/mod.rs, the root of the problem:Message::cid()(src/shim/message.rs:188) andSignedMessage::cid()(
src/message/signed_message.rs:83) both route through it. Neither memoises.Per BLS message, inside one
check_block_messagescall(
src/chain_sync/tipset_syncer.rs):tipset_syncer.rs:397—m.cid().to_bytes(), for the BLS aggregate checktipset_syncer.rs:424—to_vec(msg)?.len(), forprice_list.on_chain_messagetipset_syncer.rs:511→TipsetValidator::compute_msg_root→Cid::from_cbor_blake2b256Per SECP message, three as well, in different shapes:
tipset_syncer.rs:493—check_msg(msg.message(), ..)→to_vecMessagesrc/shim/crypto.rs:160—msg.message().cid().to_bytes()insideauthenticate_msgMessageagaintipset_syncer.rs:511→compute_msg_rootSignedMessageDelegated (EIP-155/1559) signatures cost two more:
src/shim/crypto.rs:147comparesmsg.message().cid() == filecoin_msg.cid(), building both sides from scratch.It is worse across a block's lifetime
compute_msg_rootis reached from three independent places on the same data, with no memoisationbetween them:
src/chain_sync/validation.rs:101— gossip block validationsrc/blocks/tipset.rs:613—FullTipset::persistsrc/chain_sync/tipset_syncer.rs:511—check_block_messagesA block that arrives over gossip, gets persisted, then gets fully validated re-encodes every message
it contains three additional times.
Proposed work
1. Stream into the hasher instead of buffering (highest value/effort ratio)
Change
Cid::from_cbor_blake2b256insrc/utils/cid/mod.rsto serialise directly into the Blake2bdigest via
fvm_ipld_encoding::to_writerand a smallio::Writeadapter around the hasher.Message::cid()/SignedMessage::cid()in the mempool, chain store, and eth-mapping paths.Keep
from_cbor_encoded_raw_bytes_blake2b256as-is — it has callers that already hold the bytes.2. Count bytes without allocating
to_vec(x)?.len()appears at:src/chain_sync/tipset_syncer.rs:424src/interpreter/vm.rs:433and:463(these run per message per epoch for every FVMversion, not just the pre-nv18 path — the most valuable of the set)
src/message_pool/msgpool/msg_pool.rs:587src/libp2p/chain_exchange/provider.rs:348src/message/signed_message.rs:88(SignedMessage::chain_length)A counting
io::Writesink passed tofvm_ipld_encoding::to_writermakes all of themallocation-free. Consider exposing it as
crate::utils::encoding::encoded_len(&impl Serialize).3. Cache the CID on the message (separate, larger change)
The deeper fix, and the one that actually removes the 3×.
CachingBlockHeaderalready does exactlythis for headers with a
OnceCell;Messagehas no equivalent.Bigger blast radius —
Messageis widely used and derivesClone/Hash/PartialEq, so a cachefield needs care (skip it in comparisons and hashing, and in the
serdeimpls). Worth doing onlyafter 1 and 2 are measured. Note the subtlety already documented at
src/message/signed_message.rs:75-78:SignedMessage::cid()is notCid::from_cbor_blake2b256(signed_msg)for BLS messages — it delegates to the inner message. Anycache has to preserve that.
Measuring
check_block_messagesruns on validated sync, not on snapshot import, so the headline win is livesync and
forest-toolvalidation runs — not import throughput. Measure before claimingotherwise.
Suggested:
forest-tool benchmarkagainst a calibnet snapshot, plus a targeted criterion bench overcompute_msg_rooton a realistic block (mainnet blocks carry up toBLOCK_MESSAGE_LIMIT= 10000messages; typical tipsets are in the hundreds to low thousands).
Correctness is easy to pin down: CIDs and roots must be byte-identical before and after. Existing
coverage in
src/chain_sync/validation.rs:372,386(compute_msg_root) and thesrc/message/signed_message.rsCID tests should catch a regression, and both changes aremechanical enough that a differential test over arbitrary
Messagevalues is cheap to add.Suggested split