From cb024eda5f88fcd15364979a4b0bfccc3c79cbd5 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Aug 2026 11:28:30 +0530 Subject: [PATCH 1/5] Relax finalty gated caches --- CHANGELOG.md | 2 ++ src/chain/store/chain_store.rs | 46 ++++++++++++++++++++++--- src/message_pool/msgpool/msg_pool.rs | 1 + src/rpc/methods/eth.rs | 6 ++-- src/state_manager/address_resolution.rs | 1 + 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d132e545c36..c7aa5663b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ ### Changed +- [#7356](https://github.com/ChainSafe/forest/issues/7356): Ethereum transaction receipts and ID-to-address resolutions are now cached once the EC finality calculator considers them final, instead of waiting for the static 900-epoch chain finality. + ### Removed - [#7481](https://github.com/ChainSafe/forest/issues/7481): Removed legacy database migrations from before the NV28 network upgrade. diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index b87a8a015ab..767bac806e8 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -375,11 +375,12 @@ impl ChainStore { } /// Resolves `addr` to its deterministic (public-key or delegated) form - /// using the state at `chain_finality` epochs behind `ts`. Falls back to - /// `ts` itself when the chain is younger than finality; the returned + /// using the state at [`Self::ec_calculator_finalized_epoch`]. Falls back to + /// `ts` itself when that epoch is not yet behind `ts`; the returned /// [`AtFinalityResolution`] says which of the two happened. /// - /// Matches the logic at + /// Matches the logic at , + /// except the lookback is the EC finalized epoch, not `ts.epoch() - chain_finality`. pub fn resolve_to_deterministic_address_at_finality( &self, addr: &Address, @@ -389,10 +390,11 @@ impl ChainStore { match addr.protocol() { BLS | Secp256k1 | Delegated => Ok(AtFinalityResolution::ReorgStable(*addr)), ID => { - let finality_deep = ts.epoch() > self.chain_config().policy.chain_finality; + let finalized_epoch = self.ec_calculator_finalized_epoch(); + let finality_deep = finalized_epoch > 0 && ts.epoch() > finalized_epoch; let lookback_ts = if finality_deep { self.chain_index().load_required_tipset_by_height_blocking( - ts.epoch() - self.chain_config().policy.chain_finality, + finalized_epoch, ts.shallow_clone(), ResolveNullTipset::TakeOlder, )? @@ -875,6 +877,40 @@ mod tests { assert_eq!(cs.genesis_block_header(), &gen_block); } + #[test] + fn ec_finalized_epoch_falls_back_to_chain_finality_on_degraded_chain() { + use crate::chain::store::index::tests::{genesis_tipset, persist_tipset, tipset_child}; + + const CHAIN_FINALITY: ChainEpoch = 10; + + let db = Arc::new(crate::db::MemoryDB::default()); + let mut chain_config = ChainConfig::default(); + chain_config.policy.chain_finality = CHAIN_FINALITY; + + let genesis = genesis_tipset(); + persist_tipset(&genesis, &db); + let mut head = genesis.shallow_clone(); + for epoch in 1..=30 { + head = tipset_child(&head, epoch); + persist_tipset(&head, &db); + } + + let cs = ChainStore::new(db.clone(), Arc::new(chain_config), genesis).unwrap(); + cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); + assert_eq!( + cs.ec_calculator_finalized_epoch(), + head.epoch() - CHAIN_FINALITY, + ); + + let next = tipset_child(&head, head.epoch() + 1); + persist_tipset(&next, &db); + cs.set_heaviest_tipset(next.shallow_clone()).unwrap(); + assert_eq!( + cs.ec_calculator_finalized_epoch(), + next.epoch() - CHAIN_FINALITY, + ); + } + #[test] fn block_validation_cache_basic() { let db = DbImpl::from(Arc::new(crate::db::MemoryDB::default())); diff --git a/src/message_pool/msgpool/msg_pool.rs b/src/message_pool/msgpool/msg_pool.rs index 95c0697e036..34f17197230 100644 --- a/src/message_pool/msgpool/msg_pool.rs +++ b/src/message_pool/msgpool/msg_pool.rs @@ -1039,6 +1039,7 @@ mod tests { db.put_cbor_default(head.block_headers().first()).unwrap(); let cs = ChainStore::new(db, cfg, genesis.block_headers().first().clone()).unwrap(); + cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); // f0300 exists in lookback state (root_a) → resolves successfully. let result = Provider::resolve_to_deterministic_address_at_finality( diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 5181eac7d13..81752f9fbfb 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2999,8 +2999,8 @@ struct CachedReceipt { finalized: bool, } -fn is_finalized(ctx: &Ctx, head_epoch: ChainEpoch, receipt_epoch: ChainEpoch) -> bool { - receipt_epoch <= head_epoch - ctx.chain_config().policy.chain_finality +fn is_finalized(ctx: &Ctx, receipt_epoch: ChainEpoch) -> bool { + receipt_epoch <= ctx.chain_store().ec_calculator_finalized_epoch() } async fn get_eth_transaction_receipt_with_cache( @@ -3049,7 +3049,7 @@ async fn get_eth_transaction_receipt_with_cache( } let finalized = receipt .as_ref() - .is_some_and(|r| is_finalized(&ctx, head.epoch(), r.block_number.0)); + .is_some_and(|r| is_finalized(&ctx, r.block_number.0)); Ok::<_, Uncacheable>(CachedReceipt { receipt, head_key, diff --git a/src/state_manager/address_resolution.rs b/src/state_manager/address_resolution.rs index e307e11fb1d..a6cfabedad0 100644 --- a/src/state_manager/address_resolution.rs +++ b/src/state_manager/address_resolution.rs @@ -198,6 +198,7 @@ mod tests { db.put_cbor_default(head.block_headers().first()).unwrap(); let cs = ChainStore::new(db, cfg, genesis.block_headers().first().clone()).unwrap(); + cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); let sm = StateManager::new(cs).unwrap(); (sm, head, bls_a, bls_b) } From a7f1ff43cd195e799d4fc8a4d6f1bcf999884b5b Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 20 Aug 2026 11:48:57 +0530 Subject: [PATCH 2/5] add more test --- src/chain/store/chain_store.rs | 10 +--- src/chain/store/index.rs | 33 +++++++++++-- src/state_manager/address_resolution.rs | 64 +++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 767bac806e8..61f5901263d 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -895,20 +895,12 @@ mod tests { persist_tipset(&head, &db); } - let cs = ChainStore::new(db.clone(), Arc::new(chain_config), genesis).unwrap(); + let cs = ChainStore::new(db, Arc::new(chain_config), genesis).unwrap(); cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); assert_eq!( cs.ec_calculator_finalized_epoch(), head.epoch() - CHAIN_FINALITY, ); - - let next = tipset_child(&head, head.epoch() + 1); - persist_tipset(&next, &db); - cs.set_heaviest_tipset(next.shallow_clone()).unwrap(); - assert_eq!( - cs.ec_calculator_finalized_epoch(), - next.epoch() - CHAIN_FINALITY, - ); } #[test] diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index dc926291071..3862c9fa40e 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -477,10 +477,13 @@ pub mod tests { })) } - pub fn tipset_child(parent: &Tipset, epoch: ChainEpoch) -> Tipset { - // Use a static counter to give all tipsets a unique timestamp + fn next_tipset_nonce() -> u64 { static COUNTER: AtomicU64 = AtomicU64::new(0); - let n = COUNTER.fetch_add(1, Ordering::Relaxed); + COUNTER.fetch_add(1, Ordering::Relaxed) + } + + pub fn tipset_child(parent: &Tipset, epoch: ChainEpoch) -> Tipset { + let n = next_tipset_nonce(); Tipset::from(CachingBlockHeader::new(RawBlockHeader { parents: parent.key().clone(), ticket: dummy_ticket(n as u8), @@ -490,6 +493,30 @@ pub mod tests { })) } + pub fn tipset_child_with_blocks( + parent: &Tipset, + epoch: ChainEpoch, + n: usize, + state_root: Cid, + ) -> Tipset { + assert!(n > 0, "tipset must have at least one block"); + let headers: Vec<_> = (0..n) + .map(|i| { + let nonce = next_tipset_nonce(); + CachingBlockHeader::new(RawBlockHeader { + parents: parent.key().clone(), + miner_address: Address::new_id(i as u64), + ticket: dummy_ticket(nonce as u8), + epoch, + timestamp: nonce, + state_root, + ..Default::default() + }) + }) + .collect(); + Tipset::new(headers).expect("valid multi-block tipset") + } + #[test] fn get_null_tipset() { let db = Arc::new(MemoryDB::default()); diff --git a/src/state_manager/address_resolution.rs b/src/state_manager/address_resolution.rs index a6cfabedad0..ef6cef675a4 100644 --- a/src/state_manager/address_resolution.rs +++ b/src/state_manager/address_resolution.rs @@ -284,4 +284,68 @@ mod tests { assert_eq!(resolved, bls_a); assert_eq!(sm.id_to_deterministic_address_cache().unwrap().len(), 0); } + + #[tokio::test] + async fn caches_when_ec_finality_is_shallower_than_chain_finality() { + use crate::chain::ec_finality::calculator::DEFAULT_BLOCKS_PER_EPOCH; + use crate::chain::store::index::tests::{persist_tipset, tipset_child_with_blocks}; + + const EPOCHS: ChainEpoch = 40; + let blocks_per_epoch = DEFAULT_BLOCKS_PER_EPOCH as usize; + + let db: DbImpl = Arc::new(MemoryDB::default()).into(); + let cfg = Arc::new(ChainConfig::default()); + + let bls = Address::new_bls(&[8u8; 48]).unwrap(); + let mut st = StateTree::new(&db, StateTreeVersion::V5).unwrap(); + st.set_actor( + &Address::new_id(OLD_ACTOR), + ActorState::new_empty(Cid::default(), Some(bls)), + ) + .unwrap(); + let root = st.flush().unwrap(); + + let genesis = Tipset::from(CachingBlockHeader::new(RawBlockHeader { + ticket: dummy_ticket(0), + state_root: root, + timestamp: 1, + ..Default::default() + })); + persist_tipset(&genesis, &db); + + let mut head = genesis.shallow_clone(); + for epoch in 1..=EPOCHS { + head = tipset_child_with_blocks(&head, epoch, blocks_per_epoch, root); + persist_tipset(&head, &db); + } + + let cs = ChainStore::new(db, cfg, genesis.block_headers().first().clone()).unwrap(); + cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); + + let finalized = cs.ec_calculator_finalized_epoch(); + assert!( + finalized > 0, + "healthy chain must meet the EC guarantee (got finalized_epoch={finalized})" + ); + assert!( + head.epoch() < cs.chain_config().policy.chain_finality, + "head must be younger than chain_finality so only EC can make this cacheable (head={} chain_finality={})", + head.epoch(), + cs.chain_config().policy.chain_finality, + ); + + let sm = StateManager::new(cs).unwrap(); + let resolved = sm + .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head) + .await + .unwrap(); + assert_eq!(resolved, bls); + assert_eq!( + sm.id_to_deterministic_address_cache() + .unwrap() + .get(&OLD_ACTOR), + Some(bls), + "resolution at the EC lookback must be cacheable under default chain_finality" + ); + } } From 2d54a8d963b055749acecea8fe9807bd28056e79 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 26 Aug 2026 14:54:35 +0530 Subject: [PATCH 3/5] cleanup --- src/chain/store/chain_store.rs | 41 +++++++++----------- src/rpc/methods/eth.rs | 51 ++++++++++++++++++++++++- src/state_manager/address_resolution.rs | 20 ++++++---- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 61f5901263d..093d7b337d4 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -51,7 +51,7 @@ pub type ChainEpochDelta = ChainEpoch; /// Outcome of [`ChainStore::resolve_to_deterministic_address_at_finality`]. pub enum AtFinalityResolution { - /// Resolved against a tipset at least `chain_finality` epochs behind the + /// Resolved against a tipset at least `ec_depth` epochs behind the /// requested one. The mapping is identical on every possible future /// chain, so it is safe to memoize by actor ID alone. ReorgStable(Address), @@ -374,13 +374,12 @@ impl ChainStore { &self.chain_config } - /// Resolves `addr` to its deterministic (public-key or delegated) form - /// using the state at [`Self::ec_calculator_finalized_epoch`]. Falls back to - /// `ts` itself when that epoch is not yet behind `ts`; the returned - /// [`AtFinalityResolution`] says which of the two happened. + /// Resolve `addr` to a public-key/delegated address. /// - /// Matches the logic at , - /// except the lookback is the EC finalized epoch, not `ts.epoch() - chain_finality`. + /// Looks back `ec_depth` epochs from `ts`, where `ec_depth` is how far the + /// EC calculator has finalized behind head. Uses `ts` itself if it is not + /// deeper than `ec_depth`. [`AtFinalityResolution::ReorgStable`] (cacheable) + /// only when that lookback ran. pub fn resolve_to_deterministic_address_at_finality( &self, addr: &Address, @@ -390,24 +389,22 @@ impl ChainStore { match addr.protocol() { BLS | Secp256k1 | Delegated => Ok(AtFinalityResolution::ReorgStable(*addr)), ID => { - let finalized_epoch = self.ec_calculator_finalized_epoch(); - let finality_deep = finalized_epoch > 0 && ts.epoch() > finalized_epoch; - let lookback_ts = if finality_deep { - self.chain_index().load_required_tipset_by_height_blocking( - finalized_epoch, + let ec_depth = + (self.heaviest_tipset().epoch() - self.ec_calculator_finalized_epoch()).max(0); + if ec_depth > 0 && ts.epoch() > ec_depth { + let lookback_ts = self.chain_index().load_required_tipset_by_height_blocking( + ts.epoch() - ec_depth, ts.shallow_clone(), ResolveNullTipset::TakeOlder, - )? + )?; + let state = StateTree::new_from_root(self.db(), lookback_ts.parent_state())?; + let resolved = state.resolve_to_deterministic_address(self.db(), *addr)?; + Ok(AtFinalityResolution::ReorgStable(resolved)) } else { - ts.shallow_clone() - }; - let state = StateTree::new_from_root(self.db(), lookback_ts.parent_state())?; - let resolved = state.resolve_to_deterministic_address(self.db(), *addr)?; - Ok(if finality_deep { - AtFinalityResolution::ReorgStable(resolved) - } else { - AtFinalityResolution::Unstable(resolved) - }) + let state = StateTree::new_from_root(self.db(), ts.parent_state())?; + let resolved = state.resolve_to_deterministic_address(self.db(), *addr)?; + Ok(AtFinalityResolution::Unstable(resolved)) + } } Actor => anyhow::bail!("Cannot resolve actor address to key address"), } diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index e1c7c8c3730..7f7ddd7de4a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2884,8 +2884,17 @@ struct CachedReceipt { finalized: bool, } +// Finality-gated caches we relaxed: ETH receipts, ID→address. +// Everything else is CID-keyed or not gated on 900. + +/// True once the EC calculator has finalized this epoch. Not F3: F3 can +/// sit ahead of the local head during catchup. fn is_finalized(ctx: &Ctx, receipt_epoch: ChainEpoch) -> bool { - receipt_epoch <= ctx.chain_store().ec_calculator_finalized_epoch() + receipt_is_ec_finalized(ctx.chain_store(), receipt_epoch) +} + +fn receipt_is_ec_finalized(cs: &ChainStore, receipt_epoch: ChainEpoch) -> bool { + receipt_epoch <= cs.ec_calculator_finalized_epoch() } async fn get_eth_transaction_receipt_with_cache( @@ -4142,6 +4151,46 @@ mod test { } } + #[test] + fn receipt_is_finalized_at_ec_boundary_not_chain_finality() { + use crate::chain::ec_finality::calculator::DEFAULT_BLOCKS_PER_EPOCH; + use crate::chain::index::tests::{ + genesis_tipset, persist_tipset, tipset_child_with_blocks, + }; + use crate::networks::ChainConfig; + + const EPOCHS: ChainEpoch = 40; + let blocks_per_epoch = DEFAULT_BLOCKS_PER_EPOCH as usize; + let db: DbImpl = Arc::new(MemoryDB::default()).into(); + let cfg = Arc::new(ChainConfig::default()); + + let genesis = genesis_tipset(); + persist_tipset(&genesis, &db); + let state_root = *genesis.parent_state(); + + let mut head = genesis.shallow_clone(); + for epoch in 1..=EPOCHS { + head = tipset_child_with_blocks(&head, epoch, blocks_per_epoch, state_root); + persist_tipset(&head, &db); + } + + let cs = ChainStore::new(db, cfg, genesis).unwrap(); + cs.set_heaviest_tipset(head.shallow_clone()).unwrap(); + + let finalized = cs.ec_calculator_finalized_epoch(); + assert!( + head.epoch() < cs.chain_config().policy.chain_finality, + "old head-900 rule would treat no receipt on this chain as final" + ); + assert!( + finalized > 0, + "healthy chain must meet the EC guarantee (got finalized_epoch={finalized})" + ); + assert!(receipt_is_ec_finalized(&cs, finalized)); + assert!(!receipt_is_ec_finalized(&cs, finalized + 1)); + assert!(!receipt_is_ec_finalized(&cs, head.epoch())); + } + #[test] fn execution_reverted_from_receipt_decodes_reason_and_data() { // A receipt carries its return payload CBOR-wrapped. The `Error(string)` ABI decode is diff --git a/src/state_manager/address_resolution.rs b/src/state_manager/address_resolution.rs index d1e56f6ae27..59e4fc61046 100644 --- a/src/state_manager/address_resolution.rs +++ b/src/state_manager/address_resolution.rs @@ -288,11 +288,7 @@ mod tests { #[tokio::test] async fn does_not_cache_at_exact_finality_boundary() { - // Head epoch == chain_finality: the guard is strictly `>`, so this is - // not finality-deep and the lookback degrades to resolving at the - // head's own parent state (which does contain f0300, since `root_b` - // was built on top of `root_a`). The resolution succeeds but, being - // `Unstable`, must not be cached. + // Head is exactly `ec_depth` epochs deep, so lookback does not apply. let (sm, head, bls_a, _bls_b) = setup_with_finality(2); let resolved = sm .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head) @@ -302,7 +298,7 @@ mod tests { assert_eq!( sm.id_to_deterministic_address_cache().unwrap().len(), 0, - "epoch == chain_finality is not finality-deep and must not be cached" + "epoch == ec_depth cannot look back and must not be cached" ); } @@ -334,8 +330,10 @@ mod tests { assert_eq!(sm.id_to_deterministic_address_cache().unwrap().len(), 0); } - #[tokio::test] - async fn caches_when_ec_finality_is_shallower_than_chain_finality() { + /// Healthy 40-epoch chain at 5 blocks/epoch: EC finality is shallower than + /// default `chain_finality` (900), so only the calculator can make a + /// resolution cacheable. + fn setup_healthy_ec_chain() -> (StateManager, Tipset, Address) { use crate::chain::ec_finality::calculator::DEFAULT_BLOCKS_PER_EPOCH; use crate::chain::store::index::tests::{persist_tipset, tipset_child_with_blocks}; @@ -384,6 +382,12 @@ mod tests { ); let sm = StateManager::new(cs).unwrap(); + (sm, head, bls) + } + + #[tokio::test] + async fn caches_when_ec_finality_is_shallower_than_chain_finality() { + let (sm, head, bls) = setup_healthy_ec_chain(); let resolved = sm .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head) .await From 819bb1f4ce84644f833b3fb9eb73c63661893a1d Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 26 Aug 2026 15:28:14 +0530 Subject: [PATCH 4/5] fix spellcheck --- .config/forest.dic | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/forest.dic b/.config/forest.dic index c9d66dcaef7..6874a21f991 100644 --- a/.config/forest.dic +++ b/.config/forest.dic @@ -1,4 +1,4 @@ -295 +296 ABI Algorand/M API's @@ -36,6 +36,7 @@ bootstrapper/S BuildKit butterflynet bytecode +cacheable Caddy calibnet calldata From 0d428fb4a160aca7e211cacd36bb7bc732850485 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 26 Aug 2026 15:53:05 +0530 Subject: [PATCH 5/5] add context --- src/chain/store/chain_store.rs | 36 ++++++++++++++++++++++------ src/message_pool/msgpool/provider.rs | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 093d7b337d4..dc675090aed 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -392,13 +392,35 @@ impl ChainStore { let ec_depth = (self.heaviest_tipset().epoch() - self.ec_calculator_finalized_epoch()).max(0); if ec_depth > 0 && ts.epoch() > ec_depth { - let lookback_ts = self.chain_index().load_required_tipset_by_height_blocking( - ts.epoch() - ec_depth, - ts.shallow_clone(), - ResolveNullTipset::TakeOlder, - )?; - let state = StateTree::new_from_root(self.db(), lookback_ts.parent_state())?; - let resolved = state.resolve_to_deterministic_address(self.db(), *addr)?; + let lookback_ts = self + .chain_index() + .load_required_tipset_by_height_blocking( + ts.epoch() - ec_depth, + ts.shallow_clone(), + ResolveNullTipset::TakeOlder, + ) + .with_context(|| { + format!( + "failed to load EC-finality lookback at epoch {} from {}", + ts.epoch() - ec_depth, + ts.key() + ) + })?; + let state = StateTree::new_from_root(self.db(), lookback_ts.parent_state()) + .with_context(|| { + format!( + "failed to load state for EC-finality lookback {}", + lookback_ts.key() + ) + })?; + let resolved = state + .resolve_to_deterministic_address(self.db(), *addr) + .with_context(|| { + format!( + "failed to resolve ID address {addr} at {}", + lookback_ts.key() + ) + })?; Ok(AtFinalityResolution::ReorgStable(resolved)) } else { let state = StateTree::new_from_root(self.db(), ts.parent_state())?; diff --git a/src/message_pool/msgpool/provider.rs b/src/message_pool/msgpool/provider.rs index 65f20079cde..f397ae146a2 100644 --- a/src/message_pool/msgpool/provider.rs +++ b/src/message_pool/msgpool/provider.rs @@ -111,7 +111,7 @@ impl Provider for ChainStore { ) -> Result { ChainStore::resolve_to_deterministic_address_at_finality(self, addr, ts) .map(AtFinalityResolution::into_address) - .map_err(|e| Error::Other(e.to_string())) + .map_err(|e| Error::Other(format!("{e:#}"))) } fn messages_for_tipset(&self, ts: &Tipset) -> Result>, Error> {