diff --git a/packages/ciphernode/Cargo.lock b/packages/ciphernode/Cargo.lock index 63966fe4..dc4149ec 100644 --- a/packages/ciphernode/Cargo.lock +++ b/packages/ciphernode/Cargo.lock @@ -1706,6 +1706,7 @@ dependencies = [ "alloy", "anyhow", "dirs", + "enclave-core", "figment", "serde", "shellexpand", diff --git a/packages/ciphernode/Cargo.toml b/packages/ciphernode/Cargo.toml index ae75b191..b6d37969 100644 --- a/packages/ciphernode/Cargo.toml +++ b/packages/ciphernode/Cargo.toml @@ -40,6 +40,7 @@ cipher = { path = "./cipher" } compile-time = "0.2.0" dirs = "5.0.1" data = { path = "./data" } +enclave-core = { path = "./core" } shellexpand = "3.1.0" figment = { version = "0.10.19", features = ["yaml", "test"] } fhe_rs = { package = "fhe", git = "https://github.com/gnosisguild/fhe.rs", version = "0.1.0-beta.7" } diff --git a/packages/ciphernode/aggregator/src/plaintext_aggregator.rs b/packages/ciphernode/aggregator/src/plaintext_aggregator.rs index fc5bdf60..7b833b60 100644 --- a/packages/ciphernode/aggregator/src/plaintext_aggregator.rs +++ b/packages/ciphernode/aggregator/src/plaintext_aggregator.rs @@ -221,8 +221,8 @@ impl Handler for PlaintextAggregator { impl Snapshot for PlaintextAggregator { type Snapshot = PlaintextAggregatorState; - fn snapshot(&self) -> Self::Snapshot { - self.state.clone() + fn snapshot(&self) -> Result { + Ok(self.state.clone()) } } diff --git a/packages/ciphernode/aggregator/src/publickey_aggregator.rs b/packages/ciphernode/aggregator/src/publickey_aggregator.rs index e5e3b640..10972930 100644 --- a/packages/ciphernode/aggregator/src/publickey_aggregator.rs +++ b/packages/ciphernode/aggregator/src/publickey_aggregator.rs @@ -246,8 +246,8 @@ impl Handler for PublicKeyAggregator { impl Snapshot for PublicKeyAggregator { type Snapshot = PublicKeyAggregatorState; - fn snapshot(&self) -> Self::Snapshot { - self.state.clone() + fn snapshot(&self) -> Result { + Ok(self.state.clone()) } } diff --git a/packages/ciphernode/config/Cargo.toml b/packages/ciphernode/config/Cargo.toml index e58c695f..0ce89132 100644 --- a/packages/ciphernode/config/Cargo.toml +++ b/packages/ciphernode/config/Cargo.toml @@ -11,6 +11,7 @@ figment = { workspace = true } alloy = { workspace = true } shellexpand = { workspace = true } url = { workspace = true } +enclave-core = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/packages/ciphernode/config/src/lib.rs b/packages/ciphernode/config/src/lib.rs index 83e06ce9..1d23bb7b 100644 --- a/packages/ciphernode/config/src/lib.rs +++ b/packages/ciphernode/config/src/lib.rs @@ -1,3 +1,5 @@ mod app_config; mod yaml; +mod store_keys; + pub use app_config::*; diff --git a/packages/ciphernode/config/src/store_keys.rs b/packages/ciphernode/config/src/store_keys.rs new file mode 100644 index 00000000..3cdb653f --- /dev/null +++ b/packages/ciphernode/config/src/store_keys.rs @@ -0,0 +1,53 @@ +use enclave_core::E3id; + +pub struct StoreKeys; + +impl StoreKeys { + pub fn keyshare(e3_id: &E3id) -> String { + format!("//keyshare/{e3_id}") + } + + pub fn plaintext(e3_id: &E3id) -> String { + format!("//plaintext/{e3_id}") + } + + pub fn publickey(e3_id: &E3id) -> String { + format!("//publickey/{e3_id}") + } + + pub fn fhe(e3_id: &E3id) -> String { + format!("//fhe/{e3_id}") + } + + pub fn meta(e3_id: &E3id) -> String { + format!("//meta/{e3_id}") + } + + pub fn context(e3_id: &E3id) -> String { + format!("//context/{e3_id}") + } + + pub fn router() -> String { + String::from("//router") + } + + pub fn sortition() -> String { + String::from("//sortition") + } + + pub fn eth_private_key() -> String { + String::from("//eth_private_key") + } + + pub fn libp2p_keypair() -> String { + String::from("//libp2p/keypair") + } + + pub fn enclave_sol_reader(chain_id: u64) -> String { + format!("//evm_readers/enclave/{chain_id}") + } + + pub fn ciphernode_registry_reader(chain_id: u64) -> String { + format!("//evm_readers/ciphernode_registry/{chain_id}") + } +} diff --git a/packages/ciphernode/data/src/lib.rs b/packages/ciphernode/data/src/lib.rs index b3f1ea93..42108d08 100644 --- a/packages/ciphernode/data/src/lib.rs +++ b/packages/ciphernode/data/src/lib.rs @@ -4,6 +4,7 @@ mod into_key; mod repository; mod sled_store; mod snapshot; +mod persistable; pub use data_store::*; pub use in_mem::*; diff --git a/packages/ciphernode/data/src/persistable.rs b/packages/ciphernode/data/src/persistable.rs new file mode 100644 index 00000000..68cfe729 --- /dev/null +++ b/packages/ciphernode/data/src/persistable.rs @@ -0,0 +1,431 @@ +use crate::{Checkpoint, FromSnapshotWithParams, Repository, Snapshot}; +use anyhow::*; +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Serialize}; + +pub trait PersistableData: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {} +impl PersistableData for T where T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {} + +/// AutoPersist enables a repository to generate a persistable container +#[async_trait] +pub trait AutoPersist +where + T: PersistableData, +{ + /// Load the data from the repository into an auto persist container + async fn load(&self) -> Result>; + /// Create a new auto persist container and set some data on it to send back to the repository + fn send(&self, data: Option) -> Persistable; + /// Load the data from the repository into an auto persist container. If there is no persisted data then persist the given default data + async fn load_or_default(&self, default: T) -> Result>; + /// Load the data from the repository into an auto persist container. If there is no persisted data then persist the given default data + async fn load_or_else(&self, f: F) -> Result> + where + F: Send + FnOnce() -> Result; +} + +#[async_trait] +impl AutoPersist for Repository +where + T: PersistableData, +{ + /// Load the data from the repository into an auto persist container + async fn load(&self) -> Result> { + Ok(Persistable::load(self).await?) + } + + /// Create a new auto persist container and set some data on it to send back to the repository + fn send(&self, data: Option) -> Persistable { + Persistable::new(data, self).save() + } + + /// Load the data from the repository into an auto persist container. If there is no persisted data then persist the given default data + async fn load_or_default(&self, default: T) -> Result> { + Ok(Persistable::load_or_default(self, default).await?) + } + + /// Load the data from the repository into an auto persist container. If there is no persisted data then persist the result of the callback + async fn load_or_else(&self, f: F) -> Result> + where + F: Send + FnOnce() -> Result, + { + Ok(Persistable::load_or_else(self, f).await?) + } +} + +/// A container that automatically persists it's content every time it is mutated or changed. +#[derive(Debug)] +pub struct Persistable { + data: Option, + repo: Repository, +} + +impl Persistable +where + T: PersistableData, +{ + /// Create a new container with the given option data and repository + pub fn new(data: Option, repo: &Repository) -> Self { + Self { + data, + repo: repo.clone(), + } + } + + /// Load data from the repository to the container + pub async fn load(repo: &Repository) -> Result { + let data = repo.read().await?; + + Ok(Self::new(data, repo)) + } + + /// Load the data from the repo or save and sync the given default value + pub async fn load_or_default(repo: &Repository, default: T) -> Result { + let instance = Self::new(Some(repo.read().await?.unwrap_or(default)), repo); + + Ok(instance.save()) + } + + /// Load the data from the repo or save and sync the result of the given callback + pub async fn load_or_else(repo: &Repository, f: F) -> Result + where + F: FnOnce() -> Result, + { + let data = repo + .read() + .await? + .ok_or_else(|| anyhow!("Not found")) + .or_else(|_| f())?; + + let instance = Self::new(Some(data), repo); + Ok(instance.save()) + } + + /// Save the data in the container to the database + pub fn save(self) -> Self { + self.checkpoint(); + self + } + + /// Mutate the content if it is available or return an error if either the mutator function + /// fails or if the data has not been set. + pub fn try_mutate(&mut self, mutator: F) -> Result<()> + where + F: FnOnce(T) -> Result, + { + let content = self.data.clone().ok_or(anyhow!("Data has not been set"))?; + self.data = Some(mutator(content)?); + self.checkpoint(); + Ok(()) + } + + /// Set the data on both the persistable and the repository. + pub fn set(&mut self, data: T) { + self.data = Some(data); + self.checkpoint(); + } + + /// Clear the data from both the persistable and the repository. + pub fn clear(&mut self) { + self.data = None; + self.clear_checkpoint(); + } + + /// Get the data currently stored on the container as an Option + pub fn get(&self) -> Option { + self.data.clone() + } + + /// Get the data from the container or return an error. + pub fn try_get(&self) -> Result { + self.data + .clone() + .ok_or(anyhow!("Data was not set on container.")) + } + + /// Returns true if there is data on the container and false if there is not. + pub fn has(&self) -> bool { + self.data.is_some() + } + + /// Get an immutable reference to the data on the container if the data is not set on the + /// container return an error + pub fn try_with(&self, f: F) -> Result + where + F: FnOnce(&T) -> Result, + { + match &self.data { + Some(data) => f(data), + None => Err(anyhow!("Data was not set on container.")), + } + } +} + +impl Snapshot for Persistable +where + T: PersistableData, +{ + type Snapshot = T; + fn snapshot(&self) -> Result { + Ok(self + .data + .clone() + .ok_or(anyhow!("No data stored on container"))?) + } +} + +impl Checkpoint for Persistable +where + T: PersistableData, +{ + fn repository(&self) -> &Repository { + &self.repo + } +} + +#[async_trait] +impl FromSnapshotWithParams for Persistable +where + T: PersistableData, +{ + type Params = Repository; + async fn from_snapshot(params: Repository, snapshot: T) -> Result { + Ok(Persistable::new(Some(snapshot), ¶ms)) + } +} + +#[cfg(test)] +mod tests { + use crate::{AutoPersist, DataStore, GetLog, InMemStore, Repository}; + use actix::{Actor, Addr}; + use anyhow::{anyhow, Result}; + + fn get_repo() -> (Repository, Addr) { + let addr = InMemStore::new(true).start(); + let store = DataStore::from(&addr).scope("/"); + let repo: Repository = Repository::new(store); + (repo, addr) + } + + #[actix::test] + async fn persistable_loads_with_default() -> Result<()> { + let (repo, addr) = get_repo::>(); + let container = repo + .clone() + .load_or_default(vec!["berlin".to_string()]) + .await?; + + assert_eq!(addr.send(GetLog).await?.len(), 1); + assert_eq!(repo.read().await?, Some(vec!["berlin".to_string()])); + assert_eq!(container.get(), Some(vec!["berlin".to_string()])); + Ok(()) + } + + #[actix::test] + async fn persistable_loads_with_default_override() -> Result<()> { + let (repo, _) = get_repo::>(); + repo.write(&vec!["berlin".to_string()]); + let container = repo + .clone() + .load_or_default(vec!["amsterdam".to_string()]) + .await?; + + assert_eq!(repo.read().await?, Some(vec!["berlin".to_string()])); + assert_eq!(container.get(), Some(vec!["berlin".to_string()])); + Ok(()) + } + + #[actix::test] + async fn persistable_load() -> Result<()> { + let (repo, _) = get_repo::>(); + repo.write(&vec!["berlin".to_string()]); + let container = repo.clone().load().await?; + + assert_eq!(repo.read().await?, Some(vec!["berlin".to_string()])); + assert_eq!(container.get(), Some(vec!["berlin".to_string()])); + Ok(()) + } + + #[actix::test] + async fn persistable_send() -> Result<()> { + let (repo, _) = get_repo::>(); + repo.write(&vec!["amsterdam".to_string()]); + let container = repo.clone().send(Some(vec!["berlin".to_string()])); + + assert_eq!(repo.read().await?, Some(vec!["berlin".to_string()])); + assert_eq!(container.get(), Some(vec!["berlin".to_string()])); + Ok(()) + } + + #[actix::test] + async fn persistable_mutate() -> Result<()> { + let (repo, addr) = get_repo::>(); + + let mut container = repo.clone().send(Some(vec!["berlin".to_string()])); + + container.try_mutate(|mut list| { + list.push(String::from("amsterdam")); + Ok(list) + })?; + + assert_eq!( + repo.read().await?, + Some(vec!["berlin".to_string(), "amsterdam".to_string()]) + ); + + assert_eq!(addr.send(GetLog).await?.len(), 2); + + Ok(()) + } + + #[actix::test] + async fn test_clear_persistable() -> Result<()> { + let (repo, _) = get_repo::>(); + let repo_ref = &repo; + let mut container = repo_ref.send(Some(vec!["berlin".to_string()])); + + assert!(container.has()); + container.clear(); + assert!(!container.has()); + assert_eq!(repo_ref.read().await?, None); + Ok(()) + } + + #[actix::test] + async fn test_set_persistable() -> Result<()> { + let (repo, _) = get_repo::>(); + let mut container = repo.clone().send(None); + + container.set(vec!["amsterdam".to_string()]); + + assert!(container.has()); + assert_eq!(repo.read().await?, Some(vec!["amsterdam".to_string()])); + Ok(()) + } + + #[actix::test] + async fn test_try_get_with_data() -> Result<()> { + let (repo, _) = get_repo::>(); + let container = repo.clone().send(Some(vec!["berlin".to_string()])); + + let result = container.try_get()?; + assert_eq!(result, vec!["berlin".to_string()]); + Ok(()) + } + + #[actix::test] + async fn test_try_get_without_data() { + let (repo, _) = get_repo::>(); + let container = repo.clone().send(None); + + assert!(container.try_get().is_err()); + } + + #[actix::test] + async fn test_try_with_success() -> Result<()> { + let (repo, _) = get_repo::>(); + let container = repo.clone().send(Some(vec!["berlin".to_string()])); + + let length = container.try_with(|data| Ok(data.len()))?; + assert_eq!(length, 1); + Ok(()) + } + + #[actix::test] + async fn test_try_with_failure() { + let (repo, _) = get_repo::>(); + let container = repo.clone().send(None); + + let result = container.try_with(|data| Ok(data.len())); + assert!(result.is_err()); + } + + #[actix::test] + async fn test_try_mutate_failure() { + let (repo, _) = get_repo::>(); + let mut container = repo.clone().send(None); + + let result = container.try_mutate(|mut list| { + list.push(String::from("amsterdam")); + Ok(list) + }); + assert!(result.is_err()); + } + + #[actix::test] + async fn test_mutate_with_error() -> Result<()> { + let (repo, _) = get_repo::>(); + let mut container = repo.clone().send(Some(vec!["berlin".to_string()])); + + let result = + container.try_mutate(|_| -> Result> { Err(anyhow!("Mutation failed")) }); + + assert!(result.is_err()); + // Original data should remain unchanged + assert_eq!(container.try_get()?, vec!["berlin".to_string()]); + Ok(()) + } + + #[actix::test] + async fn test_load_or_else_success_with_empty_repo() -> Result<()> { + let (repo, _) = get_repo::>(); + + let container = repo + .clone() + .load_or_else(|| Ok(vec!["amsterdam".to_string()])) + .await?; + + assert_eq!(container.try_get()?, vec!["amsterdam".to_string()]); + assert_eq!(repo.read().await?, Some(vec!["amsterdam".to_string()])); + Ok(()) + } + + #[actix::test] + async fn test_load_or_else_skips_callback_when_data_exists() -> Result<()> { + let (repo, _) = get_repo::>(); + repo.write(&vec!["berlin".to_string()]); + + let container = repo + .clone() + .load_or_else(|| { + panic!("This callback should not be called!"); + #[allow(unreachable_code)] + Ok(vec!["amsterdam".to_string()]) + }) + .await?; + + assert_eq!(container.try_get()?, vec!["berlin".to_string()]); + Ok(()) + } + + #[actix::test] + async fn test_load_or_else_propagates_callback_error() -> Result<()> { + let (repo, _) = get_repo::>(); + + let result = repo + .clone() + .load_or_else(|| Err(anyhow!("Failed to create default data"))) + .await; + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to create default data")); + assert_eq!(repo.read().await?, None); + Ok(()) + } + + #[actix::test] + async fn test_load_or_else_custom_error_message() -> Result<()> { + let (repo, _) = get_repo::>(); + let error_msg = "Custom initialization error"; + + let result = repo.load_or_else(|| Err(anyhow!(error_msg))).await; + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains(error_msg)); + Ok(()) + } + +} diff --git a/packages/ciphernode/data/src/repository.rs b/packages/ciphernode/data/src/repository.rs index 4e04fd00..f3d94963 100644 --- a/packages/ciphernode/data/src/repository.rs +++ b/packages/ciphernode/data/src/repository.rs @@ -6,7 +6,8 @@ use crate::DataStore; #[derive(Debug)] pub struct Repository { - store: DataStore, + /// store is currently set to be a scopeable key value store + store: DataStore, // this could change and be abstracted if need be _p: PhantomData, } @@ -56,11 +57,15 @@ where self.store.read().await } + pub async fn has(&self) -> bool { + self.read().await.ok().flatten().is_some() + } + pub fn write(&self, value: &T) { - self.store.write(value); + self.store.write(value) } pub fn clear(&self) { - self.store.clear(); + self.store.write::>(None) } } diff --git a/packages/ciphernode/data/src/repository_factory.rs b/packages/ciphernode/data/src/repository_factory.rs new file mode 100644 index 00000000..be9aad25 --- /dev/null +++ b/packages/ciphernode/data/src/repository_factory.rs @@ -0,0 +1,48 @@ +use crate::{DataStore, Repository}; + +pub struct Repositories { + pub store: DataStore, +} + +impl From for Repositories { + fn from(value: DataStore) -> Self { + Repositories { store: value } + } +} +impl From<&DataStore> for Repositories { + fn from(value: &DataStore) -> Self { + Repositories { + store: value.clone(), + } + } +} + +impl Repositories { + pub fn new(store: DataStore) -> Self { + Repositories { store } + } +} + +impl From> for Repositories { + fn from(value: Repository) -> Self { + let store: DataStore = value.into(); + store.into() + } +} + +pub trait RepositoriesFactory { + fn repositories(&self) -> Repositories; +} + +impl RepositoriesFactory for DataStore { + fn repositories(&self) -> Repositories { + self.into() + } +} + +impl RepositoriesFactory for Repository { + fn repositories(&self) -> Repositories { + let store: DataStore = self.into(); + store.repositories() + } +} diff --git a/packages/ciphernode/data/src/snapshot.rs b/packages/ciphernode/data/src/snapshot.rs index a0203f45..2bf91d9e 100644 --- a/packages/ciphernode/data/src/snapshot.rs +++ b/packages/ciphernode/data/src/snapshot.rs @@ -2,6 +2,7 @@ use crate::Repository; use anyhow::Result; use async_trait::async_trait; use serde::{de::DeserializeOwned, Serialize}; +use tracing::error; /// This trait enables the self type to report their state snapshot pub trait Snapshot @@ -14,7 +15,7 @@ where type Snapshot: Serialize + DeserializeOwned; /// Return the Snapshot object for the implementor - fn snapshot(&self) -> Self::Snapshot; + fn snapshot(&self) -> Result; } /// This trait enables the self type to checkpoint its state @@ -24,7 +25,19 @@ pub trait Checkpoint: Snapshot { /// Write the current snapshot to the `Repository` provided by `repository()` fn checkpoint(&self) { - self.repository().write(&self.snapshot()); + let snapshot = match self.snapshot() { + Ok(v) => v, + Err(err) => { + error!("Not saving data because '{}'", err); + return; + } + }; + + self.repository().write(&snapshot); + } + + fn clear_checkpoint(&self) { + self.repository().clear() } } diff --git a/packages/ciphernode/evm/src/event_reader.rs b/packages/ciphernode/evm/src/event_reader.rs index 4981c4f6..397ad902 100644 --- a/packages/ciphernode/evm/src/event_reader.rs +++ b/packages/ciphernode/evm/src/event_reader.rs @@ -313,8 +313,8 @@ where T: Transport + Clone + Unpin, { type Snapshot = EvmEventReaderState; - fn snapshot(&self) -> Self::Snapshot { - self.state.clone() + fn snapshot(&self) -> Result { + Ok(self.state.clone()) } } diff --git a/packages/ciphernode/fhe/src/fhe.rs b/packages/ciphernode/fhe/src/fhe.rs index f5131aec..84333de4 100644 --- a/packages/ciphernode/fhe/src/fhe.rs +++ b/packages/ciphernode/fhe/src/fhe.rs @@ -127,11 +127,11 @@ impl Fhe { impl Snapshot for Fhe { type Snapshot = FheSnapshot; - fn snapshot(&self) -> Self::Snapshot { - FheSnapshot { + fn snapshot(&self) -> Result { + Ok(FheSnapshot { crp: self.crp.to_bytes(), params: self.params.to_bytes(), - } + }) } } diff --git a/packages/ciphernode/keyshare/src/keyshare.rs b/packages/ciphernode/keyshare/src/keyshare.rs index a2e93c0e..8cd18114 100644 --- a/packages/ciphernode/keyshare/src/keyshare.rs +++ b/packages/ciphernode/keyshare/src/keyshare.rs @@ -77,10 +77,10 @@ impl Keyshare { impl Snapshot for Keyshare { type Snapshot = KeyshareState; - fn snapshot(&self) -> Self::Snapshot { - KeyshareState { + fn snapshot(&self) -> Result { + Ok(KeyshareState { secret: self.secret.clone(), - } + }) } } diff --git a/packages/ciphernode/router/src/context.rs b/packages/ciphernode/router/src/context.rs index 506e106c..f869844c 100644 --- a/packages/ciphernode/router/src/context.rs +++ b/packages/ciphernode/router/src/context.rs @@ -151,15 +151,15 @@ impl RepositoriesFactory for E3RequestContext { impl Snapshot for E3RequestContext { type Snapshot = E3RequestContextSnapshot; - fn snapshot(&self) -> Self::Snapshot { - Self::Snapshot { + fn snapshot(&self) -> Result { + Ok(Self::Snapshot { e3_id: self.e3_id.clone(), meta: self.meta.is_some(), fhe: self.fhe.is_some(), publickey: self.publickey.is_some(), plaintext: self.plaintext.is_some(), keyshare: self.keyshare.is_some(), - } + }) } } diff --git a/packages/ciphernode/router/src/e3_request_router.rs b/packages/ciphernode/router/src/e3_request_router.rs index 0890d4e0..ba2eb39c 100644 --- a/packages/ciphernode/router/src/e3_request_router.rs +++ b/packages/ciphernode/router/src/e3_request_router.rs @@ -178,14 +178,14 @@ pub struct E3RequestRouterSnapshot { impl Snapshot for E3RequestRouter { type Snapshot = E3RequestRouterSnapshot; - fn snapshot(&self) -> Self::Snapshot { + fn snapshot(&self) -> Result { let contexts = self.contexts.keys().cloned().collect(); let completed = self.completed.clone(); - Self::Snapshot { + Ok(Self::Snapshot { completed, contexts, - } + }) } } diff --git a/packages/ciphernode/router/src/hooks.rs b/packages/ciphernode/router/src/hooks.rs index c2969214..f0388937 100644 --- a/packages/ciphernode/router/src/hooks.rs +++ b/packages/ciphernode/router/src/hooks.rs @@ -57,7 +57,14 @@ impl E3Feature for FheFeature { let fhe = Arc::new(fhe_inner); // FHE doesn't implement Checkpoint so we are going to store it manually - ctx.repositories().fhe(&e3_id).write(&fhe.snapshot()); + let Ok(snapshot) = fhe.snapshot() else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!("Failed to get snapshot"), + ); + return; + }; + ctx.repositories().fhe(&e3_id).write(&snapshot); let _ = ctx.set_fhe(fhe); } diff --git a/packages/ciphernode/sortition/src/sortition.rs b/packages/ciphernode/sortition/src/sortition.rs index a41c9fa9..85f565ec 100644 --- a/packages/ciphernode/sortition/src/sortition.rs +++ b/packages/ciphernode/sortition/src/sortition.rs @@ -144,8 +144,8 @@ impl Actor for Sortition { impl Snapshot for Sortition { type Snapshot = SortitionModule; - fn snapshot(&self) -> Self::Snapshot { - self.list.clone() + fn snapshot(&self) -> Result { + Ok(self.list.clone()) } }