Skip to content

Commit

Permalink
Initial implementation of new, more convenient Map wrapping HAMT
Browse files Browse the repository at this point in the history
  • Loading branch information
anorth committed Aug 3, 2023
1 parent ae4fa9a commit b33527f
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 35 deletions.
48 changes: 16 additions & 32 deletions actors/init/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,27 @@
// SPDX-License-Identifier: Apache-2.0, MIT

use cid::Cid;
use fil_actors_runtime::{
actor_error, make_empty_map, make_map_with_root_and_bitwidth, ActorError, AsActorError,
FIRST_NON_SINGLETON_ADDR,
};
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::tuple::*;
use fvm_shared::address::{Address, Protocol};
use fvm_shared::error::ExitCode;
use fvm_shared::{ActorID, HAMT_BIT_WIDTH};
use fvm_shared::ActorID;

use fil_actors_runtime::{actor_error, ActorError, Map2, DEFAULT_CONF, FIRST_NON_SINGLETON_ADDR};

/// State is reponsible for creating
#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)]
pub struct State {
/// HAMT[Address]ActorID
pub address_map: Cid,
pub next_id: ActorID,
pub network_name: String,
}

pub type AddressMap<'bs, BS> = Map2<'bs, BS, ActorID>;

impl State {
pub fn new<BS: Blockstore>(store: &BS, network_name: String) -> Result<Self, ActorError> {
let empty_map = make_empty_map::<_, ()>(store, HAMT_BIT_WIDTH)
.flush()
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to create empty map")?;
Ok(Self { address_map: empty_map, next_id: FIRST_NON_SINGLETON_ADDR, network_name })
let empty = AddressMap::flush_empty(store, DEFAULT_CONF)?;
Ok(Self { address_map: empty, next_id: FIRST_NON_SINGLETON_ADDR, network_name })
}

/// Maps argument addresses to to a new or existing actor ID.
Expand All @@ -40,22 +37,17 @@ impl State {
robust_addr: &Address,
delegated_addr: Option<&Address>,
) -> Result<(ActorID, bool), ActorError> {
let mut map = make_map_with_root_and_bitwidth(&self.address_map, store, HAMT_BIT_WIDTH)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load address map")?;
let mut map = AddressMap::load(store, &self.address_map, DEFAULT_CONF, "addresses")?;
let (id, existing) = if let Some(delegated_addr) = delegated_addr {
// If there's a delegated address, either recall the already-mapped actor ID or
// create and map a new one.
let delegated_key = delegated_addr.to_bytes().into();
if let Some(existing_id) = map
.get(&delegated_key)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to lookup delegated address")?
{
let key = delegated_addr.to_bytes();
if let Some(existing_id) = map.get(&key)? {
(*existing_id, true)
} else {
let new_id = self.next_id;
self.next_id += 1;
map.set(delegated_key, new_id)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to map delegated address")?;
map.set(&key, new_id)?;
(new_id, false)
}
} else {
Expand All @@ -66,18 +58,15 @@ impl State {
};

// Map the robust address to the ID, failing if it's already mapped to anything.
let is_new = map
.set_if_absent(robust_addr.to_bytes().into(), id)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to map robust address")?;
let is_new = map.set_if_absent(&robust_addr.to_bytes(), id)?;
if !is_new {
return Err(actor_error!(
forbidden,
"robust address {} is already allocated in the address map",
robust_addr
));
}
self.address_map =
map.flush().context_code(ExitCode::USR_ILLEGAL_STATE, "failed to store address map")?;
self.address_map = map.flush()?;
Ok((id, existing))
}

Expand All @@ -99,13 +88,8 @@ impl State {
if addr.protocol() == Protocol::ID {
return Ok(Some(*addr));
}

let map = make_map_with_root_and_bitwidth(&self.address_map, store, HAMT_BIT_WIDTH)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load address map")?;

let found = map
.get(&addr.to_bytes())
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get address entry")?;
let map = AddressMap::load(store, &self.address_map, DEFAULT_CONF, "addresses")?;
let found = map.get(&addr.to_bytes())?;
Ok(found.copied().map(Address::new_id))
}
}
12 changes: 9 additions & 3 deletions actors/init/src/testing.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use std::collections::HashMap;

use fil_actors_runtime::{Map, MessageAccumulator, FIRST_NON_SINGLETON_ADDR};
use fil_actors_runtime::{
AsActorError, MessageAccumulator, DEFAULT_CONF, FIRST_NON_SINGLETON_ADDR,
};
use fvm_ipld_blockstore::Blockstore;
use fvm_shared::error::ExitCode;
use fvm_shared::{
address::{Address, Protocol},
ActorID,
};

use crate::state::AddressMap;
use crate::State;

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

let mut stable_address_by_id = HashMap::<ActorID, Address>::new();
let mut delegated_address_by_id = HashMap::<ActorID, Address>::new();
match Map::<_, ActorID>::load(&state.address_map, store) {

match AddressMap::load(store, &state.address_map, DEFAULT_CONF, "addresses") {
Ok(address_map) => {
let ret = address_map.for_each(|key, actor_id| {
let key_address = Address::from_bytes(key)?;
let key_address =
Address::from_bytes(key).exit_code(ExitCode::USR_ILLEGAL_STATE)?;

acc.require(
key_address.protocol() != Protocol::ID,
Expand Down
1 change: 1 addition & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ macro_rules! wasm_trampoline {
};
}

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

Expand Down
160 changes: 160 additions & 0 deletions runtime/src/util/map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::builtin::HAMT_BIT_WIDTH;
use crate::{ActorError, AsActorError, Hasher};
use anyhow::anyhow;
use cid::Cid;
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_hamt as hamt;
use fvm_shared::error::ExitCode;
use serde::de::DeserializeOwned;
use serde::Serialize;

/// Wraps a HAMT to provide a convenient map API.
/// The key type is Vec<u8>, so conversion to/from interpretations must by done by the caller.
/// Any errors are returned with exit code indicating illegal state.
/// The name is not persisted in state, but adorns any error messages.
pub struct Map2<'bs, BS, V>
where
BS: Blockstore,
V: DeserializeOwned + Serialize,
{
hamt: hamt::Hamt<&'bs BS, V, hamt::BytesKey, Hasher>,
name: &'static str,
}

trait MapKey: Sized {
fn from_bytes(b: &[u8]) -> Result<Self, String>;
fn to_bytes(&self) -> Result<Vec<u8>, String>;
}

pub type Config = hamt::Config;

pub const DEFAULT_CONF: Config =
Config { bit_width: HAMT_BIT_WIDTH, min_data_depth: 0, max_array_width: 3 };

impl<'bs, BS, V> Map2<'bs, BS, V>
where
BS: Blockstore,
V: DeserializeOwned + Serialize,
{
/// Creates a new, empty map.
pub fn empty(store: &'bs BS, config: Config, name: &'static str) -> Self {
Self { hamt: hamt::Hamt::new_with_config(store, config), name }
}

/// Creates a new empty map and flushes it to the store.
/// Returns the CID of the empty map root.
pub fn flush_empty(store: &'bs BS, config: Config) -> Result<Cid, ActorError> {
// This CID is constant regardless of the HAMT's configuration, so as an optimisation
// we could hard-code it and merely check it is already stored.
Self::empty(store, config, "empty").flush()
}

/// Loads a map from the store.
// There is no version of this method that doesn't take an explicit config parameter.
// The caller must know the configuration to interpret the HAMT correctly.
// Forcing them to provide it makes it harder to accidentally use an incorrect default.
pub fn load(
store: &'bs BS,
root: &Cid,
config: Config,
name: &'static str,
) -> Result<Self, ActorError> {
Ok(Self {
hamt: hamt::Hamt::load_with_config(root, store, config)
.with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
format!("failed to load HAMT '{}'", name)
})?,
name,
})
}

/// Flushes the map's contents to the store.
/// Returns the root node CID.
pub fn flush(&mut self) -> Result<Cid, ActorError> {
self.hamt.flush().with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
format!("failed to flush HAMT '{}'", self.name)
})
}

/// Returns a reference to the value associated with a key, if present.
pub fn get(&self, key: &[u8]) -> Result<Option<&V>, ActorError> {
self.hamt.get(key).with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
format!("failed to get from HAMT '{}'", self.name)
})
}

/// Inserts a key-value pair into the map.
/// Returns any value previously associated with the key.
pub fn set(&mut self, key: &[u8], value: V) -> Result<Option<V>, ActorError>
where
V: PartialEq,
{
self.hamt.set(key.into(), value).with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
format!("failed to set in HAMT '{}'", self.name)
})
}

/// Inserts a key-value pair only if the key does not already exist.
/// Returns whether the map was modified (i.e. key was absent).
pub fn set_if_absent(&mut self, key: &[u8], value: V) -> Result<bool, ActorError>
where
V: PartialEq,
{
self.hamt
.set_if_absent(key.into(), value)
.with_context_code(ExitCode::USR_ILLEGAL_STATE, || {
format!("failed to set in HAMT '{}'", self.name)
})
}

/// Iterates over all key-value pairs in the map.
pub fn for_each<F>(&self, mut f: F) -> Result<(), ActorError>
where
// Note the result type of F uses ActorError.
// The implementation will extract and propagate any ActorError
// wrapped in a hamt::Error::Dynamic.
F: FnMut(&[u8], &V) -> Result<(), ActorError>,
{
match self.hamt.for_each(|k, v| f(k, v).map_err(|e| anyhow!(e))) {
Ok(_) => Ok(()),
Err(hamt_err) => match hamt_err {
hamt::Error::Dynamic(e) => match e.downcast::<ActorError>() {
Ok(ae) => Err(ae),
Err(e) => Err(ActorError::illegal_state(format!(
"error traversing HAMT {}: {}",
self.name, e
))),
},
e => Err(ActorError::illegal_state(format!(
"error traversing HAMT {}: {}",
self.name, e
))),
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use fvm_ipld_blockstore::MemoryBlockstore;

#[test]
fn basic_put_get() {
let bs = MemoryBlockstore::new();
let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty");
m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap();
assert!(m.get(&[1, 2]).unwrap().is_none());
assert_eq!(&"1234".to_string(), m.get(&[1, 2, 3, 4]).unwrap().unwrap());
}

#[test]
fn for_each_callback_exitcode_propagates() {
let bs = MemoryBlockstore::new();
let mut m = Map2::<MemoryBlockstore, String>::empty(&bs, DEFAULT_CONF, "empty");
m.set(&[1, 2, 3, 4], "1234".to_string()).unwrap();
let res = m.for_each(|_, _| Err(ActorError::forbidden("test".to_string())));
assert!(res.is_err());
assert_eq!(res.unwrap_err(), ActorError::forbidden("test".to_string()));
}
}
2 changes: 2 additions & 0 deletions runtime/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub use self::batch_return::BatchReturn;
pub use self::batch_return::BatchReturnGen;
pub use self::batch_return::FailCode;
pub use self::downcast::*;
pub use self::map::*;
pub use self::mapmap::MapMap;
pub use self::message_accumulator::MessageAccumulator;
pub use self::multimap::*;
Expand All @@ -14,6 +15,7 @@ pub use self::set_multimap::SetMultimap;
mod batch_return;
pub mod cbor;
mod downcast;
mod map;
mod mapmap;
mod message_accumulator;
mod multimap;
Expand Down

0 comments on commit b33527f

Please sign in to comment.