Skip to content

Commit 26ce49c

Browse files
committed
Initial implementation of new, more convenient Map wrapping HAMT
1 parent ad4470f commit 26ce49c

File tree

5 files changed

+188
-35
lines changed

5 files changed

+188
-35
lines changed

actors/init/src/state.rs

+16-32
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,27 @@
22
// SPDX-License-Identifier: Apache-2.0, MIT
33

44
use cid::Cid;
5-
use fil_actors_runtime::{
6-
actor_error, make_empty_map, make_map_with_root_and_bitwidth, ActorError, AsActorError,
7-
FIRST_NON_SINGLETON_ADDR,
8-
};
95
use fvm_ipld_blockstore::Blockstore;
106
use fvm_ipld_encoding::tuple::*;
117
use fvm_shared::address::{Address, Protocol};
12-
use fvm_shared::error::ExitCode;
13-
use fvm_shared::{ActorID, HAMT_BIT_WIDTH};
8+
use fvm_shared::ActorID;
9+
10+
use fil_actors_runtime::{actor_error, ActorError, Map2, DEFAULT_CONF, FIRST_NON_SINGLETON_ADDR};
1411

15-
/// State is reponsible for creating
1612
#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)]
1713
pub struct State {
14+
/// HAMT[Address]ActorID
1815
pub address_map: Cid,
1916
pub next_id: ActorID,
2017
pub network_name: String,
2118
}
2219

20+
pub type AddressMap<'bs, BS> = Map2<'bs, BS, ActorID>;
21+
2322
impl State {
2423
pub fn new<BS: Blockstore>(store: &BS, network_name: String) -> Result<Self, ActorError> {
25-
let empty_map = make_empty_map::<_, ()>(store, HAMT_BIT_WIDTH)
26-
.flush()
27-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to create empty map")?;
28-
Ok(Self { address_map: empty_map, next_id: FIRST_NON_SINGLETON_ADDR, network_name })
24+
let empty = AddressMap::flush_empty(store, DEFAULT_CONF)?;
25+
Ok(Self { address_map: empty, next_id: FIRST_NON_SINGLETON_ADDR, network_name })
2926
}
3027

3128
/// Maps argument addresses to to a new or existing actor ID.
@@ -40,22 +37,17 @@ impl State {
4037
robust_addr: &Address,
4138
delegated_addr: Option<&Address>,
4239
) -> Result<(ActorID, bool), ActorError> {
43-
let mut map = make_map_with_root_and_bitwidth(&self.address_map, store, HAMT_BIT_WIDTH)
44-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load address map")?;
40+
let mut map = AddressMap::load(store, &self.address_map, DEFAULT_CONF, "addresses")?;
4541
let (id, existing) = if let Some(delegated_addr) = delegated_addr {
4642
// If there's a delegated address, either recall the already-mapped actor ID or
4743
// create and map a new one.
48-
let delegated_key = delegated_addr.to_bytes().into();
49-
if let Some(existing_id) = map
50-
.get(&delegated_key)
51-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to lookup delegated address")?
52-
{
44+
let key = delegated_addr.to_bytes();
45+
if let Some(existing_id) = map.get(&key)? {
5346
(*existing_id, true)
5447
} else {
5548
let new_id = self.next_id;
5649
self.next_id += 1;
57-
map.set(delegated_key, new_id)
58-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to map delegated address")?;
50+
map.set(&key, new_id)?;
5951
(new_id, false)
6052
}
6153
} else {
@@ -66,18 +58,15 @@ impl State {
6658
};
6759

6860
// Map the robust address to the ID, failing if it's already mapped to anything.
69-
let is_new = map
70-
.set_if_absent(robust_addr.to_bytes().into(), id)
71-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to map robust address")?;
61+
let is_new = map.set_if_absent(&robust_addr.to_bytes(), id)?;
7262
if !is_new {
7363
return Err(actor_error!(
7464
forbidden,
7565
"robust address {} is already allocated in the address map",
7666
robust_addr
7767
));
7868
}
79-
self.address_map =
80-
map.flush().context_code(ExitCode::USR_ILLEGAL_STATE, "failed to store address map")?;
69+
self.address_map = map.flush()?;
8170
Ok((id, existing))
8271
}
8372

@@ -99,13 +88,8 @@ impl State {
9988
if addr.protocol() == Protocol::ID {
10089
return Ok(Some(*addr));
10190
}
102-
103-
let map = make_map_with_root_and_bitwidth(&self.address_map, store, HAMT_BIT_WIDTH)
104-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load address map")?;
105-
106-
let found = map
107-
.get(&addr.to_bytes())
108-
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get address entry")?;
91+
let map = AddressMap::load(store, &self.address_map, DEFAULT_CONF, "addresses")?;
92+
let found = map.get(&addr.to_bytes())?;
10993
Ok(found.copied().map(Address::new_id))
11094
}
11195
}

actors/init/src/testing.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use std::collections::HashMap;
22

3-
use fil_actors_runtime::{Map, MessageAccumulator, FIRST_NON_SINGLETON_ADDR};
3+
use fil_actors_runtime::{
4+
AsActorError, MessageAccumulator, DEFAULT_CONF, FIRST_NON_SINGLETON_ADDR,
5+
};
46
use fvm_ipld_blockstore::Blockstore;
7+
use fvm_shared::error::ExitCode;
58
use fvm_shared::{
69
address::{Address, Protocol},
710
ActorID,
811
};
912

13+
use crate::state::AddressMap;
1014
use crate::State;
1115

1216
pub struct StateSummary {
@@ -31,10 +35,12 @@ pub fn check_state_invariants<BS: Blockstore>(
3135

3236
let mut stable_address_by_id = HashMap::<ActorID, Address>::new();
3337
let mut delegated_address_by_id = HashMap::<ActorID, Address>::new();
34-
match Map::<_, ActorID>::load(&state.address_map, store) {
38+
39+
match AddressMap::load(store, &state.address_map, DEFAULT_CONF, "addresses") {
3540
Ok(address_map) => {
3641
let ret = address_map.for_each(|key, actor_id| {
37-
let key_address = Address::from_bytes(key)?;
42+
let key_address =
43+
Address::from_bytes(key).exit_code(ExitCode::USR_ILLEGAL_STATE)?;
3844

3945
acc.require(
4046
key_address.protocol() != Protocol::ID,

runtime/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ macro_rules! wasm_trampoline {
4545
};
4646
}
4747

48+
/// XXX move to map
4849
#[cfg(feature = "fil-actor")]
4950
type Hasher = FvmHashSha256;
5051

runtime/src/util/map.rs

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use crate::builtin::HAMT_BIT_WIDTH;
2+
use crate::{ActorError, AsActorError, Hasher};
3+
use anyhow::anyhow;
4+
use cid::Cid;
5+
use fvm_ipld_blockstore::Blockstore;
6+
use fvm_ipld_hamt as hamt;
7+
use fvm_shared::error::ExitCode;
8+
use serde::de::DeserializeOwned;
9+
use serde::Serialize;
10+
11+
/// Wraps a HAMT to provide a convenient map API.
12+
/// The key type is Vec<u8>, so conversion to/from interpretations must by done by the caller.
13+
/// Any errors are returned with exit code indicating illegal state.
14+
/// The name is not persisted in state, but adorns any error messages.
15+
pub struct Map2<'bs, BS, V>
16+
where
17+
BS: Blockstore,
18+
V: DeserializeOwned + Serialize,
19+
{
20+
hamt: hamt::Hamt<&'bs BS, V, hamt::BytesKey, Hasher>,
21+
name: &'static str,
22+
}
23+
24+
trait MapKey: Sized {
25+
fn from_bytes(b: &[u8]) -> Result<Self, String>;
26+
fn to_bytes(&self) -> Result<Vec<u8>, String>;
27+
}
28+
29+
pub type Config = hamt::Config;
30+
31+
pub const DEFAULT_CONF: Config =
32+
Config { bit_width: HAMT_BIT_WIDTH, min_data_depth: 0, max_array_width: 3 };
33+
34+
impl<'bs, BS, V> Map2<'bs, BS, V>
35+
where
36+
BS: Blockstore,
37+
V: DeserializeOwned + Serialize,
38+
{
39+
/// Creates a new, empty map.
40+
pub fn empty(store: &'bs BS, config: Config, name: &'static str) -> Self {
41+
Self { hamt: hamt::Hamt::new_with_config(store, config), name }
42+
}
43+
44+
/// Creates a new empty map and flushes it to the store.
45+
/// Returns the CID of the empty map root.
46+
pub fn flush_empty(store: &'bs BS, config: Config) -> Result<Cid, ActorError> {
47+
// This CID is constant regardless of the HAMT's configuration, so as an optimisation
48+
// we could hard-code it and merely check it is already stored.
49+
Self::empty(store, config, "empty").flush()
50+
}
51+
52+
/// Loads a map from the store.
53+
// There is no version of this method that doesn't take an explicit config parameter.
54+
// The caller must know the configuration to interpret the HAMT correctly.
55+
// Forcing them to provide it makes it harder to accidentally use an incorrect default.
56+
pub fn load(
57+
store: &'bs BS,
58+
root: &Cid,
59+
config: Config,
60+
name: &'static str,
61+
) -> Result<Self, ActorError> {
62+
Ok(Self {
63+
hamt: hamt::Hamt::load_with_config(root, store, config)
64+
.with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
65+
format!("failed to load HAMT '{}'", name)
66+
})?,
67+
name,
68+
})
69+
}
70+
71+
/// Flushes the map's contents to the store.
72+
/// Returns the root node CID.
73+
pub fn flush(&mut self) -> Result<Cid, ActorError> {
74+
self.hamt.flush().with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
75+
format!("failed to flush HAMT '{}'", self.name)
76+
})
77+
}
78+
79+
/// Returns a reference to the value associated with a key, if present.
80+
pub fn get(&self, key: &[u8]) -> Result<Option<&V>, ActorError> {
81+
self.hamt.get(key).with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
82+
format!("failed to get from HAMT '{}'", self.name)
83+
})
84+
}
85+
86+
/// Inserts a key-value pair into the map.
87+
/// Returns any value previously associated with the key.
88+
pub fn set(&mut self, key: &[u8], value: V) -> Result<Option<V>, ActorError>
89+
where
90+
V: PartialEq,
91+
{
92+
self.hamt.set(key.into(), value).with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
93+
format!("failed to set in HAMT '{}'", self.name)
94+
})
95+
}
96+
97+
/// Inserts a key-value pair only if the key does not already exist.
98+
/// Returns whether the map was modified (i.e. key was absent).
99+
pub fn set_if_absent(&mut self, key: &[u8], value: V) -> Result<bool, ActorError>
100+
where
101+
V: PartialEq,
102+
{
103+
self.hamt
104+
.set_if_absent(key.into(), value)
105+
.with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
106+
format!("failed to set in HAMT '{}'", self.name)
107+
})
108+
}
109+
110+
/// Iterates over all key-value pairs in the map.
111+
pub fn for_each<F>(&self, mut f: F) -> Result<(), ActorError>
112+
where
113+
// Note the result type of F uses ActorError.
114+
// The implementation will extract and propagate any ActorError
115+
// wrapped in a hamt::Error::Dynamic.
116+
F: FnMut(&[u8], &V) -> Result<(), ActorError>,
117+
{
118+
match self.hamt.for_each(|k, v| f(k, v).map_err(|e| anyhow!(e))) {
119+
Ok(_) => Ok(()),
120+
Err(hamt_err) => match hamt_err {
121+
hamt::Error::Dynamic(e) => match e.downcast::<ActorError>() {
122+
Ok(ae) => Err(ae),
123+
Err(e) => Err(ActorError::illegal_state(format!(
124+
"error traversing HAMT {}: {}",
125+
self.name, e
126+
))),
127+
},
128+
e => Err(ActorError::illegal_state(format!(
129+
"error traversing HAMT {}: {}",
130+
self.name, e
131+
))),
132+
},
133+
}
134+
}
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
use fvm_ipld_blockstore::MemoryBlockstore;
141+
142+
#[test]
143+
fn basic_put_get() {
144+
let bs = MemoryBlockstore::new();
145+
let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty");
146+
m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap();
147+
assert!(m.get(&[1, 2]).unwrap().is_none());
148+
assert_eq!(&"1234".to_string(), m.get(&[1, 2, 3, 4]).unwrap().unwrap());
149+
}
150+
151+
#[test]
152+
fn for_each_callback_exitcode_propagates() {
153+
let bs = MemoryBlockstore::new();
154+
let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty");
155+
m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap();
156+
let res = m.for_each(|_, _| Err(ActorError::forbidden("test".to_string())));
157+
assert!(res.is_err());
158+
assert_eq!(res.unwrap_err(), ActorError::forbidden("test".to_string()));
159+
}
160+
}

runtime/src/util/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub use self::batch_return::BatchReturn;
55
pub use self::batch_return::BatchReturnGen;
66
pub use self::batch_return::FailCode;
77
pub use self::downcast::*;
8+
pub use self::map::*;
89
pub use self::mapmap::MapMap;
910
pub use self::message_accumulator::MessageAccumulator;
1011
pub use self::multimap::*;
@@ -14,6 +15,7 @@ pub use self::set_multimap::SetMultimap;
1415
mod batch_return;
1516
pub mod cbor;
1617
mod downcast;
18+
mod map;
1719
mod mapmap;
1820
mod message_accumulator;
1921
mod multimap;

0 commit comments

Comments
 (0)