Skip to main content

forest/state_manager/
address_resolution.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::*;
5use crate::chain::AtFinalityResolution;
6use crate::prelude::*;
7use crate::shim::address::Payload;
8use bls_signatures::{PublicKey as BlsPublicKey, Serialize as _};
9
10impl StateManager {
11    /// Returns a BLS public key from provided address
12    pub fn get_bls_public_key(
13        db: &(impl Blockstore + ShallowClone),
14        addr: Address,
15        state_cid: Cid,
16    ) -> Result<BlsPublicKey, Error> {
17        let state =
18            StateTree::new_from_root(db, &state_cid).map_err(|e| Error::Other(e.to_string()))?;
19        let kaddr = state
20            .resolve_to_deterministic_address(db, addr)
21            .context("Failed to resolve key address")?;
22
23        match kaddr.into_payload() {
24            Payload::BLS(key) => BlsPublicKey::from_bytes(&key)
25                .context("Failed to construct bls public key")
26                .map_err(Error::from),
27            _ => Err(Error::state(
28                "Address must be BLS address to load bls public key",
29            )),
30        }
31    }
32
33    /// Looks up ID [Address] from the state at the given [Tipset].
34    pub fn lookup_id(&self, addr: &Address, ts: &Tipset) -> Result<Option<Address>, Error> {
35        let state_tree =
36            StateTree::new_from_root(self.db(), ts.parent_state()).map_err(|e| format!("{e:?}"))?;
37        Ok(state_tree
38            .lookup_id(addr)
39            .map_err(|e| Error::Other(e.to_string()))?
40            .map(Address::new_id))
41    }
42
43    /// Looks up required ID [Address] from the state at the given [Tipset].
44    pub fn lookup_required_id(&self, addr: &Address, ts: &Tipset) -> Result<Address, Error> {
45        self.lookup_id(addr, ts)?
46            .ok_or_else(|| Error::Other(format!("Failed to lookup the id address {addr}")))
47    }
48
49    /// Similar to [`StateTree::resolve_to_deterministic_addr`] but does not allow [`crate::shim::address::Protocol::Actor`] type of addresses.
50    /// Uses the [`Tipset`] `ts` to generate the VM state.
51    pub async fn resolve_to_deterministic_address(
52        &self,
53        address: Address,
54        ts: &Tipset,
55    ) -> anyhow::Result<Address> {
56        use crate::shim::address::Protocol::*;
57        match address.protocol() {
58            BLS | Secp256k1 | Delegated => Ok(address),
59            Actor => anyhow::bail!("cannot resolve actor address to key address"),
60            ID => {
61                let id = address.id()?;
62                // The cache is disabled for the RPC test-snapshot generator
63                // and replay harness (see the field docs on `StateManager`).
64                let Some(cache) = &self.id_to_deterministic_address_cache else {
65                    return self.resolve_id_address_at_tipset(address, ts).await;
66                };
67                if let Some(resolved) = cache.get(&id) {
68                    return Ok(resolved);
69                }
70                // Only a resolution witnessed at a finality-deep tipset is
71                // safe to memoize by bare ID: ID assignments within the
72                // finality window can differ between competing forks, while
73                // anything at or below the lookback is identical on every
74                // possible future chain.
75                let at_finality = {
76                    let cs = self.chain_store().shallow_clone();
77                    let ts = ts.clone();
78                    tokio::task::spawn_blocking(move || {
79                        cs.resolve_to_deterministic_address_at_finality(&address, &ts)
80                    })
81                    .await
82                    .context("tokio join error")?
83                };
84                if let Ok(resolution) = at_finality {
85                    return Ok(match resolution {
86                        AtFinalityResolution::ReorgStable(resolved) => {
87                            cache.insert(id, resolved);
88                            resolved
89                        }
90                        AtFinalityResolution::Unstable(resolved) => resolved,
91                    });
92                }
93                self.resolve_id_address_at_tipset(address, ts).await
94            }
95        }
96    }
97
98    /// Resolves an ID address against `ts` without touching the cache: first
99    /// via the parent state, then by computing the tipset state if needed.
100    async fn resolve_id_address_at_tipset(
101        &self,
102        address: Address,
103        ts: &Tipset,
104    ) -> anyhow::Result<Address> {
105        // First try to resolve the actor in the parent state, so we don't have to compute anything.
106        if let Ok(state) = self.get_state_tree(ts.parent_state())
107            && let Ok(address) = state.resolve_to_deterministic_address(self.db(), address)
108        {
109            return Ok(address);
110        }
111        // If that fails, compute the tip-set and try again.
112        let TipsetState { state_root, .. } = self.load_tipset_state(ts).await?;
113        let state = self.get_state_tree(&state_root)?;
114        state.resolve_to_deterministic_address(self.db(), address)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::blocks::{CachingBlockHeader, RawBlockHeader, Tipset};
122    use crate::chain::ChainStore;
123    use crate::db::{DbImpl, MemoryDB};
124    use crate::networks::ChainConfig;
125    use crate::shim::state_tree::{ActorState, StateTree, StateTreeVersion};
126    use crate::test_utils::dummy_ticket;
127    use crate::utils::db::CborStoreExt as _;
128
129    /// Present in the state from genesis onwards, so it is visible at the
130    /// finality lookback of the head.
131    const OLD_ACTOR: u64 = 300;
132    /// Present only in the head's parent state — younger than finality.
133    const YOUNG_ACTOR: u64 = 400;
134
135    /// Builds a 3-tipset chain (genesis, ts1, head at epoch 2). Genesis and
136    /// ts1 carry `root_a` (contains f0300 -> bls_a); head carries `root_b`,
137    /// built on top of `root_a` (contains f0300 -> bls_a and f0400 -> bls_b).
138    /// Returns (state_manager, head, bls_a, bls_b).
139    fn setup_with_finality(chain_finality: ChainEpoch) -> (StateManager, Tipset, Address, Address) {
140        let db: DbImpl = Arc::new(MemoryDB::default()).into();
141
142        let mut cfg = ChainConfig::default();
143        cfg.policy.chain_finality = chain_finality;
144        let cfg = Arc::new(cfg);
145
146        let bls_a = Address::new_bls(&[8u8; 48]).unwrap();
147        let bls_b = Address::new_bls(&[9u8; 48]).unwrap();
148
149        let mut st_a = StateTree::new(&db, StateTreeVersion::V5).unwrap();
150        st_a.set_actor(
151            &Address::new_id(OLD_ACTOR),
152            ActorState::new_empty(Cid::default(), Some(bls_a)),
153        )
154        .unwrap();
155        let root_a = st_a.flush().unwrap();
156
157        // Builds on top of `root_a` (rather than a fresh tree) so f0300 stays
158        // resolvable at head's own parent state too, matching how a real
159        // state tree accumulates actors across epochs, and keeping every
160        // resolution in these tests on the cheap parent-state path.
161        let mut st_b = StateTree::new_from_root(&db, &root_a).unwrap();
162        st_b.set_actor(
163            &Address::new_id(YOUNG_ACTOR),
164            ActorState::new_empty(Cid::default(), Some(bls_b)),
165        )
166        .unwrap();
167        let root_b = st_b.flush().unwrap();
168
169        let genesis = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
170            ticket: dummy_ticket(0),
171            state_root: root_a,
172            // `StateManager::new` builds a beacon schedule from the genesis
173            // timestamp, which must be non-zero.
174            timestamp: 1,
175            ..Default::default()
176        }));
177        db.put_cbor_default(genesis.block_headers().first())
178            .unwrap();
179
180        let ts1 = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
181            parents: genesis.key().clone(),
182            ticket: dummy_ticket(1),
183            epoch: 1,
184            state_root: root_a,
185            timestamp: 1,
186            ..Default::default()
187        }));
188        db.put_cbor_default(ts1.block_headers().first()).unwrap();
189
190        let head = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
191            parents: ts1.key().clone(),
192            ticket: dummy_ticket(2),
193            epoch: 2,
194            state_root: root_b,
195            timestamp: 2,
196            ..Default::default()
197        }));
198        db.put_cbor_default(head.block_headers().first()).unwrap();
199
200        let cs = ChainStore::new(db, cfg, genesis.block_headers().first().clone()).unwrap();
201        let sm = StateManager::new(cs).unwrap();
202        (sm, head, bls_a, bls_b)
203    }
204
205    #[tokio::test]
206    async fn caches_resolution_witnessed_at_finality_lookback() {
207        let (sm, head, bls_a, bls_b) = setup_with_finality(1);
208        let resolved = sm
209            .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head)
210            .await
211            .unwrap();
212        assert_eq!(resolved, bls_a);
213        let cache = sm.id_to_deterministic_address_cache().unwrap();
214        assert_eq!(cache.get(&OLD_ACTOR), Some(bls_a));
215
216        // Prove subsequent calls are served from the cache: poison the entry
217        // and observe the poisoned value coming back.
218        cache.insert(OLD_ACTOR, bls_b);
219        let resolved = sm
220            .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head)
221            .await
222            .unwrap();
223        assert_eq!(resolved, bls_b);
224    }
225
226    #[tokio::test]
227    async fn does_not_cache_actor_younger_than_finality() {
228        let (sm, head, _bls_a, bls_b) = setup_with_finality(1);
229        // f0400 is absent at the lookback: resolvable at the head, uncached.
230        let resolved = sm
231            .resolve_to_deterministic_address(Address::new_id(YOUNG_ACTOR), &head)
232            .await
233            .unwrap();
234        assert_eq!(resolved, bls_b);
235        let cache = sm.id_to_deterministic_address_cache().unwrap();
236        assert_eq!(cache.get(&YOUNG_ACTOR), None);
237    }
238
239    #[tokio::test]
240    async fn does_not_cache_on_chain_younger_than_finality() {
241        // Finality deeper than the whole chain: the lookback would degrade to
242        // resolving at `ts` itself, which is not reorg-stable — never cache.
243        let (sm, head, _bls_a, bls_b) = setup_with_finality(900);
244        let resolved = sm
245            .resolve_to_deterministic_address(Address::new_id(YOUNG_ACTOR), &head)
246            .await
247            .unwrap();
248        assert_eq!(resolved, bls_b);
249        assert_eq!(
250            sm.id_to_deterministic_address_cache().unwrap().len(),
251            0,
252            "nothing may be cached without a finality-deep witness"
253        );
254    }
255
256    #[tokio::test]
257    async fn does_not_cache_at_exact_finality_boundary() {
258        // Head epoch == chain_finality: the guard is strictly `>`, so this is
259        // not finality-deep and the lookback degrades to resolving at the
260        // head's own parent state (which does contain f0300, since `root_b`
261        // was built on top of `root_a`). The resolution succeeds but, being
262        // `Unstable`, must not be cached.
263        let (sm, head, bls_a, _bls_b) = setup_with_finality(2);
264        let resolved = sm
265            .resolve_to_deterministic_address(Address::new_id(OLD_ACTOR), &head)
266            .await
267            .unwrap();
268        assert_eq!(resolved, bls_a);
269        assert_eq!(
270            sm.id_to_deterministic_address_cache().unwrap().len(),
271            0,
272            "epoch == chain_finality is not finality-deep and must not be cached"
273        );
274    }
275
276    #[tokio::test]
277    async fn non_id_addresses_bypass_cache_and_lookback() {
278        let (sm, head, bls_a, _bls_b) = setup_with_finality(1);
279        let resolved = sm
280            .resolve_to_deterministic_address(bls_a, &head)
281            .await
282            .unwrap();
283        assert_eq!(resolved, bls_a);
284        assert_eq!(sm.id_to_deterministic_address_cache().unwrap().len(), 0);
285    }
286}