diff --git a/Cargo.lock b/Cargo.lock index 4b3dda3c..34f41e03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3947,7 +3947,6 @@ name = "kolme" version = "0.1.0" dependencies = [ "absurd-future", - "anyhow", "axum 0.8.4", "base64 0.22.1", "borsh 1.5.7", @@ -4047,7 +4046,6 @@ dependencies = [ name = "kolme-store" version = "0.1.0" dependencies = [ - "anyhow", "enum_dispatch", "fjall", "merkle-map", @@ -4056,6 +4054,7 @@ dependencies = [ "serde_json", "smallvec", "sqlx", + "strum 0.27.1", "thiserror 2.0.12", "tokio", "tracing", diff --git a/packages/examples/cosmos-bridge/src/lib.rs b/packages/examples/cosmos-bridge/src/lib.rs index 64245467..9f34aa24 100644 --- a/packages/examples/cosmos-bridge/src/lib.rs +++ b/packages/examples/cosmos-bridge/src/lib.rs @@ -144,7 +144,7 @@ impl KolmeApp for CosmosBridgeApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(State { hi_count: 0 }) } @@ -152,14 +152,14 @@ impl KolmeApp for CosmosBridgeApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { BridgeMessage::SayHi {} => ctx.state_mut().hi_count += 1, BridgeMessage::SendTo { address, amount } => { let chain = match address.get_address_hrp().as_str() { "osmo" => ExternalChain::OsmosisTestnet, "neutron" => ExternalChain::NeutronTestnet, - _ => anyhow::bail!("Unsupported wallet address: {address}"), + _ => return Err(KolmeError::other("Unsupported wallet address")), }; ctx.withdraw_asset( AssetId(1), @@ -184,11 +184,11 @@ struct RandomU32; impl KolmeDataRequest for RandomU32 { type Response = u32; - async fn load(self, _: &App) -> Result { + async fn load(self, _: &App) -> Result { Ok(rand::random()) } - async fn validate(self, _: &App, _: &Self::Response) -> Result<()> { + async fn validate(self, _: &App, _: &Self::Response) -> Result<(), KolmeDataError> { // No validation possible Ok(()) } @@ -219,7 +219,7 @@ pub async fn serve(kolme: Kolme, bind: SocketAddr) -> Result<() } Ok(Err(e)) => { set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } diff --git a/packages/examples/kademlia-discovery/src/lib.rs b/packages/examples/kademlia-discovery/src/lib.rs index 4ed25f47..3dab84c9 100644 --- a/packages/examples/kademlia-discovery/src/lib.rs +++ b/packages/examples/kademlia-discovery/src/lib.rs @@ -106,7 +106,7 @@ impl KolmeApp for KademliaTestApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(State { hi_count: 0 }) } @@ -114,7 +114,7 @@ impl KolmeApp for KademliaTestApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { KademliaTestMessage::SayHi {} => ctx.state_mut().hi_count += 1, } @@ -128,11 +128,11 @@ struct RandomU32; impl KolmeDataRequest for RandomU32 { type Response = u32; - async fn load(self, _: &App) -> Result { + async fn load(self, _: &App) -> Result { Ok(rand::random()) } - async fn validate(self, _: &App, _: &Self::Response) -> Result<()> { + async fn validate(self, _: &App, _: &Self::Response) -> Result<(), KolmeDataError> { // No validation possible Ok(()) } @@ -247,7 +247,7 @@ pub async fn new_version_node(api_server_port: u16) -> Result<()> { } Ok(Err(e)) => { set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } @@ -315,7 +315,7 @@ pub async fn validators( } Ok(Err(e)) => { set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } diff --git a/packages/examples/p2p/src/lib.rs b/packages/examples/p2p/src/lib.rs index 456a9bce..79d28d43 100644 --- a/packages/examples/p2p/src/lib.rs +++ b/packages/examples/p2p/src/lib.rs @@ -84,7 +84,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(SampleState { hi_count: 0 }) } @@ -92,7 +92,7 @@ impl KolmeApp for SampleKolmeApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { SampleMessage::SayHi {} => ctx.state_mut().hi_count += 1, } @@ -150,7 +150,7 @@ pub async fn api_server(bind: SocketAddr) -> Result<()> { } Ok(Err(e)) => { set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } diff --git a/packages/examples/six-sigma/src/lib.rs b/packages/examples/six-sigma/src/lib.rs index 2c8c3415..2823cba2 100644 --- a/packages/examples/six-sigma/src/lib.rs +++ b/packages/examples/six-sigma/src/lib.rs @@ -199,7 +199,7 @@ impl KolmeApp for SixSigmaApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(State::new([admin_secret_key().public_key()])) } @@ -207,19 +207,27 @@ impl KolmeApp for SixSigmaApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { AppMessage::SendFunds { asset_id, amount } => { - ctx.state_mut().send_funds(*asset_id, *amount) + ctx.state_mut() + .send_funds(*asset_id, *amount) + .map_err(KolmeError::other)?; + Ok(()) } AppMessage::Init => { let signing_key = ctx.get_signing_key(); - ctx.state_mut().initialize(signing_key) + ctx.state_mut() + .initialize(signing_key) + .map_err(KolmeError::other)?; + Ok(()) } AppMessage::AddMarket { id, asset_id, name } => { let signing_key = ctx.get_signing_key(); ctx.state_mut() .add_market(signing_key, *id, *asset_id, name) + .map_err(KolmeError::other)?; + Ok(()) } AppMessage::PlaceBet { wallet, @@ -232,22 +240,26 @@ impl KolmeApp for SixSigmaApp { .get_account_balances(&sender) .map_or(Default::default(), Clone::clone); let odds = ctx.load_data(OddsSource).await?; - let change = ctx.state_mut().place_bet( - sender, - balances, - *market_id, - Wallet(wallet.clone()), - *amount, - *outcome, - odds, - )?; + let change = ctx + .state_mut() + .place_bet( + sender, + balances, + *market_id, + Wallet(wallet.clone()), + *amount, + *outcome, + odds, + ) + .map_err(KolmeError::other)?; change_balance(ctx, &change) } AppMessage::SettleMarket { market_id, outcome } => { let signing_key = ctx.get_signing_key(); - let balance_changes = - ctx.state_mut() - .settle_market(signing_key, *market_id, *outcome)?; + let balance_changes = ctx + .state_mut() + .settle_market(signing_key, *market_id, *outcome) + .map_err(KolmeError::other)?; for change in balance_changes { change_balance(ctx, &change)? } @@ -269,7 +281,7 @@ impl fmt::Debug for SixSigmaApp { fn change_balance( ctx: &mut ExecutionContext<'_, SixSigmaApp>, change: &BalanceChange, -) -> Result<()> { +) -> Result<(), KolmeError> { match change { BalanceChange::Mint { asset_id, @@ -310,18 +322,18 @@ struct OddsSource; impl KolmeDataRequest for OddsSource { type Response = Odds; - async fn load(self, _: &App) -> Result { + async fn load(self, _: &App) -> Result { Ok([dec!(1.8), dec!(2.5), dec!(6.5)]) } - async fn validate(self, _: &App, _: &Self::Response) -> Result<()> { + async fn validate(self, _: &App, _: &Self::Response) -> Result<(), KolmeDataError> { // No validation possible Ok(()) } } pub struct Tasks { - pub set: JoinSet>, + pub set: JoinSet>, pub processor: Option, pub listener: Option, pub approver: Option, @@ -358,7 +370,8 @@ impl Tasks { pub fn spawn_api_server(&mut self) { let api_server = ApiServer::new(self.kolme.clone()); - self.api_server = Some(self.set.spawn(api_server.run(self.bind))); + let bind = self.bind; + self.api_server = Some(self.set.spawn(api_server.run(bind))); } } @@ -386,7 +399,7 @@ pub async fn serve( } Ok(Err(e)) => { tasks.set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } @@ -478,7 +491,7 @@ impl TxLogger { Self { kolme, path } } - async fn run(self) -> Result<()> { + async fn run(self) -> Result<(), KolmeError> { let mut file = File::create(self.path)?; let mut height = BlockHeight::start(); loop { diff --git a/packages/examples/solana-cosmos-bridge/src/lib.rs b/packages/examples/solana-cosmos-bridge/src/lib.rs index 855657d9..1eb57086 100644 --- a/packages/examples/solana-cosmos-bridge/src/lib.rs +++ b/packages/examples/solana-cosmos-bridge/src/lib.rs @@ -131,7 +131,7 @@ impl KolmeApp for SolanaCosmosBridgeApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(State) } @@ -139,7 +139,7 @@ impl KolmeApp for SolanaCosmosBridgeApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { BridgeMessage::ToSolana { to, amount } => { ctx.withdraw_asset( @@ -202,7 +202,7 @@ pub async fn serve( } Ok(Err(e)) => { set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } diff --git a/packages/integration-tests/tests/balances.rs b/packages/integration-tests/tests/balances.rs index c4b0948e..077259c9 100644 --- a/packages/integration-tests/tests/balances.rs +++ b/packages/integration-tests/tests/balances.rs @@ -91,7 +91,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -99,7 +99,7 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { Ok(()) } } diff --git a/packages/integration-tests/tests/cosmos-migration.rs b/packages/integration-tests/tests/cosmos-migration.rs index 4446703c..9091ea71 100644 --- a/packages/integration-tests/tests/cosmos-migration.rs +++ b/packages/integration-tests/tests/cosmos-migration.rs @@ -79,7 +79,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -87,7 +87,7 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { Ok(()) } } diff --git a/packages/integration-tests/tests/key-rotation.rs b/packages/integration-tests/tests/key-rotation.rs index 79e2d892..8ce5c3d7 100644 --- a/packages/integration-tests/tests/key-rotation.rs +++ b/packages/integration-tests/tests/key-rotation.rs @@ -137,7 +137,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -145,7 +145,7 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { Ok(()) } } diff --git a/packages/integration-tests/tests/solana-migration.rs b/packages/integration-tests/tests/solana-migration.rs index 08366712..3b73fc64 100644 --- a/packages/integration-tests/tests/solana-migration.rs +++ b/packages/integration-tests/tests/solana-migration.rs @@ -80,7 +80,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -88,7 +88,7 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { Ok(()) } } diff --git a/packages/integration-tests/tests/testapp.rs b/packages/integration-tests/tests/testapp.rs index e224b8c3..bae82f24 100644 --- a/packages/integration-tests/tests/testapp.rs +++ b/packages/integration-tests/tests/testapp.rs @@ -1,12 +1,11 @@ use anyhow::{Context, Result}; use futures_util::future::join_all; use futures_util::StreamExt; -use kolme::ApiNotification; use kolme::{ - testtasks::TestTasks, AccountNonce, ApiServer, AssetId, BankMessage, BlockHeight, - ExecutionContext, GenesisInfo, Kolme, KolmeApp, KolmeStore, MerkleDeserialize, - MerkleDeserializer, MerkleSerialError, MerkleSerialize, MerkleSerializer, Message, Processor, - Transaction, ValidatorSet, + testtasks::TestTasks, AccountNonce, ApiNotification, ApiServer, AssetId, BankMessage, + BlockHeight, ExecutionContext, GenesisInfo, Kolme, KolmeApp, KolmeError, KolmeStore, + MerkleDeserialize, MerkleDeserializer, MerkleSerialError, MerkleSerialize, MerkleSerializer, + Message, Processor, Transaction, ValidatorSet, }; use rust_decimal::dec; use serde::{Deserialize, Serialize}; @@ -89,7 +88,7 @@ impl KolmeApp for TestApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(TestState { counter: 0 }) } @@ -97,7 +96,7 @@ impl KolmeApp for TestApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { TestMessage::Increment => { ctx.state_mut().counter += 1; diff --git a/packages/integration-tests/tests/validate_from.rs b/packages/integration-tests/tests/validate_from.rs index bf1cafbb..74117fcc 100644 --- a/packages/integration-tests/tests/validate_from.rs +++ b/packages/integration-tests/tests/validate_from.rs @@ -91,7 +91,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -99,7 +99,7 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { Ok(()) } } diff --git a/packages/kolme-store/Cargo.toml b/packages/kolme-store/Cargo.toml index 1f738cea..a9a6a911 100644 --- a/packages/kolme-store/Cargo.toml +++ b/packages/kolme-store/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow.workspace = true enum_dispatch = { workspace = true } fjall.workspace = true merkle-map.workspace = true @@ -18,6 +17,7 @@ sqlx = { workspace = true, features = [ "runtime-tokio", "tls-rustls", ] } +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio.workspace = true tracing = { workspace = true } diff --git a/packages/kolme-store/src/error.rs b/packages/kolme-store/src/error.rs index 95bbb18e..a9bc578f 100644 --- a/packages/kolme-store/src/error.rs +++ b/packages/kolme-store/src/error.rs @@ -1,15 +1,26 @@ use merkle_map::{MerkleSerialError, Sha256Hash}; +#[derive(Debug, Clone, Copy, strum::Display)] +pub enum StorageBackend { + Fjall, + Postgres, + InMemory, +} + #[derive(thiserror::Error, Debug)] pub enum KolmeStoreError { - #[error(transparent)] - Custom(Box), + #[error("Custom error: {0}")] + Custom(String), + #[error(transparent)] Merkle(#[from] MerkleSerialError), + #[error("Block not found in storage: {height}")] BlockNotFound { height: u64 }, - #[error("KolmeStore::delete_block is not supported by this store: {0}")] - UnsupportedDeleteOperation(&'static str), + + #[error("KolmeStore::delete_block is not supported by this store: {backend}")] + UnsupportedDeleteOperation { backend: StorageBackend }, + // kolme#144 - Reports a diverging hash with same height #[error("Block with height {height} in database with different hash {existing}, trying to add {adding}")] ConflictingBlockInDb { @@ -20,14 +31,30 @@ pub enum KolmeStoreError { // kolme#144 - Reports a double insert (Block already exists with same hash and insert) #[error("Block already in database: {height}")] MatchingBlockAlreadyInserted { height: u64 }, + #[error("Transaction is already present in database: {txhash}")] TxAlreadyInDb { txhash: Sha256Hash }, - #[error("{0}")] - Other(String), -} + #[error("get_height_for_tx: invalid height bytes in {backend} store for tx {txhash:?}, bytes: {bytes:?}, reason: {reason}")] + InvalidHeight { + backend: StorageBackend, + txhash: Sha256Hash, + bytes: Vec, + #[source] + reason: std::array::TryFromSliceError, + }, + + #[error("Merkle validation error: child hash {child} not found")] + MissingMerkleChild { child: Sha256Hash }, + + #[error("Invalid genesis message count in first block")] + InvalidGenesisMessageCount, + + #[error("Invalid message type in first block: expected Genesis")] + InvalidFirstBlockMessageType, +} impl KolmeStoreError { - pub fn custom(e: E) -> Self { - Self::Custom(Box::new(e)) + pub fn custom(e: E) -> Self { + Self::Custom(e.to_string()) } } diff --git a/packages/kolme-store/src/fjall.rs b/packages/kolme-store/src/fjall.rs index 1d30161e..6d499e30 100644 --- a/packages/kolme-store/src/fjall.rs +++ b/packages/kolme-store/src/fjall.rs @@ -1,8 +1,7 @@ use crate::{ - r#trait::KolmeBackingStore, KolmeConstructLock, KolmeStoreError, RemoteDataListener, - StorableBlock, + error::StorageBackend, r#trait::KolmeBackingStore, KolmeConstructLock, KolmeStoreError, + RemoteDataListener, StorableBlock, }; -use anyhow::Context; use merkle_map::{MerkleDeserializeRaw, MerkleSerializeRaw, MerkleStore as _, Sha256Hash}; use std::path::Path; @@ -16,7 +15,7 @@ pub struct Store { } impl Store { - pub fn new(fjall_dir: impl AsRef) -> anyhow::Result { + pub fn new(fjall_dir: impl AsRef) -> Result { let merkle = merkle::MerkleFjallStore::new(fjall_dir)?; Ok(Self { merkle }) @@ -41,7 +40,9 @@ impl KolmeBackingStore for Store { } async fn delete_block(&self, _height: u64) -> Result<(), KolmeStoreError> { - Err(KolmeStoreError::UnsupportedDeleteOperation("Fjall")) + Err(KolmeStoreError::UnsupportedDeleteOperation { + backend: StorageBackend::Fjall, + }) } async fn take_construct_lock(&self) -> Result { @@ -55,13 +56,25 @@ impl KolmeBackingStore for Store { self.merkle.clone().load_by_hash(hash) } - async fn get_height_for_tx(&self, txhash: Sha256Hash) -> anyhow::Result> { - let Some(height) = self.merkle.handle.get(tx_key(txhash))? else { + async fn get_height_for_tx(&self, txhash: Sha256Hash) -> Result, KolmeStoreError> { + let Some(height) = self + .merkle + .handle + .get(tx_key(txhash)) + .map_err(KolmeStoreError::custom)? + else { return Ok(None); }; let height = match <[u8; 8]>::try_from(&*height) { Ok(height) => u64::from_be_bytes(height), - Err(e) => anyhow::bail!("get_height_for_tx: invalid height in Fjall store: {e}"), + Err(e) => { + return Err(KolmeStoreError::InvalidHeight { + backend: StorageBackend::Fjall, + txhash, + bytes: height.to_vec(), + reason: e, + }); + } }; Ok(Some(height)) } @@ -73,7 +86,7 @@ impl KolmeBackingStore for Store { let (key, _hash_bytes) = latest.map_err(KolmeStoreError::custom)?; let key = (*key) .strip_prefix(b"block:") - .ok_or_else(|| KolmeStoreError::Other("Fjall key missing block: prefix".to_owned()))?; + .ok_or_else(|| KolmeStoreError::custom("Fjall key missing block: prefix"))?; let height = <[u8; 8]>::try_from(key).map_err(KolmeStoreError::custom)?; Ok(Some(u64::from_be_bytes(height))) } @@ -166,13 +179,13 @@ impl KolmeBackingStore for Store { async fn add_merkle_layer( &self, layer: &merkle_map::MerkleLayerContents, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeStoreError> { let mut merkle = self.merkle.clone(); merkle.save_by_hash(layer).await?; Ok(()) } - async fn save(&self, value: &T) -> anyhow::Result + async fn save(&self, value: &T) -> Result where T: merkle_map::MerkleSerializeRaw, { @@ -189,21 +202,25 @@ impl KolmeBackingStore for Store { merkle_map::load(&mut store, hash).await } - async fn archive_block(&self, height: u64) -> anyhow::Result<()> { + async fn archive_block(&self, height: u64) -> Result<(), KolmeStoreError> { self.merkle .handle .insert(LATEST_ARCHIVED_HEIGHT_KEY, height.to_be_bytes()) - .context("Unable to update partition with given height")?; + .map_err(|e| { + KolmeStoreError::custom(format!( + "Unable to update partition with given height: {e}" + )) + })?; Ok(()) } - async fn get_latest_archived_block_height(&self) -> anyhow::Result> { + async fn get_latest_archived_block_height(&self) -> Result, KolmeStoreError> { Ok(self .merkle .handle .get(LATEST_ARCHIVED_HEIGHT_KEY) - .context("Unable to retrieve latest height")? + .map_err(|e| KolmeStoreError::Custom(format!("Unable to retrieve latest height: {e}")))? .map(|contents| u64::from_be_bytes(std::array::from_fn(|i| contents[i])))) } diff --git a/packages/kolme-store/src/fjall/merkle.rs b/packages/kolme-store/src/fjall/merkle.rs index b2f24d1d..c64bb991 100644 --- a/packages/kolme-store/src/fjall/merkle.rs +++ b/packages/kolme-store/src/fjall/merkle.rs @@ -116,10 +116,9 @@ fn render_children(children: &[Sha256Hash]) -> Vec { fn parse_children(children: &[u8]) -> Result, MerkleSerialError> { if children.len() % 32 != 0 { - return Err(MerkleSerialError::custom(std::io::Error::new( - std::io::ErrorKind::Other, - "Children in fjall store not a multiple of 32 bytes", - ))); + return Err(MerkleSerialError::InvalidChildrenLength { + len: children.len(), + }); } let count = children.len() / 32; let mut v = SmallVec::with_capacity(count); diff --git a/packages/kolme-store/src/in_memory.rs b/packages/kolme-store/src/in_memory.rs index 1411bbc3..778cfdb7 100644 --- a/packages/kolme-store/src/in_memory.rs +++ b/packages/kolme-store/src/in_memory.rs @@ -81,7 +81,7 @@ impl KolmeBackingStore for Store { Ok(self.0.read().await.blocks.contains_key(&height)) } - async fn get_height_for_tx(&self, txhash: Sha256Hash) -> Result, anyhow::Error> { + async fn get_height_for_tx(&self, txhash: Sha256Hash) -> Result, KolmeStoreError> { Ok(self.0.read().await.txhashes.get(&txhash).copied()) } @@ -145,13 +145,13 @@ impl KolmeBackingStore for Store { async fn add_merkle_layer( &self, layer: &merkle_map::MerkleLayerContents, - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeStoreError> { let mut merkle = self.get_merkle_store().await; merkle.save_by_hash(layer).await?; Ok(()) } - async fn save(&self, value: &T) -> anyhow::Result + async fn save(&self, value: &T) -> Result where T: merkle_map::MerkleSerializeRaw, { @@ -167,12 +167,12 @@ impl KolmeBackingStore for Store { merkle_map::load(&mut merkle, hash).await } - async fn archive_block(&self, height: u64) -> anyhow::Result<()> { + async fn archive_block(&self, height: u64) -> Result<(), KolmeStoreError> { self.0.write().await.latest_archived_block = Some(height); Ok(()) } - async fn get_latest_archived_block_height(&self) -> anyhow::Result> { + async fn get_latest_archived_block_height(&self) -> Result, KolmeStoreError> { Ok(self.0.read().await.latest_archived_block) } diff --git a/packages/kolme-store/src/lib.rs b/packages/kolme-store/src/lib.rs index bff2a8f0..0332e9bb 100644 --- a/packages/kolme-store/src/lib.rs +++ b/packages/kolme-store/src/lib.rs @@ -39,22 +39,28 @@ pub enum RemoteDataListener { } impl KolmeStore { - pub async fn new_postgres(url: &str) -> anyhow::Result { + pub async fn new_postgres(url: &str) -> Result { Ok(KolmeStore::KolmePostgresStore( - postgres::Store::new(url).await?, + postgres::Store::new(url) + .await + .map_err(KolmeStoreError::custom)?, )) } pub async fn new_postgres_with_options( connect: PgConnectOptions, options: PoolOptions, - ) -> anyhow::Result { + ) -> Result { Ok(KolmeStore::KolmePostgresStore( - postgres::Store::new_with_options(connect, options).await?, + postgres::Store::new_with_options(connect, options) + .await + .map_err(KolmeStoreError::custom)?, )) } - pub fn new_fjall(fjall_dir: impl AsRef) -> anyhow::Result { - Ok(KolmeStore::KolmeFjallStore(fjall::Store::new(fjall_dir)?)) + pub fn new_fjall(fjall_dir: impl AsRef) -> Result { + Ok(KolmeStore::KolmeFjallStore( + fjall::Store::new(fjall_dir).map_err(KolmeStoreError::custom)?, + )) } pub fn new_in_memory() -> Self { diff --git a/packages/kolme-store/src/postgres.rs b/packages/kolme-store/src/postgres.rs index e14e92c5..24724512 100644 --- a/packages/kolme-store/src/postgres.rs +++ b/packages/kolme-store/src/postgres.rs @@ -1,9 +1,9 @@ +use crate::error::StorageBackend; use crate::{ r#trait::{BackingRemoteDataListener, KolmeBackingStore}, BlockHashes, HasBlockHashes, KolmeConstructLock, KolmeStoreError, RemoteDataListener, StorableBlock, }; -use anyhow::Context as _; use merkle_map::{ MerkleDeserializeRaw, MerkleLayerContents, MerkleSerialError, MerkleSerializeRaw, MerkleStore as _, Sha256Hash, @@ -14,6 +14,7 @@ use sqlx::{ Executor, Postgres, }; use std::{collections::HashMap, sync::Arc}; + pub mod merkle; pub struct ConstructLock { @@ -35,26 +36,28 @@ pub struct Store { } impl Store { - pub async fn new(url: &str) -> anyhow::Result { - let connect_options = url.parse()?; + pub async fn new(url: &str) -> Result { + let connect_options = url.parse().map_err(KolmeStoreError::custom)?; Self::new_with_options(connect_options, PoolOptions::new().max_connections(5)).await } pub async fn new_with_options( connect: PgConnectOptions, options: PoolOptions, - ) -> anyhow::Result { + ) -> Result { let pool = options .connect_with(connect) .await - .context("Could not connect to the database") - .inspect_err(|err| tracing::error!("{err:?}"))?; + .inspect_err(|err| tracing::error!("{err:?}")) + .map_err(|e| { + KolmeStoreError::custom(format!("Could not connect to the database: {e}")) + })?; sqlx::migrate!() .run(&pool) .await - .context("Unable to execute migrations") - .inspect_err(|err| tracing::error!("{err:?}"))?; + .inspect_err(|err| tracing::error!("{err:?}")) + .map_err(|e| KolmeStoreError::custom(format!("Unable to execute migrations: {e}")))?; Ok(Self { pool, @@ -140,8 +143,11 @@ impl KolmeBackingStore for Store { .map_err(KolmeStoreError::custom) .inspect_err(|err| tracing::error!("{err:?}")) } + async fn delete_block(&self, _height: u64) -> Result<(), KolmeStoreError> { - Err(KolmeStoreError::UnsupportedDeleteOperation("Postgres")) + Err(KolmeStoreError::UnsupportedDeleteOperation { + backend: StorageBackend::Postgres, + }) } async fn take_construct_lock(&self) -> Result { @@ -184,14 +190,13 @@ impl KolmeBackingStore for Store { merkle.load_by_hashes(&[hash], &mut dest).await?; Ok(dest.remove(&hash)) } - async fn get_height_for_tx(&self, txhash: Sha256Hash) -> anyhow::Result> { + async fn get_height_for_tx(&self, txhash: Sha256Hash) -> Result, KolmeStoreError> { let txhash = txhash.as_array().as_slice(); let height = sqlx::query_scalar!("SELECT height FROM blocks WHERE txhash=$1 LIMIT 1", txhash) .fetch_optional(&self.pool) .await - .context("Unable to query tx height") - .inspect_err(|err| tracing::error!("{err:?}"))?; + .map_err(|e| KolmeStoreError::custom(format!("Unable to query tx height: {e}")))?; match height { None => Ok(None), Some(height) => Ok(Some(height.try_into().map_err(KolmeStoreError::custom)?)), @@ -273,7 +278,7 @@ impl KolmeBackingStore for Store { .fetch_one(&self.pool) .await .map_err(KolmeStoreError::custom)? - .ok_or(KolmeStoreError::Other( + .ok_or(KolmeStoreError::Custom( "Impossible empty result from a SELECT EXISTS query in has_block".to_owned(), )) } @@ -364,7 +369,7 @@ impl KolmeBackingStore for Store { Ok(()) } - async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> anyhow::Result<()> { + async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> Result<(), KolmeStoreError> { let mut merkle = self.new_store(); merkle.save_by_hash(layer).await?; self.consume_stores(&self.pool, [merkle]).await?; @@ -372,7 +377,7 @@ impl KolmeBackingStore for Store { Ok(()) } - async fn save(&self, value: &T) -> anyhow::Result { + async fn save(&self, value: &T) -> Result { let mut store = self.new_store(); let contents = merkle_map::save(&mut store, value).await?; self.consume_stores(&self.pool, [store]).await?; @@ -387,24 +392,24 @@ impl KolmeBackingStore for Store { merkle_map::load::(&mut store, hash).await } - async fn get_latest_archived_block_height(&self) -> anyhow::Result> { + async fn get_latest_archived_block_height(&self) -> Result, KolmeStoreError> { sqlx::query_scalar!( r#" SELECT height as "height!" FROM latest_archived_block_height "# ) .fetch_optional(&self.pool) - .await? - .map(|x| u64::try_from(x).map_err(anyhow::Error::from)) + .await + .map_err(KolmeStoreError::custom)? + .map(|x| { + u64::try_from(x) + .map_err(|e| KolmeStoreError::custom(format!("Unable to start database: {e}"))) + }) .transpose() } - async fn archive_block(&self, height: u64) -> anyhow::Result<()> { - let mut tx = self - .pool - .begin() - .await - .context("Unable to start database")?; + async fn archive_block(&self, height: u64) -> Result<(), KolmeStoreError> { + let mut tx = self.pool.begin().await.map_err(KolmeStoreError::custom)?; sqlx::query!( r#" @@ -417,7 +422,9 @@ impl KolmeBackingStore for Store { ) .execute(&mut *tx) .await - .context("Unable to store latest archived block height")?; + .map_err(|e| { + KolmeStoreError::custom(format!("Unable to store latest archived block height: {e}")) + })?; sqlx::query!( r#" @@ -426,12 +433,15 @@ impl KolmeBackingStore for Store { ) .execute(&mut *tx) .await - .context("Unable to refresh materialized view")?; - - tx.commit() - .await - .context("Unable to commit archive block height changes")?; - + .map_err(|e| { + KolmeStoreError::custom(format!("Unable to refresh materialized view: {e}")) + })?; + + tx.commit().await.map_err(|e| { + KolmeStoreError::custom(format!( + "Unable to commit archive block height changes: {e}" + )) + })?; Ok(()) } diff --git a/packages/kolme-store/src/trait.rs b/packages/kolme-store/src/trait.rs index 229302f8..ca1c7ea4 100644 --- a/packages/kolme-store/src/trait.rs +++ b/packages/kolme-store/src/trait.rs @@ -16,7 +16,7 @@ pub trait KolmeBackingStore { &self, hash: Sha256Hash, ) -> Result, MerkleSerialError>; - async fn get_height_for_tx(&self, txhash: Sha256Hash) -> anyhow::Result>; + async fn get_height_for_tx(&self, txhash: Sha256Hash) -> Result, KolmeStoreError>; async fn load_latest_block(&self) -> Result, KolmeStoreError>; async fn load_block( @@ -32,12 +32,12 @@ pub trait KolmeBackingStore { async fn add_block(&self, block: &StorableBlock) -> Result<(), KolmeStoreError> where Block: serde::Serialize + MerkleSerializeRaw + HasBlockHashes; - async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> anyhow::Result<()>; + async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> Result<(), KolmeStoreError>; - async fn archive_block(&self, height: u64) -> anyhow::Result<()>; - async fn get_latest_archived_block_height(&self) -> anyhow::Result>; + async fn archive_block(&self, height: u64) -> Result<(), KolmeStoreError>; + async fn get_latest_archived_block_height(&self) -> Result, KolmeStoreError>; - async fn save(&self, value: &T) -> anyhow::Result + async fn save(&self, value: &T) -> Result where T: MerkleSerializeRaw; async fn load(&self, hash: Sha256Hash) -> Result diff --git a/packages/kolme-test/src/auth.rs b/packages/kolme-test/src/auth.rs index 9b781d27..68e76c48 100644 --- a/packages/kolme-test/src/auth.rs +++ b/packages/kolme-test/src/auth.rs @@ -77,7 +77,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -85,8 +85,8 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { - Err(anyhow::anyhow!("execute not implemented")) + ) -> Result<(), KolmeError> { + Err(KolmeError::other("execute not implemented")) } } diff --git a/packages/kolme-test/src/kademlia.rs b/packages/kolme-test/src/kademlia.rs index 64fdb848..ffd42fbf 100644 --- a/packages/kolme-test/src/kademlia.rs +++ b/packages/kolme-test/src/kademlia.rs @@ -2,7 +2,7 @@ use std::net::TcpListener; use anyhow::Result; use kademlia_discovery::client; -use kolme::{testtasks::TestTasks, SecretKey}; +use kolme::{testtasks::TestTasks, KolmeError, SecretKey}; #[tokio::test] async fn ensure_kademlia_discovery_works() { @@ -25,7 +25,7 @@ async fn kademlia_discovery_inner(testtasks: TestTasks, (): ()) { testtasks.try_spawn(kademlia_discovery_client(port, signing_secret)); } -async fn kademlia_discovery_client(port: u16, signing_secret: SecretKey) -> Result<()> { +async fn kademlia_discovery_client(port: u16, signing_secret: SecretKey) -> Result<(), KolmeError> { client(&format!("ws://localhost:{port}"), signing_secret, false) .await .unwrap(); diff --git a/packages/kolme-test/src/kolme_app.rs b/packages/kolme-test/src/kolme_app.rs index f933bbac..7ed8ee1b 100644 --- a/packages/kolme-test/src/kolme_app.rs +++ b/packages/kolme-test/src/kolme_app.rs @@ -99,7 +99,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(SampleState { hi_count: 0 }) } @@ -107,7 +107,7 @@ impl KolmeApp for SampleKolmeApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { SampleMessage::SayHi {} => ctx.state_mut().hi_count += 1, } diff --git a/packages/kolme-test/src/max_tx_height.rs b/packages/kolme-test/src/max_tx_height.rs index 2197e0a9..adef4e87 100644 --- a/packages/kolme-test/src/max_tx_height.rs +++ b/packages/kolme-test/src/max_tx_height.rs @@ -44,15 +44,13 @@ async fn max_tx_height_inner(testtasks: TestTasks, (): ()) { let e: KolmeError = kolme .sign_propose_await_transaction(&secret, tx_builder) .await - .unwrap_err() - .downcast() - .unwrap(); + .unwrap_err(); match e { - KolmeError::PastMaxHeight { + KolmeError::Transaction(TransactionError::PastMaxHeight { txhash: _, max_height, proposed_height, - } => { + }) => { assert_eq!(latest.next(), proposed_height); assert_eq!(max, max_height); } diff --git a/packages/kolme-test/src/multiple_processors.rs b/packages/kolme-test/src/multiple_processors.rs index d27c0f82..70f20af7 100644 --- a/packages/kolme-test/src/multiple_processors.rs +++ b/packages/kolme-test/src/multiple_processors.rs @@ -134,7 +134,7 @@ async fn multiple_processors_inner( (kolmes, all_txhashes, highest_block) } -async fn check_failed_txs(kolme: Kolme) -> Result<()> { +async fn check_failed_txs(kolme: Kolme) -> Result<(), KolmeError> { let mut failed_txs = kolme.subscribe_failed_txs(); let failed = failed_txs.recv().await?; let FailedTransaction { @@ -142,16 +142,16 @@ async fn check_failed_txs(kolme: Kolme) -> Result<()> { proposed_height, error, } = failed.message.as_inner(); - Err(anyhow::anyhow!( + Err(KolmeError::Other(format!( "Error with transaction {txhash} for block {proposed_height}: {error}" - )) + ))) } async fn client( kolmes: Arc<[Kolme]>, all_txhashes: Arc>>, highest_block: Arc>, -) -> Result<()> { +) -> Result<(), KolmeError> { for _ in 0..10 { let (kolme, secret) = { let mut rng = rand::thread_rng(); diff --git a/packages/kolme-test/src/offline_simple.rs b/packages/kolme-test/src/offline_simple.rs index 23fb8f59..6cbff82e 100644 --- a/packages/kolme-test/src/offline_simple.rs +++ b/packages/kolme-test/src/offline_simple.rs @@ -102,7 +102,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> anyhow::Result { + fn new_state(&self) -> Result { Ok(SampleState {}) } @@ -110,8 +110,8 @@ impl KolmeApp for SampleKolmeApp { &self, _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, - ) -> anyhow::Result<()> { - Err(anyhow::anyhow!("execute not implemented")) + ) -> Result<(), KolmeError> { + Err(KolmeError::other("execute not implemented")) } } diff --git a/packages/kolme-test/src/p2p_large.rs b/packages/kolme-test/src/p2p_large.rs index 066a9ba3..92829185 100644 --- a/packages/kolme-test/src/p2p_large.rs +++ b/packages/kolme-test/src/p2p_large.rs @@ -85,7 +85,7 @@ impl KolmeApp for SampleKolmeApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(SampleState { next_hi: 0, payloads: MerkleMap::new(), @@ -96,7 +96,7 @@ impl KolmeApp for SampleKolmeApp { &self, ctx: &mut ExecutionContext<'_, Self>, msg: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { match msg { SampleMessage::SayHi { payload } => { let state = ctx.state_mut(); diff --git a/packages/kolme-test/src/tx_evicted_mempool.rs b/packages/kolme-test/src/tx_evicted_mempool.rs index 9d1acdf5..6b429744 100644 --- a/packages/kolme-test/src/tx_evicted_mempool.rs +++ b/packages/kolme-test/src/tx_evicted_mempool.rs @@ -56,7 +56,7 @@ async fn evicts_same_tx_mempool_inner(test_tasks: TestTasks, (): ()) { }); } -async fn repeat_client(kolme: Kolme) -> Result<()> { +async fn repeat_client(kolme: Kolme) -> Result<(), KolmeError> { let secret = SecretKey::random(); let tx = Arc::new( @@ -130,7 +130,7 @@ async fn no_op_node( kolme: Kolme, receiver: oneshot::Receiver<()>, data: Arc>>, -) -> Result<()> { +) -> Result<(), KolmeError> { receiver.await.ok(); let hashes = data.lock().unwrap().clone(); assert_eq!(hashes.len(), 5, "Five transactions expected"); @@ -160,7 +160,7 @@ async fn client( kolme: Kolme, sender: oneshot::Sender<()>, data: Arc>>, -) -> Result<()> { +) -> Result<(), KolmeError> { for _ in 0..5 { let secret = SecretKey::random(); diff --git a/packages/kolme-test/src/upgrade.rs b/packages/kolme-test/src/upgrade.rs index 668b3e41..b5b0504e 100644 --- a/packages/kolme-test/src/upgrade.rs +++ b/packages/kolme-test/src/upgrade.rs @@ -56,7 +56,7 @@ impl KolmeApp for SampleKolmeApp1 { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(SampleState { hi_count1: 0, hi_count2: 0, @@ -67,7 +67,7 @@ impl KolmeApp for SampleKolmeApp1 { &self, ctx: &mut ExecutionContext<'_, Self>, SampleMessage::SayHi: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { ctx.state_mut().hi_count1 += 1; Ok(()) } @@ -86,7 +86,7 @@ impl KolmeApp for SampleKolmeApp2 { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { SampleKolmeApp1 { genesis: self.genesis.clone(), } @@ -97,7 +97,7 @@ impl KolmeApp for SampleKolmeApp2 { &self, ctx: &mut ExecutionContext<'_, Self>, SampleMessage::SayHi: &Self::Message, - ) -> Result<()> { + ) -> Result<(), KolmeError> { // This is the only difference between the two versions! // But it results in totally different resulting app states. ctx.state_mut().hi_count2 += 1; diff --git a/packages/kolme-test/src/validations.rs b/packages/kolme-test/src/validations.rs index f74cc45a..f5ac73c6 100644 --- a/packages/kolme-test/src/validations.rs +++ b/packages/kolme-test/src/validations.rs @@ -61,7 +61,7 @@ async fn test_invalid_hashes_inner(testtasks: TestTasks, (): ()) { kolme: &Kolme, mut block: Block, f: impl FnOnce(&mut Block), - ) -> anyhow::Result<()> { + ) -> Result<(), KolmeError> { f(&mut block); let signed = TaggedJson::new(block) .unwrap() diff --git a/packages/kolme/Cargo.toml b/packages/kolme/Cargo.toml index 9605687b..cdea8171 100644 --- a/packages/kolme/Cargo.toml +++ b/packages/kolme/Cargo.toml @@ -7,7 +7,6 @@ license = "MIT" resolver = "2" [dependencies] -anyhow = { workspace = true } axum = { workspace = true, features = ["ws"] } base64 = { workspace = true } borsh = { workspace = true } diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index bdb982f6..243b90a3 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -1,6 +1,3 @@ -use std::time::Duration; - -use anyhow::{bail, Context}; use axum::{ extract::{ ws::{Message as WsMessage, WebSocket}, @@ -11,7 +8,9 @@ use axum::{ routing::{get, put}, Json, Router, }; +use kolme_store::KolmeStoreError; use reqwest::{Method, StatusCode}; +use std::time::Duration; use tower_http::cors::{Any, CorsLayer}; use version_compare::Version; @@ -19,6 +18,85 @@ use crate::*; pub use axum; +#[derive(thiserror::Error, Debug)] +pub enum KolmeApiError { + #[error("Block {0} not found")] + BlockNotFound(BlockHeight), + + #[error("Chain version {requested} not found, earliest is {earliest}")] + ChainVersionNotFound { requested: String, earliest: String }, + + #[error("Unable to compare chain versions")] + VersionComparisonFailed, + + #[error("Could not find any block with chain version {0}")] + BlockNotFoundOnChainVersion(String), + + #[error("Invalid chain version: {0}")] + InvalidChainVersion(String), + + #[error("No blocks in chain")] + NoBlocksInChain, + + #[error("Timeout waiting for block {0}")] + BlockTimeout(BlockHeight), + + #[error("Failed to load block {0}")] + BlockLoadFailed(BlockHeight), + + #[error("Could not find first block for chain version {0}")] + FirstBlockNotFound(String), + + #[error("Could not find last block for chain version {0}")] + LastBlockNotFound(String), + + #[error("Underflow in prev() operation")] + UnderflowInPrev, + + #[error("Merkle serialization error")] + MerkleSerial(#[from] MerkleSerialError), + + #[error(transparent)] + KolmeStore(#[from] KolmeStoreError), + + #[error(transparent)] + BlockHeight(#[from] BlockHeightError), + + #[error(transparent)] + RecvError(#[from] tokio::sync::watch::error::RecvError), + + #[error("Broadcast receive error")] + BroadcastRecv(#[from] tokio::sync::broadcast::error::RecvError), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Transaction already in mempool")] + TxAlreadyInMempool, + + #[error("Transaction already included in block {0}")] + TxAlreadyInBlock(BlockHeight), + + #[error("Transaction error: {0}")] + Transaction(String), +} + +impl From> for KolmeApiError { + fn from(e: ProposeTransactionError) -> Self { + match e { + ProposeTransactionError::InMempool => KolmeApiError::TxAlreadyInMempool, + + ProposeTransactionError::InBlock(block) => { + KolmeApiError::TxAlreadyInBlock(block.height()) + } + + ProposeTransactionError::Failed(failed) => { + KolmeApiError::Transaction(failed.message.as_inner().error.to_string()) + } + } + } +} + pub struct ApiServer { kolme: Kolme, extra_routes: Option, @@ -38,7 +116,7 @@ impl ApiServer { self } - pub async fn run(self, addr: A) -> Result<()> { + pub async fn run(self, addr: A) -> Result<(), KolmeError> { let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST, Method::PUT]) .allow_origin(Any) @@ -52,9 +130,7 @@ impl ApiServer { let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!("Starting API server on {:?}", listener.local_addr()?); - axum::serve(listener, app) - .await - .map_err(anyhow::Error::from) + axum::serve(listener, app).await.map_err(KolmeError::from) } } @@ -121,13 +197,14 @@ async fn broadcast( async fn broadcast_inner( kolme: Kolme, tx: SignedTransaction, -) -> Result { +) -> Result { let txhash = tx.hash(); kolme .read() .execute_transaction(&tx, Timestamp::now(), BlockDataHandling::NoPriorData) .await?; kolme.propose_transaction(Arc::new(tx))?; + Ok(txhash) } @@ -158,7 +235,7 @@ async fn get_block( async fn get_block_inner( kolme: &Kolme, height: BlockHeight, -) -> Result { +) -> Result { #[derive(serde::Serialize)] struct Response<'a, App: KolmeApp> { code_version: &'a String, @@ -208,9 +285,9 @@ struct BlockResponse { async fn block_response( kolme: &Kolme, block: BlockHeight, -) -> Result { +) -> Result { let Some(signed_block) = kolme.get_block(block).await? else { - bail!("Block {} not found", block); + return Err(KolmeApiError::BlockNotFound(block)); }; let framework_hash = signed_block.block.as_inner().framework_state; @@ -226,16 +303,16 @@ async fn block_response( async fn find_block_height( kolme: &Kolme, chain_version: &Version<'_>, -) -> Result { +) -> Result { let next_height = kolme.read().get_next_height(); let mut start_block = BlockHeight::start(); - let mut end_block = next_height.prev().context("No blocks in chain")?; + let mut end_block = next_height.prev().ok_or(KolmeApiError::NoBlocksInChain)?; while start_block.0 <= end_block.0 { let middle_block = start_block.increasing_middle(end_block)?; let response = block_response(kolme, middle_block).await?; - let existing_version = - Version::from(&response.chain_version).context("Invalid version from response")?; + let existing_version = Version::from(&response.chain_version) + .ok_or_else(|| KolmeApiError::InvalidChainVersion(response.chain_version.clone()))?; let result = chain_version.compare(existing_version); match result { version_compare::Cmp::Eq => return Ok(response), @@ -244,24 +321,27 @@ async fn find_block_height( // Search in the lower half. if middle_block.is_start() { // We are at the beginning and the version is still too high. - bail!( - "Chain version {} not found, earliest is {}", - chain_version, - response.chain_version - ); + return Err(KolmeApiError::ChainVersionNotFound { + requested: chain_version.to_string(), + earliest: response.chain_version, + }); } - end_block = middle_block.prev().context("Underflow in prev")?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } version_compare::Cmp::Gt | version_compare::Cmp::Ge => { // The version we want is newer than the one at `middle_block`. // Search in the upper half. start_block = middle_block.next(); } - version_compare::Cmp::Ne => bail!("Not able to compare version"), + version_compare::Cmp::Ne => { + return Err(KolmeApiError::VersionComparisonFailed); + } } } - bail!("Could not find a block with chain version {chain_version}"); + Err(KolmeApiError::BlockNotFoundOnChainVersion( + chain_version.to_string(), + )) } /// Find the first block height with a particular chain version @@ -269,7 +349,7 @@ async fn find_first_block( kolme: &Kolme, chain_version: &Version<'_>, mut end_block: BlockHeight, -) -> Result { +) -> Result { let mut start_block = BlockHeight::start(); let mut first_block = None; @@ -277,28 +357,30 @@ async fn find_first_block( let middle_block = start_block.increasing_middle(end_block)?; let response = block_response(kolme, middle_block).await?; - let response_chain_version = - Version::from(&response.chain_version).context("Invalid chain version")?; + let response_chain_version = Version::from(&response.chain_version).ok_or( + KolmeApiError::InvalidChainVersion(response.chain_version.to_owned()), + )?; if response_chain_version == *chain_version { first_block = Some(middle_block); if middle_block.is_start() { break; } - end_block = middle_block.prev().context("Underflow in prev")?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } else if response_chain_version.compare(chain_version) == version_compare::Cmp::Lt { start_block = middle_block.next(); } else { if middle_block.is_start() { break; } - end_block = middle_block.prev().context("Underflow in prev")?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } } - first_block.context(format!( - "Could not find first block for chain version {chain_version}" - )) + match first_block { + Some(block_height) => Ok(block_height), + None => Err(KolmeApiError::FirstBlockNotFound(chain_version.to_string())), + } } /// Find the last block height with a particular chain version @@ -306,9 +388,9 @@ async fn find_last_block( kolme: &Kolme, chain_version: &Version<'_>, mut start_block: BlockHeight, -) -> Result { +) -> Result { let next_height = kolme.read().get_next_height(); - let latest_block = next_height.prev().context("No blocks in chain")?; + let latest_block = next_height.prev().ok_or(KolmeApiError::NoBlocksInChain)?; let mut end_block = latest_block; let mut last_block = None; @@ -316,8 +398,9 @@ async fn find_last_block( let middle_block = start_block.increasing_middle(end_block)?; let response = block_response(kolme, middle_block).await?; - let response_chain_version = - Version::from(&response.chain_version).context("Invalid chain version")?; + let response_chain_version = Version::from(&response.chain_version).ok_or( + KolmeApiError::InvalidChainVersion(response.chain_version.to_owned()), + )?; if response_chain_version == *chain_version { last_block = Some(middle_block); @@ -326,15 +409,13 @@ async fn find_last_block( if middle_block.is_start() { break; } - end_block = middle_block.prev().context("Underflow in prev")?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } else { start_block = middle_block.next(); } } - last_block.context(format!( - "Could not find last block for chain version {chain_version}" - )) + last_block.ok_or_else(|| KolmeApiError::LastBlockNotFound(chain_version.to_string())) } #[derive(serde::Deserialize)] @@ -358,7 +439,7 @@ async fn fork_info( } }; - let result: Result = async { + let result: Result = async { let found_block = find_block_height(&kolme, &chain_version).await?; let first_block = find_first_block(&kolme, &chain_version, found_block.block_height).await?; @@ -411,7 +492,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke async fn get_next_latest( latest: &mut tokio::sync::watch::Receiver>>>, - ) -> Result>> { + ) -> Result>, KolmeApiError> { loop { latest.changed().await?; if let Some(latest) = latest.borrow().clone().as_ref() { @@ -432,8 +513,8 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke next_height = next_height.next(); block.map(|block| Action::Raw(RawMessage::Block(block))) } - failed = failed_txs.recv() => failed.map(|failed| Action::Raw(RawMessage::Failed(failed))).map_err(anyhow::Error::from), - latest = get_next_latest(&mut latest) => latest.map(|latest| Action::Raw(RawMessage::Latest(latest))), + failed = failed_txs.recv() => failed.map(|failed| Action::Raw(RawMessage::Failed(failed))).map_err(KolmeError::from), + latest = get_next_latest(&mut latest) => latest.map(|latest| Action::Raw(RawMessage::Latest(latest))).map_err(KolmeError::from), }; let action = match action { @@ -480,7 +561,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke async fn to_api_notification( kolme: &Kolme, raw: RawMessage, -) -> Result> { +) -> Result, KolmeApiError> { match raw { RawMessage::Block(block) => { let height = block.height(); @@ -490,8 +571,9 @@ async fn to_api_notification( kolme.wait_for_block(height), ) .await - .with_context(|| format!("Loading logs: took too long to load block {height}"))? - .with_context(|| format!("Failed to get block {height} in order to load logs"))?; + .map_err(|_| KolmeApiError::BlockTimeout(height))? + .map_err(|_| KolmeApiError::BlockLoadFailed(height))?; + let logs = kolme.load_logs(block.as_inner().logs).await?.into(); Ok(ApiNotification::NewBlock { block, logs }) } diff --git a/packages/kolme/src/approver.rs b/packages/kolme/src/approver.rs index 09c8616b..0f2fcdc1 100644 --- a/packages/kolme/src/approver.rs +++ b/packages/kolme/src/approver.rs @@ -10,7 +10,7 @@ impl Approver { Approver { kolme, secret } } - pub async fn run(self) -> Result<()> { + pub async fn run(self) -> Result<(), KolmeError> { let mut new_block = self.kolme.subscribe_new_block(); self.catch_up_approvals_all().await?; loop { @@ -19,7 +19,7 @@ impl Approver { } } - async fn catch_up_approvals_all(&self) -> Result<()> { + async fn catch_up_approvals_all(&self) -> Result<(), KolmeError> { let kolme = self.kolme.read(); for (chain, _) in kolme.get_bridge_contracts().iter() { self.catch_up_approvals(&kolme, chain).await?; @@ -27,7 +27,11 @@ impl Approver { Ok(()) } - async fn catch_up_approvals(&self, kolme: &KolmeRead, chain: ExternalChain) -> Result<()> { + async fn catch_up_approvals( + &self, + kolme: &KolmeRead, + chain: ExternalChain, + ) -> Result<(), KolmeError> { let Some((action_id, action)) = kolme.get_next_bridge_action(chain)? else { return Ok(()); }; diff --git a/packages/kolme/src/common.rs b/packages/kolme/src/common.rs index c9ae4135..d55373ec 100644 --- a/packages/kolme/src/common.rs +++ b/packages/kolme/src/common.rs @@ -111,9 +111,9 @@ impl MerkleDeserialize for SignedTaggedJson { } impl SignedTaggedJson { - pub fn verify_signature(&self) -> Result { + pub fn verify_signature(&self) -> Result { PublicKey::recover_from_msg(self.message.as_bytes(), &self.signature, self.recovery_id) - .map_err(anyhow::Error::from) + .map_err(KolmeError::from) } pub(crate) fn message_hash(&self) -> Sha256Hash { @@ -151,7 +151,7 @@ impl std::fmt::Debug for TaggedJson { } impl TaggedJson { - pub fn new(value: T) -> Result { + pub fn new(value: T) -> Result { let serialized = serde_json::to_string(&value)?; let hash = Sha256Hash::hash(&serialized); Ok(TaggedJson { @@ -161,7 +161,7 @@ impl TaggedJson { }) } - pub fn sign(self, key: &SecretKey) -> Result> { + pub fn sign(self, key: &SecretKey) -> Result, KolmeError> { let SignatureWithRecovery { recid, sig } = key.sign_recoverable(self.as_bytes())?; Ok(SignedTaggedJson { message: self, diff --git a/packages/kolme/src/core/execute.rs b/packages/kolme/src/core/execute.rs index adc79efa..68368ea8 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -2,6 +2,118 @@ use std::collections::VecDeque; use crate::core::*; +#[derive(thiserror::Error, Debug)] +pub enum KolmeExecuteError { + #[error("Listener pubkey not allowed for this event")] + InvalidListenerPubkey, + + #[error("Listener has already signed this event")] + DuplicateListenerSignature, + + #[error("Genesis message must be signed by the processor")] + InvalidGenesisPubkey { + expected: Box, + actual: Box, + }, + + #[error("Chain code version mismatch: code={code}, chain={chain}")] + VersionMismatch { + code: String, + chain: String, + txhash: TxHash, + }, + + #[error("Unexpected extra data loads during block validation")] + ExtraDataLoads, + + #[error("Genesis message does not match expected value")] + GenesisMismatch, + + #[error("Not enough approver signatures: needed {needed}, got {actual}")] + NotEnoughApprovers { needed: u16, actual: usize }, + + #[error("Processor approval already exists for this action")] + ProcessorAlreadyApproved, + + #[error("Duplicate approver signatures found")] + DuplicateApproverEntries, + + #[error("Cannot approve bridge action with a non-approver pubkey: {pubkey}")] + NonApproverSignature { pubkey: Box }, + + #[error("Bridge action already approved with pubkey {pubkey}")] + DuplicateApproverSignature { + action_id: BridgeActionId, + chain: ExternalChain, + pubkey: Box, + }, + + #[error("Processor signature invalid")] + InvalidProcessorSignature { + expected: Box, + actual: Box, + }, + + #[error("Approver signature invalid: signer {pubkey}")] + InvalidApproverSignature { pubkey: Box }, + + #[error("Mismatch in prior data loads")] + DataLoadMismatch, + + #[error("Cannot remove signing key from account")] + CannotRemoveSigningKey { + key: Box, + account: AccountId, + }, + + #[error("Invalid data load request: expected {expected}, got {actual}. Parse expected: {prev_req}, parse actual: {req}")] + InvalidDataLoadRequest { + expected: String, + actual: String, + prev_req: String, + req: String, + }, + + #[error("Public key error: {0}")] + PublicKeyError(#[from] shared::cryptography::PublicKeyError), + + #[error(transparent)] + State(#[from] CoreStateError), + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + Data(#[from] KolmeDataError), + + #[error("Cannot approve missing bridge action {action_id} for chain {chain}")] + MissingBridgeAction { + chain: ExternalChain, + action_id: BridgeActionId, + }, + + #[error(transparent)] + Accounts(#[from] AccountsError), + + #[error("Cannot report on an action when no pending actions are present")] + NoPendingActionsToReport, + + #[error("No pending action {action_id} found for {chain}")] + NoPendingActionsOnChain { + action_id: BridgeActionId, + chain: ExternalChain, + }, + + #[error("Specified an unknown proposal ID {admin_proposal_id}")] + UnknownAdminProposalId { admin_proposal_id: AdminProposalId }, +} + +#[derive(Debug)] +pub enum ValidatorRole { + Approver, + Listener, +} + /// Execution context for a single message. pub struct ExecutionContext<'a, App: KolmeApp> { framework_state: FrameworkState, @@ -70,7 +182,7 @@ pub enum BlockDataHandling { } impl KolmeRead { - fn validate_tx(&self, tx: &SignedTransaction) -> Result<()> { + fn validate_tx(&self, tx: &SignedTransaction) -> Result<(), KolmeError> { // Ensure that the signature is valid tx.validate_signature()?; @@ -79,7 +191,14 @@ impl KolmeRead { // Make sure this is a genesis event if and only if we have no events so far if self.get_next_height().is_start() { tx.ensure_is_genesis()?; - anyhow::ensure!(tx.pubkey == self.get_processor_pubkey()); + let expected = self.get_processor_pubkey(); + let actual = tx.pubkey; + if actual != expected { + return Err(KolmeError::from(KolmeExecuteError::InvalidGenesisPubkey { + expected: Box::new(expected), + actual: Box::new(actual), + })); + } } else { tx.ensure_no_genesis()?; }; @@ -93,12 +212,19 @@ impl KolmeRead { signed_tx: &SignedTransaction, timestamp: Timestamp, block_data_handling: BlockDataHandling, - ) -> Result> { + ) -> Result, KolmeError> { // If we're running different code versions, we can't // get reproducible results. let chain_version = self.get_chain_version(); let code_version = self.get_code_version(); - anyhow::ensure!(chain_version == code_version, "Cannot execute transaction {}, current code version is {code_version}, but chain is running {chain_version}", signed_tx.hash()); + if chain_version != code_version { + return Err(KolmeExecuteError::VersionMismatch { + code: code_version.clone(), + chain: chain_version.clone(), + txhash: signed_tx.hash(), + } + .into()); + } self.validate_tx(signed_tx)?; let tx = signed_tx.0.message.as_inner(); @@ -148,7 +274,9 @@ impl KolmeRead { } => { // For a proper validation, every piece of data loaded during execution // must be used during validation. - anyhow::ensure!(loads.is_empty()); + if !loads.is_empty() { + return Err(KolmeExecuteError::ExtraDataLoads.into()); + } } } @@ -163,11 +291,17 @@ impl KolmeRead { } impl ExecutionContext<'_, App> { - async fn execute_message(&mut self, app: &App, message: &Message) -> Result<()> { + async fn execute_message( + &mut self, + app: &App, + message: &Message, + ) -> Result<(), KolmeError> { match message { Message::Genesis(actual) => { let expected = app.genesis_info(); - anyhow::ensure!(expected == actual); + if expected != actual { + return Err(KolmeExecuteError::GenesisMismatch.into()); + } } Message::App(msg) => { app.execute(self, msg).await?; @@ -204,26 +338,36 @@ impl ExecutionContext<'_, App> { chain: ExternalChain, event: &BridgeEvent, event_id: BridgeEventId, - ) -> Result<()> { - anyhow::ensure!(self + ) -> Result<(), KolmeError> { + if !self .framework_state .get_validator_set() .listeners - .contains(&self.pubkey), - "Received a listener message for bridge event ID {event_id} on {chain}, but provided pubkey {} is not part of the listener set {:?}", - self.pubkey, - self.framework_state.get_validator_set().listeners - ); + .contains(&self.pubkey) + { + return Err(KolmeError::NotInValidatorSet { + signer: Box::new(self.pubkey), + role: ValidatorRole::Listener, + }); + } let state = self.framework_state.chains.get_mut(chain)?; let attestations = match state.pending_events.get_mut(&event_id) { Some(pending) => { - anyhow::ensure!(pending.event == *event); + if pending.event != *event { + return Err(KolmeError::Execution( + KolmeExecutionError::MismatchedBridgeEvent, + )); + } &mut pending.attestations } None => { - anyhow::ensure!(event_id == state.next_event_id); + if event_id != state.next_event_id { + return Err(KolmeError::Execution( + KolmeExecutionError::UnexpectedBridgeEventId, + )); + } state.next_event_id = event_id.next(); state.pending_events.insert( event_id, @@ -243,14 +387,16 @@ impl ExecutionContext<'_, App> { let was_inserted = attestations.insert(self.pubkey); // Make sure it wasn't already approved - anyhow::ensure!(was_inserted); + if !was_inserted { + return Err(KolmeExecuteError::DuplicateListenerSignature.into()); + } // Now that we've added a signature, go through all pending events // in order and process them if they have sufficient attestations. self.process_ready_events(chain) } - fn process_ready_events(&mut self, chain: ExternalChain) -> Result<()> { + fn process_ready_events(&mut self, chain: ExternalChain) -> Result<(), KolmeError> { fn get_next_ready_event( framework_state: &FrameworkState, chain: ExternalChain, @@ -303,9 +449,11 @@ impl ExecutionContext<'_, App> { BridgeEvent::Instantiated { contract } => { let config = &mut self.framework_state.chains.get_mut(chain)?.config; match config.bridge { - BridgeContract::NeededCosmosBridge { .. } | - BridgeContract::NeededSolanaBridge { .. } => (), - BridgeContract::Deployed(_) => anyhow::bail!("Already have a bridge contract for {chain:?}, just received another from a listener"), + BridgeContract::NeededCosmosBridge { .. } + | BridgeContract::NeededSolanaBridge { .. } => (), + BridgeContract::Deployed(_) => { + return Err(KolmeError::BridgeAlreadyDeployed { chain }); + } } config.bridge = BridgeContract::Deployed(contract.clone()); } @@ -334,11 +482,10 @@ impl ExecutionContext<'_, App> { }; let amount = asset_config.to_decimal(*amount)?; - self.framework_state.accounts.mint( - account_id, - asset_config.asset_id, - amount, - )?; + self.framework_state + .accounts + .mint(account_id, asset_config.asset_id, amount) + .map_err(KolmeError::Accounts)?; self.framework_state .chains .get_mut(chain)? @@ -359,10 +506,25 @@ impl ExecutionContext<'_, App> { let next_action_id = actions .keys() .next() - .context("Cannot report on an action when no pending actions present")?; - anyhow::ensure!(*next_action_id == action_id); - let (old_id, _old) = actions.remove(&action_id).unwrap(); - anyhow::ensure!(old_id == action_id); + .ok_or(KolmeExecuteError::NoPendingActionsToReport)?; + + if *next_action_id != action_id { + return Err(KolmeError::ActionIdMismatch { + expected: *next_action_id, + found: action_id, + }); + } + + let (old_id, _old) = actions + .remove(&action_id) + .expect("pending actions must contain the action_id being completed"); + + if old_id != action_id { + return Err(KolmeError::ActionIdMismatch { + expected: old_id, + found: action_id, + }); + } } } } @@ -375,27 +537,37 @@ impl ExecutionContext<'_, App> { chain: ExternalChain, action_id: BridgeActionId, signature: SignatureWithRecovery, - ) -> Result<()> { + ) -> Result<(), KolmeExecuteError> { let action = self .framework_state .chains .get_mut(chain)? .pending_actions .get_mut(&action_id) - .with_context(|| { - format!("Cannot approve missing bridge action ID {action_id} for chain {chain}") - })?; + .ok_or(KolmeExecuteError::MissingBridgeAction { chain, action_id })?; let key = signature.validate(action.payload.as_bytes())?; // Using config.as_ref() instead of framework_state.get_config to work around // a borrow conflict with the mutable borrow above - anyhow::ensure!(self + if !self .framework_state .validator_set .as_ref() .approvers - .contains(&key)); + .contains(&key) + { + return Err(KolmeExecuteError::NonApproverSignature { + pubkey: Box::new(key), + }); + } + let old = action.approvals.insert(key, signature); - anyhow::ensure!(old.is_none(), "Cannot approve bridge action ID {action_id} for chain {chain} with already-used public key {key}"); + if old.is_some() { + return Err(KolmeExecuteError::DuplicateApproverSignature { + action_id, + chain, + pubkey: Box::new(key), + }); + } Ok(()) } @@ -405,15 +577,15 @@ impl ExecutionContext<'_, App> { action_id: BridgeActionId, processor: &SignatureWithRecovery, approvers: &[SignatureWithRecovery], - ) -> Result<()> { - anyhow::ensure!( - approvers.len() - >= self - .framework_state - .get_validator_set() - .needed_approvers - .into() - ); + ) -> Result<(), KolmeError> { + let needed = self.framework_state.get_validator_set().needed_approvers as usize; + if approvers.len() < needed { + return Err(KolmeExecuteError::NotEnoughApprovers { + needed: needed as u16, + actual: approvers.len(), + } + .into()); + } let action = self .framework_state @@ -421,28 +593,47 @@ impl ExecutionContext<'_, App> { .get_mut(chain)? .pending_actions .get_mut(&action_id) - .with_context(|| format!("No pending action {action_id} found for {chain}"))?; + .ok_or(KolmeExecuteError::NoPendingActionsOnChain { action_id, chain })?; - anyhow::ensure!(action.processor.is_none()); + if action.processor.is_some() { + return Err(KolmeExecuteError::ProcessorAlreadyApproved.into()); + } let payload = action.payload.as_bytes(); let processor_key = processor.validate(payload)?; - anyhow::ensure!(processor_key == self.framework_state.validator_set.as_ref().processor); + let expected = self.framework_state.validator_set.as_ref().processor; + + if processor_key != expected { + return Err(KolmeExecuteError::InvalidProcessorSignature { + expected: Box::new(expected), + actual: Box::new(processor_key), + } + .into()); + } let approvers_checked = approvers .iter() .map(|sig| { let pubkey = sig.validate(payload)?; - anyhow::ensure!(self + if !self .framework_state .validator_set .as_ref() .approvers - .contains(&pubkey)); + .contains(&pubkey) + { + return Err(KolmeError::from( + KolmeExecuteError::InvalidApproverSignature { + pubkey: Box::new(pubkey), + }, + )); + } Ok(pubkey) }) .collect::, _>>()?; - anyhow::ensure!(approvers.len() == approvers_checked.len()); + if approvers_checked.len() != approvers.len() { + return Err(KolmeExecuteError::DuplicateApproverEntries.into()); + } action.processor = Some(*processor); @@ -517,7 +708,11 @@ impl ExecutionContext<'_, App> { self.framework_state.accounts.get_assets(account_id) } - fn add_action(&mut self, chain: ExternalChain, action: ExecAction) -> Result { + fn add_action( + &mut self, + chain: ExternalChain, + action: ExecAction, + ) -> Result { let state = self.framework_state.chains.get_mut(chain)?; let id = state.next_action_id; let payload = action.to_payload(chain, &state.config, id)?; @@ -535,7 +730,7 @@ impl ExecutionContext<'_, App> { } /// Add an action on all chains - fn add_action_all_chains(&mut self, action: ExecAction) -> Result<()> { + fn add_action_all_chains(&mut self, action: ExecAction) -> Result<(), KolmeError> { let chains = self.framework_state.chains.keys().collect::>(); for chain in chains { self.add_action(chain, action.clone())?; @@ -552,12 +747,13 @@ impl ExecutionContext<'_, App> { source: AccountId, wallet: &Wallet, amount: Decimal, - ) -> Result { + ) -> Result { let config = self.framework_state.get_asset_config(chain, asset_id)?; let (amount_dec, amount_u128) = config.to_u128(amount)?; self.framework_state .accounts - .burn(source, asset_id, amount_dec)?; + .burn(source, asset_id, amount_dec) + .map_err(KolmeError::Accounts)?; self.framework_state .chains .get_mut(chain)? @@ -583,7 +779,7 @@ impl ExecutionContext<'_, App> { source: AccountId, dest: AccountId, amount: Decimal, - ) -> Result<()> { + ) -> Result<(), KolmeError> { self.burn_asset(asset_id, source, amount)?; self.mint_asset(asset_id, dest, amount)?; Ok(()) @@ -595,10 +791,11 @@ impl ExecutionContext<'_, App> { asset_id: AssetId, recipient: AccountId, amount: Decimal, - ) -> Result<()> { + ) -> Result<(), KolmeError> { self.framework_state .accounts - .mint(recipient, asset_id, amount)?; + .mint(recipient, asset_id, amount) + .map_err(KolmeError::Accounts)?; Ok(()) } @@ -610,26 +807,34 @@ impl ExecutionContext<'_, App> { asset_id: AssetId, owner: AccountId, amount: Decimal, - ) -> Result<()> { + ) -> Result<(), KolmeError> { self.framework_state .accounts - .burn(owner, asset_id, amount)?; + .burn(owner, asset_id, amount) + .map_err(KolmeError::Accounts)?; Ok(()) } pub async fn load_data>( &mut self, req: Req, - ) -> Result { + ) -> Result { let request_str = serde_json::to_string(&req)?; let res = match &mut self.block_data_handling { BlockDataHandling::PriorData { loads, validation } => { let BlockDataLoad { request, response } = loads .pop_front() - .context("Incorrect number of data loads")?; + .ok_or(KolmeExecuteError::DataLoadMismatch)?; let prev_req = serde_json::from_str::(&request)?; let prev_res = serde_json::from_str(&response)?; - anyhow::ensure!(prev_req == req); + if prev_req != req { + return Err(KolmeExecuteError::InvalidDataLoadRequest { + expected: request, + actual: request_str.clone(), + prev_req: serde_json::to_string(&prev_req)?, + req: serde_json::to_string(&req)?, + }); + } match validation { DataLoadValidation::ValidateDataLoads => { req.validate(self.app, &prev_res).await?; @@ -648,7 +853,7 @@ impl ExecutionContext<'_, App> { Ok(res) } - fn bank(&mut self, bank: &BankMessage) -> Result<()> { + fn bank(&mut self, bank: &BankMessage) -> Result<(), KolmeError> { match bank { BankMessage::Withdraw { asset, @@ -670,51 +875,70 @@ impl ExecutionContext<'_, App> { Ok(()) } - fn auth(&mut self, auth: &AuthMessage) -> Result<()> { + fn auth(&mut self, auth: &AuthMessage) -> Result<(), KolmeExecuteError> { match auth { AuthMessage::AddPublicKey { key } => { self.framework_state .accounts - .add_pubkey_to_account_error_overlap(self.get_sender_id(), *key)?; + .add_pubkey_to_account_error_overlap(self.get_sender_id(), *key) + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::RemovePublicKey { key } => { - anyhow::ensure!(key != &self.signing_key, "Cannot remove public key {key} from account {} with a transaction signed by the same key", self.get_sender_id()); + if key == &self.signing_key { + return Err(KolmeExecuteError::CannotRemoveSigningKey { + key: Box::new(*key), + account: self.get_sender_id(), + }); + } + self.framework_state .accounts - .remove_pubkey_from_account(self.get_sender_id(), *key)?; + .remove_pubkey_from_account(self.get_sender_id(), *key) + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::AddWallet { wallet } => { self.framework_state .accounts - .add_wallet_to_account(self.get_sender_id(), wallet)?; + .add_wallet_to_account(self.get_sender_id(), wallet) + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::RemoveWallet { wallet } => { self.framework_state .accounts - .remove_wallet_from_account(self.get_sender_id(), wallet)?; + .remove_wallet_from_account(self.get_sender_id(), wallet) + .map_err(KolmeExecuteError::Accounts)?; } } Ok(()) } - fn admin(&mut self, admin: &AdminMessage) -> Result<()> { + fn admin(&mut self, admin: &AdminMessage) -> Result<(), KolmeError> { match admin { AdminMessage::SelfReplace(self_replace) => { let signer = self_replace.verify_signature()?; - anyhow::ensure!(signer == self.pubkey); + if signer != self.pubkey { + return Err(KolmeError::InvalidSelfReplaceSigner); + } fn set_helper( validator_set: &mut ValidatorSet, is_approver: bool, sender: PublicKey, replacement: PublicKey, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let set = if is_approver { &mut validator_set.approvers } else { &mut validator_set.listeners }; if !set.remove(&sender) { - anyhow::bail!("Signing public key {} is not a member of the {} set and cannot self-replace", sender, if is_approver {"approver"}else{"listener"}); + return Err(KolmeError::NotInValidatorSet { + signer: Box::new(sender), + role: if is_approver { + ValidatorRole::Approver + } else { + ValidatorRole::Listener + }, + }); } set.insert(replacement); Ok(()) @@ -727,7 +951,9 @@ impl ExecutionContext<'_, App> { if config.processor == signer { config.processor = replacement; } else { - anyhow::bail!("Signing public key {} is not the current processor and cannot self-replace", self.pubkey); + return Err(KolmeError::NotProcessor { + signer: Box::new(self.pubkey), + }); } } ValidatorType::Listener => { @@ -742,7 +968,9 @@ impl ExecutionContext<'_, App> { } AdminMessage::NewSet { validator_set } => { let signer = validator_set.verify_signature()?; - anyhow::ensure!(signer == self.pubkey); + if signer != self.pubkey { + return Err(KolmeError::InvalidSelfReplaceSigner); + } self.framework_state .validator_set .as_ref() @@ -757,7 +985,9 @@ impl ExecutionContext<'_, App> { } AdminMessage::MigrateContract(migrate) => { let signer = migrate.verify_signature()?; - anyhow::ensure!(signer == self.pubkey); + if signer != self.pubkey { + return Err(KolmeError::InvalidSelfReplaceSigner); + } self.framework_state .validator_set .as_ref() @@ -771,7 +1001,9 @@ impl ExecutionContext<'_, App> { } AdminMessage::Upgrade(upgrade) => { let signer = upgrade.verify_signature()?; - anyhow::ensure!(signer == self.pubkey); + if signer != self.pubkey { + return Err(KolmeError::InvalidSelfReplaceSigner); + } self.framework_state .validator_set .as_ref() @@ -793,29 +1025,31 @@ impl ExecutionContext<'_, App> { .ensure_is_validator(self.pubkey)?; let state = self.framework_state.admin_proposal_state.as_mut(); - let pending = state - .proposals - .get_mut(admin_proposal_id) - .with_context(|| { - format!("Specified an unknown proposal ID {admin_proposal_id}") - })?; + let pending = state.proposals.get_mut(admin_proposal_id).ok_or( + KolmeExecuteError::UnknownAdminProposalId { + admin_proposal_id: *admin_proposal_id, + }, + )?; let pubkey = signature.validate(pending.payload.as_bytes())?; - anyhow::ensure!(pubkey == self.pubkey); + if pubkey != self.pubkey { + return Err(KolmeError::InvalidSelfReplaceSigner); + } let old_value = pending.approvals.insert(pubkey, *signature); - anyhow::ensure!( - old_value.is_none(), - "{} already approved proposal {admin_proposal_id}", - self.pubkey - ); + if old_value.is_some() { + return Err(KolmeError::AlreadyApprovedProposal { + signer: self.pubkey, + proposal_id: *admin_proposal_id, + }); + } self.check_pending_proposals()?; } } Ok(()) } - fn check_pending_proposals(&mut self) -> Result<()> { + fn check_pending_proposals(&mut self) -> Result<(), KolmeError> { if let Some((id, PendingProposal { payload, approvals })) = self.find_approved_proposal() { self.log_event(LogEvent::AdminProposalApproved(id))?; match payload { @@ -861,12 +1095,12 @@ impl ExecutionContext<'_, App> { self.logs.last_mut().unwrap().push(msg.into()); } - pub fn log_event(&mut self, event: LogEvent) -> Result<()> { + pub fn log_event(&mut self, event: LogEvent) -> Result<(), KolmeError> { self.log_json(&event) } /// Log any serializable value as JSON. - pub fn log_json(&mut self, msg: &T) -> Result<()> { + pub fn log_json(&mut self, msg: &T) -> Result<(), KolmeError> { let json = serde_json::to_string(msg)?; self.log(json); Ok(()) diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 36f826c4..0e0c3b15 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -3,6 +3,8 @@ mod import_export; mod mempool; mod store; +pub use import_export::KolmeImportExportError; + use block_info::BlockState; pub(super) use block_info::{BlockInfo, MaybeBlockInfo}; use kolme_store::{BackingRemoteDataListener, KolmeConstructLock, KolmeStoreError, StorableBlock}; @@ -22,6 +24,66 @@ pub use mempool::ProposeTransactionError; use crate::core::*; +#[derive(thiserror::Error, Debug)] +pub enum KolmeCoreError { + #[error("Executed height mismatch: expected {expected}, actual {actual}")] + ExecutedHeight { + expected: BlockHeight, + actual: BlockHeight, + }, + + #[error("Executed loads mismatch")] + ExecutedLoads { + expected: Vec, + actual: Vec, + }, + + #[error("Framework state hash mismatch")] + FrameworkStateHash { + expected: Sha256Hash, + actual: Sha256Hash, + }, + + #[error("App state hash mismatch")] + AppStateHash { + expected: Sha256Hash, + actual: Sha256Hash, + }, + + #[error("Logs hash mismatch")] + LogsHash { + expected: Sha256Hash, + actual: Sha256Hash, + }, + + #[error("Missing framework merkle layer {hash}")] + MissingFrameworkMerkleLayer { hash: Sha256Hash }, + + #[error("Missing app merkle layer {hash}")] + MissingAppMerkleLayer { hash: Sha256Hash }, + + #[error("Missing logs merkle layer {hash}")] + MissingLogMerkleLayer { hash: Sha256Hash }, + + #[error("Expected block {height} not found during resync")] + BlockNotFoundDuringResync { height: BlockHeight }, + + #[error("Block {height} not available")] + BlockNotFound { height: BlockHeight }, + + #[error("Missing merkle layer {hash} in source store")] + MissingMerkleLayer { hash: Sha256Hash }, + + #[error("Unable to mark block {height} as archived: {source}")] + ArchiveBlockFailed { + height: BlockHeight, + source: KolmeStoreError, + }, + + #[error("Unable to retrieve latest archived block height: {source}")] + GetLatestArchivedBlockFailed { source: KolmeStoreError }, +} + /// A running instance of Kolme for the given application. /// /// This type represents the core execution environment for Kolme. @@ -186,7 +248,10 @@ impl Kolme { self.inner.mempool.add(tx) } - fn verify_processor_signature(&self, signed: &SignedTaggedJson) -> Result<()> { + fn verify_processor_signature( + &self, + signed: &SignedTaggedJson, + ) -> Result<(), KolmeError> { // Note that during a key rotation, we will have a switch in the // processor, and during that period some signatures will be // incorrectly excluded. That's acceptable, we expect the new block data @@ -204,9 +269,10 @@ impl Kolme { if pubkey == processor { Ok(()) } else { - Err(anyhow::anyhow!( - "Latest block was signed by {pubkey}, but processor is {processor}" - )) + Err(KolmeError::InvalidBlockProcessor { + expected_processor: Box::new(processor), + actual_processor: Box::new(pubkey), + }) } } @@ -255,7 +321,7 @@ impl Kolme { pub async fn propose_and_await_transaction( &self, tx: Arc>, - ) -> Result>> { + ) -> Result>, TransactionError> { let txhash = tx.hash(); match tokio::time::timeout( self.tx_await_duration, @@ -263,17 +329,15 @@ impl Kolme { ) .await { - Ok(res) => res, - Err(e) => Err(anyhow::Error::from(e).context(format!( - "Timed out proposing and awaiting transaction {txhash}" - ))), + Ok(res) => Ok(res?), + Err(_) => Err(TransactionError::TimeoutProposingTx { txhash }), } } async fn propose_and_await_transaction_inner( &self, tx: Arc>, - ) -> Result>> { + ) -> Result>, TransactionError> { let mut new_block = self.subscribe_new_block(); let mut failed_tx = self.subscribe_failed_txs(); let txhash = tx.hash(); @@ -290,7 +354,7 @@ impl Kolme { } Err(ProposeTransactionError::Failed(failed)) => { debug_assert_eq!(failed.message.as_inner().txhash, txhash); - break Err(failed.message.as_inner().error.clone().into()); + break Err(failed.message.as_inner().error.clone()); } } @@ -314,7 +378,7 @@ impl Kolme { &self, secret: &SecretKey, tx_builder: T, - ) -> Result>> { + ) -> Result>, KolmeError> { self.sign_propose_await_transaction_inner(secret, tx_builder.into()) .await } @@ -323,7 +387,7 @@ impl Kolme { &self, secret: &SecretKey, tx_builder: TxBuilder, - ) -> Result>> { + ) -> Result>, KolmeError> { let pubkey = secret.public_key(); let (next_block_height, mut nonce) = { let kolme_r = self.read(); @@ -341,30 +405,34 @@ impl Kolme { nonce, )?); match self.propose_and_await_transaction_inner(tx).await { - Ok(block) => break Ok(block), + Ok(block) => return Ok(block), Err(e) => { - if let Some(KolmeError::InvalidNonce { + if let TransactionError::InvalidNonce { pubkey: _, account_id: _, expected, actual, - }) = e.downcast_ref() + } = e { if actual < expected && attempt < MAX_NONCE_ATTEMPTS { - tracing::warn!("Retrying with new nonce, attempt {attempt}/{MAX_NONCE_ATTEMPTS}. Retrieved attempted nonce from framework state with next_block_height {next_block_height}. Error: {e}"); + tracing::warn!( + "Retrying with new nonce, attempt {attempt}/{MAX_NONCE_ATTEMPTS}. \ + Retrieved attempted nonce from framework state with next_block_height {next_block_height}. \ + Error: {e}" + ); attempt += 1; - nonce = *expected; + nonce = expected; continue; } } - break Err(e); + return Err(KolmeError::Transaction(e)); } } } } /// Resync with the database. - pub async fn resync(&self) -> Result<()> { + pub async fn resync(&self) -> Result<(), KolmeError> { if let Some(height) = self.inner.store.load_latest_block().await? { if self.read().get_next_height() < height.next() { let block = self @@ -372,7 +440,7 @@ impl Kolme { .store .load_signed_block(height) .await? - .with_context(|| format!("Expected block {height} not found during resync"))?; + .ok_or_else(|| KolmeCoreError::BlockNotFoundDuringResync { height })?; let (framework_state, app_state) = tokio::try_join!( self.load_framework_state(block.as_inner().framework_state), @@ -398,7 +466,10 @@ impl Kolme { /// Validate and append the given block. /// /// Responsible for validating signatures and state transitions. - pub async fn add_block(&self, signed_block: Arc>) -> Result<()> { + pub async fn add_block( + &self, + signed_block: Arc>, + ) -> Result<(), KolmeError> { self.add_block_with(signed_block, DataLoadValidation::ValidateDataLoads) .await } @@ -407,42 +478,43 @@ impl Kolme { &self, signed_block: Arc>, data_load_validation: DataLoadValidation, - ) -> Result<()> { + ) -> Result<(), KolmeError> { // Make sure we're at the right height for this and the correct processor is signing this. let kolme = self.read(); // FIXME add support for adding old blocks instead if kolme.get_next_height() != signed_block.height() { - anyhow::bail!( - "Tried to add block with height {}, but next expected height is {}", - signed_block.height(), - kolme.get_next_height() - ); + return Err(KolmeError::UnexpectedBlockHeight { + received: signed_block.height(), + expected: kolme.get_next_height(), + }); } let actual_parent = kolme.get_current_block_hash(); let block_parent = signed_block.0.message.as_inner().parent; - anyhow::ensure!( - actual_parent == block_parent, - "Tried to add block height {}, but actual parent has block hash {actual_parent} and block specifies {block_parent}", - signed_block.height() - ); + if actual_parent != block_parent { + return Err(KolmeError::BlockParentMismatch { + actual: actual_parent, + expected: block_parent, + }); + } let expected_processor = kolme.get_framework_state().get_validator_set().processor; let actual_processor = signed_block.0.message.as_inner().processor; - anyhow::ensure!( - expected_processor == actual_processor, - "Received block signed by processor {actual_processor}, but the real processor is {expected_processor}" - ); + if expected_processor != actual_processor { + return Err(KolmeError::InvalidBlockProcessor { + expected_processor: Box::new(expected_processor), + actual_processor: Box::new(actual_processor), + }); + } // Ensure the max height is respected if present if let Some(max_height) = signed_block.tx().0.message.as_inner().max_height { if max_height < signed_block.height() { - return Err(KolmeError::PastMaxHeight { + return Err(KolmeError::Transaction(TransactionError::PastMaxHeight { txhash: signed_block.tx().hash(), max_height, proposed_height: signed_block.height(), - } - .into()); + })); } } @@ -465,8 +537,22 @@ impl Kolme { }, ) .await?; - anyhow::ensure!(height == signed_block.height()); - anyhow::ensure!(loads == block.loads); + + if height != signed_block.height() { + return Err(KolmeCoreError::ExecutedHeight { + expected: signed_block.height(), + actual: height, + } + .into()); + } + + if loads != block.loads { + return Err(KolmeCoreError::ExecutedLoads { + expected: block.loads.clone(), + actual: loads.clone(), + } + .into()); + } self.add_executed_block(ExecutedBlock { signed_block, @@ -475,6 +561,7 @@ impl Kolme { logs, }) .await + .map_err(KolmeError::from) } /// Add a block that has already been executed. @@ -483,7 +570,7 @@ impl Kolme { pub(crate) async fn add_executed_block( &self, executed_block: ExecutedBlock, - ) -> Result<()> { + ) -> Result<(), TransactionError> { let ExecutedBlock { signed_block, framework_state, @@ -495,14 +582,59 @@ impl Kolme { let logs: Arc<[_]> = logs.into(); let height = signed_block.height(); - let framework_state_hash = self.inner.store.save(&framework_state).await?; - anyhow::ensure!(framework_state_hash == signed_block.0.message.as_inner().framework_state); + let framework_state_hash = self + .inner + .store + .save(&framework_state) + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))?; + let expected_fw = signed_block.0.message.as_inner().framework_state; + + if framework_state_hash != expected_fw { + return Err(TransactionError::CoreError( + KolmeCoreError::FrameworkStateHash { + expected: expected_fw, + actual: framework_state_hash, + } + .to_string(), + )); + } - let app_state_hash = self.inner.store.save(&app_state).await?; - anyhow::ensure!(app_state_hash == signed_block.0.message.as_inner().app_state); + let app_state_hash = self + .inner + .store + .save(&app_state) + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))?; + let expected_app = signed_block.0.message.as_inner().app_state; + + if app_state_hash != expected_app { + return Err(TransactionError::CoreError( + KolmeCoreError::AppStateHash { + expected: expected_app, + actual: app_state_hash, + } + .to_string(), + )); + } - let logs_hash = self.inner.store.save(&logs).await?; - anyhow::ensure!(logs_hash == signed_block.0.message.as_inner().logs); + let logs_hash = self + .inner + .store + .save(&logs) + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))?; + let expected_logs = signed_block.0.message.as_inner().logs; + + if logs_hash != expected_logs { + return Err(TransactionError::CoreError( + KolmeCoreError::LogsHash { + expected: expected_logs, + actual: logs_hash, + } + .to_string(), + )); + } self.inner .store @@ -512,7 +644,8 @@ impl Kolme { txhash: signed_block.tx().hash().0, block: signed_block.clone(), }) - .await?; + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))?; self.inner.mempool.add_signed_block(signed_block.clone()); if let Some(tx) = self.inner.landed_txs.get() { @@ -541,7 +674,12 @@ impl Kolme { } // Update the archive if appropriate - if self.get_next_to_archive().await? == height { + if self + .get_next_to_archive() + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))? + == height + { if let Err(e) = self.inner.store.archive_block(height).await { tracing::warn!("Unable to mark block {height} as archived: {e}"); } @@ -565,40 +703,40 @@ impl Kolme { pub async fn add_block_with_state( &self, signed_block: Arc>, - ) -> Result<()> { + ) -> Result<(), KolmeError> { // Don't accept blocks we already have if self.has_block(signed_block.height()).await? { - anyhow::bail!( - "Tried to add block with height {}, but it's already present in the store.", - signed_block.height() - ); + return Err(KolmeError::BlockAlreadyExists { + height: signed_block.height(), + }); } let kolme = self.read(); let expected_processor = kolme.get_framework_state().get_validator_set().processor; let actual_processor = signed_block.0.message.as_inner().processor; - anyhow::ensure!( - expected_processor == actual_processor, - "Received block signed by processor {actual_processor}, but the real processor is {expected_processor}" - ); + if expected_processor != actual_processor { + return Err(KolmeError::InvalidBlockProcessor { + expected_processor: Box::new(expected_processor), + actual_processor: Box::new(actual_processor), + }); + } signed_block.validate_signature()?; let block = signed_block.0.message.as_inner(); - anyhow::ensure!( - self.has_merkle_hash(block.framework_state).await?, - "Framework state {} not written to Merkle store", - block.framework_state - ); - anyhow::ensure!( - self.has_merkle_hash(block.app_state).await?, - "App state {} not written to Merkle store", - block.app_state - ); - anyhow::ensure!( - self.has_merkle_hash(block.logs).await?, - "Logs {} not written to Merkle store", - block.logs - ); + let fw_hash = block.framework_state; + if !self.has_merkle_hash(fw_hash).await? { + return Err(KolmeCoreError::MissingFrameworkMerkleLayer { hash: fw_hash }.into()); + } + + let app_hash = block.app_state; + if !self.has_merkle_hash(app_hash).await? { + return Err(KolmeCoreError::MissingAppMerkleLayer { hash: app_hash }.into()); + } + + let logs_hash = block.logs; + if !self.has_merkle_hash(logs_hash).await? { + return Err(KolmeCoreError::MissingLogMerkleLayer { hash: logs_hash }.into()); + } self.inner .store @@ -645,7 +783,7 @@ impl Kolme { app: App, code_version: impl Into, store: KolmeStore, - ) -> Result { + ) -> Result { let current_block = MaybeBlockInfo::::load(&store, &app).await?; let inner = KolmeInner { store, @@ -718,7 +856,7 @@ impl Kolme { pub async fn wait_for_block( &self, height: BlockHeight, - ) -> Result>> { + ) -> Result>, KolmeError> { // Optimization for the common case. if let Some(storable_block) = self.get_block(height).await? { return Ok(storable_block.block); @@ -773,7 +911,7 @@ impl Kolme { } /// Wait until the given transaction is published - pub async fn wait_for_tx(&self, tx: TxHash) -> Result { + pub async fn wait_for_tx(&self, tx: TxHash) -> Result { let mut new_block = self.subscribe_new_block(); loop { if let Some(height) = self.get_tx_height(tx).await? { @@ -805,7 +943,7 @@ impl Kolme { } /// Wait for the given public key to have an account ID and then return it. - pub async fn wait_account_for_key(&self, pubkey: PublicKey) -> Result { + pub async fn wait_account_for_key(&self, pubkey: PublicKey) -> Result { loop { let kolme = self.read(); if let Some((id, _)) = kolme @@ -821,7 +959,7 @@ impl Kolme { } /// Wait for the given wallet to have an account ID and then return it. - pub async fn wait_account_for_wallet(&self, wallet: &Wallet) -> Result { + pub async fn wait_account_for_wallet(&self, wallet: &Wallet) -> Result { loop { let kolme = self.read(); if let Some((id, _)) = kolme @@ -841,7 +979,7 @@ impl Kolme { &self, chain: ExternalChain, event_id: BridgeEventId, - ) -> Result<()> { + ) -> Result<(), KolmeError> { loop { let kolme = self.read(); let state = kolme.get_framework_state().chains.get(chain)?; @@ -862,7 +1000,7 @@ impl Kolme { &self, chain: ExternalChain, action_id: BridgeActionId, - ) -> Result<()> { + ) -> Result<(), KolmeError> { loop { let kolme = self.read(); let state = kolme.get_framework_state().chains.get(chain)?; @@ -880,11 +1018,14 @@ impl Kolme { } } - pub async fn get_log_events_for(&self, height: BlockHeight) -> Result> { + pub async fn get_log_events_for( + &self, + height: BlockHeight, + ) -> Result, KolmeError> { let block = self .get_block(height) .await? - .with_context(|| format!("get_log_events_for({height}: block not available"))?; + .ok_or_else(|| KolmeCoreError::BlockNotFound { height })?; let logs = self.load_logs(block.block.as_inner().logs).await?; Ok(logs .iter() @@ -912,7 +1053,7 @@ impl Kolme { } #[cfg(feature = "cosmwasm")] - pub async fn get_cosmos(&self, chain: CosmosChain) -> Result { + pub async fn get_cosmos(&self, chain: CosmosChain) -> Result { if let Some(cosmos) = self.inner.cosmos_conns.read().await.get(&chain) { return Ok(cosmos.clone()); } @@ -981,7 +1122,10 @@ impl Kolme { } #[cfg(feature = "solana")] - pub async fn get_solana_pubsub_client(&self, chain: SolanaChain) -> Result { + pub async fn get_solana_pubsub_client( + &self, + chain: SolanaChain, + ) -> Result { // TODO do we need caching here? let endpoint = self @@ -1005,7 +1149,7 @@ impl Kolme { } /// Take a lock on constructing new blocks. - pub(crate) async fn take_construct_lock(&self) -> Result { + pub(crate) async fn take_construct_lock(&self) -> Result { self.inner.store.take_construct_lock().await } @@ -1017,7 +1161,7 @@ impl Kolme { /// Returns a hash of the genesis info. /// /// Purpose: this provides a unique identifier for a chain. - pub fn get_genesis_hash(&self) -> Result { + pub fn get_genesis_hash(&self) -> Result { let info = self.inner.app.genesis_info(); let info = serde_json::to_vec(info)?; Ok(Sha256Hash::hash(&info)) @@ -1049,7 +1193,10 @@ impl Kolme { /// Add a Merkle layer for this hash. /// /// Invariant: you must ensure that all children are already stored. - pub(crate) async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> Result<()> { + pub(crate) async fn add_merkle_layer( + &self, + layer: &MerkleLayerContents, + ) -> Result<(), KolmeStoreError> { self.inner.store.add_merkle_layer(layer).await } @@ -1062,7 +1209,7 @@ impl Kolme { } /// Ingest all blocks from the given Kolme into this one. - pub async fn ingest_blocks_from(&self, other: &Self) -> Result<()> { + pub async fn ingest_blocks_from(&self, other: &Self) -> Result<(), KolmeError> { loop { let to_archive = self.get_next_to_archive().await?; let Some(block) = other.get_block(to_archive).await? else { @@ -1079,7 +1226,7 @@ impl Kolme { } } - async fn ingest_layer_from(&self, other: &Self, hash: Sha256Hash) -> Result<()> { + async fn ingest_layer_from(&self, other: &Self, hash: Sha256Hash) -> Result<(), KolmeError> { enum Work { Process(Sha256Hash), Write(Box), @@ -1094,7 +1241,7 @@ impl Kolme { let layer = other .get_merkle_layer(hash) .await? - .with_context(|| format!("Missing layer {hash} in source store"))?; + .ok_or_else(|| KolmeCoreError::MissingMerkleLayer { hash })?; let children = layer.children.clone(); work_queue.push(Work::Write(Box::new(layer))); for child in children { @@ -1136,11 +1283,14 @@ impl Kolme { pub async fn get_block( &self, height: BlockHeight, - ) -> Result>>> { + ) -> Result>>, KolmeStoreError> { self.inner.store.load_block(height).await } - pub async fn get_framework(&self, hash: Sha256Hash) -> Result { + pub async fn get_framework( + &self, + hash: Sha256Hash, + ) -> Result { let result = self.inner.store.load(hash).await?; Ok(result) } @@ -1156,12 +1306,15 @@ impl Kolme { } /// Get the block height for the given transaction, if present. - pub async fn get_tx_height(&self, tx: TxHash) -> Result> { + pub async fn get_tx_height(&self, tx: TxHash) -> Result, KolmeStoreError> { self.inner.store.get_height_for_tx(tx).await } /// Get the block containing the given transaction, if present. - pub async fn get_tx_block(&self, tx: TxHash) -> Result>>> { + pub async fn get_tx_block( + &self, + tx: TxHash, + ) -> Result>>, KolmeError> { let Some(height) = self.get_tx_height(tx).await? else { return Ok(None); }; @@ -1172,29 +1325,29 @@ impl Kolme { pub async fn load_block( &self, height: BlockHeight, - ) -> Result>> { + ) -> Result>, KolmeError> { self.get_block(height) .await? .ok_or(KolmeStoreError::BlockNotFound { height: height.0 }.into()) } /// Marks the current block to not be resynced by the Archiver - pub async fn archive_block(&self, height: BlockHeight) -> Result<()> { - self.inner + pub async fn archive_block(&self, height: BlockHeight) -> Result<(), KolmeError> { + Ok(self + .inner .store .archive_block(height) .await - .with_context(|| format!("Unable to mark block {} as archived", height.0)) + .map_err(|e| KolmeCoreError::ArchiveBlockFailed { height, source: e })?) } /// Obtains the latest block synced by the Archiver, if it exists - pub async fn get_latest_archived_block(&self) -> Result> { + pub async fn get_latest_archived_block(&self) -> Result, KolmeStoreError> { Ok(self .inner .store .get_latest_archived_block_height() - .await - .context("Unable to retrieve latest archived block height")? + .await? .map(BlockHeight)) } @@ -1202,7 +1355,7 @@ impl Kolme { /// /// This will report errors during data load and then return the earliest /// block height, essentially restarting the archive process. - pub async fn get_next_to_archive(&self) -> Result { + pub async fn get_next_to_archive(&self) -> Result { let mut next = self .get_latest_archived_block() .await? @@ -1217,7 +1370,7 @@ impl Kolme { Ok(next) } - async fn init_remote_data_listener(&self) -> Result<()> { + async fn init_remote_data_listener(&self) -> Result<(), KolmeError> { let Some(mut listener) = self.inner.store.listen_remote_data().await? else { return Ok(()); }; @@ -1282,7 +1435,7 @@ impl KolmeRead { pub fn get_next_bridge_action( &self, chain: ExternalChain, - ) -> Result> { + ) -> Result, KolmeError> { Ok(self .get_framework_state() .chains @@ -1338,7 +1491,7 @@ impl KolmeRead { &self, secret: &SecretKey, tx_builder: T, - ) -> Result> { + ) -> Result, KolmeError> { let pubkey = secret.public_key(); let nonce = self.get_next_nonce(pubkey).1; self.create_signed_transaction_with(secret, tx_builder, pubkey, nonce) @@ -1350,7 +1503,7 @@ impl KolmeRead { tx_builder: T, pubkey: PublicKey, nonce: AccountNonce, - ) -> Result> { + ) -> Result, KolmeError> { let TxBuilder { messages, max_height, diff --git a/packages/kolme/src/core/kolme/block_info.rs b/packages/kolme/src/core/kolme/block_info.rs index f3439551..215a267b 100644 --- a/packages/kolme/src/core/kolme/block_info.rs +++ b/packages/kolme/src/core/kolme/block_info.rs @@ -48,7 +48,7 @@ impl MaybeBlockInfo { } } - pub(super) async fn load(store: &KolmeStore, app: &App) -> Result { + pub(super) async fn load(store: &KolmeStore, app: &App) -> Result { // Use a JoinSet for convenient, so that the task will be canceled when the set // is dropped. let mut set = JoinSet::new(); @@ -63,11 +63,10 @@ impl MaybeBlockInfo { }); let res = match output { Some(height) => { - let storable = store.load_block(height).await?.with_context(|| { - format!( - "Latest block height is {height}, but it wasn't found in the data store" - ) - })?; + let storable = store + .load_block(height) + .await? + .ok_or_else(|| KolmeError::BlockMissingInStore { height })?; let (framework_state, app_state) = tokio::try_join!( store.load(storable.block.as_inner().framework_state), store.load(storable.block.as_inner().app_state) diff --git a/packages/kolme/src/core/kolme/import_export.rs b/packages/kolme/src/core/kolme/import_export.rs index 47cfcbec..b5f8d95f 100644 --- a/packages/kolme/src/core/kolme/import_export.rs +++ b/packages/kolme/src/core/kolme/import_export.rs @@ -11,12 +11,42 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; use crate::*; +#[derive(thiserror::Error, Debug)] +pub enum KolmeImportExportError { + #[error("Child hash {child} was not previously written")] + ChildHashNotPreviouslyWritten { child: Sha256Hash }, + + #[error("Merkle hash {child} not found in Merkle store for parent {parent}")] + MissingMerkleHashInStore { + child: Sha256Hash, + parent: Sha256Hash, + }, + + #[error("Missing layer {hash}")] + MissingLayer { hash: Sha256Hash }, + + #[error("Logic error: writing layer {parent} but its child {child} is not yet written")] + LogicChildNotYetWritten { + parent: Sha256Hash, + child: Sha256Hash, + }, + + #[error("Import blocks failed, found unexpected byte {byte}")] + UnexpectedByte { byte: u8 }, + + #[error("Payload is too large")] + PayloadTooLarge, + + #[error("Too many children")] + TooManyChildren, +} + impl Kolme { pub async fn export_blocks_to, R: RangeBounds>( &self, dest: P, range: R, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let dest = tokio::fs::File::create(dest).await?; let mut dest = BufWriter::new(dest); let mut curr = match range.start_bound() { @@ -44,7 +74,7 @@ impl Kolme { dest: &mut BufWriter, written_layers: &mut std::collections::HashSet, height: BlockHeight, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let block = self.load_block(height).await?; enum Work { @@ -66,7 +96,7 @@ impl Kolme { let layer = self .get_merkle_layer(hash) .await? - .with_context(|| format!("Missing layer {hash}"))?; + .ok_or(KolmeImportExportError::MissingLayer { hash })?; let children = layer.children.clone(); work_queue.push(Work::Write(hash, Box::new(layer))); for child in children { @@ -88,7 +118,7 @@ impl Kolme { Ok(()) } - pub async fn import_blocks_from>(&self, src: P) -> Result<()> { + pub async fn import_blocks_from>(&self, src: P) -> Result<(), KolmeError> { let src = tokio::fs::File::open(src).await?; let mut src = tokio::io::BufReader::new(src); let mut hashes = HashSet::new(); @@ -118,12 +148,22 @@ impl Kolme { let mut buff = [0u8; 32]; src.read_exact(&mut buff).await?; let child = Sha256Hash::from_array(buff); - anyhow::ensure!( - hashes.contains(&child), - "Child hash {} was not previously written.", - child - ); - anyhow::ensure!(self.has_merkle_hash(child).await?, "Merkle hash {child} of a child not found in Merkle store for parent {}", payload.hash()); + + if !hashes.contains(&child) { + return Err(KolmeImportExportError::ChildHashNotPreviouslyWritten { + child, + } + .into()); + } + + let parent = payload.hash(); + if !self.has_merkle_hash(child).await? { + return Err(KolmeImportExportError::MissingMerkleHashInStore { + child, + parent, + } + .into()); + } children.push(child); } let hash = payload.hash(); @@ -145,7 +185,7 @@ impl Kolme { self.add_block_with_state(block).await?; } } - b => anyhow::bail!("Import blocks failed, found unexpected byte {b}"), + b => return Err(KolmeImportExportError::UnexpectedByte { byte: b }.into()), } } } @@ -156,18 +196,25 @@ async fn write_layer( hash: Sha256Hash, layer: &MerkleLayerContents, written_layers: &mut HashSet, -) -> Result<()> { +) -> Result<(), KolmeError> { dest.write_u8(0).await?; - dest.write_u32(u32::try_from(layer.payload.len()).context("Payload is too large")?) - .await?; + dest.write_u32( + u32::try_from(layer.payload.len()).map_err(|_| KolmeImportExportError::PayloadTooLarge)?, + ) + .await?; dest.write_all(layer.payload.bytes()).await?; - dest.write_u32(u32::try_from(layer.children.len()).context("Too many children")?) - .await?; + dest.write_u32( + u32::try_from(layer.children.len()).map_err(|_| KolmeImportExportError::TooManyChildren)?, + ) + .await?; for child in &layer.children { - anyhow::ensure!( - written_layers.contains(child), - "Logic error, writing layer {hash} but its child {child} is not yet written" - ); + if !written_layers.contains(child) { + return Err(KolmeImportExportError::LogicChildNotYetWritten { + parent: hash, + child: *child, + } + .into()); + } dest.write_all(child.as_array()).await?; } written_layers.insert(hash); @@ -177,7 +224,7 @@ async fn write_layer( async fn write_block( dest: &mut BufWriter, block: &SignedBlock, -) -> Result<()> { +) -> Result<(), KolmeError> { let serialized = serde_json::to_vec(block)?; dest.write_u8(1).await?; dest.write_u32(u32::try_from(serialized.len())?).await?; diff --git a/packages/kolme/src/core/kolme/store.rs b/packages/kolme/src/core/kolme/store.rs index 51ae6e3c..1ed20893 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -36,22 +36,24 @@ impl From for KolmeStore { } impl KolmeStore { - pub async fn new_postgres(url: &str) -> Result { + pub async fn new_postgres(url: &str) -> Result { KolmeStoreInner::new_postgres(url) .await .map(KolmeStore::from) + .map_err(KolmeError::from) } pub async fn new_postgres_with_options( connect: PgConnectOptions, options: PoolOptions, - ) -> Result { + ) -> Result { KolmeStoreInner::new_postgres_with_options(connect, options) .await .map(KolmeStore::from) + .map_err(KolmeError::from) } - pub fn new_fjall(dir: impl AsRef) -> Result { + pub fn new_fjall(dir: impl AsRef) -> Result { KolmeStoreInner::new_fjall(dir).map(KolmeStore::from) } @@ -60,25 +62,32 @@ impl KolmeStore { } /// Ensures that either we have no blocks yet, or the first block has matching genesis info. - pub(super) async fn validate_genesis_info(&self, expected: &GenesisInfo) -> Result<()> { + pub(super) async fn validate_genesis_info( + &self, + expected: &GenesisInfo, + ) -> Result<(), KolmeError> { if let Some(actual) = self.load_genesis_info().await? { - anyhow::ensure!( - &actual == expected, - "Mismatched genesis info.\nActual: {actual:?}\nExpected: {expected:?}" - ); + if &actual != expected { + return Err(KolmeError::MismatchedGenesisInfo { + actual, + expected: expected.clone(), + }); + } } Ok(()) } - async fn load_genesis_info(&self) -> Result> { + async fn load_genesis_info(&self) -> Result, KolmeError> { let Some(block) = self.load_signed_block(BlockHeight::start()).await? else { return Ok(None); }; let messages = &block.tx().0.message.as_inner().messages; - anyhow::ensure!(messages.len() == 1); + if messages.len() != 1 { + return Err(KolmeError::InvalidGenesisMessageCount); + } match messages.first().unwrap() { Message::Genesis(genesis_info) => Ok(Some(genesis_info.clone())), - _ => Err(anyhow::anyhow!("Invalid messages in first block")), + _ => Err(KolmeError::InvalidFirstBlockMessageType), } } @@ -88,7 +97,7 @@ impl KolmeStore { self.inner.clear_blocks().await } - pub(crate) async fn take_construct_lock(&self) -> Result { + pub(crate) async fn take_construct_lock(&self) -> Result { Ok(self.inner.take_construct_lock().await?) } @@ -101,37 +110,44 @@ impl KolmeStore { self.inner.has_merkle_hash(hash).await } - pub(crate) async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> Result<()> { + pub(crate) async fn add_merkle_layer( + &self, + layer: &MerkleLayerContents, + ) -> Result<(), KolmeStoreError> { for child in &layer.children { - anyhow::ensure!(self.has_merkle_hash(*child).await?); + if !self.has_merkle_hash(*child).await? { + return Err(KolmeStoreError::MissingMerkleChild { child: *child }); + } } self.inner.add_merkle_layer(layer).await } - pub(crate) async fn archive_block(&self, height: BlockHeight) -> Result<()> { + pub(crate) async fn archive_block(&self, height: BlockHeight) -> Result<(), KolmeStoreError> { self.inner.archive_block(height.0).await } - pub(crate) async fn get_latest_archived_block_height(&self) -> Result> { + pub(crate) async fn get_latest_archived_block_height( + &self, + ) -> Result, KolmeStoreError> { self.inner.get_latest_archived_block_height().await } } impl KolmeStore { - pub async fn load_latest_block(&self) -> Result> { + pub async fn load_latest_block(&self) -> Result, KolmeError> { Ok(self.inner.load_latest_block().await?.map(BlockHeight)) } pub async fn load_block( &self, height: BlockHeight, - ) -> Result>>> { + ) -> Result>>, KolmeStoreError> { if let Some(storable) = self.block_cache.read().peek(&height) { return Ok(Some(storable.clone())); } - Ok(self.inner.load_block(height.0).await?) + self.inner.load_block(height.0).await } pub async fn has_block(&self, height: BlockHeight) -> Result { @@ -145,7 +161,7 @@ impl KolmeStore { pub async fn load_signed_block( &self, height: BlockHeight, - ) -> Result>>> { + ) -> Result>>, KolmeError> { if let Some(storable) = self.block_cache.read().peek(&height) { return Ok(Some(storable.block.clone())); } @@ -156,7 +172,10 @@ impl KolmeStore { .await?) } - pub(super) async fn get_height_for_tx(&self, txhash: TxHash) -> Result> { + pub(super) async fn get_height_for_tx( + &self, + txhash: TxHash, + ) -> Result, KolmeStoreError> { Ok(self .inner .get_height_for_tx(txhash.0) @@ -167,7 +186,7 @@ impl KolmeStore { pub(super) async fn add_block( &self, block: StorableBlock>, - ) -> Result<()> { + ) -> Result<(), KolmeStoreError> { let insertion_result = self.inner.add_block(&block).await; match insertion_result { Err(KolmeStoreError::MatchingBlockAlreadyInserted { .. }) | Ok(_) => { @@ -179,12 +198,15 @@ impl KolmeStore { Ok(()) } - Err(e) => Err(e.into()), + Err(e) => Err(e), } } /// Save data to the merkle store. - pub async fn save(&self, value: &T) -> Result { + pub async fn save( + &self, + value: &T, + ) -> Result { self.inner.save(value).await } diff --git a/packages/kolme/src/core/kolme_app.rs b/packages/kolme/src/core/kolme_app.rs index 101a19e6..a4431e63 100644 --- a/packages/kolme/src/core/kolme_app.rs +++ b/packages/kolme/src/core/kolme_app.rs @@ -24,14 +24,26 @@ pub trait KolmeApp: Send + Sync + Clone + 'static { fn genesis_info(&self) -> &GenesisInfo; /// Generate a blank state. - fn new_state(&self) -> Result; + fn new_state(&self) -> Result; /// Execute a message. fn execute( &self, ctx: &mut ExecutionContext, msg: &Self::Message, - ) -> impl std::future::Future> + Send; + ) -> impl std::future::Future> + Send; +} + +#[derive(thiserror::Error, Debug)] +pub enum KolmeDataError { + #[error("Data validation failed")] + ValidationFailed, + + #[error("Data mismatch")] + Mismatch, + + #[error("Invalid data request")] + InvalidRequest, } pub trait KolmeDataRequest: @@ -41,9 +53,9 @@ pub trait KolmeDataRequest: /// Do an initial load of the data #[allow(async_fn_in_trait)] - async fn load(self, app: &App) -> Result; + async fn load(self, app: &App) -> Result; /// Validate previously loaded data #[allow(async_fn_in_trait)] - async fn validate(self, app: &App, prev_res: &Self::Response) -> Result<()>; + async fn validate(self, app: &App, prev_res: &Self::Response) -> Result<(), KolmeDataError>; } diff --git a/packages/kolme/src/core/state.rs b/packages/kolme/src/core/state.rs index 9be777e0..0cf34337 100644 --- a/packages/kolme/src/core/state.rs +++ b/packages/kolme/src/core/state.rs @@ -69,7 +69,7 @@ impl ExecutionContext<'_, App> { payload: ProposalPayload, pubkey: PublicKey, sigrec: SignatureWithRecovery, - ) -> Result<()> { + ) -> Result<(), KolmeError> { // Check to ensure we don't already have this proposal. for (id, existing) in &self .framework_state() @@ -77,10 +77,9 @@ impl ExecutionContext<'_, App> { .as_ref() .proposals { - anyhow::ensure!( - existing.payload != payload, - "Identical proposal {id} already exists" - ); + if existing.payload == payload { + return Err(KolmeError::DuplicateAdminProposal { id: *id }); + } } let state = self.framework_state_mut().admin_proposal_state.as_mut(); diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 55f0ac6e..817b0b41 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -24,6 +24,8 @@ use crate::*; pub use accounts::{Account, Accounts, AccountsError}; pub use error::KolmeError; +pub use error::KolmeExecutionError; +pub use error::TransactionError; #[cfg(feature = "solana")] /// Wrapper around the Solana RPC client to hide sensitive information. @@ -67,10 +69,11 @@ impl ToMerkleKey for ExternalChain { impl FromMerkleKey for ExternalChain { fn from_merkle_key(bytes: &[u8]) -> Result { - std::str::from_utf8(bytes) - .map_err(MerkleSerialError::custom)? - .parse() - .map_err(MerkleSerialError::custom) + let s = std::str::from_utf8(bytes)?; + s.parse() + .map_err(|_| MerkleSerialError::InvalidExternalChain { + value: s.to_string(), + }) } } @@ -137,7 +140,7 @@ impl CosmosChain { } #[cfg(feature = "cosmwasm")] - pub async fn make_client(self) -> Result { + pub async fn make_client(self) -> Result { let network = match self { Self::OsmosisTestnet => cosmos::CosmosNetwork::OsmosisTestnet, Self::NeutronTestnet => cosmos::CosmosNetwork::NeutronTestnet, @@ -197,12 +200,12 @@ impl SolanaClientEndpoint { }) } - pub async fn make_pubsub_client(self) -> Result { + pub async fn make_pubsub_client(self) -> Result { match self { SolanaClientEndpoint::Static(url) => SolanaPubsubClient::new(url).await, SolanaClientEndpoint::Arc(url) => SolanaPubsubClient::new(&url).await, } - .map_err(anyhow::Error::from) + .map_err(KolmeError::SolanaPubsubError) } } @@ -292,10 +295,11 @@ impl MerkleDeserialize for ExternalChain { deserializer: &mut MerkleDeserializer, _version: usize, ) -> Result { - deserializer - .load_str()? - .parse() - .map_err(MerkleSerialError::custom) + let s = deserializer.load_str()?; + s.parse() + .map_err(|_| MerkleSerialError::InvalidExternalChain { + value: s.to_string(), + }) } } @@ -346,19 +350,23 @@ pub struct ChainState { } impl ChainState { - pub(crate) fn deposit(&mut self, asset_id: AssetId, amount: Decimal) -> Result<()> { + pub(crate) fn deposit(&mut self, asset_id: AssetId, amount: Decimal) -> Result<(), KolmeError> { let old = self.assets.entry(asset_id).or_default(); - *old = old.checked_add(amount).with_context(|| { - format!("Overflow while depositing asset {asset_id}, amount == {amount}") - })?; + *old = old + .checked_add(amount) + .ok_or(KolmeError::OverflowWhileDepositing { asset_id, amount })?; Ok(()) } - pub(crate) fn withdraw(&mut self, asset_id: AssetId, amount: Decimal) -> Result<()> { + pub(crate) fn withdraw( + &mut self, + asset_id: AssetId, + amount: Decimal, + ) -> Result<(), KolmeError> { let old = self.assets.entry(asset_id).or_default(); - *old = old.checked_sub(amount).with_context(|| { - format!("Insufficient funds while withdrawing asset {asset_id}, amount == {amount}") - })?; + *old = old + .checked_sub(amount) + .ok_or(KolmeError::InsufficientFundsWhileWithdrawing { asset_id, amount })?; Ok(()) } } @@ -781,13 +789,22 @@ impl MerkleDeserializeRaw for AccountNonce { } impl TryFrom for AccountNonce { - type Error = anyhow::Error; + type Error = KolmeError; - fn try_from(value: i64) -> Result { + fn try_from(value: i64) -> Result { Ok(AccountNonce(value.try_into()?)) } } +#[derive(thiserror::Error, Debug)] +pub enum BlockHeightError { + #[error("Invalid block height: start={start}, end={end}")] + InvalidBlockHeight { + start: BlockHeight, + end: BlockHeight, + }, +} + /// Height of a block #[derive( serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash, Debug, @@ -810,9 +827,12 @@ impl BlockHeight { self.0 == 0 } - pub fn increasing_middle(&self, block_height: BlockHeight) -> Result { + pub fn increasing_middle( + &self, + block_height: BlockHeight, + ) -> Result { if self.0 >= block_height.0 { - return Err(KolmeError::InvalidBlockHeight { + return Err(BlockHeightError::InvalidBlockHeight { start: *self, end: block_height, }); @@ -833,10 +853,10 @@ impl Display for BlockHeight { } impl TryFrom for BlockHeight { - type Error = anyhow::Error; + type Error = KolmeError; - fn try_from(value: i64) -> Result { - value.try_into().map_err(anyhow::Error::from).map(Self) + fn try_from(value: i64) -> Result { + value.try_into().map_err(KolmeError::from).map(Self) } } @@ -911,9 +931,16 @@ impl MerkleDeserializeRaw for Wallet { pub struct SignedBlock(pub SignedTaggedJson>); impl SignedBlock { - pub fn validate_signature(&self) -> Result<()> { + pub fn validate_signature(&self) -> Result<(), KolmeError> { let pubkey = self.0.verify_signature()?; - anyhow::ensure!(pubkey == self.0.message.as_inner().processor); + let expected = self.0.message.as_inner().processor; + if pubkey != expected { + return Err(KolmeError::InvalidBlockProcessorSignature { + expected: Box::new(expected), + actual: Box::new(pubkey), + }); + } + Ok(()) } @@ -1025,9 +1052,19 @@ pub struct Block { pub struct SignedTransaction(pub SignedTaggedJson>); impl SignedTransaction { - pub fn validate_signature(&self) -> Result<()> { - let pubkey = self.0.verify_signature()?; - anyhow::ensure!(pubkey == self.0.message.as_inner().pubkey); + pub fn validate_signature(&self) -> Result<(), KolmeError> { + let pubkey = self + .0 + .verify_signature() + .map_err(|_| KolmeError::SignatureVerificationFailed)?; + let expected = self.0.message.as_inner().pubkey; + if pubkey != expected { + return Err(KolmeError::InvalidTransactionSignature { + expected: Box::new(expected), + actual: Box::new(pubkey), + }); + } + Ok(()) } } @@ -1040,15 +1077,22 @@ impl SignedTransaction { } impl Transaction { - pub fn ensure_is_genesis(&self) -> Result<()> { - anyhow::ensure!(self.messages.len() == 1); - anyhow::ensure!(matches!(self.messages[0], Message::Genesis(_))); + pub fn ensure_is_genesis(&self) -> Result<(), KolmeError> { + if self.messages.len() != 1 { + return Err(KolmeError::InvalidGenesisTransaction); + } + + if !matches!(self.messages[0], Message::Genesis(_)) { + return Err(KolmeError::InvalidGenesisTransaction); + } Ok(()) } - pub fn ensure_no_genesis(&self) -> Result<()> { + pub fn ensure_no_genesis(&self) -> Result<(), KolmeError> { for msg in &self.messages { - anyhow::ensure!(!matches!(msg, Message::Genesis(_))); + if matches!(msg, Message::Genesis(_)) { + return Err(KolmeError::InvalidGenesisTransaction); + } } Ok(()) } @@ -1065,7 +1109,7 @@ pub struct Transaction { } impl Transaction { - pub fn sign(self, key: &SecretKey) -> Result> { + pub fn sign(self, key: &SecretKey) -> Result, KolmeError> { Ok(SignedTransaction(TaggedJson::new(self)?.sign(key)?)) } } @@ -1277,7 +1321,7 @@ impl AdminMessage { validator_type: ValidatorType, replacement: PublicKey, current: &SecretKey, - ) -> Result { + ) -> Result { let self_replace = SelfReplace { validator_type, replacement, @@ -1287,7 +1331,7 @@ impl AdminMessage { Ok(AdminMessage::SelfReplace(Box::new(signed))) } - pub fn new_set(set: ValidatorSet, proposer: &SecretKey) -> Result { + pub fn new_set(set: ValidatorSet, proposer: &SecretKey) -> Result { let json = TaggedJson::new(set)?; let signed = json.sign(proposer)?; Ok(AdminMessage::NewSet { @@ -1295,7 +1339,10 @@ impl AdminMessage { }) } - pub fn upgrade(desired_version: impl Into, proposer: &SecretKey) -> Result { + pub fn upgrade( + desired_version: impl Into, + proposer: &SecretKey, + ) -> Result { let json = TaggedJson::new(Upgrade { desired_version: desired_version.into(), })?; @@ -1307,7 +1354,7 @@ impl AdminMessage { admin_proposal_id: AdminProposalId, payload: &ProposalPayload, validator: &SecretKey, - ) -> Result { + ) -> Result { let signature = validator.sign_recoverable(payload.as_bytes())?; Ok(AdminMessage::Approve { admin_proposal_id, @@ -1391,7 +1438,7 @@ pub struct GenesisInfo { } impl GenesisInfo { - pub fn validate(&self) -> Result<()> { + pub fn validate(&self) -> Result<(), KolmeError> { self.validator_set.validate()?; Ok(()) } @@ -1448,14 +1495,16 @@ pub struct ConfiguredChains(pub(crate) BTreeMap); impl ConfiguredChains { #[cfg(feature = "solana")] - pub fn insert_solana(&mut self, chain: SolanaChain, config: ChainConfig) -> Result<()> { + pub fn insert_solana( + &mut self, + chain: SolanaChain, + config: ChainConfig, + ) -> Result<(), KolmeError> { use kolme_solana_bridge_client::pubkey::Pubkey; match &config.bridge { BridgeContract::NeededCosmosBridge { .. } => { - return Err(anyhow::anyhow!( - "Trying to configure a Cosmos contract as a Solana bridge." - )) + return Err(KolmeError::CosmosBridgeConfiguredAsSolana); } BridgeContract::NeededSolanaBridge { program_id } => Pubkey::from_str(program_id)?, BridgeContract::Deployed(program_id) => Pubkey::from_str(program_id)?, @@ -1467,14 +1516,16 @@ impl ConfiguredChains { } #[cfg(feature = "cosmwasm")] - pub fn insert_cosmos(&mut self, chain: CosmosChain, config: ChainConfig) -> Result<()> { + pub fn insert_cosmos( + &mut self, + chain: CosmosChain, + config: ChainConfig, + ) -> Result<(), KolmeError> { use cosmos::Address; match &config.bridge { BridgeContract::NeededSolanaBridge { .. } => { - return Err(anyhow::anyhow!( - "Trying to configure a Solana program as a Cosmos bridge." - )) + return Err(KolmeError::SolanaBridgeConfiguredAsCosmos); } BridgeContract::NeededCosmosBridge { .. } => (), BridgeContract::Deployed(program_id) => { @@ -1488,24 +1539,20 @@ impl ConfiguredChains { } #[cfg(feature = "pass_through")] - pub fn insert_pass_through(&mut self, config: ChainConfig) -> Result<()> { + pub fn insert_pass_through(&mut self, config: ChainConfig) -> Result<(), KolmeError> { if let BridgeContract::Deployed(_) = config.bridge { if self .0 .get(&ExternalChain::PassThrough) .is_some_and(|existing| *existing != config) { - Err(anyhow::anyhow!( - "Multiple pass-through bridges are not supported" - )) + Err(KolmeError::MultiplePassThroughBridgesUnsupported) } else { self.0.insert(ExternalChain::PassThrough, config); Ok(()) } } else { - Err(anyhow::anyhow!( - "Pass-through bridge can't require Cosmos or Solana bridge contract" - )) + Err(KolmeError::InvalidPassThroughBridgeType) } } } @@ -1548,7 +1595,7 @@ impl ExecAction { chain: ExternalChain, config: &ChainConfig, id: BridgeActionId, - ) -> Result { + ) -> Result { #[cfg(feature = "cosmwasm")] use shared::cosmos; #[cfg(feature = "solana")] @@ -1571,7 +1618,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == *id) - .context("Unsupported asset ID")? + .ok_or(KolmeError::UnsupportedAssetId)? .0; let denom = denom.0.clone(); @@ -1603,8 +1650,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == coin.id) - .context("Unsupported asset ID")?; - + .ok_or(KolmeError::UnsupportedAssetId)?; coins.push((&asset.0 .0, coin.amount)); } @@ -1721,9 +1767,9 @@ impl ExecAction { ExecAction::MigrateContract { migrate_contract } => { let contract_addr = match &config.bridge { BridgeContract::Deployed(addr) => addr.clone(), - _ => anyhow::bail!( - "Unable to migrate contract for chain {chain}: contract isn't deployed" - ), + _ => { + return Err(KolmeError::ContractNotDeployed { chain }); + } }; match migrate_contract.as_inner() { @@ -1773,13 +1819,11 @@ impl ExecAction { } #[cfg(feature = "solana")] -fn serialize_solana_payload(payload: &shared::solana::Payload) -> Result { - let len = borsh::object_length(&payload) - .map_err(|x| anyhow::anyhow!("Error serializing Solana bridge payload: {:?}", x))?; +fn serialize_solana_payload(payload: &shared::solana::Payload) -> Result { + let len = borsh::object_length(&payload)?; let mut buf = Vec::with_capacity(len); - borsh::BorshSerialize::serialize(&payload, &mut buf) - .map_err(|x| anyhow::anyhow!("Error serializing Solana bridge payload: {:?}", x))?; + borsh::BorshSerialize::serialize(&payload, &mut buf)?; let payload = base64::engine::general_purpose::STANDARD.encode(&buf); @@ -1797,7 +1841,7 @@ pub struct FailedTransaction { pub txhash: TxHash, /// Block height we attempted to generate. pub proposed_height: BlockHeight, - pub error: KolmeError, + pub error: TransactionError, } impl Display for FailedTransaction { @@ -1863,15 +1907,21 @@ impl SolanaClient { Self(SolanaRpcClient::new(url)) } - pub async fn with_redacted_error<'client, F, Fut, T>(&'client self, func: F) -> Result + pub async fn with_redacted_error<'client, F, Fut, T>( + &'client self, + func: F, + ) -> Result where F: FnOnce(&'client SolanaRpcClient) -> Fut, Fut: std::future::Future> + 'client, { - func(&self.0).await.map_err(redact_solana_error) + Ok(func(&self.0).await.map_err(redact_solana_error)?) } - pub async fn get_account(&self, pubkey: &SolanaPubkey) -> Result { + pub async fn get_account( + &self, + pubkey: &SolanaPubkey, + ) -> Result { self.with_redacted_error(|client| client.get_account(pubkey)) .await } @@ -1879,8 +1929,10 @@ impl SolanaClient { pub async fn get_signatures_for_address( &self, address: &SolanaPubkey, - ) -> Result> - { + ) -> Result< + Vec, + KolmeError, + > { self.with_redacted_error(|client| client.get_signatures_for_address(address)) .await } @@ -1889,13 +1941,15 @@ impl SolanaClient { &self, signature: &SolanaSignature, encoding: solana_transaction_status_client_types::UiTransactionEncoding, - ) -> Result - { + ) -> Result< + solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta, + KolmeError, + > { self.with_redacted_error(|client| client.get_transaction(signature, encoding)) .await } - pub async fn get_latest_blockhash(&self) -> Result { + pub async fn get_latest_blockhash(&self) -> Result { self.with_redacted_error(|client| client.get_latest_blockhash()) .await } @@ -1903,7 +1957,7 @@ impl SolanaClient { pub async fn send_and_confirm_transaction( &self, transaction: &impl solana_rpc_client::rpc_client::SerializableTransaction, - ) -> Result { + ) -> Result { self.with_redacted_error(|client| client.send_and_confirm_transaction(transaction)) .await } diff --git a/packages/kolme/src/core/types/accounts.rs b/packages/kolme/src/core/types/accounts.rs index c4fdced1..76e81edf 100644 --- a/packages/kolme/src/core/types/accounts.rs +++ b/packages/kolme/src/core/types/accounts.rs @@ -22,6 +22,48 @@ pub enum AccountsError { asset_id: AssetId, to_burn: Decimal, }, + + #[error("Pubkey {key} already in use")] + PubkeyAlreadyInUse { key: Box }, + + #[error("Wallet {wallet} already in use")] + WalletAlreadyInUse { wallet: Wallet }, + + #[error( + "Cannot remove pubkey {key} from account {id}, it's actually connected to {actual_id}" + )] + PubkeyAccountMismatch { + key: Box, + id: AccountId, + actual_id: AccountId, + }, + + #[error( + "Cannot remove wallet {wallet} from account {id}, it's actually connected to {actual_id}" + )] + WalletAccountMismatch { + wallet: Wallet, + id: AccountId, + actual_id: AccountId, + }, + + #[error( + "New account for pubkey {pubkey} expects an initial nonce of {expected}, received {actual}" + )] + InvalidInitialNonce { + pubkey: Box, + expected: AccountNonce, + actual: AccountNonce, + }, + + #[error("Account {account_id} not found")] + AccountNotFound { account_id: AccountId }, + + #[error("Pubkey {key} not found")] + PubkeyNotFound { key: Box }, + + #[error("Wallet {wallet} not found")] + WalletNotFound { wallet: Wallet }, } /// Track all information on accounts. @@ -153,15 +195,14 @@ impl Accounts { &mut self, account_id: AccountId, key: PublicKey, - ) -> Result<()> { - anyhow::ensure!( - !self.pubkeys.contains_key(&key), - "Pubkey {key} already in use" - ); + ) -> Result<(), AccountsError> { + if self.pubkeys.contains_key(&key) { + return Err(AccountsError::PubkeyAlreadyInUse { key: Box::new(key) }); + } let account = self .accounts .get_mut(&account_id) - .with_context(|| format!("Account ID {account_id} not found"))?; + .ok_or(AccountsError::AccountNotFound { account_id })?; self.pubkeys.insert(key, account_id); account.pubkeys.insert(key); Ok(()) @@ -233,15 +274,19 @@ impl Accounts { &mut self, id: AccountId, key: PublicKey, - ) -> Result<()> { + ) -> Result<(), AccountsError> { let (_, actual_id) = self .pubkeys .remove(&key) - .with_context(|| format!("Cannot remove unknown pubkey {key}"))?; - anyhow::ensure!( - id == actual_id, - "Cannot remove pubkey {key} from account {id}, it's actually connected to {actual_id}" - ); + .ok_or(AccountsError::PubkeyNotFound { key: Box::new(key) })?; + if id != actual_id { + return Err(AccountsError::PubkeyAccountMismatch { + key: Box::new(key), + id, + actual_id, + }); + } + let was_present = self.accounts.get_mut(&id).unwrap().pubkeys.remove(&key); assert!(was_present); Ok(()) @@ -251,15 +296,17 @@ impl Accounts { &mut self, account_id: AccountId, wallet: &Wallet, - ) -> Result<()> { - anyhow::ensure!( - !self.wallets.contains_key(wallet), - "Wallet {wallet} already in use" - ); + ) -> Result<(), AccountsError> { + if self.wallets.contains_key(wallet) { + return Err(AccountsError::WalletAlreadyInUse { + wallet: wallet.clone(), + }); + } + let account = self .accounts .get_mut(&account_id) - .with_context(|| format!("Account ID {account_id} not found"))?; + .ok_or(AccountsError::AccountNotFound { account_id })?; self.wallets.insert(wallet.clone(), account_id); account.wallets.insert(wallet.clone()); Ok(()) @@ -269,12 +316,21 @@ impl Accounts { &mut self, id: AccountId, wallet: &Wallet, - ) -> Result<()> { + ) -> Result<(), AccountsError> { let (_, actual_id) = self .wallets .remove(wallet) - .with_context(|| format!("Cannot remove unknown wallet {wallet}"))?; - anyhow::ensure!(id == actual_id, "Cannot remove wallet {wallet} from account {id}, it's actually connected to {actual_id}"); + .ok_or(AccountsError::WalletNotFound { + wallet: wallet.clone(), + })?; + if id != actual_id { + return Err(AccountsError::WalletAccountMismatch { + wallet: wallet.clone(), + id, + actual_id, + }); + } + let was_present = self.accounts.get_mut(&id).unwrap().wallets.remove(wallet); assert!(was_present); Ok(()) @@ -287,7 +343,7 @@ impl Accounts { &mut self, pubkey: PublicKey, nonce: AccountNonce, - ) -> Result { + ) -> Result { match self.pubkeys.get(&pubkey) { Some(account_id) => { let account = self.accounts.get_mut(account_id).unwrap(); @@ -297,8 +353,7 @@ impl Accounts { account_id: *account_id, expected: account.next_nonce, actual: nonce, - } - .into()); + }); } account.next_nonce = account.next_nonce.next(); Ok(*account_id) @@ -308,7 +363,13 @@ impl Accounts { let account = self.accounts.get_or_default(account_id); self.pubkeys.insert(pubkey, account_id); account.pubkeys.insert(pubkey); - anyhow::ensure!(nonce == account.next_nonce, "New account for pubkey {pubkey} expects an initial nonce of {}, received {nonce}", account.next_nonce); + if nonce != account.next_nonce { + return Err(KolmeError::Accounts(AccountsError::InvalidInitialNonce { + pubkey: Box::new(pubkey), + expected: account.next_nonce, + actual: nonce, + })); + } account.next_nonce = account.next_nonce.next(); Ok(account_id) } @@ -337,17 +398,21 @@ impl MerkleDeserialize for Accounts { for wallet in &account.wallets { let x = wallets.insert(wallet.clone(), *id); if let Some((_, id2)) = x { - return Err(MerkleSerialError::Other(format!( - "Wallet {wallet} used in both account {id} and {id2}" - ))); + return Err(MerkleSerialError::WalletUsedInMultipleAccounts { + wallet: wallet.to_string(), + id: id.to_string(), + other_id: id2.to_string(), + }); } } for pubkey in &account.pubkeys { let x = pubkeys.insert(*pubkey, *id); if let Some((_, id2)) = x { - return Err(MerkleSerialError::Other(format!( - "Pubkey {pubkey} used in both account {id} and {id2}" - ))); + return Err(MerkleSerialError::PubkeyUsedInMultipleAccounts { + pubkey: pubkey.to_string(), + id: id.to_string(), + other_id: id2.to_string(), + }); } } } diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index c74fbad8..6e660dc3 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -1,6 +1,14 @@ -use crate::core::*; +use crate::api_server::KolmeApiError; +use crate::listener::{cosmos::CosmosListenerError, solana::ListenerSolanaError}; +use crate::{core::*, submitter::SubmitterError}; +use cosmos::error::{AddressError, WalletError}; +use cosmos::{error::BuilderError, CosmosConfigError}; +use kolme_solana_bridge_client::pubkey::ParsePubkeyError; +use kolme_store::KolmeStoreError; +use solana_signature::ParseSignatureError; +use tokio::sync::watch::error::RecvError; -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum KolmeError { #[error("Invalid nonce provided for pubkey {pubkey}, account {account_id}. Expected: {expected}. Received: {actual}.")] InvalidNonce { @@ -9,21 +17,384 @@ pub enum KolmeError { expected: AccountNonce, actual: AccountNonce, }, - /// A transaction had a max height set, but the chain has already moved past that height. - /// - /// The `max_height` field represents the max height specified by the client. - /// `proposed_height` is the height at which we tried to add this transaction. + + #[error("Already have a bridge contract for {chain:?}, just received another from a listener")] + BridgeAlreadyDeployed { chain: ExternalChain }, + + #[error( + "Signing public key {signer} is not a member of the {role:?} set and cannot self-replace" + )] + NotInValidatorSet { + signer: Box, + role: ValidatorRole, + }, + + #[error("Signing public key {signer} is not the current processor and cannot self-replace")] + NotProcessor { signer: Box }, + + #[error("Validator self-replace signature doesn't match the current validator pubkey")] + InvalidSelfReplaceSigner, + + #[error("Tried to add block with height {received}, but next expected height is {expected}")] + UnexpectedBlockHeight { + received: BlockHeight, + expected: BlockHeight, + }, + + #[error("Tried to add block with height {height}, but it's already present in the store")] + BlockAlreadyExists { height: BlockHeight }, + + #[error("Received block signed by processor {actual_processor}, but the real processor is {expected_processor}")] + InvalidBlockProcessor { + expected_processor: Box, + actual_processor: Box, + }, + + #[error("Unable to migrate contract for chain {chain:?}: contract isn't deployed")] + ContractNotDeployed { chain: ExternalChain }, + + #[error("Already have a deployed contract on {chain:?}")] + ContractAlreadyDeployed { chain: ExternalChain }, + + #[cfg(feature = "pass_through")] + #[error("No wait for pass-through contract is expected")] + UnexpectedPassThroughContract, + + #[error("Persistent task exited unexpectedly")] + PersistentTaskExited, + + #[error("Task exited with an error: {error}")] + TaskErrored { error: String }, + + #[error("Task panicked: {details}")] + TaskPanicked { details: String }, + + #[error("Trying to configure a Cosmos contract as a Solana bridge")] + CosmosBridgeConfiguredAsSolana, + + #[error("Trying to configure a Solana program as a Cosmos bridge")] + SolanaBridgeConfiguredAsCosmos, + + #[cfg(feature = "pass_through")] + #[error("Multiple pass-through bridges are not supported")] + MultiplePassThroughBridgesUnsupported, + + #[cfg(feature = "pass_through")] + #[error("Pass-through bridge can't require Cosmos or Solana bridge contract")] + InvalidPassThroughBridgeType, + + #[error("Expected exactly one message in the first block, but found a different number")] + InvalidGenesisMessageCount, + + #[error("Invalid messages in first block")] + InvalidFirstBlockMessageType, + + #[error("Listener panicked: {details}")] + ListenerPanicked { details: String }, + + #[error("Block parent mismatch: actual {actual}, expected {expected}")] + BlockParentMismatch { + actual: BlockHash, + expected: BlockHash, + }, + + #[error("Action ID mismatch: expected {expected}, found {found}")] + ActionIdMismatch { + expected: BridgeActionId, + found: BridgeActionId, + }, + + #[error("Validator {signer} already approved proposal {proposal_id}")] + AlreadyApprovedProposal { + signer: PublicKey, + proposal_id: AdminProposalId, + }, + + #[error("Failed to execute signed Cosmos bridge transaction: {0}")] + CosmosExecutionFailed(#[from] cosmos::Error), + + #[error("API server error")] + ApiServerError(#[from] std::io::Error), + + #[error("Mismatched genesis info: actual {actual:?}, expected {expected:?}")] + MismatchedGenesisInfo { + actual: GenesisInfo, + expected: GenesisInfo, + }, + + #[error("Identical proposal {id} already exists")] + DuplicateAdminProposal { id: AdminProposalId }, + + #[error("Invalid signature: expected signer {expected}, actual {actual}")] + InvalidSignature { + expected: Box, + actual: Box, + }, + + #[error("Executed block height mismatch: expected {expected}, got {actual}")] + ExecutedHeightMismatch { + expected: BlockHeight, + actual: BlockHeight, + }, + + #[error("Submitter error: {0}")] + Submitter(#[from] SubmitterError), + + #[error("Import/export error: {0}")] + ImportExport(#[from] KolmeImportExportError), + + #[error("Core error: {0}")] + CoreError(#[from] KolmeCoreError), + + #[error("Listener error: {0}")] + ListenerError(#[from] CosmosListenerError), + + #[error("Execution error: {0}")] + ExecuteError(#[from] KolmeExecuteError), + + #[error("Execution error: {0}")] + Execution(#[from] KolmeExecutionError), + + #[error("Store error: {0}")] + StoreError(#[from] KolmeStoreError), + + #[error("API server error")] + ApiError(#[from] KolmeApiError), + + #[error(transparent)] + Secretkey(#[from] SecretKeyError), + + #[error("Transaction already in mempool")] + TxAlreadyInMempool, + + #[error("Transaction already included in block {0}")] + TxAlreadyInBlock(BlockHeight), + + #[error("Failed to serialize Solana payload to Borsh")] + SolanaPayloadSerializationError(#[source] std::io::Error), + + #[error("Failed to build Solana initialization transaction")] + SolanaInitTxBuildFailed(#[source] std::io::Error), + + #[error("Failed to create Solana pubsub client")] + SolanaPubsubError(#[from] solana_client::nonblocking::pubsub_client::PubsubClientError), + + #[error("Bridge program {program} hasn't been initialized yet")] + UninitializedSolanaBridge { program: String }, + + #[error("Error deserializing Solana bridge state: {details}")] + InvalidSolanaBridgeState { details: String }, + + #[error("Error deserializing Solana bridge message from logs: {details}")] + InvalidSolanaBridgeLogMessage { details: String }, + + #[error(transparent)] + Transaction(#[from] TransactionError), + + #[error("Broadcast receive error")] + BroadcastRecv(#[from] tokio::sync::broadcast::error::RecvError), + + #[error("Solana listener error: {0}")] + ListenerSolanaError(#[from] ListenerSolanaError), + + #[error("Address error")] + Address(#[from] AddressError), + + #[error("Action error")] + ActionError(String), + + #[error("Wallet error")] + Wallet(#[from] WalletError), + + #[error("Parse pubkey error")] + ParsePubkey(#[from] ParsePubkeyError), + + #[error("CoreState error")] + CoreState(#[from] CoreStateError), + + #[error("Asset error")] + Asset(#[from] AssetError), + + #[error("Accounts error")] + Accounts(#[from] AccountsError), + + #[error("Validator set error")] + ValidatorSet(#[from] ValidatorSetError), + + #[error("Public key error")] + PublicKey(#[from] PublicKeyError), + + #[error("Merkle serialization error")] + MerkleSerial(#[from] MerkleSerialError), + + #[error("JSON error")] + Json(#[from] serde_json::Error), + + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + + #[error(transparent)] + RecvError(#[from] RecvError), + + #[error(transparent)] + CosmosConfigError(#[from] CosmosConfigError), + + #[error(transparent)] + BuilderError(#[from] BuilderError), + + #[error(transparent)] + AxumError(#[from] axum::Error), + + #[error(transparent)] + ParseSignatureError(#[from] ParseSignatureError), + + #[error("TryFromInt error")] + TryFromInt(#[from] std::num::TryFromIntError), + + #[error("Base64 decode error")] + Base64(#[from] base64::DecodeError), + + #[error("WebSocket stream terminated")] + WebSocketClosed, + + #[error("WebSocket error")] + WebSocket(#[from] tokio_tungstenite::tungstenite::Error), + + #[error("Solana client error")] + SolanaClient(#[from] solana_client::client_error::ClientError), + + #[error("{0}")] + Other(String), + + #[error("Latest block height is {height}, but it wasn't found in the data store")] + BlockMissingInStore { height: BlockHeight }, + + #[error("Emit latest block: no blocks available")] + NoBlocksAvailable, + + #[error("Block signed by invalid processor: expected {expected}, got {actual}")] + InvalidBlockProcessorSignature { + expected: Box, + actual: Box, + }, + + #[error("Transaction signed by invalid key: expected {expected}, got {actual}")] + InvalidTransactionSignature { + expected: Box, + actual: Box, + }, + + #[error("Genesis transaction format invalid")] + InvalidGenesisTransaction, + + #[error("Failed to verify transaction signature")] + SignatureVerificationFailed, + + #[error("Overflow while depositing asset {asset_id}, amount == {amount}")] + OverflowWhileDepositing { asset_id: AssetId, amount: Decimal }, + + #[error("Insufficient funds while withdrawing asset {asset_id}, amount == {amount}")] + InsufficientFundsWhileWithdrawing { asset_id: AssetId, amount: Decimal }, + #[error("Unsupported asset ID")] + UnsupportedAssetId, +} + +impl KolmeError { + pub fn other(e: E) -> Self { + Self::Other(e.to_string()) + } +} + +impl From> for KolmeError { + fn from(e: ProposeTransactionError) -> Self { + match e { + ProposeTransactionError::InMempool => KolmeError::TxAlreadyInMempool, + + ProposeTransactionError::InBlock(block) => KolmeError::TxAlreadyInBlock(block.height()), + + ProposeTransactionError::Failed(failed) => { + KolmeError::Transaction(failed.message.as_inner().error.clone()) + } + } + } +} + +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum TransactionError { + #[error("{0}")] + Other(String), + + #[error("KolmeStoreError: {0}")] + StoreError(String), + + #[error("KolmeCoreError: {0}")] + CoreError(String), + + #[error("KolmeMerkleSerialError: {0}")] + MerkleError(String), + + #[error("Block with height {height} in database with different hash {existing}, trying to add {adding}")] + ConflictingBlockInDb { + height: u64, + adding: Sha256Hash, + existing: Sha256Hash, + }, + + #[error("Executed height mismatch: expected {expected}, got {actual}")] + ExecutedHeightMismatch { + expected: BlockHeight, + actual: BlockHeight, + }, + #[error("Transaction {txhash} has max height of {max_height}, but proposed block height is {proposed_height}")] PastMaxHeight { txhash: TxHash, max_height: BlockHeight, proposed_height: BlockHeight, }, - #[error("Start height {start} is greater than {end} height")] - InvalidBlockHeight { - start: BlockHeight, - end: BlockHeight, + + #[error("Timed out proposing transaction {txhash}")] + TimeoutProposingTx { txhash: TxHash }, + + #[error("Invalid nonce provided for pubkey {pubkey}, account {account_id}. Expected: {expected}. Received: {actual}.")] + InvalidNonce { + pubkey: Box, + account_id: AccountId, + expected: AccountNonce, + actual: AccountNonce, }, - #[error("{0}")] - Other(String), +} + +impl From for TransactionError { + fn from(err: KolmeError) -> Self { + match err { + KolmeError::ExecutedHeightMismatch { expected, actual } => { + TransactionError::ExecutedHeightMismatch { expected, actual } + } + KolmeError::StoreError(e) => TransactionError::StoreError(e.to_string()), + KolmeError::CoreError(e) => TransactionError::CoreError(e.to_string()), + KolmeError::MerkleSerial(e) => TransactionError::MerkleError(e.to_string()), + KolmeError::InvalidNonce { + pubkey, + account_id, + expected, + actual, + } => TransactionError::InvalidNonce { + pubkey: pubkey.clone(), + account_id, + expected, + actual, + }, + _ => TransactionError::Other(err.to_string()), + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum KolmeExecutionError { + #[error("Mismatched bridge event")] + MismatchedBridgeEvent, + + #[error("Unexpected bridge event ID")] + UnexpectedBridgeEventId, } diff --git a/packages/kolme/src/gossip.rs b/packages/kolme/src/gossip.rs index 4bc402cf..3dd261d4 100644 --- a/packages/kolme/src/gossip.rs +++ b/packages/kolme/src/gossip.rs @@ -144,7 +144,7 @@ impl GossipBuilder { self } - pub fn build(self, kolme: Kolme) -> Result> { + pub fn build(self, kolme: Kolme) -> Result, KolmeError> { // Create the Gossipsub topics tracing::info!( "Genesis info: {}", diff --git a/packages/kolme/src/gossip/sync_manager.rs b/packages/kolme/src/gossip/sync_manager.rs index af393a8b..d36a31ab 100644 --- a/packages/kolme/src/gossip/sync_manager.rs +++ b/packages/kolme/src/gossip/sync_manager.rs @@ -136,7 +136,7 @@ impl SyncManager { &mut self, gossip: &Gossip, height: BlockHeight, - ) -> Result<()> { + ) -> Result<(), KolmeError> { if gossip.kolme.has_block(height).await? || self.needed_blocks.contains_key(&height) { return Ok(()); } @@ -189,7 +189,7 @@ impl SyncManager { gossip: &Gossip, hash: Sha256Hash, layer: Arc, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let Some(mut entry) = self.needed_blocks.first_entry() else { return Ok(()); }; @@ -238,7 +238,7 @@ impl SyncManager { pub(super) async fn get_data_requests( &mut self, gossip: &Gossip, - ) -> Result> { + ) -> Result, KolmeError> { // We need to make sure that we are correctly performing // requests in order. That means that, in some cases, we'll // need to insert earlier block requests so that we fill in @@ -267,7 +267,7 @@ impl SyncManager { } } - async fn add_missing_needed_blocks(&mut self, gossip: &Gossip) -> Result<()> { + async fn add_missing_needed_blocks(&mut self, gossip: &Gossip) -> Result<(), KolmeError> { // First determine if we need to force sync from the beginning. match gossip.sync_mode { // State mode allows us to just grab the blocks we need. @@ -307,7 +307,7 @@ impl SyncManager { gossip: &Gossip, height: BlockHeight, waiting: &mut WaitingBlock, - ) -> Result>> { + ) -> Result>, KolmeError> { let label = DataRequest::Block(height); // Check if it got added to our store in the meanwhile if gossip.kolme.has_block(height).await? { @@ -380,7 +380,7 @@ impl SyncManager { async fn get_block_data_requests( gossip: &Gossip, pending: &mut PendingBlock, - ) -> Result>> { + ) -> Result>, KolmeError> { let mut active_count = 0; let mut layers_to_drop = SmallVec::<[Sha256Hash; DEFAULT_REQUEST_COUNT]>::new(); let mut res = SmallVec::new(); @@ -433,7 +433,7 @@ impl SyncManager { gossip: &Gossip, pending: &mut PendingBlock, hash: Sha256Hash, - ) -> Result<()> { + ) -> Result<(), KolmeError> { if gossip.kolme.has_merkle_hash(hash).await? { // Awesome, we have the hash stored, we can drop it from pending and continue upstream. pending.pending_layers.remove(&hash); diff --git a/packages/kolme/src/gossip/websockets.rs b/packages/kolme/src/gossip/websockets.rs index 4b32104b..dba0b8c5 100644 --- a/packages/kolme/src/gossip/websockets.rs +++ b/packages/kolme/src/gossip/websockets.rs @@ -16,6 +16,12 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use super::GossipMessage; use crate::*; +#[derive(thiserror::Error, Debug)] +enum WebsocketError { + #[error(transparent)] + InvalidJson(#[from] serde_json::Error), +} + pub(super) struct WebsocketsManager { tx_gossip: Sender>, rx_message: tokio::sync::mpsc::Receiver>, @@ -65,7 +71,7 @@ impl WebsocketsManager { websockets_servers: Vec, local_display_name: &str, kolme: Kolme, - ) -> Result { + ) -> Result { let tx_gossip = Sender::new(100); let (tx_message, rx_message) = tokio::sync::mpsc::channel(100); let local_display_name: Arc = local_display_name.into(); @@ -139,7 +145,7 @@ async fn launch_client_inner( tx_message: &mut tokio::sync::mpsc::Sender>, server: &str, latest: Option>>, -) -> Result<()> { +) -> Result<(), KolmeError> { let (stream, res) = tokio_tungstenite::connect_async(server).await?; tracing::debug!(%local_display_name,"launch_client_inner on {server}: got res {res:?}"); ws_helper(rx_gossip, tx_message, stream, &local_display_name, latest).await; @@ -182,7 +188,7 @@ async fn launch_server(server_state: ServerState, bind: std: async fn launch_server_inner( server_state: ServerState, bind: std::net::SocketAddr, -) -> Result<()> { +) -> Result<(), KolmeError> { let listener = tokio::net::TcpListener::bind(bind).await?; let router = axum::Router::new() .route("/", get(ws_handler_wrapper)) @@ -226,7 +232,7 @@ enum WebsocketsRecv { Close, Skip, Payload(Box>), - Err(anyhow::Error), + Err(WebsocketError), } trait WebSocketWrapper { @@ -235,7 +241,7 @@ trait WebSocketWrapper { &mut self, payload: GossipMessage, local_display_name: &str, - ) -> Result<()>; + ) -> Result<(), KolmeError>; } impl WebSocketWrapper for WebSocket { @@ -274,7 +280,7 @@ impl WebSocketWrapper for WebSocket { &mut self, payload: GossipMessage, _: &str, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let payload = serde_json::to_string(&payload)?; self.send(axum::extract::ws::Message::text(payload)).await?; Ok(()) @@ -317,7 +323,7 @@ impl WebSocketWrapper for WebSocketStream> { &mut self, payload: GossipMessage, _: &str, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let payload = serde_json::to_string(&payload)?; self.send(tokio_tungstenite::tungstenite::Message::text(payload)) .await?; diff --git a/packages/kolme/src/lib.rs b/packages/kolme/src/lib.rs index a6e0b079..b4fc7f31 100644 --- a/packages/kolme/src/lib.rs +++ b/packages/kolme/src/lib.rs @@ -31,6 +31,5 @@ pub use shared::{cryptography::*, types::*}; pub use submitter::Submitter; pub use upgrader::Upgrader; -pub(crate) use anyhow::{Context, Result}; pub(crate) use std::collections::{BTreeMap, BTreeSet}; pub(crate) use std::sync::Arc; diff --git a/packages/kolme/src/listener/cosmos.rs b/packages/kolme/src/listener/cosmos.rs index 49f85a36..48fefccd 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -3,15 +3,43 @@ use std::mem; use super::get_next_bridge_event_id; use crate::*; use ::cosmos::{Contract, Cosmos}; +use cosmos::error::AddressError; use cosmwasm_std::Coin; use shared::cosmos::{BridgeEventMessage, GetEventResp, QueryMsg}; +#[derive(thiserror::Error, Debug)] +pub enum CosmosListenerError { + #[error("Code ID mismatch: expected {expected}, actual {actual}")] + CodeId { expected: u64, actual: u64 }, + + #[error("Processor mismatch")] + Processor, + + #[error("Listeners mismatch")] + Listeners, + + #[error("Needed listeners mismatch")] + NeededListeners, + + #[error("Approvers mismatch")] + Approvers, + + #[error("Needed approvers mismatch")] + NeededApprovers, + + #[error("Invalid contract address: {0}")] + InvalidAddress(#[from] AddressError), + + #[error(transparent)] + CosmosError(#[from] cosmos::Error), +} + pub async fn listen( kolme: Kolme, secret: SecretKey, chain: CosmosChain, contract: String, -) -> Result<()> { +) -> Result<(), KolmeError> { let kolme_r = kolme.read(); let cosmos = kolme_r.get_cosmos(chain).await?; @@ -40,7 +68,7 @@ async fn listen_once( chain: CosmosChain, contract: &Contract, next_bridge_event_id: &mut BridgeEventId, -) -> Result<()> { +) -> Result<(), KolmeError> { match contract .query(&QueryMsg::GetEvent { id: *next_bridge_event_id, @@ -69,14 +97,24 @@ pub async fn sanity_check_contract( contract: &str, expected_code_id: u64, info: &GenesisInfo, -) -> Result<()> { - let contract = cosmos.make_contract(contract.parse()?); - let actual_code_id = contract.info().await?.code_id; - - anyhow::ensure!( - actual_code_id == expected_code_id, - "Code ID mismatch, expected {expected_code_id}, but {contract} has {actual_code_id}" +) -> Result<(), CosmosListenerError> { + let contract = cosmos.make_contract( + contract + .parse::() + .map_err(CosmosListenerError::InvalidAddress)?, ); + let actual_code_id = contract + .info() + .await + .map_err(CosmosListenerError::from)? + .code_id; + + if actual_code_id != expected_code_id { + return Err(CosmosListenerError::CodeId { + expected: expected_code_id, + actual: actual_code_id, + }); + } let shared::cosmos::State { set: @@ -89,13 +127,29 @@ pub async fn sanity_check_contract( }, next_event_id: _, next_action_id: _, - } = contract.query(shared::cosmos::QueryMsg::Config {}).await?; + } = contract + .query(shared::cosmos::QueryMsg::Config {}) + .await + .map_err(CosmosListenerError::from)?; + + if info.validator_set.processor != processor { + return Err(CosmosListenerError::Processor); + } + if listeners != info.validator_set.listeners { + return Err(CosmosListenerError::Listeners); + } - anyhow::ensure!(info.validator_set.processor == processor); - anyhow::ensure!(listeners == info.validator_set.listeners); - anyhow::ensure!(needed_listeners == info.validator_set.needed_listeners); - anyhow::ensure!(approvers == info.validator_set.approvers); - anyhow::ensure!(needed_approvers == info.validator_set.needed_approvers); + if needed_listeners != info.validator_set.needed_listeners { + return Err(CosmosListenerError::NeededListeners); + } + + if approvers != info.validator_set.approvers { + return Err(CosmosListenerError::Approvers); + } + + if needed_approvers != info.validator_set.needed_approvers { + return Err(CosmosListenerError::NeededApprovers); + } Ok(()) } diff --git a/packages/kolme/src/listener/mod.rs b/packages/kolme/src/listener/mod.rs index da166cf9..12ed8c86 100644 --- a/packages/kolme/src/listener/mod.rs +++ b/packages/kolme/src/listener/mod.rs @@ -1,9 +1,10 @@ #[cfg(feature = "cosmwasm")] -mod cosmos; +pub mod cosmos; #[cfg(feature = "solana")] -mod solana; +pub mod solana; use crate::*; +use futures_util::TryFutureExt; use tokio::task::JoinSet; pub struct Listener { @@ -32,8 +33,8 @@ impl Listener { Listener { kolme, secret } } - pub async fn run(self, name: ChainName) -> Result<()> { - let mut set = JoinSet::new(); + pub async fn run(self, name: ChainName) -> Result<(), KolmeError> { + let mut set = JoinSet::>::new(); tracing::debug!("Listen on {name:?}"); match name { @@ -42,12 +43,15 @@ impl Listener { { let contracts = self.wait_for_contracts(name).await?; for (chain, contract) in contracts { - set.spawn(cosmos::listen( - self.kolme.clone(), - self.secret.clone(), - chain.to_cosmos_chain().unwrap(), - contract, - )); + set.spawn( + cosmos::listen( + self.kolme.clone(), + self.secret.clone(), + chain.to_cosmos_chain().unwrap(), + contract, + ) + .map_err(KolmeError::from), + ); } } } @@ -83,7 +87,9 @@ impl Listener { match res { Err(e) => { set.abort_all(); - return Err(anyhow::anyhow!("Listener panicked: {e}")); + return Err(KolmeError::ListenerPanicked { + details: e.to_string(), + }); } Ok(Err(e)) => return Err(e), Ok(Ok(())) => (), @@ -93,7 +99,10 @@ impl Listener { Ok(()) } - async fn wait_for_contracts(&self, name: ChainName) -> Result> { + async fn wait_for_contracts( + &self, + name: ChainName, + ) -> Result, KolmeError> { let mut new_block = self.kolme.subscribe_new_block(); let mut mempool = self.kolme.subscribe_mempool_additions(); loop { @@ -131,7 +140,11 @@ impl Listener { } } - async fn try_new_contract(&self, chain: ExternalChain, contract: &str) -> Result<()> { + async fn try_new_contract( + &self, + chain: ExternalChain, + contract: &str, + ) -> Result<(), KolmeError> { let kolme = self.kolme.read(); let next = get_next_bridge_event_id(&kolme, self.secret.public_key(), chain); if next != BridgeEventId::start() { @@ -140,10 +153,10 @@ impl Listener { let config = &kolme.get_bridge_contracts().get(chain)?.config; if let BridgeContract::Deployed(_) = config.bridge { - anyhow::bail!("Already have a deployed contract on {chain:?}") + return Err(KolmeError::ContractAlreadyDeployed { chain }); }; - let res: Result<()> = match ChainKind::from(chain) { + let res: Result<(), KolmeError> = match ChainKind::from(chain) { #[cfg(feature = "cosmwasm")] ChainKind::Cosmos(chain) => { let cosmos = kolme.get_cosmos(chain).await?; @@ -151,7 +164,9 @@ impl Listener { BridgeContract::NeededCosmosBridge { code_id } => code_id, BridgeContract::NeededSolanaBridge { .. } => unreachable!(), BridgeContract::Deployed(_) => { - anyhow::bail!("Already have a deployed contract on {chain:?}") + return Err(KolmeError::ContractAlreadyDeployed { + chain: chain.into(), + }); } }; @@ -162,6 +177,7 @@ impl Listener { self.kolme.get_app().genesis_info(), ) .await + .map_err(KolmeError::from) } #[cfg(not(feature = "cosmwasm"))] ChainKind::Cosmos(_) => Ok(()), @@ -180,7 +196,7 @@ impl Listener { ChainKind::Solana(_) => Ok(()), #[cfg(feature = "pass_through")] ChainKind::PassThrough => { - anyhow::bail!("No wait for pass-through contract is expected") + return Err(KolmeError::UnexpectedPassThroughContract); } }; diff --git a/packages/kolme/src/listener/solana.rs b/packages/kolme/src/listener/solana.rs index ebaf97bd..6951155c 100644 --- a/packages/kolme/src/listener/solana.rs +++ b/packages/kolme/src/listener/solana.rs @@ -15,6 +15,24 @@ use tokio::time; use super::*; +#[derive(thiserror::Error, Debug)] +pub enum ListenerSolanaError { + #[error("Processor mismatch between genesis info and on-chain state")] + Processor, + + #[error("Listeners mismatch between genesis info and on-chain state")] + Listeners, + + #[error("Approvers mismatch between genesis info and on-chain state")] + Approvers, + + #[error("Needed listeners mismatch between genesis info and on-chain state")] + NeededListeners, + + #[error("Needed approvers mismatch between genesis info and on-chain state")] + NeededApprovers, +} + pub async fn listen( kolme: Kolme, secret: SecretKey, @@ -35,27 +53,53 @@ pub async fn sanity_check_contract( client: &SolanaClient, program: &str, info: &GenesisInfo, -) -> Result<()> { +) -> Result<(), KolmeError> { let program_id = Pubkey::from_str(program)?; let state_acc = kolme_solana_bridge_client::derive_state_pda(&program_id); let acc = client.get_account(&state_acc).await?; if acc.owner != program_id || acc.data.len() < 2 { - return Err(anyhow::anyhow!( - "Bridge program {program} hasn't been initialized yet." - )); + return Err(KolmeError::UninitializedSolanaBridge { + program: program.to_string(), + }); } // Skip the first two bytes which are the discriminator byte and the bump seed respectively. - let state: BridgeState = BorshDeserialize::try_from_slice(&acc.data[2..]) - .map_err(|x| anyhow::anyhow!("Error deserializing Solana bridge state: {:?}", x))?; + let state: BridgeState = BorshDeserialize::try_from_slice(&acc.data[2..]).map_err(|x| { + KolmeError::InvalidSolanaBridgeState { + details: format!("{x:?}"), + } + })?; + + if info.validator_set.processor != state.set.processor { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::Processor, + )); + } + if info.validator_set.listeners != state.set.listeners { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::Listeners, + )); + } + + if info.validator_set.approvers != state.set.approvers { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::Approvers, + )); + } + + if info.validator_set.needed_listeners != state.set.needed_listeners { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::NeededListeners, + )); + } - anyhow::ensure!(info.validator_set.processor == state.set.processor); - anyhow::ensure!(info.validator_set.listeners == state.set.listeners); - anyhow::ensure!(info.validator_set.approvers == state.set.approvers); - anyhow::ensure!(info.validator_set.needed_listeners == state.set.needed_listeners); - anyhow::ensure!(info.validator_set.needed_approvers == state.set.needed_approvers); + if info.validator_set.needed_approvers != state.set.needed_approvers { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::NeededApprovers, + )); + } Ok(()) } @@ -65,7 +109,7 @@ async fn listen_internal( secret: &SecretKey, chain: SolanaChain, contract: &str, -) -> Result<()> { +) -> Result<(), KolmeError> { let contract_pubkey = Pubkey::from_str_const(contract); let client = kolme.get_solana_client(chain).await; @@ -165,7 +209,7 @@ async fn catch_up( last_seen: BridgeEventId, chain: SolanaChain, contract: &Pubkey, -) -> Result> { +) -> Result, KolmeError> { tracing::info!("Catching up on missing bridge events until {}.", last_seen); let mut messages = vec![]; @@ -233,7 +277,7 @@ async fn catch_up( Ok(Some(BridgeEventId(latest_id))) } -fn extract_bridge_message_from_logs(logs: &[String]) -> Result> { +fn extract_bridge_message_from_logs(logs: &[String]) -> Result, KolmeError> { const PROGRAM_DATA_LOG: &str = "Program data: "; // Our program data should always be the last "Program data:" entry even if CPI was invoked. @@ -245,11 +289,10 @@ fn extract_bridge_message_from_logs(logs: &[String]) -> Result::try_from_slice(&bytes).map_err(|x| { - anyhow::anyhow!( - "Error deserializing Solana bridge message from logs: {:?}", - x - ) + let result = ::try_from_slice(&bytes).map_err(|e| { + KolmeError::InvalidSolanaBridgeLogMessage { + details: format!("{e:?}"), + } }); match result { @@ -261,7 +304,6 @@ fn extract_bridge_message_from_logs(logs: &[String]) -> Result, payload: &str, -) -> Result { +) -> Result { let url = format!("http://localhost:{port}/actions"); tracing::debug!("Sending bridge action to {url}"); let resp = client @@ -117,7 +117,7 @@ impl PassThrough { } } - pub async fn run(self, addr: A) -> Result<()> { + pub async fn run(self, addr: A) -> Result<(), KolmeError> { let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST, Method::PUT]) .allow_origin(Any) @@ -137,9 +137,7 @@ impl PassThrough { "Starting PassThrough server on {:?}", listener.local_addr()? ); - axum::serve(listener, app) - .await - .map_err(anyhow::Error::from) + axum::serve(listener, app).await.map_err(KolmeError::from) } } @@ -147,7 +145,7 @@ pub async fn listen( kolme: Kolme, secret: SecretKey, port: String, -) -> Result<()> { +) -> Result<(), KolmeError> { tracing::debug!("pass through listen"); let mut next_bridge_event_id = get_next_bridge_event_id( &kolme.read(), @@ -160,7 +158,8 @@ pub async fn listen( let (mut ws, _) = connect_async(&ws_url).await.unwrap(); loop { - let message = ws.next().await.context("WebSocket stream terminated")??; //receiver.recv().await?; + let message = ws.next().await.ok_or(KolmeError::WebSocketClosed)??; + let message = serde_json::from_slice::(&message.into_data())?; tracing::debug!("Received {}", serde_json::to_string(&message).unwrap()); let message = to_kolme_message::( diff --git a/packages/kolme/src/processor.rs b/packages/kolme/src/processor.rs index bccc982e..4f700b91 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -1,8 +1,5 @@ -use std::{collections::HashMap, convert::Infallible, time::Instant}; - -use kolme_store::KolmeStoreError; - use crate::*; +use std::{collections::HashMap, convert::Infallible, time::Instant}; pub struct Processor { kolme: Kolme, @@ -90,7 +87,7 @@ impl Processor { panic!("Unexpected exit in processor"); } - async fn ensure_genesis_event(&self) -> Result<()> { + async fn ensure_genesis_event(&self) -> Result<(), KolmeError> { if self.kolme.read().get_next_height().is_start() { let code_version = self.kolme.get_code_version(); let kolme = self.kolme.read(); @@ -111,17 +108,17 @@ impl Processor { /// Get the correct secret key for the current validator set. /// /// If we don't have it, returns an error. - fn get_correct_secret(&self, kolme: &KolmeRead) -> Result<&SecretKey> { + fn get_correct_secret(&self, kolme: &KolmeRead) -> Result<&SecretKey, TransactionError> { let pubkey = &kolme.get_framework_state().get_validator_set().processor; - self.secrets.get(pubkey).with_context(|| { + self.secrets.get(pubkey).ok_or_else(|| { let pubkeys = self.secrets.keys().collect::>(); - format!( + TransactionError::Other(format!( "Current processor pubkey is {pubkey}, but we don't have the matching secret key, we have: {pubkeys:?}" - ) + )) }) } - pub async fn create_genesis_event(&self) -> Result<()> { + pub async fn create_genesis_event(&self) -> Result<(), KolmeError> { let info = self.kolme.get_app().genesis_info().clone(); let kolme = self.kolme.read(); let secret = self.get_correct_secret(&kolme)?; @@ -135,17 +132,17 @@ impl Processor { .await?; if let Err(e) = self.kolme.add_executed_block(executed_block).await { // kolme#144 - Discard unneeded fields - if let Some(KolmeStoreError::ConflictingBlockInDb { .. }) = e.downcast_ref() { + if let TransactionError::ConflictingBlockInDb { .. } = &e { self.kolme.resync().await?; } - Err(e) + Err(KolmeError::Transaction(e)) } else { self.emit_latest().ok(); Ok(()) } } - async fn add_transaction(&self, tx: SignedTransaction) -> Result<()> { + async fn add_transaction(&self, tx: SignedTransaction) -> Result<(), KolmeError> { // We'll retry adding a transaction multiple times before giving up. // We only retry if the transaction is still not present in the database, // and our failure is because of a block creation race condition. @@ -179,11 +176,11 @@ impl Processor { .await; if let Err(e) = &res { // kolme#144 - Discard unneeded fields - if let Some(KolmeStoreError::ConflictingBlockInDb { + if let TransactionError::ConflictingBlockInDb { height, adding, existing, - }) = e.downcast_ref() + } = e { tracing::warn!( "Unexpected BlockAlreadyInDb while adding transaction, construction lock should have prevented this. Height: {height}. Adding: {adding}. Existing: {existing}." @@ -194,10 +191,7 @@ impl Processor { let failed = FailedTransaction { txhash, proposed_height, - error: match e.downcast_ref::() { - Some(e) => e.clone(), - None => KolmeError::Other(e.to_string()), - }, + error: e.clone(), }; let failed = TaggedJson::new(failed)?; let key = self.get_correct_secret(&self.kolme.read())?; @@ -226,23 +220,29 @@ impl Processor { ); } self.emit_latest().ok(); - res + Ok(res?) } async fn construct_block( &self, tx: SignedTransaction, proposed_height: BlockHeight, - ) -> Result> { + ) -> Result, TransactionError> { // Stop any changes from happening while we're processing. let kolme = self.kolme.read(); let secret = self.get_correct_secret(&kolme)?; let txhash = tx.hash(); - if kolme.get_tx_height(txhash).await?.is_some() { - return Err(anyhow::Error::from(KolmeStoreError::TxAlreadyInDb { - txhash: txhash.0, - })); + if kolme + .get_tx_height(txhash) + .await + .map_err(|e| TransactionError::StoreError(e.to_string()))? + .is_some() + { + return Err(TransactionError::Other(format!( + "TxAlreadyInDb: {}", + txhash.0 + ))); } let now = Timestamp::now(); @@ -255,17 +255,23 @@ impl Processor { height, } = kolme .execute_transaction(&tx, now, BlockDataHandling::NoPriorData) - .await?; - anyhow::ensure!(height == proposed_height); + .await + .map_err(TransactionError::from)?; + + if height != proposed_height { + return Err(TransactionError::ExecutedHeightMismatch { + expected: proposed_height, + actual: height, + }); + } if let Some(max_height) = tx.0.message.as_inner().max_height { if max_height < proposed_height { - return Err(KolmeError::PastMaxHeight { + return Err(TransactionError::PastMaxHeight { txhash, max_height, proposed_height, - } - .into()); + }); } } @@ -275,13 +281,24 @@ impl Processor { processor: secret.public_key(), height: proposed_height, parent: kolme.get_current_block_hash(), - framework_state: merkle_map::api::serialize(&framework_state)?.hash(), - app_state: merkle_map::api::serialize(&app_state)?.hash(), + framework_state: merkle_map::api::serialize(&framework_state) + .map_err(|e| TransactionError::MerkleError(e.to_string()))? + .hash(), + app_state: merkle_map::api::serialize(&app_state) + .map_err(|e| TransactionError::MerkleError(e.to_string()))? + .hash(), loads, - logs: merkle_map::api::serialize(&logs)?.hash(), + logs: merkle_map::api::serialize(&logs) + .map_err(|e| TransactionError::MerkleError(e.to_string()))? + .hash(), }; - let block = TaggedJson::new(approved_block)?; - let signed_block = Arc::new(SignedBlock(block.sign(secret)?)); + let block = + TaggedJson::new(approved_block).map_err(|e| TransactionError::Other(e.to_string()))?; + let signed_block = Arc::new(SignedBlock( + block + .sign(secret) + .map_err(|e| TransactionError::Other(e.to_string()))?, + )); Ok(ExecutedBlock { signed_block, framework_state, @@ -298,7 +315,7 @@ impl Processor { } } - async fn approve_actions(&self, chain: ExternalChain) -> Result<()> { + async fn approve_actions(&self, chain: ExternalChain) -> Result<(), KolmeError> { // We only need to bother approving one action at a time. Each time we // approve an action, it produces a new block, which will allow us to check if we // need to approve anything else. @@ -312,7 +329,13 @@ impl Processor { let mut approvers = vec![]; for (key, sig) in &action.approvals { let key2 = sig.validate(action.payload.as_bytes())?; - anyhow::ensure!(key == &key2); + if key != &key2 { + return Err(KolmeError::InvalidSignature { + expected: Box::new(*key), + actual: Box::new(key2), + }); + } + if kolme.get_approver_pubkeys().contains(key) { approvers.push(*sig); } @@ -346,13 +369,13 @@ impl Processor { Ok(()) } - fn emit_latest(&self) -> Result<()> { + fn emit_latest(&self) -> Result<(), KolmeError> { let height = self .kolme .read() .get_next_height() .prev() - .context("Emit latest block: no blocks available")?; + .ok_or_else(|| KolmeError::NoBlocksAvailable)?; let latest = LatestBlock { height, when: jiff::Timestamp::now(), diff --git a/packages/kolme/src/submitter/cosmos.rs b/packages/kolme/src/submitter/cosmos.rs index ff881274..2e5fc252 100644 --- a/packages/kolme/src/submitter/cosmos.rs +++ b/packages/kolme/src/submitter/cosmos.rs @@ -8,7 +8,7 @@ pub async fn instantiate( seed_phrase: &SeedPhrase, code_id: u64, set: ValidatorSet, -) -> Result { +) -> Result { let msg = InstantiateMsg { set }; let wallet = seed_phrase.with_hrp(cosmos.get_address_hrp())?; @@ -45,7 +45,7 @@ pub async fn execute( processor: SignatureWithRecovery, approvals: &BTreeMap, payload: &str, -) -> Result { +) -> Result { tracing::info!("Executing signed message on bridge: {contract}"); let msg = ExecuteMsg::Signed { @@ -70,8 +70,7 @@ pub async fn execute( "Cosmos submitter failed to execute signed transaction: {}", e ); - - Err(anyhow::anyhow!(e)) + Err(KolmeError::CosmosExecutionFailed(e)) } } } diff --git a/packages/kolme/src/submitter/mod.rs b/packages/kolme/src/submitter/mod.rs index 584a36b8..93bafac1 100644 --- a/packages/kolme/src/submitter/mod.rs +++ b/packages/kolme/src/submitter/mod.rs @@ -10,6 +10,12 @@ use utils::trigger::Trigger; use crate::*; +#[derive(thiserror::Error, Debug)] +pub enum SubmitterError { + #[error("Pass-through submission attempted on wrong chain: expected PassThrough, got {chain}")] + InvalidPassThroughChain { chain: ExternalChain }, +} + /// Component which submits necessary transactions to the blockchain. pub struct Submitter { kolme: Kolme, @@ -96,7 +102,7 @@ impl Submitter { } } - pub async fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<(), KolmeError> { let chains = self .kolme .read() @@ -150,7 +156,7 @@ impl Submitter { _ = listen_genesis_available.listen() => (), } } - anyhow::Ok(()) + Ok(()) }; let ongoing = async { @@ -171,7 +177,7 @@ impl Submitter { /// Submit 0 transactions (if nothing is needed) or the next event's transactions. /// /// We only do 0 or 1, since we always wait for listeners to confirm that our actions succeeded before continuing. - async fn submit_zero_or_one(&mut self, chains: &[ExternalChain]) -> Result<()> { + async fn submit_zero_or_one(&mut self, chains: &[ExternalChain]) -> Result<(), KolmeError> { // TODO we can probably unify genesis and other actions into a single per-chain feed let genesis_action = self.kolme.read().get_next_genesis_action(); if let Some(genesis_action) = genesis_action { @@ -187,7 +193,7 @@ impl Submitter { Ok(()) } - async fn handle_genesis(&mut self, genesis_action: GenesisAction) -> Result<()> { + async fn handle_genesis(&mut self, genesis_action: GenesisAction) -> Result<(), KolmeError> { match genesis_action { #[cfg(feature = "cosmwasm")] GenesisAction::InstantiateCosmos { @@ -249,7 +255,7 @@ impl Submitter { } } - fn propose(kolme: &Kolme, chain: ExternalChain, addr: String) -> Result<()> { + fn propose(kolme: &Kolme, chain: ExternalChain, addr: String) -> Result<(), KolmeError> { // We broadcast our own transaction for genesis instantiation, using an // arbitrary secret key. The listeners will watch for such transactions // and, if they're satisfied with our generated contracts, rebroadcast @@ -280,7 +286,7 @@ impl Submitter { approvals, processor, }: &PendingBridgeAction, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let Some(processor) = processor else { return Ok(()); }; @@ -348,7 +354,9 @@ impl Submitter { } #[cfg(feature = "pass_through")] ChainArgs::PassThrough { port } => { - anyhow::ensure!(chain == ExternalChain::PassThrough); + if chain != ExternalChain::PassThrough { + return Err(SubmitterError::InvalidPassThroughChain { chain }.into()); + } let client = self.kolme.read().get_pass_through_client(); tracing::info!("Executing pass through contract: {contract}"); diff --git a/packages/kolme/src/submitter/solana.rs b/packages/kolme/src/submitter/solana.rs index f6c8bd7d..5fdcd0d4 100644 --- a/packages/kolme/src/submitter/solana.rs +++ b/packages/kolme/src/submitter/solana.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use base64::Engine; use borsh::BorshDeserialize; use kolme_solana_bridge_client::{ @@ -7,6 +5,8 @@ use kolme_solana_bridge_client::{ ComputeBudgetInstruction, }; use shared::solana::{InitializeIxData, Payload, SignedAction, SignedMsgIxData}; +use solana_rpc_client_api::client_error::ErrorKind; +use std::str::FromStr; use super::*; @@ -15,14 +15,14 @@ pub async fn instantiate( keypair: &Keypair, program_id: &str, set: ValidatorSet, -) -> Result<()> { +) -> Result<(), KolmeError> { tracing::info!("Instantiate new contract: {program_id}"); let data = InitializeIxData { set }; let program_pubkey = Pubkey::from_str(program_id)?; let blockhash = client.get_latest_blockhash().await?; - let tx = init_tx(program_pubkey, blockhash, keypair, &data).map_err(|x| anyhow::anyhow!(x))?; + let tx = init_tx(program_pubkey, blockhash, keypair, &data)?; client.send_and_confirm_transaction(&tx).await?; @@ -37,10 +37,10 @@ pub async fn execute( approvals: &BTreeMap, payload_b64: String, fee_per_cu: Option, -) -> Result { +) -> Result { let payload_bytes = base64::engine::general_purpose::STANDARD.decode(&payload_b64)?; - let payload: Payload = BorshDeserialize::try_from_slice(&payload_bytes) - .map_err(|x| anyhow::anyhow!("Error deserializing Solana bridge payload: {:?}", x))?; + let payload: Payload = + BorshDeserialize::try_from_slice(&payload_bytes).map_err(KolmeError::from)?; tracing::info!( "Executing signed message on bridge {program_id}: {:?}", @@ -86,19 +86,28 @@ pub async fn execute( keypair, &data, &metas, - ) - .map_err(|x| anyhow::anyhow!(x))?; + )?; match client.send_and_confirm_transaction(&tx).await { Ok(sig) => Ok(sig.to_string()), Err(e) => { - tracing::error!( - "Solana submitter failed to execute signed transaction: {}, error kind: {:?}", - e, - e.root_cause() - .downcast_ref::() - .map(|e| &e.kind) - ); + match &e { + KolmeError::SolanaClient(client_err) => match &client_err.kind { + ErrorKind::RpcError(rpc) => { + tracing::error!("Solana RPC error on {program_id}: {:?}", rpc); + } + ErrorKind::TransactionError(tx) => { + tracing::error!("Solana TX error on {program_id}: {:?}", tx); + } + other => { + tracing::error!("Solana client error on {program_id}: {:?}", other); + } + }, + other => { + tracing::error!("Execution failed on {program_id}: {:?}", other); + } + } + Err(e) } } diff --git a/packages/kolme/src/testtasks.rs b/packages/kolme/src/testtasks.rs index bd71138e..5460e122 100644 --- a/packages/kolme/src/testtasks.rs +++ b/packages/kolme/src/testtasks.rs @@ -5,12 +5,13 @@ use std::sync::{ Arc, }; +use crate::KolmeError; use tokio::sync::mpsc::error::TryRecvError; #[derive(Clone)] pub struct TestTasks { send_keep_running: tokio::sync::watch::Sender, - send_error: tokio::sync::mpsc::Sender, + send_error: tokio::sync::mpsc::Sender, running_count: Arc, } @@ -58,13 +59,13 @@ impl TestTasks { { self.try_spawn_persistent(async move { task.await; - anyhow::Ok(()) + Ok::<_, KolmeError>(()) }); } pub fn try_spawn_persistent(&self, task: F) where F: std::future::Future> + Send + 'static, - anyhow::Error: From, + KolmeError: From, T: Send + 'static, { self.spawn_helper(true, task) @@ -76,14 +77,14 @@ impl TestTasks { { self.try_spawn(async move { task.await; - anyhow::Ok(()) + Ok::<(), KolmeError>(()) }) } pub fn try_spawn(&self, task: F) where F: std::future::Future> + Send + 'static, - anyhow::Error: From, + KolmeError: From, { self.spawn_helper(false, task) } @@ -91,7 +92,7 @@ impl TestTasks { fn spawn_helper(&self, persistent: bool, task: F) where F: std::future::Future> + Send + 'static, - anyhow::Error: From, + KolmeError: From, T: Send + 'static, { let tasks = self.clone(); @@ -105,7 +106,7 @@ impl TestTasks { } // Spawn the actual worker. - let handle = tokio::spawn(async move { task.await.map_err(anyhow::Error::from) }); + let handle = tokio::spawn(async move { task.await.map_err(KolmeError::from) }); // Spawn the first watchdog. It waits for the overall runtime to finish, // either because all tasks are done or because an error occurred, @@ -134,13 +135,17 @@ impl TestTasks { match res { Ok(Ok(_)) => { if persistent { - Some(anyhow::anyhow!("Persistent task exited unexpectedly")) + Some(KolmeError::PersistentTaskExited) } else { None } } - Ok(Err(e)) => Some(e.context("Task exited with an error")), - Err(e) => Some(anyhow::anyhow!("Task panicked: {e}")), + Ok(Err(e)) => Some(KolmeError::TaskErrored { + error: e.to_string(), + }), + Err(e) => Some(KolmeError::TaskPanicked { + details: e.to_string(), + }), } } else { None diff --git a/packages/kolme/src/upgrader.rs b/packages/kolme/src/upgrader.rs index 6fee1e57..cd0cfa69 100644 --- a/packages/kolme/src/upgrader.rs +++ b/packages/kolme/src/upgrader.rs @@ -32,7 +32,7 @@ impl Upgrader { } } - async fn run_single(&self) -> Result<()> { + async fn run_single(&self) -> Result<(), KolmeError> { let kolme = self.kolme.read(); let framework_state = kolme.get_framework_state(); @@ -91,7 +91,11 @@ impl Upgrader { Ok(()) } - async fn vote_on_upgrade(&self, id: AdminProposalId, payload: &ProposalPayload) -> Result<()> { + async fn vote_on_upgrade( + &self, + id: AdminProposalId, + payload: &ProposalPayload, + ) -> Result<(), KolmeError> { self.kolme .sign_propose_await_transaction( &self.secret, @@ -105,7 +109,7 @@ impl Upgrader { Ok(()) } - async fn propose_upgrade(&self) -> Result<()> { + async fn propose_upgrade(&self) -> Result<(), KolmeError> { self.kolme .sign_propose_await_transaction( &self.secret, diff --git a/packages/kolme/src/utils/solana.rs b/packages/kolme/src/utils/solana.rs index f0ed56b3..b87a56fc 100644 --- a/packages/kolme/src/utils/solana.rs +++ b/packages/kolme/src/utils/solana.rs @@ -3,7 +3,7 @@ use solana_rpc_client_api::client_error; /// Helper function to redact and wrap Solana RPC errors to hide sensitive information -pub fn redact_solana_error(mut error: client_error::Error) -> anyhow::Error { +pub fn redact_solana_error(mut error: client_error::Error) -> client_error::Error { if let client_error::ErrorKind::Reqwest(mut reqwest_error) = error.kind { let url = reqwest_error.url_mut(); if let Some(url) = url { @@ -11,7 +11,7 @@ pub fn redact_solana_error(mut error: client_error::Error) -> anyhow::Error { } error.kind = client_error::ErrorKind::Reqwest(reqwest_error); } - anyhow::Error::new(error) + error } #[cfg(test)] diff --git a/packages/merkle-map/src/api.rs b/packages/merkle-map/src/api.rs index 69c33df5..6e06c2a9 100644 --- a/packages/merkle-map/src/api.rs +++ b/packages/merkle-map/src/api.rs @@ -12,7 +12,7 @@ use crate::*; impl MerkleSerialError { pub fn custom(e: E) -> Self { - Self::Custom(Box::new(e)) + Self::Custom(e.to_string()) } } diff --git a/packages/merkle-map/src/merkle_deserializer.rs b/packages/merkle-map/src/merkle_deserializer.rs index 1ed2ffe9..d15a2866 100644 --- a/packages/merkle-map/src/merkle_deserializer.rs +++ b/packages/merkle-map/src/merkle_deserializer.rs @@ -78,7 +78,7 @@ impl MerkleDeserializer { /// Load bytes and then UTF-8 decode them. pub fn load_str(&mut self) -> Result<&str, MerkleSerialError> { let bytes = self.load_bytes()?; - std::str::from_utf8(bytes).map_err(MerkleSerialError::custom) + Ok(std::str::from_utf8(bytes)?) } pub fn load_usize(&mut self) -> Result { @@ -129,7 +129,7 @@ impl MerkleDeserializer { pub fn load_json(&mut self) -> Result { let bytes = self.load_bytes()?; - serde_json::from_slice(bytes).map_err(MerkleSerialError::custom) + Ok(serde_json::from_slice(bytes)?) } pub(crate) fn get_position(&self) -> usize { diff --git a/packages/merkle-map/src/merkle_serializer.rs b/packages/merkle-map/src/merkle_serializer.rs index a5317fb1..7a84a2b7 100644 --- a/packages/merkle-map/src/merkle_serializer.rs +++ b/packages/merkle-map/src/merkle_serializer.rs @@ -83,7 +83,7 @@ impl MerkleSerializer { /// Store a JSON-encoded version of this content. pub fn store_json(&mut self, t: &T) -> Result<(), MerkleSerialError> { - let bytes = serde_json::to_vec(t).map_err(MerkleSerialError::custom)?; + let bytes = serde_json::to_vec(t)?; self.store_slice(&bytes); Ok(()) } diff --git a/packages/merkle-map/src/traits/from_merkle_key.rs b/packages/merkle-map/src/traits/from_merkle_key.rs index 79e4eef2..4c4b9a39 100644 --- a/packages/merkle-map/src/traits/from_merkle_key.rs +++ b/packages/merkle-map/src/traits/from_merkle_key.rs @@ -7,9 +7,7 @@ use crate::*; impl FromMerkleKey for String { fn from_merkle_key(bytes: &[u8]) -> Result { - std::str::from_utf8(bytes) - .map(String::from) - .map_err(MerkleSerialError::custom) + Ok(std::str::from_utf8(bytes)?.to_string()) } } @@ -22,20 +20,30 @@ impl FromMerkleKey for u8 { } } } + impl FromMerkleKey for u32 { fn from_merkle_key(bytes: &[u8]) -> Result { - bytes - .try_into() - .map(u32::from_be_bytes) - .map_err(MerkleSerialError::custom) + let arr: [u8; 4] = + bytes + .try_into() + .map_err(|_| MerkleSerialError::InvalidMerkleKeyLength { + expected: 4, + actual: bytes.len(), + })?; + Ok(u32::from_be_bytes(arr)) } } + impl FromMerkleKey for u64 { fn from_merkle_key(bytes: &[u8]) -> Result { - bytes - .try_into() - .map(u64::from_be_bytes) - .map_err(MerkleSerialError::custom) + let arr: [u8; 8] = + bytes + .try_into() + .map_err(|_| MerkleSerialError::InvalidMerkleKeyLength { + expected: 8, + actual: bytes.len(), + })?; + Ok(u64::from_be_bytes(arr)) } } diff --git a/packages/merkle-map/src/traits/merkle_deserialize.rs b/packages/merkle-map/src/traits/merkle_deserialize.rs index 19eded0c..3419a85c 100644 --- a/packages/merkle-map/src/traits/merkle_deserialize.rs +++ b/packages/merkle-map/src/traits/merkle_deserialize.rs @@ -65,9 +65,7 @@ impl MerkleDeserializeRaw for String { deserializer: &mut MerkleDeserializer, ) -> Result { let bytes = deserializer.load_bytes()?; - std::str::from_utf8(bytes) - .map(ToOwned::to_owned) - .map_err(MerkleSerialError::custom) + Ok(std::str::from_utf8(bytes)?.to_owned()) } } @@ -86,9 +84,7 @@ impl MerkleDeserializeRaw for Option { match deserializer.pop_byte()? { 0 => Ok(None), 1 => T::merkle_deserialize_raw(deserializer).map(Some), - x => Err(MerkleSerialError::Other(format!( - "When deserializing an Option, invalid byte {x}" - ))), + x => Err(MerkleSerialError::InvalidOptionByte { byte: x }), } } } @@ -241,7 +237,7 @@ impl MerkleDeserializeRaw for RecoveryId { deserializer: &mut MerkleDeserializer, ) -> Result { let byte = deserializer.pop_byte()?; - RecoveryId::from_byte(byte).map_err(MerkleSerialError::custom) + RecoveryId::from_byte(byte).map_err(|_| MerkleSerialError::InvalidRecoveryId { byte }) } } @@ -292,7 +288,7 @@ impl MerkleDeserializeRaw for Timestamp { ) -> Result { let as_bytes: [u8; 128 / 8] = deserializer.load_array()?; Timestamp::from_nanosecond(i128::from_le_bytes(as_bytes)) - .map_err(|e| MerkleSerialError::Other(format!("When deserializing Timestamp: {e}"))) + .map_err(MerkleSerialError::InvalidTimestamp) } } diff --git a/packages/merkle-map/src/types.rs b/packages/merkle-map/src/types.rs index 8adb59fe..3c0a9bfe 100644 --- a/packages/merkle-map/src/types.rs +++ b/packages/merkle-map/src/types.rs @@ -111,24 +111,32 @@ where pub enum MerkleSerialError { #[error("Insufficient input when parsing buffer")] InsufficientInput, + #[error("A usize value would be larger than the machine representation")] UsizeOverflow, + #[error( "Unexpected magic byte to distinguish Tree from Leaf, expected 0 or 1, but got {byte}" )] UnexpectedMagicByte { byte: u8 }, + #[error("Invalid byte at start of tree, expected 0 or 1, but got {byte}")] InvalidTreeStart { byte: u8 }, + #[error("Leftover input was unconsumed")] TooMuchInput, + #[error("Serialized content was invalid")] InvalidSerializedContent, + #[error("Hashes not found in store: {hashes:?}")] HashesNotFound { hashes: HashSet, }, + #[error("Leaf content limit exceeded: limit {limit}, actual {actual}")] LeafContentLimitExceeded { limit: usize, actual: usize }, + #[error("Unexpected version number during deserialization of {type_name}, received {actual}, but highest supported is {highest_supported} at position {offset}")] UnexpectedVersion { highest_supported: usize, @@ -136,8 +144,45 @@ pub enum MerkleSerialError { type_name: &'static str, offset: usize, }, - #[error(transparent)] - Custom(Box), - #[error("{0}")] - Other(String), + + #[error("Merkle error: {0}")] + Custom(String), + + #[error("Children buffer length {len} is not a multiple of 32 bytes")] + InvalidChildrenLength { len: usize }, + + #[error("Invalid UTF-8 during deserialization")] + InvalidUtf8(#[from] std::str::Utf8Error), + + #[error("Invalid JSON during deserialization")] + InvalidJson(#[from] serde_json::Error), + + #[error("Invalid external chain identifier: {value}")] + InvalidExternalChain { value: String }, + + #[error("Invalid merkle key length: expected {expected}, got {actual}")] + InvalidMerkleKeyLength { expected: usize, actual: usize }, + + #[error("Invalid recovery id byte: {byte}")] + InvalidRecoveryId { byte: u8 }, + + #[error("Invalid byte {byte} when deserializing Option")] + InvalidOptionByte { byte: u8 }, + + #[error("Invalid timestamp during deserialization")] + InvalidTimestamp(#[from] jiff::Error), + + #[error("Wallet {wallet} used in both account {id} and {other_id}")] + WalletUsedInMultipleAccounts { + wallet: String, + id: String, + other_id: String, + }, + + #[error("Pubkey {pubkey} used in both account {id} and {other_id}")] + PubkeyUsedInMultipleAccounts { + pubkey: String, + id: String, + other_id: String, + }, }