From 0a9aeaa9c943d3cedaff99c10a2b31a3bb3b6eb8 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Tue, 30 Dec 2025 11:34:27 -0600 Subject: [PATCH 01/12] KolmeError improvements --- packages/examples/cosmos-bridge/src/lib.rs | 6 +- .../examples/kademlia-discovery/src/lib.rs | 6 +- packages/examples/p2p/src/lib.rs | 4 +- packages/examples/six-sigma/src/lib.rs | 14 +- .../examples/solana-cosmos-bridge/src/lib.rs | 8 +- packages/kolme-store/src/error.rs | 51 ++- packages/kolme-store/src/fjall.rs | 30 +- packages/kolme-store/src/fjall/merkle.rs | 7 +- packages/kolme-store/src/in_memory.rs | 2 +- packages/kolme-store/src/postgres.rs | 14 +- packages/kolme-store/src/trait.rs | 2 +- packages/kolme-test/Cargo.toml | 2 +- packages/kolme-test/src/max_tx_height.rs | 8 +- packages/kolme-test/src/validations.rs | 2 +- packages/kolme/src/api_server.rs | 10 +- packages/kolme/src/approver.rs | 2 +- packages/kolme/src/common.rs | 4 +- packages/kolme/src/core/execute.rs | 288 ++++++++++++---- packages/kolme/src/core/kolme.rs | 266 +++++++++++---- .../kolme/src/core/kolme/import_export.rs | 51 ++- packages/kolme/src/core/kolme/store.rs | 34 +- packages/kolme/src/core/state.rs | 7 +- packages/kolme/src/core/types.rs | 132 +++++--- packages/kolme/src/core/types/accounts.rs | 94 ++++-- packages/kolme/src/core/types/error.rs | 315 +++++++++++++++++- packages/kolme/src/listener/cosmos.rs | 85 +++-- packages/kolme/src/listener/mod.rs | 46 ++- packages/kolme/src/listener/solana.rs | 78 ++++- packages/kolme/src/pass_through.rs | 8 +- packages/kolme/src/processor.rs | 106 +++--- packages/kolme/src/submitter/cosmos.rs | 5 +- packages/kolme/src/submitter/mod.rs | 12 +- packages/kolme/src/submitter/solana.rs | 6 +- packages/kolme/src/testtasks.rs | 23 +- packages/merkle-map/src/api.rs | 2 +- packages/merkle-map/src/impls/blanket.rs | 2 +- .../merkle-map/src/merkle_deserializer.rs | 4 +- packages/merkle-map/src/merkle_serializer.rs | 2 +- .../merkle-map/src/traits/from_merkle_key.rs | 30 +- .../src/traits/merkle_deserialize.rs | 12 +- packages/merkle-map/src/types.rs | 55 ++- 41 files changed, 1400 insertions(+), 435 deletions(-) diff --git a/packages/examples/cosmos-bridge/src/lib.rs b/packages/examples/cosmos-bridge/src/lib.rs index 64245467..4033457a 100644 --- a/packages/examples/cosmos-bridge/src/lib.rs +++ b/packages/examples/cosmos-bridge/src/lib.rs @@ -200,7 +200,7 @@ pub async fn serve(kolme: Kolme, bind: SocketAddr) -> Result<() let processor = Processor::new(kolme.clone(), my_secret_key().clone()); set.spawn(absurd_future(processor.run())); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(listener.run(ChainName::Cosmos)); + set.spawn(async move { listener.run(ChainName::Cosmos).await }); let approver = Approver::new(kolme.clone(), my_secret_key().clone()); set.spawn(approver.run()); let submitter = Submitter::new_cosmos( @@ -209,7 +209,7 @@ pub async fn serve(kolme: Kolme, bind: SocketAddr) -> Result<() ); set.spawn(submitter.run()); let api_server = ApiServer::new(kolme); - set.spawn(api_server.run(bind)); + set.spawn(async move { api_server.run(bind).await }); while let Some(res) = set.join_next().await { match res { @@ -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..3a4c14c6 100644 --- a/packages/examples/kademlia-discovery/src/lib.rs +++ b/packages/examples/kademlia-discovery/src/lib.rs @@ -216,7 +216,7 @@ pub async fn new_version_node(api_server_port: u16) -> Result<()> { ) .await?; - let mut set = JoinSet::new(); + let mut set: JoinSet> = JoinSet::new(); let processor = Processor::new(kolme.clone(), my_secret_key().clone()); // Processor consumes mempool transactions and add new transactions into blockchain storage. @@ -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..5978c7e6 100644 --- a/packages/examples/p2p/src/lib.rs +++ b/packages/examples/p2p/src/lib.rs @@ -140,7 +140,7 @@ pub async fn api_server(bind: SocketAddr) -> Result<()> { let gossip = GossipBuilder::new().build(kolme.clone())?; set.spawn(absurd_future(gossip.run())); let api_server = ApiServer::new(kolme); - set.spawn(api_server.run(bind)); + set.spawn(async move { api_server.run(bind).await }); while let Some(res) = set.join_next().await { match res { @@ -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..e484fa06 100644 --- a/packages/examples/six-sigma/src/lib.rs +++ b/packages/examples/six-sigma/src/lib.rs @@ -321,7 +321,7 @@ impl KolmeDataRequest for OddsSource { } pub struct Tasks { - pub set: JoinSet>, + pub set: JoinSet>, pub processor: Option, pub listener: Option, pub approver: Option, @@ -341,7 +341,10 @@ impl Tasks { let chain = self.kolme.get_app().chain; let listener = Listener::new(self.kolme.clone(), my_secret_key().clone()); - self.listener = Some(self.set.spawn(listener.run(chain.name()))); + self.listener = Some( + self.set + .spawn(async move { listener.run(chain.name()).await }), + ); } pub fn spawn_approver(&mut self) { @@ -358,7 +361,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(async move { api_server.run(bind).await })); } } @@ -386,7 +390,7 @@ pub async fn serve( } Ok(Err(e)) => { tasks.set.abort_all(); - return Err(e); + return Err(e.into()); } Ok(Ok(())) => (), } @@ -478,7 +482,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..9a6bd10e 100644 --- a/packages/examples/solana-cosmos-bridge/src/lib.rs +++ b/packages/examples/solana-cosmos-bridge/src/lib.rs @@ -177,10 +177,10 @@ pub async fn serve( set.spawn(absurd_future::absurd_future(processor.run())); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(listener.run(ChainName::Cosmos)); + set.spawn(async move { listener.run(ChainName::Cosmos).await }); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(listener.run(ChainName::Solana)); + set.spawn(async move { listener.run(ChainName::Solana).await }); let approver = Approver::new(kolme.clone(), my_secret_key().clone()); set.spawn(approver.run()); @@ -192,7 +192,7 @@ pub async fn serve( set.spawn(submitter.run()); let api_server = ApiServer::new(kolme); - set.spawn(api_server.run(bind)); + set.spawn(async move { api_server.run(bind).await }); while let Some(res) = set.join_next().await { match res { @@ -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/kolme-store/src/error.rs b/packages/kolme-store/src/error.rs index 95bbb18e..de151563 100644 --- a/packages/kolme-store/src/error.rs +++ b/packages/kolme-store/src/error.rs @@ -1,15 +1,36 @@ use merkle_map::{MerkleSerialError, Sha256Hash}; +#[derive(Debug, Clone, Copy)] +pub enum StorageBackend { + Fjall, + Postgres, + InMemory, +} + +impl std::fmt::Display for StorageBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StorageBackend::Fjall => write!(f, "Fjall"), + StorageBackend::Postgres => write!(f, "Postgres"), + StorageBackend::InMemory => write!(f, "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 +41,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, + reason: String, + }, + + #[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)) + Self::Custom(e.to_string()) } } diff --git a/packages/kolme-store/src/fjall.rs b/packages/kolme-store/src/fjall.rs index 1d30161e..6461cc6a 100644 --- a/packages/kolme-store/src/fjall.rs +++ b/packages/kolme-store/src/fjall.rs @@ -1,6 +1,6 @@ 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}; @@ -16,7 +16,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 +41,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 +57,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.to_string(), + }); + } }; Ok(Some(height)) } @@ -73,7 +87,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".to_owned()))?; let height = <[u8; 8]>::try_from(key).map_err(KolmeStoreError::custom)?; Ok(Some(u64::from_be_bytes(height))) } 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..65efa2e1 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()) } diff --git a/packages/kolme-store/src/postgres.rs b/packages/kolme-store/src/postgres.rs index e14e92c5..91529f9f 100644 --- a/packages/kolme-store/src/postgres.rs +++ b/packages/kolme-store/src/postgres.rs @@ -1,3 +1,4 @@ +use crate::error::StorageBackend; use crate::{ r#trait::{BackingRemoteDataListener, KolmeBackingStore}, BlockHashes, HasBlockHashes, KolmeConstructLock, KolmeStoreError, RemoteDataListener, @@ -14,6 +15,7 @@ use sqlx::{ Executor, Postgres, }; use std::{collections::HashMap, sync::Arc}; + pub mod merkle; pub struct ConstructLock { @@ -140,8 +142,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 +189,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(KolmeStoreError::custom)?; match height { None => Ok(None), Some(height) => Ok(Some(height.try_into().map_err(KolmeStoreError::custom)?)), @@ -273,7 +277,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(), )) } diff --git a/packages/kolme-store/src/trait.rs b/packages/kolme-store/src/trait.rs index 229302f8..72bf126a 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( diff --git a/packages/kolme-test/Cargo.toml b/packages/kolme-test/Cargo.toml index cd759c65..26c65f01 100644 --- a/packages/kolme-test/Cargo.toml +++ b/packages/kolme-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" anyhow = { workspace = true } jiff = { workspace = true } kademlia-discovery = { workspace = true } -kolme = { workspace = true } +kolme = { workspace = true, features = ["solana"] } kolme-store = { workspace = true } merkle-map = { workspace = true } parking_lot = { workspace = true } 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/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/src/api_server.rs b/packages/kolme/src/api_server.rs index bdb982f6..2a4b8ba3 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -38,7 +38,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 +52,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) } } @@ -432,8 +430,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 { diff --git a/packages/kolme/src/approver.rs b/packages/kolme/src/approver.rs index 09c8616b..9aac9576 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 { diff --git a/packages/kolme/src/common.rs b/packages/kolme/src/common.rs index c9ae4135..0329c7b9 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 { diff --git a/packages/kolme/src/core/execute.rs b/packages/kolme/src/core/execute.rs index adc79efa..45778695 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -2,6 +2,70 @@ use std::collections::VecDeque; use crate::core::*; +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +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 { 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}")] + InvalidDataLoadRequest { expected: String, actual: String }, +} + /// Execution context for a single message. pub struct ExecutionContext<'a, App: KolmeApp> { framework_state: FrameworkState, @@ -79,7 +143,15 @@ 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(KolmeExecuteError::InvalidGenesisPubkey { + expected: Box::new(expected), + actual: Box::new(actual), + } + .into()); + } } else { tx.ensure_no_genesis()?; }; @@ -93,12 +165,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 +227,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()); + } } } @@ -167,7 +248,9 @@ impl ExecutionContext<'_, App> { 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 +287,33 @@ 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(KolmeExecuteError::InvalidListenerPubkey.into()); + } 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 +333,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 +395,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 +428,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(|e| KolmeError::Accounts(Box::new(e)))?; self.framework_state .chains .get_mut(chain)? @@ -360,9 +453,26 @@ impl ExecutionContext<'_, App> { .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); + + if *next_action_id != action_id { + return Err(KolmeError::ActionIdMismatch { + expected: *next_action_id, + found: action_id, + }); + } + + let Some((old_id, _old)) = actions.remove(&action_id) else { + return Err(KolmeError::ActionError(format!( + "Expected action ID {action_id:?} not found in pending actions" + ))); + }; + + if old_id != action_id { + return Err(KolmeError::ActionIdMismatch { + expected: old_id, + found: action_id, + }); + } } } } @@ -388,14 +498,26 @@ impl ExecutionContext<'_, App> { 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), + } + .into()); + } + 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 { + pubkey: Box::new(key), + } + .into()); + } Ok(()) } @@ -406,14 +528,14 @@ impl ExecutionContext<'_, App> { processor: &SignatureWithRecovery, approvers: &[SignatureWithRecovery], ) -> Result<()> { - anyhow::ensure!( - approvers.len() - >= self - .framework_state - .get_validator_set() - .needed_approvers - .into() - ); + 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 @@ -423,26 +545,45 @@ impl ExecutionContext<'_, App> { .get_mut(&action_id) .with_context(|| format!("No pending action {action_id} found for {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); @@ -629,7 +770,13 @@ impl ExecutionContext<'_, App> { .context("Incorrect number of data loads")?; 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(), + } + .into()); + } match validation { DataLoadValidation::ValidateDataLoads => { req.validate(self.app, &prev_res).await?; @@ -678,7 +825,14 @@ impl ExecutionContext<'_, App> { .add_pubkey_to_account_error_overlap(self.get_sender_id(), *key)?; } 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(), + } + .into()); + } + self.framework_state .accounts .remove_pubkey_from_account(self.get_sender_id(), *key)?; @@ -697,24 +851,29 @@ impl ExecutionContext<'_, App> { 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 { "approver" } else { "listener" }.to_string(), + }); } set.insert(replacement); Ok(()) @@ -727,7 +886,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 +903,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 +920,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 +936,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() @@ -801,14 +968,17 @@ impl ExecutionContext<'_, App> { })?; 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()?; } } diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 36f826c4..23d060cb 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,48 @@ 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 }, +} + /// A running instance of Kolme for the given application. /// /// This type represents the core execution environment for Kolme. @@ -255,7 +299,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 +307,18 @@ 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(e) => Err(TransactionError::TimeoutProposingTx { + txhash, + elapsed: e.to_string(), + }), } } 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 +335,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 +359,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 +368,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,23 +386,27 @@ 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(crate::KolmeError::Transaction(e)); } } } @@ -398,7 +447,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 +459,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: Box::new(actual_parent), + expected: Box::new(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 +518,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 +542,7 @@ impl Kolme { logs, }) .await + .map_err(KolmeError::from) } /// Add a block that has already been executed. @@ -483,7 +551,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 +563,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 +625,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 +655,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 +684,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 @@ -718,7 +837,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); @@ -981,7 +1100,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 @@ -1156,7 +1278,7 @@ 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 } diff --git a/packages/kolme/src/core/kolme/import_export.rs b/packages/kolme/src/core/kolme/import_export.rs index 47cfcbec..b9ef8685 100644 --- a/packages/kolme/src/core/kolme/import_export.rs +++ b/packages/kolme/src/core/kolme/import_export.rs @@ -11,6 +11,24 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; use crate::*; +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +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("Logic error: writing layer {parent} but its child {child} is not yet written")] + LogicChildNotYetWritten { + parent: Sha256Hash, + child: Sha256Hash, + }, +} + impl Kolme { pub async fn export_blocks_to, R: RangeBounds>( &self, @@ -118,12 +136,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(); @@ -164,10 +192,13 @@ async fn write_layer( dest.write_u32(u32::try_from(layer.children.len()).context("Too many children")?) .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); diff --git a/packages/kolme/src/core/kolme/store.rs b/packages/kolme/src/core/kolme/store.rs index 51ae6e3c..88230622 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -36,19 +36,21 @@ 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 { @@ -62,23 +64,28 @@ 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<()> { 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(), + } + .into()); + } } 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), } } @@ -103,7 +110,9 @@ impl KolmeStore { pub(crate) async fn add_merkle_layer(&self, layer: &MerkleLayerContents) -> Result<()> { 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 }.into()); + } } self.inner.add_merkle_layer(layer).await @@ -156,7 +165,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) diff --git a/packages/kolme/src/core/state.rs b/packages/kolme/src/core/state.rs index 9be777e0..766d07ac 100644 --- a/packages/kolme/src/core/state.rs +++ b/packages/kolme/src/core/state.rs @@ -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 }.into()); + } } 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..39784b6d 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -24,6 +24,26 @@ use crate::*; pub use accounts::{Account, Accounts, AccountsError}; pub use error::KolmeError; +pub use error::KolmeExecutionError; +pub use error::TransactionError; + +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum KolmeTypesError { + #[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, +} #[cfg(feature = "solana")] /// Wrapper around the Solana RPC client to hide sensitive information. @@ -67,10 +87,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(), + }) } } @@ -197,12 +218,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 +313,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,7 +368,7 @@ 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}") @@ -781,9 +803,9 @@ 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()?)) } } @@ -833,10 +855,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) } } @@ -913,7 +935,15 @@ pub struct SignedBlock(pub SignedTaggedJson>); impl SignedBlock { pub fn validate_signature(&self) -> Result<()> { 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(KolmeTypesError::InvalidBlockProcessorSignature { + expected: Box::new(expected), + actual: Box::new(pubkey), + } + .into()); + } + Ok(()) } @@ -1027,7 +1057,15 @@ pub struct SignedTransaction(pub SignedTaggedJson SignedTransaction { pub fn validate_signature(&self) -> Result<()> { let pubkey = self.0.verify_signature()?; - anyhow::ensure!(pubkey == self.0.message.as_inner().pubkey); + let expected = self.0.message.as_inner().pubkey; + if pubkey != expected { + return Err(KolmeTypesError::InvalidTransactionSignature { + expected: Box::new(expected), + actual: Box::new(pubkey), + } + .into()); + } + Ok(()) } } @@ -1041,14 +1079,21 @@ 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(_))); + if self.messages.len() != 1 { + return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + } + + if !matches!(self.messages[0], Message::Genesis(_)) { + return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + } Ok(()) } pub fn ensure_no_genesis(&self) -> Result<()> { for msg in &self.messages { - anyhow::ensure!(!matches!(msg, Message::Genesis(_))); + if matches!(msg, Message::Genesis(_)) { + return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + } } Ok(()) } @@ -1448,14 +1493,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 +1514,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 +1537,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 +1593,7 @@ impl ExecAction { chain: ExternalChain, config: &ChainConfig, id: BridgeActionId, - ) -> Result { + ) -> Result { #[cfg(feature = "cosmwasm")] use shared::cosmos; #[cfg(feature = "solana")] @@ -1721,9 +1766,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 +1818,12 @@ impl ExecAction { } #[cfg(feature = "solana")] -fn serialize_solana_payload(payload: &shared::solana::Payload) -> Result { +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))?; 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).map_err(KolmeError::from)?; 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 { diff --git a/packages/kolme/src/core/types/accounts.rs b/packages/kolme/src/core/types/accounts.rs index c4fdced1..8bbe97a0 100644 --- a/packages/kolme/src/core/types/accounts.rs +++ b/packages/kolme/src/core/types/accounts.rs @@ -22,6 +22,39 @@ pub enum AccountsError { asset_id: AssetId, to_burn: Decimal, }, + + #[error("Pubkey {key} already in use")] + PubkeyAlreadyInUse { key: PublicKey }, + + #[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: PublicKey, + 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: PublicKey, + expected: AccountNonce, + actual: AccountNonce, + }, } /// Track all information on accounts. @@ -154,10 +187,9 @@ impl Accounts { account_id: AccountId, key: PublicKey, ) -> Result<()> { - anyhow::ensure!( - !self.pubkeys.contains_key(&key), - "Pubkey {key} already in use" - ); + if self.pubkeys.contains_key(&key) { + return Err(AccountsError::PubkeyAlreadyInUse { key }.into()); + } let account = self .accounts .get_mut(&account_id) @@ -238,10 +270,10 @@ impl Accounts { .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}" - ); + if id != actual_id { + return Err(AccountsError::PubkeyAccountMismatch { key, id, actual_id }.into()); + } + let was_present = self.accounts.get_mut(&id).unwrap().pubkeys.remove(&key); assert!(was_present); Ok(()) @@ -252,10 +284,13 @@ impl Accounts { account_id: AccountId, wallet: &Wallet, ) -> Result<()> { - anyhow::ensure!( - !self.wallets.contains_key(wallet), - "Wallet {wallet} already in use" - ); + if self.wallets.contains_key(wallet) { + return Err(AccountsError::WalletAlreadyInUse { + wallet: wallet.clone(), + } + .into()); + } + let account = self .accounts .get_mut(&account_id) @@ -274,7 +309,15 @@ impl Accounts { .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}"); + if id != actual_id { + return Err(AccountsError::WalletAccountMismatch { + wallet: wallet.clone(), + id, + actual_id, + } + .into()); + } + let was_present = self.accounts.get_mut(&id).unwrap().wallets.remove(wallet); assert!(was_present); Ok(()) @@ -308,7 +351,14 @@ 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(AccountsError::InvalidInitialNonce { + pubkey, + expected: account.next_nonce, + actual: nonce, + } + .into()); + } account.next_nonce = account.next_nonce.next(); Ok(account_id) } @@ -337,17 +387,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..fc39f50e 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -1,6 +1,10 @@ -use crate::core::*; +use crate::listener::{cosmos::CosmosListenerError, solana::ListenerSolanaError}; +use crate::{core::*, submitter::SubmitterError}; +use cosmos::error::{AddressError, WalletError}; +use kolme_solana_bridge_client::pubkey::ParsePubkeyError; +use kolme_store::KolmeStoreError; -#[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 +13,310 @@ 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("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("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: String, + }, + + #[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: Box, + expected: Box, + }, + + #[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("Types error: {0}")] + TypesError(#[from] KolmeTypesError), + + #[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("Failed to serialize Solana payload to Borsh")] + SolanaPayloadSerializationError(std::io::Error), + + #[error("Failed to build Solana initialization transaction")] + SolanaInitTxBuildFailed(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("Start height {start} is greater than {end} height")] InvalidBlockHeight { start: BlockHeight, end: BlockHeight, }, + + #[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] Box), + + #[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("TryFromInt error")] + TryFromInt(#[from] std::num::TryFromIntError), + + #[error("Base64 decode error")] + Base64(#[from] base64::DecodeError), + + #[error("WebSocket error")] + WebSocket(#[from] tokio_tungstenite::tungstenite::Error), + + #[error("Solana client error")] + SolanaClient(#[from] solana_client::client_error::ClientError), + #[error("{0}")] Other(String), } + +impl From for KolmeError { + fn from(e: anyhow::Error) -> Self { + let other = format!("Error from Anyhow: {e}"); + if let Ok(inner) = e.downcast::() { + return inner; + } + KolmeError::Other(other) + } +} + +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum TransactionError { + #[error("Store error: {0}")] + StoreError(String), + + #[error("Core error: {0}")] + CoreError(String), + + #[error("Conflicting block in DB")] + 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("Timed out proposing transaction")] + TimeoutProposingTx { txhash: TxHash, elapsed: String }, + + #[error("Invalid nonce provided for pubkey {pubkey}, account {account_id}. Expected: {expected}. Received: {actual}.")] + InvalidNonce { + pubkey: Box, + account_id: AccountId, + expected: AccountNonce, + actual: AccountNonce, + }, +} + +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::InvalidNonce { + pubkey, + account_id, + expected, + actual, + } => TransactionError::InvalidNonce { + pubkey: pubkey.clone(), + account_id, + expected, + actual, + }, + _ => TransactionError::CoreError(err.to_string()), + } + } +} + +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum KolmeExecutionError { + #[error("Mismatched bridge event")] + MismatchedBridgeEvent, + + #[error("Unexpected bridge event ID")] + UnexpectedBridgeEventId, +} diff --git a/packages/kolme/src/listener/cosmos.rs b/packages/kolme/src/listener/cosmos.rs index 49f85a36..e6ce5916 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -6,16 +6,40 @@ use ::cosmos::{Contract, Cosmos}; 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(transparent)] + Anyhow(#[from] anyhow::Error), +} + pub async fn listen( kolme: Kolme, secret: SecretKey, chain: CosmosChain, contract: String, -) -> Result<()> { +) -> Result<(), CosmosListenerError> { let kolme_r = kolme.read(); let cosmos = kolme_r.get_cosmos(chain).await?; - let contract = cosmos.make_contract(contract.parse()?); + let contract = cosmos.make_contract(contract.parse().map_err(anyhow::Error::from)?); let mut next_bridge_event_id = get_next_bridge_event_id(&kolme_r, secret.public_key(), chain.into()); @@ -40,21 +64,24 @@ async fn listen_once( chain: CosmosChain, contract: &Contract, next_bridge_event_id: &mut BridgeEventId, -) -> Result<()> { +) -> Result<(), CosmosListenerError> { match contract .query(&QueryMsg::GetEvent { id: *next_bridge_event_id, }) - .await? + .await + .map_err(anyhow::Error::from)? { GetEventResp::Found { message } => { - let message = serde_json::from_slice::(&message)?; + let message = serde_json::from_slice::(&message) + .map_err(anyhow::Error::from)?; let message = to_kolme_message::(message, chain.into(), *next_bridge_event_id); kolme .sign_propose_await_transaction(secret, vec![message]) - .await?; + .await + .map_err(anyhow::Error::from)?; *next_bridge_event_id = next_bridge_event_id.next(); @@ -69,14 +96,16 @@ 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(anyhow::Error::from)?); + let actual_code_id = contract.info().await.map_err(anyhow::Error::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 +118,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(anyhow::Error::from)?; - 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 info.validator_set.processor != processor { + return Err(CosmosListenerError::Processor); + } + if listeners != info.validator_set.listeners { + return Err(CosmosListenerError::Listeners); + } + + 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..d458f017 100644 --- a/packages/kolme/src/listener/mod.rs +++ b/packages/kolme/src/listener/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "cosmwasm")] -mod cosmos; +pub mod cosmos; #[cfg(feature = "solana")] -mod solana; +pub mod solana; use crate::*; use tokio::task::JoinSet; @@ -32,8 +32,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 +42,17 @@ 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, - )); + use futures_util::TryFutureExt; + + set.spawn( + cosmos::listen( + self.kolme.clone(), + self.secret.clone(), + chain.to_cosmos_chain().unwrap(), + contract, + ) + .map_err(KolmeError::from), + ); } } } @@ -83,7 +88,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(())) => (), @@ -131,7 +138,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 +151,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 +162,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 +175,7 @@ impl Listener { self.kolme.get_app().genesis_info(), ) .await + .map_err(KolmeError::from) } #[cfg(not(feature = "cosmwasm"))] ChainKind::Cosmos(_) => Ok(()), @@ -180,7 +194,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..c5d9eae2 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:?}"), + } + })?; - 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.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, + )); + } + + if info.validator_set.needed_approvers != state.set.needed_approvers { + return Err(KolmeError::ListenerSolanaError( + ListenerSolanaError::NeededApprovers, + )); + } Ok(()) } @@ -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(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(), diff --git a/packages/kolme/src/processor.rs b/packages/kolme/src/processor.rs index bccc982e..7c94dd48 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,20 +108,20 @@ 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::CoreError(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)?; + let secret = self.get_correct_secret(&kolme).map_err(KolmeError::from)?; let signed = self .kolme .read() @@ -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,13 +191,12 @@ 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())?; + let key = self + .get_correct_secret(&self.kolme.read()) + .map_err(KolmeError::from)?; failed.sign(key) })(); @@ -226,23 +222,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::CoreError(e.to_string()))? + .is_some() + { + return Err(TransactionError::StoreError(format!( + "TxAlreadyInDb: {}", + txhash.0 + ))); } let now = Timestamp::now(); @@ -255,17 +257,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 +283,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::CoreError(e.to_string()))? + .hash(), + app_state: merkle_map::api::serialize(&app_state) + .map_err(|e| TransactionError::CoreError(e.to_string()))? + .hash(), loads, - logs: merkle_map::api::serialize(&logs)?.hash(), + logs: merkle_map::api::serialize(&logs) + .map_err(|e| TransactionError::CoreError(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::CoreError(e.to_string()))?; + let signed_block = Arc::new(SignedBlock( + block + .sign(secret) + .map_err(|e| TransactionError::CoreError(e.to_string()))?, + )); Ok(ExecutedBlock { signed_block, framework_state, @@ -303,7 +322,7 @@ impl Processor { // approve an action, it produces a new block, which will allow us to check if we // need to approve anything else. let kolme = self.kolme.read(); - let secret = self.get_correct_secret(&kolme)?; + let secret = self.get_correct_secret(&kolme).map_err(KolmeError::from)?; let Some((action_id, action)) = kolme.get_next_bridge_action(chain)? else { return Ok(()); @@ -312,7 +331,14 @@ 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), + } + .into()); + } + if kolme.get_approver_pubkeys().contains(key) { approvers.push(*sig); } @@ -359,7 +385,7 @@ impl Processor { }; let json = TaggedJson::new(latest)?; let kolme = self.kolme.read(); - let secret = self.get_correct_secret(&kolme)?; + let secret = self.get_correct_secret(&kolme).map_err(KolmeError::from)?; let signed = json.sign(secret)?; self.kolme.update_latest_block(Arc::new(signed)); Ok(()) diff --git a/packages/kolme/src/submitter/cosmos.rs b/packages/kolme/src/submitter/cosmos.rs index ff881274..289c39f7 100644 --- a/packages/kolme/src/submitter/cosmos.rs +++ b/packages/kolme/src/submitter/cosmos.rs @@ -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..8ccc9ba8 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, Clone, serde::Serialize, serde::Deserialize)] +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() @@ -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..4a1fcfdc 100644 --- a/packages/kolme/src/submitter/solana.rs +++ b/packages/kolme/src/submitter/solana.rs @@ -22,7 +22,7 @@ pub async fn instantiate( 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).map_err(KolmeError::from)?; client.send_and_confirm_transaction(&tx).await?; @@ -37,7 +37,7 @@ 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))?; @@ -99,7 +99,7 @@ pub async fn execute( .downcast_ref::() .map(|e| &e.kind) ); - Err(e) + Err(e.into()) } } } diff --git a/packages/kolme/src/testtasks.rs b/packages/kolme/src/testtasks.rs index bd71138e..653f75a3 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) @@ -83,7 +84,7 @@ impl TestTasks { 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/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/impls/blanket.rs b/packages/merkle-map/src/impls/blanket.rs index 6f72a88a..0ad65dcb 100644 --- a/packages/merkle-map/src/impls/blanket.rs +++ b/packages/merkle-map/src/impls/blanket.rs @@ -29,7 +29,7 @@ impl MerkleDeserializeRaw for T { Err(MerkleSerialError::UnexpectedVersion { highest_supported, actual: version, - type_name: std::any::type_name::(), + type_name: std::any::type_name::().to_string(), offset: deserializer.get_position(), }) } else { 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..42b698c6 100644 --- a/packages/merkle-map/src/types.rs +++ b/packages/merkle-map/src/types.rs @@ -111,33 +111,78 @@ 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, actual: usize, - type_name: &'static str, + type_name: String, 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, + }, } From 37a5a7499a43f1ce8d6eadadb12ec86115627cfc Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Wed, 7 Jan 2026 17:13:30 -0600 Subject: [PATCH 02/12] Refactor error handling --- packages/examples/cosmos-bridge/src/lib.rs | 4 +- .../examples/kademlia-discovery/src/lib.rs | 4 +- packages/examples/six-sigma/src/lib.rs | 4 +- packages/kolme/src/core/execute.rs | 75 ++++++++++++------- packages/kolme/src/core/kolme_app.rs | 16 +++- packages/kolme/src/core/types.rs | 23 +++--- packages/kolme/src/core/types/accounts.rs | 37 +++++---- 7 files changed, 107 insertions(+), 56 deletions(-) diff --git a/packages/examples/cosmos-bridge/src/lib.rs b/packages/examples/cosmos-bridge/src/lib.rs index 4033457a..b313ccb9 100644 --- a/packages/examples/cosmos-bridge/src/lib.rs +++ b/packages/examples/cosmos-bridge/src/lib.rs @@ -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(()) } diff --git a/packages/examples/kademlia-discovery/src/lib.rs b/packages/examples/kademlia-discovery/src/lib.rs index 3a4c14c6..4721a7fa 100644 --- a/packages/examples/kademlia-discovery/src/lib.rs +++ b/packages/examples/kademlia-discovery/src/lib.rs @@ -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(()) } diff --git a/packages/examples/six-sigma/src/lib.rs b/packages/examples/six-sigma/src/lib.rs index e484fa06..9f39f7fe 100644 --- a/packages/examples/six-sigma/src/lib.rs +++ b/packages/examples/six-sigma/src/lib.rs @@ -310,11 +310,11 @@ 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(()) } diff --git a/packages/kolme/src/core/execute.rs b/packages/kolme/src/core/execute.rs index 45778695..d366f9ae 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -2,7 +2,7 @@ use std::collections::VecDeque; use crate::core::*; -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum KolmeExecuteError { #[error("Listener pubkey not allowed for this event")] InvalidListenerPubkey, @@ -64,6 +64,30 @@ pub enum KolmeExecuteError { #[error("Invalid data load request: expected {expected}, got {actual}")] InvalidDataLoadRequest { expected: String, actual: 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)] + Types(#[from] KolmeTypesError), + + #[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] Box), } /// Execution context for a single message. @@ -134,7 +158,7 @@ pub enum BlockDataHandling { } impl KolmeRead { - fn validate_tx(&self, tx: &SignedTransaction) -> Result<()> { + fn validate_tx(&self, tx: &SignedTransaction) -> Result<(), KolmeExecuteError> { // Ensure that the signature is valid tx.validate_signature()?; @@ -149,8 +173,7 @@ impl KolmeRead { return Err(KolmeExecuteError::InvalidGenesisPubkey { expected: Box::new(expected), actual: Box::new(actual), - } - .into()); + }); } } else { tx.ensure_no_genesis()?; @@ -244,7 +267,11 @@ 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(); @@ -485,16 +512,14 @@ 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 @@ -507,16 +532,14 @@ impl ExecutionContext<'_, App> { { return Err(KolmeExecuteError::NonApproverSignature { pubkey: Box::new(key), - } - .into()); + }); } let old = action.approvals.insert(key, signature); if old.is_some() { return Err(KolmeExecuteError::DuplicateApproverSignature { pubkey: Box::new(key), - } - .into()); + }); } Ok(()) } @@ -527,7 +550,7 @@ impl ExecutionContext<'_, App> { action_id: BridgeActionId, processor: &SignatureWithRecovery, approvers: &[SignatureWithRecovery], - ) -> Result<()> { + ) -> Result<(), KolmeError> { let needed = self.framework_state.get_validator_set().needed_approvers as usize; if approvers.len() < needed { return Err(KolmeExecuteError::NotEnoughApprovers { @@ -761,21 +784,20 @@ impl ExecutionContext<'_, App> { 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)?; if prev_req != req { return Err(KolmeExecuteError::InvalidDataLoadRequest { expected: request, actual: request_str.clone(), - } - .into()); + }); } match validation { DataLoadValidation::ValidateDataLoads => { @@ -817,35 +839,38 @@ 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(|e| KolmeExecuteError::Accounts(Box::new(e)))?; } AuthMessage::RemovePublicKey { key } => { if key == &self.signing_key { return Err(KolmeExecuteError::CannotRemoveSigningKey { key: Box::new(*key), account: self.get_sender_id(), - } - .into()); + }); } self.framework_state .accounts - .remove_pubkey_from_account(self.get_sender_id(), *key)?; + .remove_pubkey_from_account(self.get_sender_id(), *key) + .map_err(|e| KolmeExecuteError::Accounts(Box::new(e)))?; } 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(|e| KolmeExecuteError::Accounts(Box::new(e)))?; } 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(|e| KolmeExecuteError::Accounts(Box::new(e)))?; } } Ok(()) diff --git a/packages/kolme/src/core/kolme_app.rs b/packages/kolme/src/core/kolme_app.rs index 101a19e6..02902374 100644 --- a/packages/kolme/src/core/kolme_app.rs +++ b/packages/kolme/src/core/kolme_app.rs @@ -34,6 +34,18 @@ pub trait KolmeApp: Send + Sync + Clone + 'static { ) -> 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: serde::Serialize + serde::de::DeserializeOwned + PartialEq { @@ -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/types.rs b/packages/kolme/src/core/types.rs index 39784b6d..3c8154a8 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -43,6 +43,9 @@ pub enum KolmeTypesError { #[error("Genesis transaction format invalid")] InvalidGenesisTransaction, + + #[error("Failed to verify transaction signature")] + SignatureVerificationFailed, } #[cfg(feature = "solana")] @@ -1055,15 +1058,17 @@ pub struct Block { pub struct SignedTransaction(pub SignedTaggedJson>); impl SignedTransaction { - pub fn validate_signature(&self) -> Result<()> { - let pubkey = self.0.verify_signature()?; + pub fn validate_signature(&self) -> Result<(), KolmeTypesError> { + let pubkey = self + .0 + .verify_signature() + .map_err(|_| KolmeTypesError::SignatureVerificationFailed)?; let expected = self.0.message.as_inner().pubkey; if pubkey != expected { return Err(KolmeTypesError::InvalidTransactionSignature { expected: Box::new(expected), actual: Box::new(pubkey), - } - .into()); + }); } Ok(()) @@ -1078,21 +1083,21 @@ impl SignedTransaction { } impl Transaction { - pub fn ensure_is_genesis(&self) -> Result<()> { + pub fn ensure_is_genesis(&self) -> Result<(), KolmeTypesError> { if self.messages.len() != 1 { - return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + return Err(KolmeTypesError::InvalidGenesisTransaction); } if !matches!(self.messages[0], Message::Genesis(_)) { - return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + return Err(KolmeTypesError::InvalidGenesisTransaction); } Ok(()) } - pub fn ensure_no_genesis(&self) -> Result<()> { + pub fn ensure_no_genesis(&self) -> Result<(), KolmeTypesError> { for msg in &self.messages { if matches!(msg, Message::Genesis(_)) { - return Err(KolmeTypesError::InvalidGenesisTransaction.into()); + return Err(KolmeTypesError::InvalidGenesisTransaction); } } Ok(()) diff --git a/packages/kolme/src/core/types/accounts.rs b/packages/kolme/src/core/types/accounts.rs index 8bbe97a0..c8868450 100644 --- a/packages/kolme/src/core/types/accounts.rs +++ b/packages/kolme/src/core/types/accounts.rs @@ -55,6 +55,15 @@ pub enum AccountsError { expected: AccountNonce, actual: AccountNonce, }, + + #[error("Account {account_id} not found")] + AccountNotFound { account_id: AccountId }, + + #[error("Pubkey {key} not found")] + PubkeyNotFound { key: PublicKey }, + + #[error("Wallet {wallet} not found")] + WalletNotFound { wallet: Wallet }, } /// Track all information on accounts. @@ -186,14 +195,14 @@ impl Accounts { &mut self, account_id: AccountId, key: PublicKey, - ) -> Result<()> { + ) -> Result<(), AccountsError> { if self.pubkeys.contains_key(&key) { - return Err(AccountsError::PubkeyAlreadyInUse { key }.into()); + return Err(AccountsError::PubkeyAlreadyInUse { 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(()) @@ -265,13 +274,13 @@ 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}"))?; + .ok_or(AccountsError::PubkeyNotFound { key })?; if id != actual_id { - return Err(AccountsError::PubkeyAccountMismatch { key, id, actual_id }.into()); + return Err(AccountsError::PubkeyAccountMismatch { key, id, actual_id }); } let was_present = self.accounts.get_mut(&id).unwrap().pubkeys.remove(&key); @@ -283,18 +292,17 @@ impl Accounts { &mut self, account_id: AccountId, wallet: &Wallet, - ) -> Result<()> { + ) -> Result<(), AccountsError> { if self.wallets.contains_key(wallet) { return Err(AccountsError::WalletAlreadyInUse { wallet: wallet.clone(), - } - .into()); + }); } 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(()) @@ -304,18 +312,19 @@ 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}"))?; + .ok_or(AccountsError::WalletNotFound { + wallet: wallet.clone(), + })?; if id != actual_id { return Err(AccountsError::WalletAccountMismatch { wallet: wallet.clone(), id, actual_id, - } - .into()); + }); } let was_present = self.accounts.get_mut(&id).unwrap().wallets.remove(wallet); From 14ac2c3a486ec4db1cd7d4437aa431f1225ae5a4 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Thu, 15 Jan 2026 18:20:12 -0600 Subject: [PATCH 03/12] keep deleting anyhow --- packages/kolme-store/src/error.rs | 3 +- packages/kolme-store/src/fjall.rs | 13 ++++---- packages/kolme-store/src/in_memory.rs | 8 ++--- packages/kolme-store/src/lib.rs | 18 +++++++---- packages/kolme-store/src/postgres.rs | 43 +++++++++++--------------- packages/kolme-store/src/trait.rs | 8 ++--- packages/kolme/src/api_server.rs | 2 +- packages/kolme/src/core/kolme.rs | 17 +++++++--- packages/kolme/src/core/kolme/store.rs | 20 ++++++++---- 9 files changed, 72 insertions(+), 60 deletions(-) diff --git a/packages/kolme-store/src/error.rs b/packages/kolme-store/src/error.rs index de151563..88295890 100644 --- a/packages/kolme-store/src/error.rs +++ b/packages/kolme-store/src/error.rs @@ -62,9 +62,8 @@ pub enum KolmeStoreError { #[error("Invalid message type in first block: expected Genesis")] InvalidFirstBlockMessageType, } - impl KolmeStoreError { - pub fn custom(e: E) -> Self { + 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 6461cc6a..7ccb29ed 100644 --- a/packages/kolme-store/src/fjall.rs +++ b/packages/kolme-store/src/fjall.rs @@ -2,7 +2,6 @@ use crate::{ error::StorageBackend, r#trait::KolmeBackingStore, KolmeConstructLock, KolmeStoreError, RemoteDataListener, StorableBlock, }; -use anyhow::Context; use merkle_map::{MerkleDeserializeRaw, MerkleSerializeRaw, MerkleStore as _, Sha256Hash}; use std::path::Path; @@ -180,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, { @@ -203,21 +202,21 @@ 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(KolmeStoreError::custom)?; 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(KolmeStoreError::custom)? .map(|contents| u64::from_be_bytes(std::array::from_fn(|i| contents[i])))) } diff --git a/packages/kolme-store/src/in_memory.rs b/packages/kolme-store/src/in_memory.rs index 65efa2e1..778cfdb7 100644 --- a/packages/kolme-store/src/in_memory.rs +++ b/packages/kolme-store/src/in_memory.rs @@ -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 91529f9f..b1a59b29 100644 --- a/packages/kolme-store/src/postgres.rs +++ b/packages/kolme-store/src/postgres.rs @@ -4,7 +4,6 @@ use crate::{ BlockHashes, HasBlockHashes, KolmeConstructLock, KolmeStoreError, RemoteDataListener, StorableBlock, }; -use anyhow::Context as _; use merkle_map::{ MerkleDeserializeRaw, MerkleLayerContents, MerkleSerialError, MerkleSerializeRaw, MerkleStore as _, Sha256Hash, @@ -37,26 +36,26 @@ 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(KolmeStoreError::custom)?; sqlx::migrate!() .run(&pool) .await - .context("Unable to execute migrations") - .inspect_err(|err| tracing::error!("{err:?}"))?; + .inspect_err(|err| tracing::error!("{err:?}")) + .map_err(KolmeStoreError::custom)?; Ok(Self { pool, @@ -368,7 +367,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?; @@ -376,7 +375,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?; @@ -391,24 +390,21 @@ 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(KolmeStoreError::custom)) .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#" @@ -421,7 +417,7 @@ impl KolmeBackingStore for Store { ) .execute(&mut *tx) .await - .context("Unable to store latest archived block height")?; + .map_err(KolmeStoreError::custom)?; sqlx::query!( r#" @@ -430,12 +426,9 @@ 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(KolmeStoreError::custom)?; + tx.commit().await.map_err(KolmeStoreError::custom)?; Ok(()) } diff --git a/packages/kolme-store/src/trait.rs b/packages/kolme-store/src/trait.rs index 72bf126a..ca1c7ea4 100644 --- a/packages/kolme-store/src/trait.rs +++ b/packages/kolme-store/src/trait.rs @@ -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/src/api_server.rs b/packages/kolme/src/api_server.rs index 2a4b8ba3..eb3f4908 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -156,7 +156,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, diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 23d060cb..839ff8fa 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -230,7 +230,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 @@ -248,9 +251,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), + }) } } @@ -1171,7 +1175,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 } diff --git a/packages/kolme/src/core/kolme/store.rs b/packages/kolme/src/core/kolme/store.rs index 88230622..9db5532a 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -53,7 +53,7 @@ impl KolmeStore { .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) } @@ -108,21 +108,26 @@ 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 { if !self.has_merkle_hash(*child).await? { - return Err(KolmeStoreError::MissingMerkleChild { child: *child }.into()); + 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 } } @@ -196,7 +201,10 @@ impl KolmeStore { } /// 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 } From 618fca23f0b8ade39db8922339d3f946f8dd7875 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Fri, 16 Jan 2026 15:55:20 -0600 Subject: [PATCH 04/12] more changes --- packages/kolme/src/api_server.rs | 137 +++++++++++++----- .../kolme/src/core/kolme/import_export.rs | 5 +- packages/kolme/src/core/types.rs | 3 +- packages/kolme/src/core/types/error.rs | 27 ++++ packages/kolme/src/gossip/websockets.rs | 8 +- packages/kolme/src/listener/cosmos.rs | 37 +++-- packages/kolme/src/processor.rs | 9 +- packages/kolme/src/submitter/mod.rs | 2 +- packages/kolme/src/submitter/solana.rs | 6 +- packages/kolme/src/testtasks.rs | 2 +- 10 files changed, 174 insertions(+), 62 deletions(-) diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index eb3f4908..dca14e5d 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}, @@ -12,6 +9,7 @@ use axum::{ Json, Router, }; use reqwest::{Method, StatusCode}; +use std::time::Duration; use tower_http::cors::{Any, CorsLayer}; use version_compare::Version; @@ -19,6 +17,45 @@ 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), +} + pub struct ApiServer { kolme: Kolme, extra_routes: Option, @@ -119,13 +156,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) } @@ -206,13 +244,16 @@ 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(KolmeError::from(KolmeApiError::BlockNotFound(block))); }; let framework_hash = signed_block.block.as_inner().framework_state; - let state = kolme.get_framework(framework_hash).await?; + let state = kolme + .get_framework(framework_hash) + .await + .map_err(KolmeError::from)?; let chain_version = state.get_chain_version().clone(); Ok(BlockResponse { chain_version, @@ -224,16 +265,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), @@ -242,24 +283,29 @@ 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(KolmeError::from(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(KolmeError::from(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(KolmeError::from(KolmeApiError::VersionComparisonFailed)); + } } } - bail!("Could not find a block with chain version {chain_version}"); + Err(KolmeError::from( + KolmeApiError::BlockNotFoundOnChainVersion(chain_version.to_string()), + )) } /// Find the first block height with a particular chain version @@ -267,7 +313,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; @@ -276,27 +322,36 @@ async fn find_first_block( let response = block_response(kolme, middle_block).await?; let response_chain_version = - Version::from(&response.chain_version).context("Invalid chain version")?; + Version::from(&response.chain_version).ok_or(KolmeError::from( + 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(KolmeError::from(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(KolmeError::from(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(KolmeError::from(KolmeApiError::FirstBlockNotFound( + chain_version.to_string(), + ))), + } } /// Find the last block height with a particular chain version @@ -304,9 +359,11 @@ 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(KolmeError::from(KolmeApiError::NoBlocksInChain))?; let mut end_block = latest_block; let mut last_block = None; @@ -315,7 +372,9 @@ async fn find_last_block( let response = block_response(kolme, middle_block).await?; let response_chain_version = - Version::from(&response.chain_version).context("Invalid chain version")?; + Version::from(&response.chain_version).ok_or(KolmeError::from( + KolmeApiError::InvalidChainVersion(response.chain_version.to_owned()), + ))?; if response_chain_version == *chain_version { last_block = Some(middle_block); @@ -324,15 +383,20 @@ 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(KolmeError::from(KolmeApiError::UnderflowInPrev))?; } else { start_block = middle_block.next(); } } - last_block.context(format!( - "Could not find last block for chain version {chain_version}" - )) + match last_block { + Some(block_height) => Ok(block_height), + None => Err(KolmeError::from(KolmeApiError::LastBlockNotFound( + chain_version.to_string(), + ))), + } } #[derive(serde::Deserialize)] @@ -478,7 +542,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(); @@ -488,8 +552,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/core/kolme/import_export.rs b/packages/kolme/src/core/kolme/import_export.rs index b9ef8685..825b8ebc 100644 --- a/packages/kolme/src/core/kolme/import_export.rs +++ b/packages/kolme/src/core/kolme/import_export.rs @@ -27,6 +27,9 @@ pub enum KolmeImportExportError { parent: Sha256Hash, child: Sha256Hash, }, + + #[error("Import blocks failed, found unexpected byte {byte}")] + UnexpectedByte { byte: u8 }, } impl Kolme { @@ -173,7 +176,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()), } } } diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 3c8154a8..45bb77e5 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -1824,8 +1824,7 @@ 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))?; + let len = borsh::object_length(&payload).map_err(KolmeError::from)?; let mut buf = Vec::with_capacity(len); borsh::BorshSerialize::serialize(&payload, &mut buf).map_err(KolmeError::from)?; diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index fc39f50e..20b6ccec 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -1,3 +1,4 @@ +use crate::api_server::KolmeApiError; use crate::listener::{cosmos::CosmosListenerError, solana::ListenerSolanaError}; use crate::{core::*, submitter::SubmitterError}; use cosmos::error::{AddressError, WalletError}; @@ -157,6 +158,18 @@ pub enum KolmeError { #[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(std::io::Error), @@ -249,6 +262,20 @@ impl From for KolmeError { } } +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("Store error: {0}")] diff --git a/packages/kolme/src/gossip/websockets.rs b/packages/kolme/src/gossip/websockets.rs index 4b32104b..639e5672 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>, @@ -226,7 +232,7 @@ enum WebsocketsRecv { Close, Skip, Payload(Box>), - Err(anyhow::Error), + Err(WebsocketError), } trait WebSocketWrapper { diff --git a/packages/kolme/src/listener/cosmos.rs b/packages/kolme/src/listener/cosmos.rs index e6ce5916..1f65096d 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -3,6 +3,7 @@ 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}; @@ -26,8 +27,11 @@ pub enum CosmosListenerError { #[error("Needed approvers mismatch")] NeededApprovers, + #[error("Invalid contract address: {0}")] + InvalidAddress(#[from] AddressError), + #[error(transparent)] - Anyhow(#[from] anyhow::Error), + CosmosError(#[from] cosmos::Error), } pub async fn listen( @@ -35,11 +39,11 @@ pub async fn listen( secret: SecretKey, chain: CosmosChain, contract: String, -) -> Result<(), CosmosListenerError> { +) -> Result<(), KolmeError> { let kolme_r = kolme.read(); - let cosmos = kolme_r.get_cosmos(chain).await?; - let contract = cosmos.make_contract(contract.parse().map_err(anyhow::Error::from)?); + let cosmos = kolme_r.get_cosmos(chain).await.map_err(KolmeError::from)?; + let contract = cosmos.make_contract(contract.parse().map_err(KolmeError::from)?); let mut next_bridge_event_id = get_next_bridge_event_id(&kolme_r, secret.public_key(), chain.into()); @@ -64,24 +68,23 @@ async fn listen_once( chain: CosmosChain, contract: &Contract, next_bridge_event_id: &mut BridgeEventId, -) -> Result<(), CosmosListenerError> { +) -> Result<(), KolmeError> { match contract .query(&QueryMsg::GetEvent { id: *next_bridge_event_id, }) .await - .map_err(anyhow::Error::from)? + .map_err(KolmeError::from)? { GetEventResp::Found { message } => { - let message = serde_json::from_slice::(&message) - .map_err(anyhow::Error::from)?; + let message = + serde_json::from_slice::(&message).map_err(KolmeError::from)?; let message = to_kolme_message::(message, chain.into(), *next_bridge_event_id); kolme .sign_propose_await_transaction(secret, vec![message]) - .await - .map_err(anyhow::Error::from)?; + .await?; *next_bridge_event_id = next_bridge_event_id.next(); @@ -97,8 +100,16 @@ pub async fn sanity_check_contract( expected_code_id: u64, info: &GenesisInfo, ) -> Result<(), CosmosListenerError> { - let contract = cosmos.make_contract(contract.parse().map_err(anyhow::Error::from)?); - let actual_code_id = contract.info().await.map_err(anyhow::Error::from)?.code_id; + 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 { @@ -121,7 +132,7 @@ pub async fn sanity_check_contract( } = contract .query(shared::cosmos::QueryMsg::Config {}) .await - .map_err(anyhow::Error::from)?; + .map_err(CosmosListenerError::from)?; if info.validator_set.processor != processor { return Err(CosmosListenerError::Processor); diff --git a/packages/kolme/src/processor.rs b/packages/kolme/src/processor.rs index 7c94dd48..858ee7e4 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -317,7 +317,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. @@ -335,8 +335,7 @@ impl Processor { return Err(KolmeError::InvalidSignature { expected: Box::new(*key), actual: Box::new(key2), - } - .into()); + }); } if kolme.get_approver_pubkeys().contains(key) { @@ -354,7 +353,9 @@ impl Processor { return Ok(()); } - let processor = secret.sign_recoverable(&action.payload)?; + let processor = secret + .sign_recoverable(&action.payload) + .map_err(KolmeError::from)?; let tx = kolme.create_signed_transaction( secret, diff --git a/packages/kolme/src/submitter/mod.rs b/packages/kolme/src/submitter/mod.rs index 8ccc9ba8..855bf429 100644 --- a/packages/kolme/src/submitter/mod.rs +++ b/packages/kolme/src/submitter/mod.rs @@ -156,7 +156,7 @@ impl Submitter { _ = listen_genesis_available.listen() => (), } } - anyhow::Ok(()) + Ok(()) }; let ongoing = async { diff --git a/packages/kolme/src/submitter/solana.rs b/packages/kolme/src/submitter/solana.rs index 4a1fcfdc..81c4997b 100644 --- a/packages/kolme/src/submitter/solana.rs +++ b/packages/kolme/src/submitter/solana.rs @@ -39,8 +39,8 @@ pub async fn execute( fee_per_cu: Option, ) -> 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}: {:?}", @@ -87,7 +87,7 @@ pub async fn execute( &data, &metas, ) - .map_err(|x| anyhow::anyhow!(x))?; + .map_err(KolmeError::from)?; match client.send_and_confirm_transaction(&tx).await { Ok(sig) => Ok(sig.to_string()), diff --git a/packages/kolme/src/testtasks.rs b/packages/kolme/src/testtasks.rs index 653f75a3..5460e122 100644 --- a/packages/kolme/src/testtasks.rs +++ b/packages/kolme/src/testtasks.rs @@ -77,7 +77,7 @@ impl TestTasks { { self.try_spawn(async move { task.await; - anyhow::Ok(()) + Ok::<(), KolmeError>(()) }) } From caccb13a88bcd9f9699682e0335d2161893b683a Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Fri, 16 Jan 2026 18:03:05 -0600 Subject: [PATCH 05/12] delete anyhow from kolme/src/lib.rs --- packages/examples/cosmos-bridge/src/lib.rs | 6 +- .../examples/kademlia-discovery/src/lib.rs | 4 +- packages/examples/p2p/src/lib.rs | 4 +- packages/examples/six-sigma/src/lib.rs | 18 ++-- .../examples/solana-cosmos-bridge/src/lib.rs | 4 +- packages/kolme/src/api_server.rs | 13 ++- packages/kolme/src/approver.rs | 8 +- packages/kolme/src/common.rs | 4 +- packages/kolme/src/core/execute.rs | 60 ++++++++----- packages/kolme/src/core/kolme.rs | 85 ++++++++++++------- packages/kolme/src/core/kolme/block_info.rs | 11 ++- .../kolme/src/core/kolme/import_export.rs | 33 ++++--- packages/kolme/src/core/kolme/store.rs | 18 ++-- packages/kolme/src/core/kolme_app.rs | 4 +- packages/kolme/src/core/state.rs | 4 +- packages/kolme/src/core/types.rs | 78 +++++++++++------ packages/kolme/src/core/types/accounts.rs | 18 ++-- packages/kolme/src/core/types/error.rs | 30 +++++++ packages/kolme/src/gossip.rs | 2 +- packages/kolme/src/gossip/sync_manager.rs | 14 +-- packages/kolme/src/gossip/websockets.rs | 14 +-- packages/kolme/src/lib.rs | 1 - packages/kolme/src/listener/cosmos.rs | 2 +- packages/kolme/src/listener/mod.rs | 5 +- packages/kolme/src/listener/solana.rs | 6 +- packages/kolme/src/pass_through.rs | 10 ++- packages/kolme/src/processor.rs | 4 +- packages/kolme/src/submitter/cosmos.rs | 2 +- packages/kolme/src/submitter/mod.rs | 8 +- packages/kolme/src/submitter/solana.rs | 12 +-- packages/kolme/src/upgrader.rs | 10 ++- 31 files changed, 311 insertions(+), 181 deletions(-) diff --git a/packages/examples/cosmos-bridge/src/lib.rs b/packages/examples/cosmos-bridge/src/lib.rs index b313ccb9..dbc013aa 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".to_owned())), }; ctx.withdraw_asset( AssetId(1), diff --git a/packages/examples/kademlia-discovery/src/lib.rs b/packages/examples/kademlia-discovery/src/lib.rs index 4721a7fa..46b4861e 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, } diff --git a/packages/examples/p2p/src/lib.rs b/packages/examples/p2p/src/lib.rs index 5978c7e6..2a68bf11 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, } diff --git a/packages/examples/six-sigma/src/lib.rs b/packages/examples/six-sigma/src/lib.rs index 9f39f7fe..22fe77b5 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::from)?; + 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::from)?; + 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::from)?; + Ok(()) } AppMessage::PlaceBet { wallet, @@ -269,7 +277,7 @@ impl fmt::Debug for SixSigmaApp { fn change_balance( ctx: &mut ExecutionContext<'_, SixSigmaApp>, change: &BalanceChange, -) -> Result<()> { +) -> Result<(), KolmeError> { match change { BalanceChange::Mint { asset_id, diff --git a/packages/examples/solana-cosmos-bridge/src/lib.rs b/packages/examples/solana-cosmos-bridge/src/lib.rs index 9a6bd10e..fe8e9569 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( diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index dca14e5d..7b4a9548 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -250,10 +250,7 @@ async fn block_response( }; let framework_hash = signed_block.block.as_inner().framework_state; - let state = kolme - .get_framework(framework_hash) - .await - .map_err(KolmeError::from)?; + let state = kolme.get_framework(framework_hash).await?; let chain_version = state.get_chain_version().clone(); Ok(BlockResponse { chain_version, @@ -420,7 +417,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?; @@ -473,9 +470,9 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke async fn get_next_latest( latest: &mut tokio::sync::watch::Receiver>>>, - ) -> Result>> { + ) -> Result>, KolmeError> { loop { - latest.changed().await?; + latest.changed().await.map_err(KolmeError::from)?; if let Some(latest) = latest.borrow().clone().as_ref() { break Ok(latest.clone()); } @@ -495,7 +492,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke block.map(|block| Action::Raw(RawMessage::Block(block))) } 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), + latest = get_next_latest(&mut latest) => latest.map(|latest| Action::Raw(RawMessage::Latest(latest))), }; let action = match action { diff --git a/packages/kolme/src/approver.rs b/packages/kolme/src/approver.rs index 9aac9576..0f2fcdc1 100644 --- a/packages/kolme/src/approver.rs +++ b/packages/kolme/src/approver.rs @@ -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 0329c7b9..d55373ec 100644 --- a/packages/kolme/src/common.rs +++ b/packages/kolme/src/common.rs @@ -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 d366f9ae..a0d3ff1f 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -88,6 +88,18 @@ pub enum KolmeExecuteError { #[error(transparent)] Accounts(#[from] Box), + + #[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 }, } /// Execution context for a single message. @@ -479,7 +491,7 @@ impl ExecutionContext<'_, App> { let next_action_id = actions .keys() .next() - .context("Cannot report on an action when no pending actions present")?; + .ok_or(KolmeExecuteError::NoPendingActionsToReport)?; if *next_action_id != action_id { return Err(KolmeError::ActionIdMismatch { @@ -566,7 +578,7 @@ 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 })?; if action.processor.is_some() { return Err(KolmeExecuteError::ProcessorAlreadyApproved.into()); @@ -681,7 +693,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)?; @@ -699,7 +715,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())?; @@ -716,12 +732,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(|e| KolmeError::Accounts(Box::new(e)))?; self.framework_state .chains .get_mut(chain)? @@ -747,7 +764,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(()) @@ -759,10 +776,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(|e| KolmeError::Accounts(Box::new(e)))?; Ok(()) } @@ -774,10 +792,11 @@ 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(|e| KolmeError::Accounts(Box::new(e)))?; Ok(()) } @@ -817,7 +836,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, @@ -985,12 +1004,11 @@ 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())?; if pubkey != self.pubkey { @@ -1010,7 +1028,7 @@ impl ExecutionContext<'_, App> { 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 { @@ -1056,12 +1074,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 839ff8fa..2b094e8c 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -64,6 +64,24 @@ pub enum KolmeCoreError { #[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. @@ -417,7 +435,7 @@ impl Kolme { } /// 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 @@ -425,7 +443,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), @@ -768,7 +786,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, @@ -896,7 +914,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? { @@ -928,7 +946,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 @@ -944,7 +962,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 @@ -964,7 +982,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)?; @@ -985,7 +1003,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)?; @@ -1003,11 +1021,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() @@ -1035,7 +1056,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()); } @@ -1131,7 +1152,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 } @@ -1143,7 +1164,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)) @@ -1191,7 +1212,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 { @@ -1208,7 +1229,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), @@ -1223,7 +1244,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 { @@ -1265,11 +1286,11 @@ impl Kolme { pub async fn get_block( &self, height: BlockHeight, - ) -> Result>>> { + ) -> Result>>, KolmeError> { 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) } @@ -1290,7 +1311,10 @@ impl Kolme { } /// 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); }; @@ -1301,29 +1325,30 @@ 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, KolmeError> { Ok(self .inner .store .get_latest_archived_block_height() .await - .context("Unable to retrieve latest archived block height")? + .map_err(|e| KolmeCoreError::GetLatestArchivedBlockFailed { source: e })? .map(BlockHeight)) } @@ -1331,7 +1356,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? @@ -1346,7 +1371,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(()); }; @@ -1411,7 +1436,7 @@ impl KolmeRead { pub fn get_next_bridge_action( &self, chain: ExternalChain, - ) -> Result> { + ) -> Result, KolmeError> { Ok(self .get_framework_state() .chains @@ -1467,7 +1492,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) @@ -1479,7 +1504,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 825b8ebc..284334ca 100644 --- a/packages/kolme/src/core/kolme/import_export.rs +++ b/packages/kolme/src/core/kolme/import_export.rs @@ -22,6 +22,9 @@ pub enum KolmeImportExportError { 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, @@ -30,6 +33,12 @@ pub enum KolmeImportExportError { #[error("Import blocks failed, found unexpected byte {byte}")] UnexpectedByte { byte: u8 }, + + #[error("Payload is too large")] + PayloadTooLarge, + + #[error("Too many children")] + TooManyChildren, } impl Kolme { @@ -37,7 +46,7 @@ impl Kolme { &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() { @@ -65,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 { @@ -87,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 { @@ -109,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(); @@ -187,13 +196,17 @@ 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 { if !written_layers.contains(child) { return Err(KolmeImportExportError::LogicChildNotYetWritten { @@ -211,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 9db5532a..7a9be0fc 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -62,14 +62,16 @@ 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? { if &actual != expected { return Err(KolmeError::MismatchedGenesisInfo { actual, expected: expected.clone(), - } - .into()); + }); } } Ok(()) @@ -95,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?) } @@ -133,14 +135,14 @@ impl KolmeStore { } 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>>, KolmeError> { if let Some(storable) = self.block_cache.read().peek(&height) { return Ok(Some(storable.clone())); } @@ -159,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())); } @@ -184,7 +186,7 @@ impl KolmeStore { pub(super) async fn add_block( &self, block: StorableBlock>, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let insertion_result = self.inner.add_block(&block).await; match insertion_result { Err(KolmeStoreError::MatchingBlockAlreadyInserted { .. }) | Ok(_) => { diff --git a/packages/kolme/src/core/kolme_app.rs b/packages/kolme/src/core/kolme_app.rs index 02902374..a4431e63 100644 --- a/packages/kolme/src/core/kolme_app.rs +++ b/packages/kolme/src/core/kolme_app.rs @@ -24,14 +24,14 @@ 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)] diff --git a/packages/kolme/src/core/state.rs b/packages/kolme/src/core/state.rs index 766d07ac..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() @@ -78,7 +78,7 @@ impl ExecutionContext<'_, App> { .proposals { if existing.payload == payload { - return Err(KolmeError::DuplicateAdminProposal { id: *id }.into()); + return Err(KolmeError::DuplicateAdminProposal { id: *id }); } } diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 45bb77e5..ac4a386d 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -46,6 +46,14 @@ pub enum KolmeTypesError { #[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, } #[cfg(feature = "solana")] @@ -161,7 +169,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, @@ -373,17 +381,21 @@ pub struct ChainState { impl ChainState { 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(KolmeTypesError::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(KolmeTypesError::InsufficientFundsWhileWithdrawing { asset_id, amount })?; Ok(()) } } @@ -936,7 +948,7 @@ 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()?; let expected = self.0.message.as_inner().processor; if pubkey != expected { @@ -1115,7 +1127,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)?)) } } @@ -1327,7 +1339,7 @@ impl AdminMessage { validator_type: ValidatorType, replacement: PublicKey, current: &SecretKey, - ) -> Result { + ) -> Result { let self_replace = SelfReplace { validator_type, replacement, @@ -1337,7 +1349,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 { @@ -1345,7 +1357,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(), })?; @@ -1357,7 +1372,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, @@ -1441,7 +1456,7 @@ pub struct GenesisInfo { } impl GenesisInfo { - pub fn validate(&self) -> Result<()> { + pub fn validate(&self) -> Result<(), KolmeError> { self.validator_set.validate()?; Ok(()) } @@ -1621,7 +1636,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == *id) - .context("Unsupported asset ID")? + .ok_or(KolmeTypesError::UnsupportedAssetId)? .0; let denom = denom.0.clone(); @@ -1653,8 +1668,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == coin.id) - .context("Unsupported asset ID")?; - + .ok_or(KolmeTypesError::UnsupportedAssetId)?; coins.push((&asset.0 .0, coin.amount)); } @@ -1911,15 +1925,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 } @@ -1927,8 +1947,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 } @@ -1937,13 +1959,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 } @@ -1951,7 +1975,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 c8868450..edb345cc 100644 --- a/packages/kolme/src/core/types/accounts.rs +++ b/packages/kolme/src/core/types/accounts.rs @@ -339,7 +339,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(); @@ -349,8 +349,7 @@ impl Accounts { account_id: *account_id, expected: account.next_nonce, actual: nonce, - } - .into()); + }); } account.next_nonce = account.next_nonce.next(); Ok(*account_id) @@ -361,12 +360,13 @@ impl Accounts { self.pubkeys.insert(pubkey, account_id); account.pubkeys.insert(pubkey); if nonce != account.next_nonce { - return Err(AccountsError::InvalidInitialNonce { - pubkey, - expected: account.next_nonce, - actual: nonce, - } - .into()); + return Err(KolmeError::Accounts(Box::new( + AccountsError::InvalidInitialNonce { + pubkey, + expected: account.next_nonce, + actual: nonce, + }, + ))); } account.next_nonce = account.next_nonce.next(); Ok(account_id) diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index 20b6ccec..6e35a0fa 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -2,8 +2,11 @@ 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)] pub enum KolmeError { @@ -236,12 +239,33 @@ pub enum KolmeError { #[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), @@ -250,6 +274,12 @@ pub enum KolmeError { #[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, } impl From for KolmeError { 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 639e5672..0b425125 100644 --- a/packages/kolme/src/gossip/websockets.rs +++ b/packages/kolme/src/gossip/websockets.rs @@ -71,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(); @@ -145,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; @@ -188,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)) @@ -241,7 +241,7 @@ trait WebSocketWrapper { &mut self, payload: GossipMessage, local_display_name: &str, - ) -> Result<()>; + ) -> Result<(), KolmeError>; } impl WebSocketWrapper for WebSocket { @@ -280,8 +280,8 @@ impl WebSocketWrapper for WebSocket { &mut self, payload: GossipMessage, _: &str, - ) -> Result<()> { - let payload = serde_json::to_string(&payload)?; + ) -> Result<(), KolmeError> { + let payload = serde_json::to_string(&payload).map_err(KolmeError::from)?; self.send(axum::extract::ws::Message::text(payload)).await?; Ok(()) } @@ -323,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 1f65096d..a3de7ad5 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -42,7 +42,7 @@ pub async fn listen( ) -> Result<(), KolmeError> { let kolme_r = kolme.read(); - let cosmos = kolme_r.get_cosmos(chain).await.map_err(KolmeError::from)?; + let cosmos = kolme_r.get_cosmos(chain).await?; let contract = cosmos.make_contract(contract.parse().map_err(KolmeError::from)?); let mut next_bridge_event_id = diff --git a/packages/kolme/src/listener/mod.rs b/packages/kolme/src/listener/mod.rs index d458f017..060e840f 100644 --- a/packages/kolme/src/listener/mod.rs +++ b/packages/kolme/src/listener/mod.rs @@ -100,7 +100,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 { diff --git a/packages/kolme/src/listener/solana.rs b/packages/kolme/src/listener/solana.rs index c5d9eae2..09a7b2f1 100644 --- a/packages/kolme/src/listener/solana.rs +++ b/packages/kolme/src/listener/solana.rs @@ -109,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; @@ -209,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![]; @@ -222,7 +222,7 @@ async fn catch_up( continue; } - let sig = Signature::from_str(&tx.signature)?; + let sig = Signature::from_str(&tx.signature).map_err(KolmeError::from)?; let tx = client .get_transaction(&sig, UiTransactionEncoding::Binary) .await? diff --git a/packages/kolme/src/pass_through.rs b/packages/kolme/src/pass_through.rs index e4bafb80..41f13a4c 100644 --- a/packages/kolme/src/pass_through.rs +++ b/packages/kolme/src/pass_through.rs @@ -89,7 +89,7 @@ pub async fn execute( processor: SignatureWithRecovery, approvals: &BTreeMap, payload: &str, -) -> Result { +) -> Result { let url = format!("http://localhost:{port}/actions"); tracing::debug!("Sending bridge action to {url}"); let resp = client @@ -101,7 +101,7 @@ pub async fn execute( }) .send() .await?; - resp.error_for_status()?; + resp.error_for_status().map_err(KolmeError::from)?; Ok("no-tx-hash-for-pass-through".to_string()) } @@ -158,7 +158,11 @@ 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)? + .map_err(KolmeError::from)?; 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 858ee7e4..bb7b42cb 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -373,13 +373,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 289c39f7..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())?; diff --git a/packages/kolme/src/submitter/mod.rs b/packages/kolme/src/submitter/mod.rs index 855bf429..7c93c63c 100644 --- a/packages/kolme/src/submitter/mod.rs +++ b/packages/kolme/src/submitter/mod.rs @@ -177,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 { @@ -193,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 { @@ -255,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 @@ -286,7 +286,7 @@ impl Submitter { approvals, processor, }: &PendingBridgeAction, - ) -> Result<()> { + ) -> Result<(), KolmeError> { let Some(processor) = processor else { return Ok(()); }; diff --git a/packages/kolme/src/submitter/solana.rs b/packages/kolme/src/submitter/solana.rs index 81c4997b..5a702bb9 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 std::error::Error; +use std::str::FromStr; use super::*; @@ -15,7 +15,7 @@ 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 }; @@ -95,11 +95,11 @@ pub async fn execute( tracing::error!( "Solana submitter failed to execute signed transaction: {}, error kind: {:?}", e, - e.root_cause() - .downcast_ref::() + e.source() + .and_then(|s| s.downcast_ref::()) .map(|e| &e.kind) ); - Err(e.into()) + Err(e) } } } 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, From 48f2e75c2f57f026bffc78b5536c6aa51dca1187 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Fri, 16 Jan 2026 18:19:36 -0600 Subject: [PATCH 06/12] fix tests --- packages/integration-tests/tests/balances.rs | 4 ++-- packages/integration-tests/tests/cosmos-migration.rs | 4 ++-- packages/integration-tests/tests/key-rotation.rs | 4 ++-- packages/integration-tests/tests/solana-migration.rs | 4 ++-- packages/integration-tests/tests/testapp.rs | 6 +++--- packages/integration-tests/tests/validate_from.rs | 4 ++-- packages/kolme-test/src/auth.rs | 6 +++--- packages/kolme-test/src/kolme_app.rs | 4 ++-- packages/kolme-test/src/offline_simple.rs | 6 +++--- packages/kolme-test/src/p2p_large.rs | 4 ++-- packages/kolme-test/src/upgrade.rs | 8 ++++---- 11 files changed, 27 insertions(+), 27 deletions(-) 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..2dd1c7c9 100644 --- a/packages/integration-tests/tests/testapp.rs +++ b/packages/integration-tests/tests/testapp.rs @@ -1,13 +1,13 @@ 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, }; +use kolme::{ApiNotification, KolmeError}; use rust_decimal::dec; use serde::{Deserialize, Serialize}; use serde_json::{self, Value}; @@ -89,7 +89,7 @@ impl KolmeApp for TestApp { &self.genesis } - fn new_state(&self) -> Result { + fn new_state(&self) -> Result { Ok(TestState { counter: 0 }) } @@ -97,7 +97,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-test/src/auth.rs b/packages/kolme-test/src/auth.rs index 9b781d27..7f30d4c7 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::from(anyhow::anyhow!("execute not implemented"))) } } 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/offline_simple.rs b/packages/kolme-test/src/offline_simple.rs index 23fb8f59..37095928 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::from(anyhow::anyhow!("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/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; From 1e9bec4be26a46e99be65eb4e42193273df22fa1 Mon Sep 17 00:00:00 2001 From: Emanuel Borsboom Date: Mon, 19 Jan 2026 11:56:38 -0800 Subject: [PATCH 07/12] Remove anyhow dependency from kolme and kolme-store --- Cargo.lock | 2 -- packages/examples/cosmos-bridge/src/lib.rs | 2 +- packages/examples/six-sigma/src/lib.rs | 34 +++++++++++-------- packages/kolme-store/Cargo.toml | 1 - packages/kolme-test/src/auth.rs | 2 +- packages/kolme-test/src/kademlia.rs | 4 +-- .../kolme-test/src/multiple_processors.rs | 8 ++--- packages/kolme-test/src/offline_simple.rs | 2 +- packages/kolme-test/src/tx_evicted_mempool.rs | 6 ++-- packages/kolme/Cargo.toml | 1 - packages/kolme/src/core/types/error.rs | 10 ++---- packages/kolme/src/utils/solana.rs | 4 +-- 12 files changed, 36 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b3dda3c..16fcd2e8 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", diff --git a/packages/examples/cosmos-bridge/src/lib.rs b/packages/examples/cosmos-bridge/src/lib.rs index dbc013aa..738d55ee 100644 --- a/packages/examples/cosmos-bridge/src/lib.rs +++ b/packages/examples/cosmos-bridge/src/lib.rs @@ -159,7 +159,7 @@ impl KolmeApp for CosmosBridgeApp { let chain = match address.get_address_hrp().as_str() { "osmo" => ExternalChain::OsmosisTestnet, "neutron" => ExternalChain::NeutronTestnet, - _ => return Err(KolmeError::Other("Unsupported wallet address".to_owned())), + _ => return Err(KolmeError::other("Unsupported wallet address")), }; ctx.withdraw_asset( AssetId(1), diff --git a/packages/examples/six-sigma/src/lib.rs b/packages/examples/six-sigma/src/lib.rs index 22fe77b5..5f2c644d 100644 --- a/packages/examples/six-sigma/src/lib.rs +++ b/packages/examples/six-sigma/src/lib.rs @@ -212,21 +212,21 @@ impl KolmeApp for SixSigmaApp { AppMessage::SendFunds { asset_id, amount } => { ctx.state_mut() .send_funds(*asset_id, *amount) - .map_err(KolmeError::from)?; + .map_err(KolmeError::other)?; Ok(()) } AppMessage::Init => { let signing_key = ctx.get_signing_key(); ctx.state_mut() .initialize(signing_key) - .map_err(KolmeError::from)?; + .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::from)?; + .map_err(KolmeError::other)?; Ok(()) } AppMessage::PlaceBet { @@ -240,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)? } diff --git a/packages/kolme-store/Cargo.toml b/packages/kolme-store/Cargo.toml index 1f738cea..179e415e 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 diff --git a/packages/kolme-test/src/auth.rs b/packages/kolme-test/src/auth.rs index 7f30d4c7..68e76c48 100644 --- a/packages/kolme-test/src/auth.rs +++ b/packages/kolme-test/src/auth.rs @@ -86,7 +86,7 @@ impl KolmeApp for SampleKolmeApp { _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, ) -> Result<(), KolmeError> { - Err(KolmeError::from(anyhow::anyhow!("execute not implemented"))) + 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/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 37095928..6cbff82e 100644 --- a/packages/kolme-test/src/offline_simple.rs +++ b/packages/kolme-test/src/offline_simple.rs @@ -111,7 +111,7 @@ impl KolmeApp for SampleKolmeApp { _ctx: &mut ExecutionContext<'_, Self>, _msg: &Self::Message, ) -> Result<(), KolmeError> { - Err(KolmeError::from(anyhow::anyhow!("execute not implemented"))) + Err(KolmeError::other("execute not implemented")) } } 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/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/core/types/error.rs b/packages/kolme/src/core/types/error.rs index 6e35a0fa..b8451ef7 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -282,13 +282,9 @@ pub enum KolmeError { NoBlocksAvailable, } -impl From for KolmeError { - fn from(e: anyhow::Error) -> Self { - let other = format!("Error from Anyhow: {e}"); - if let Ok(inner) = e.downcast::() { - return inner; - } - KolmeError::Other(other) +impl KolmeError { + pub fn other(e: E) -> Self { + Self::Other(e.to_string()) } } 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)] From de9e4e26b987398574b7421ec6b06490e898676c Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Tue, 20 Jan 2026 17:15:32 -0600 Subject: [PATCH 08/12] refactorcode and recover context --- Cargo.lock | 1 + packages/examples/cosmos-bridge/src/lib.rs | 4 +- .../examples/kademlia-discovery/src/lib.rs | 2 +- packages/examples/p2p/src/lib.rs | 2 +- packages/examples/six-sigma/src/lib.rs | 7 +-- .../examples/solana-cosmos-bridge/src/lib.rs | 6 +- packages/integration-tests/tests/testapp.rs | 9 ++- packages/kolme-store/Cargo.toml | 1 + packages/kolme-store/src/error.rs | 15 +---- packages/kolme-store/src/fjall.rs | 12 ++-- packages/kolme-store/src/postgres.rs | 29 +++++++--- packages/kolme-test/Cargo.toml | 2 +- packages/kolme/src/api_server.rs | 9 +-- packages/kolme/src/core/execute.rs | 36 +++++++----- packages/kolme/src/core/kolme.rs | 9 +-- .../kolme/src/core/kolme/import_export.rs | 2 +- packages/kolme/src/core/types.rs | 58 +++++-------------- packages/kolme/src/core/types/error.rs | 39 ++++++++++--- packages/kolme/src/listener/cosmos.rs | 2 +- packages/kolme/src/listener/mod.rs | 3 +- packages/kolme/src/submitter/mod.rs | 2 +- packages/merkle-map/src/impls/blanket.rs | 2 +- packages/merkle-map/src/types.rs | 2 +- 23 files changed, 127 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16fcd2e8..34f41e03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4054,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 738d55ee..9f34aa24 100644 --- a/packages/examples/cosmos-bridge/src/lib.rs +++ b/packages/examples/cosmos-bridge/src/lib.rs @@ -200,7 +200,7 @@ pub async fn serve(kolme: Kolme, bind: SocketAddr) -> Result<() let processor = Processor::new(kolme.clone(), my_secret_key().clone()); set.spawn(absurd_future(processor.run())); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(async move { listener.run(ChainName::Cosmos).await }); + set.spawn(listener.run(ChainName::Cosmos)); let approver = Approver::new(kolme.clone(), my_secret_key().clone()); set.spawn(approver.run()); let submitter = Submitter::new_cosmos( @@ -209,7 +209,7 @@ pub async fn serve(kolme: Kolme, bind: SocketAddr) -> Result<() ); set.spawn(submitter.run()); let api_server = ApiServer::new(kolme); - set.spawn(async move { api_server.run(bind).await }); + set.spawn(api_server.run(bind)); while let Some(res) = set.join_next().await { match res { diff --git a/packages/examples/kademlia-discovery/src/lib.rs b/packages/examples/kademlia-discovery/src/lib.rs index 46b4861e..3dab84c9 100644 --- a/packages/examples/kademlia-discovery/src/lib.rs +++ b/packages/examples/kademlia-discovery/src/lib.rs @@ -216,7 +216,7 @@ pub async fn new_version_node(api_server_port: u16) -> Result<()> { ) .await?; - let mut set: JoinSet> = JoinSet::new(); + let mut set = JoinSet::new(); let processor = Processor::new(kolme.clone(), my_secret_key().clone()); // Processor consumes mempool transactions and add new transactions into blockchain storage. diff --git a/packages/examples/p2p/src/lib.rs b/packages/examples/p2p/src/lib.rs index 2a68bf11..79d28d43 100644 --- a/packages/examples/p2p/src/lib.rs +++ b/packages/examples/p2p/src/lib.rs @@ -140,7 +140,7 @@ pub async fn api_server(bind: SocketAddr) -> Result<()> { let gossip = GossipBuilder::new().build(kolme.clone())?; set.spawn(absurd_future(gossip.run())); let api_server = ApiServer::new(kolme); - set.spawn(async move { api_server.run(bind).await }); + set.spawn(api_server.run(bind)); while let Some(res) = set.join_next().await { match res { diff --git a/packages/examples/six-sigma/src/lib.rs b/packages/examples/six-sigma/src/lib.rs index 5f2c644d..2823cba2 100644 --- a/packages/examples/six-sigma/src/lib.rs +++ b/packages/examples/six-sigma/src/lib.rs @@ -353,10 +353,7 @@ impl Tasks { let chain = self.kolme.get_app().chain; let listener = Listener::new(self.kolme.clone(), my_secret_key().clone()); - self.listener = Some( - self.set - .spawn(async move { listener.run(chain.name()).await }), - ); + self.listener = Some(self.set.spawn(listener.run(chain.name()))); } pub fn spawn_approver(&mut self) { @@ -374,7 +371,7 @@ impl Tasks { pub fn spawn_api_server(&mut self) { let api_server = ApiServer::new(self.kolme.clone()); let bind = self.bind; - self.api_server = Some(self.set.spawn(async move { api_server.run(bind).await })); + self.api_server = Some(self.set.spawn(api_server.run(bind))); } } diff --git a/packages/examples/solana-cosmos-bridge/src/lib.rs b/packages/examples/solana-cosmos-bridge/src/lib.rs index fe8e9569..1eb57086 100644 --- a/packages/examples/solana-cosmos-bridge/src/lib.rs +++ b/packages/examples/solana-cosmos-bridge/src/lib.rs @@ -177,10 +177,10 @@ pub async fn serve( set.spawn(absurd_future::absurd_future(processor.run())); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(async move { listener.run(ChainName::Cosmos).await }); + set.spawn(listener.run(ChainName::Cosmos)); let listener = Listener::new(kolme.clone(), my_secret_key().clone()); - set.spawn(async move { listener.run(ChainName::Solana).await }); + set.spawn(listener.run(ChainName::Solana)); let approver = Approver::new(kolme.clone(), my_secret_key().clone()); set.spawn(approver.run()); @@ -192,7 +192,7 @@ pub async fn serve( set.spawn(submitter.run()); let api_server = ApiServer::new(kolme); - set.spawn(async move { api_server.run(bind).await }); + set.spawn(api_server.run(bind)); while let Some(res) = set.join_next().await { match res { diff --git a/packages/integration-tests/tests/testapp.rs b/packages/integration-tests/tests/testapp.rs index 2dd1c7c9..bae82f24 100644 --- a/packages/integration-tests/tests/testapp.rs +++ b/packages/integration-tests/tests/testapp.rs @@ -2,12 +2,11 @@ use anyhow::{Context, Result}; use futures_util::future::join_all; use futures_util::StreamExt; 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 kolme::{ApiNotification, KolmeError}; use rust_decimal::dec; use serde::{Deserialize, Serialize}; use serde_json::{self, Value}; diff --git a/packages/kolme-store/Cargo.toml b/packages/kolme-store/Cargo.toml index 179e415e..a9a6a911 100644 --- a/packages/kolme-store/Cargo.toml +++ b/packages/kolme-store/Cargo.toml @@ -17,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 88295890..a9bc578f 100644 --- a/packages/kolme-store/src/error.rs +++ b/packages/kolme-store/src/error.rs @@ -1,22 +1,12 @@ use merkle_map::{MerkleSerialError, Sha256Hash}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, strum::Display)] pub enum StorageBackend { Fjall, Postgres, InMemory, } -impl std::fmt::Display for StorageBackend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StorageBackend::Fjall => write!(f, "Fjall"), - StorageBackend::Postgres => write!(f, "Postgres"), - StorageBackend::InMemory => write!(f, "InMemory"), - } - } -} - #[derive(thiserror::Error, Debug)] pub enum KolmeStoreError { #[error("Custom error: {0}")] @@ -50,7 +40,8 @@ pub enum KolmeStoreError { backend: StorageBackend, txhash: Sha256Hash, bytes: Vec, - reason: String, + #[source] + reason: std::array::TryFromSliceError, }, #[error("Merkle validation error: child hash {child} not found")] diff --git a/packages/kolme-store/src/fjall.rs b/packages/kolme-store/src/fjall.rs index 7ccb29ed..6d499e30 100644 --- a/packages/kolme-store/src/fjall.rs +++ b/packages/kolme-store/src/fjall.rs @@ -72,7 +72,7 @@ impl KolmeBackingStore for Store { backend: StorageBackend::Fjall, txhash, bytes: height.to_vec(), - reason: e.to_string(), + reason: e, }); } }; @@ -86,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::Custom("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))) } @@ -206,7 +206,11 @@ impl KolmeBackingStore for Store { self.merkle .handle .insert(LATEST_ARCHIVED_HEIGHT_KEY, height.to_be_bytes()) - .map_err(KolmeStoreError::custom)?; + .map_err(|e| { + KolmeStoreError::custom(format!( + "Unable to update partition with given height: {e}" + )) + })?; Ok(()) } @@ -216,7 +220,7 @@ impl KolmeBackingStore for Store { .merkle .handle .get(LATEST_ARCHIVED_HEIGHT_KEY) - .map_err(KolmeStoreError::custom)? + .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/postgres.rs b/packages/kolme-store/src/postgres.rs index b1a59b29..24724512 100644 --- a/packages/kolme-store/src/postgres.rs +++ b/packages/kolme-store/src/postgres.rs @@ -49,13 +49,15 @@ impl Store { .connect_with(connect) .await .inspect_err(|err| tracing::error!("{err:?}")) - .map_err(KolmeStoreError::custom)?; + .map_err(|e| { + KolmeStoreError::custom(format!("Could not connect to the database: {e}")) + })?; sqlx::migrate!() .run(&pool) .await .inspect_err(|err| tracing::error!("{err:?}")) - .map_err(KolmeStoreError::custom)?; + .map_err(|e| KolmeStoreError::custom(format!("Unable to execute migrations: {e}")))?; Ok(Self { pool, @@ -194,7 +196,7 @@ impl KolmeBackingStore for Store { sqlx::query_scalar!("SELECT height FROM blocks WHERE txhash=$1 LIMIT 1", txhash) .fetch_optional(&self.pool) .await - .map_err(KolmeStoreError::custom)?; + .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)?)), @@ -399,7 +401,10 @@ impl KolmeBackingStore for Store { .fetch_optional(&self.pool) .await .map_err(KolmeStoreError::custom)? - .map(|x| u64::try_from(x).map_err(KolmeStoreError::custom)) + .map(|x| { + u64::try_from(x) + .map_err(|e| KolmeStoreError::custom(format!("Unable to start database: {e}"))) + }) .transpose() } @@ -417,7 +422,9 @@ impl KolmeBackingStore for Store { ) .execute(&mut *tx) .await - .map_err(KolmeStoreError::custom)?; + .map_err(|e| { + KolmeStoreError::custom(format!("Unable to store latest archived block height: {e}")) + })?; sqlx::query!( r#" @@ -426,9 +433,15 @@ impl KolmeBackingStore for Store { ) .execute(&mut *tx) .await - .map_err(KolmeStoreError::custom)?; - - tx.commit().await.map_err(KolmeStoreError::custom)?; + .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-test/Cargo.toml b/packages/kolme-test/Cargo.toml index 26c65f01..cd759c65 100644 --- a/packages/kolme-test/Cargo.toml +++ b/packages/kolme-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" anyhow = { workspace = true } jiff = { workspace = true } kademlia-discovery = { workspace = true } -kolme = { workspace = true, features = ["solana"] } +kolme = { workspace = true } kolme-store = { workspace = true } merkle-map = { workspace = true } parking_lot = { workspace = true } diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index 7b4a9548..d5271008 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -388,12 +388,9 @@ async fn find_last_block( } } - match last_block { - Some(block_height) => Ok(block_height), - None => Err(KolmeError::from(KolmeApiError::LastBlockNotFound( - chain_version.to_string(), - ))), - } + last_block.ok_or_else(|| { + KolmeError::from(KolmeApiError::LastBlockNotFound(chain_version.to_string())) + }) } #[derive(serde::Deserialize)] diff --git a/packages/kolme/src/core/execute.rs b/packages/kolme/src/core/execute.rs index a0d3ff1f..71908c5d 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -42,7 +42,11 @@ pub enum KolmeExecuteError { NonApproverSignature { pubkey: Box }, #[error("Bridge action already approved with pubkey {pubkey}")] - DuplicateApproverSignature { pubkey: Box }, + DuplicateApproverSignature { + action_id: BridgeActionId, + chain: ExternalChain, + pubkey: Box, + }, #[error("Processor signature invalid")] InvalidProcessorSignature { @@ -62,8 +66,13 @@ pub enum KolmeExecuteError { account: AccountId, }, - #[error("Invalid data load request: expected {expected}, got {actual}")] - InvalidDataLoadRequest { expected: String, actual: String }, + #[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), @@ -74,9 +83,6 @@ pub enum KolmeExecuteError { #[error(transparent)] Json(#[from] serde_json::Error), - #[error(transparent)] - Types(#[from] KolmeTypesError), - #[error(transparent)] Data(#[from] KolmeDataError), @@ -170,7 +176,7 @@ pub enum BlockDataHandling { } impl KolmeRead { - fn validate_tx(&self, tx: &SignedTransaction) -> Result<(), KolmeExecuteError> { + fn validate_tx(&self, tx: &SignedTransaction) -> Result<(), KolmeError> { // Ensure that the signature is valid tx.validate_signature()?; @@ -182,10 +188,10 @@ impl KolmeRead { let expected = self.get_processor_pubkey(); let actual = tx.pubkey; if actual != expected { - return Err(KolmeExecuteError::InvalidGenesisPubkey { + return Err(KolmeError::from(KolmeExecuteError::InvalidGenesisPubkey { expected: Box::new(expected), actual: Box::new(actual), - }); + })); } } else { tx.ensure_no_genesis()?; @@ -500,11 +506,9 @@ impl ExecutionContext<'_, App> { }); } - let Some((old_id, _old)) = actions.remove(&action_id) else { - return Err(KolmeError::ActionError(format!( - "Expected action ID {action_id:?} not found in pending actions" - ))); - }; + 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 { @@ -550,6 +554,8 @@ impl ExecutionContext<'_, App> { let old = action.approvals.insert(key, signature); if old.is_some() { return Err(KolmeExecuteError::DuplicateApproverSignature { + action_id, + chain, pubkey: Box::new(key), }); } @@ -816,6 +822,8 @@ impl ExecutionContext<'_, App> { 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 { diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 2b094e8c..b77dcdb9 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -330,10 +330,7 @@ impl Kolme { .await { Ok(res) => Ok(res?), - Err(e) => Err(TransactionError::TimeoutProposingTx { - txhash, - elapsed: e.to_string(), - }), + Err(_) => Err(TransactionError::TimeoutProposingTx { txhash }), } } @@ -496,8 +493,8 @@ impl Kolme { let block_parent = signed_block.0.message.as_inner().parent; if actual_parent != block_parent { return Err(KolmeError::BlockParentMismatch { - actual: Box::new(actual_parent), - expected: Box::new(block_parent), + actual: actual_parent, + expected: block_parent, }); } diff --git a/packages/kolme/src/core/kolme/import_export.rs b/packages/kolme/src/core/kolme/import_export.rs index 284334ca..b5f8d95f 100644 --- a/packages/kolme/src/core/kolme/import_export.rs +++ b/packages/kolme/src/core/kolme/import_export.rs @@ -11,7 +11,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; use crate::*; -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum KolmeImportExportError { #[error("Child hash {child} was not previously written")] ChildHashNotPreviouslyWritten { child: Sha256Hash }, diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index ac4a386d..10399ce8 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -27,35 +27,6 @@ pub use error::KolmeError; pub use error::KolmeExecutionError; pub use error::TransactionError; -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum KolmeTypesError { - #[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, -} - #[cfg(feature = "solana")] /// Wrapper around the Solana RPC client to hide sensitive information. pub struct SolanaClient(SolanaRpcClient); @@ -383,7 +354,7 @@ impl ChainState { let old = self.assets.entry(asset_id).or_default(); *old = old .checked_add(amount) - .ok_or(KolmeTypesError::OverflowWhileDepositing { asset_id, amount })?; + .ok_or(KolmeError::OverflowWhileDepositing { asset_id, amount })?; Ok(()) } @@ -395,7 +366,7 @@ impl ChainState { let old = self.assets.entry(asset_id).or_default(); *old = old .checked_sub(amount) - .ok_or(KolmeTypesError::InsufficientFundsWhileWithdrawing { asset_id, amount })?; + .ok_or(KolmeError::InsufficientFundsWhileWithdrawing { asset_id, amount })?; Ok(()) } } @@ -952,11 +923,10 @@ impl SignedBlock { let pubkey = self.0.verify_signature()?; let expected = self.0.message.as_inner().processor; if pubkey != expected { - return Err(KolmeTypesError::InvalidBlockProcessorSignature { + return Err(KolmeError::InvalidBlockProcessorSignature { expected: Box::new(expected), actual: Box::new(pubkey), - } - .into()); + }); } Ok(()) @@ -1070,14 +1040,14 @@ pub struct Block { pub struct SignedTransaction(pub SignedTaggedJson>); impl SignedTransaction { - pub fn validate_signature(&self) -> Result<(), KolmeTypesError> { + pub fn validate_signature(&self) -> Result<(), KolmeError> { let pubkey = self .0 .verify_signature() - .map_err(|_| KolmeTypesError::SignatureVerificationFailed)?; + .map_err(|_| KolmeError::SignatureVerificationFailed)?; let expected = self.0.message.as_inner().pubkey; if pubkey != expected { - return Err(KolmeTypesError::InvalidTransactionSignature { + return Err(KolmeError::InvalidTransactionSignature { expected: Box::new(expected), actual: Box::new(pubkey), }); @@ -1095,21 +1065,21 @@ impl SignedTransaction { } impl Transaction { - pub fn ensure_is_genesis(&self) -> Result<(), KolmeTypesError> { + pub fn ensure_is_genesis(&self) -> Result<(), KolmeError> { if self.messages.len() != 1 { - return Err(KolmeTypesError::InvalidGenesisTransaction); + return Err(KolmeError::InvalidGenesisTransaction); } if !matches!(self.messages[0], Message::Genesis(_)) { - return Err(KolmeTypesError::InvalidGenesisTransaction); + return Err(KolmeError::InvalidGenesisTransaction); } Ok(()) } - pub fn ensure_no_genesis(&self) -> Result<(), KolmeTypesError> { + pub fn ensure_no_genesis(&self) -> Result<(), KolmeError> { for msg in &self.messages { if matches!(msg, Message::Genesis(_)) { - return Err(KolmeTypesError::InvalidGenesisTransaction); + return Err(KolmeError::InvalidGenesisTransaction); } } Ok(()) @@ -1636,7 +1606,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == *id) - .ok_or(KolmeTypesError::UnsupportedAssetId)? + .ok_or(KolmeError::UnsupportedAssetId)? .0; let denom = denom.0.clone(); @@ -1668,7 +1638,7 @@ impl ExecAction { .assets .iter() .find(|(_name, config)| config.asset_id == coin.id) - .ok_or(KolmeTypesError::UnsupportedAssetId)?; + .ok_or(KolmeError::UnsupportedAssetId)?; coins.push((&asset.0 .0, coin.amount)); } diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index b8451ef7..5f1255e2 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -94,8 +94,8 @@ pub enum KolmeError { #[error("Block parent mismatch: actual {actual}, expected {expected}")] BlockParentMismatch { - actual: Box, - expected: Box, + actual: BlockHash, + expected: BlockHash, }, #[error("Action ID mismatch: expected {expected}, found {found}")] @@ -143,9 +143,6 @@ pub enum KolmeError { #[error("Import/export error: {0}")] ImportExport(#[from] KolmeImportExportError), - #[error("Types error: {0}")] - TypesError(#[from] KolmeTypesError), - #[error("Core error: {0}")] CoreError(#[from] KolmeCoreError), @@ -280,6 +277,32 @@ pub enum KolmeError { #[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 { @@ -330,8 +353,8 @@ pub enum TransactionError { proposed_height: BlockHeight, }, - #[error("Timed out proposing transaction")] - TimeoutProposingTx { txhash: TxHash, elapsed: String }, + #[error("Timed out proposing transaction {txhash}")] + TimeoutProposingTx { txhash: TxHash }, #[error("Invalid nonce provided for pubkey {pubkey}, account {account_id}. Expected: {expected}. Received: {actual}.")] InvalidNonce { @@ -365,7 +388,7 @@ impl From for TransactionError { } } -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum KolmeExecutionError { #[error("Mismatched bridge event")] MismatchedBridgeEvent, diff --git a/packages/kolme/src/listener/cosmos.rs b/packages/kolme/src/listener/cosmos.rs index a3de7ad5..6373202b 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -43,7 +43,7 @@ pub async fn listen( let kolme_r = kolme.read(); let cosmos = kolme_r.get_cosmos(chain).await?; - let contract = cosmos.make_contract(contract.parse().map_err(KolmeError::from)?); + let contract = cosmos.make_contract(contract.parse()?); let mut next_bridge_event_id = get_next_bridge_event_id(&kolme_r, secret.public_key(), chain.into()); diff --git a/packages/kolme/src/listener/mod.rs b/packages/kolme/src/listener/mod.rs index 060e840f..12ed8c86 100644 --- a/packages/kolme/src/listener/mod.rs +++ b/packages/kolme/src/listener/mod.rs @@ -4,6 +4,7 @@ pub mod cosmos; pub mod solana; use crate::*; +use futures_util::TryFutureExt; use tokio::task::JoinSet; pub struct Listener { @@ -42,8 +43,6 @@ impl Listener { { let contracts = self.wait_for_contracts(name).await?; for (chain, contract) in contracts { - use futures_util::TryFutureExt; - set.spawn( cosmos::listen( self.kolme.clone(), diff --git a/packages/kolme/src/submitter/mod.rs b/packages/kolme/src/submitter/mod.rs index 7c93c63c..93bafac1 100644 --- a/packages/kolme/src/submitter/mod.rs +++ b/packages/kolme/src/submitter/mod.rs @@ -10,7 +10,7 @@ use utils::trigger::Trigger; use crate::*; -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum SubmitterError { #[error("Pass-through submission attempted on wrong chain: expected PassThrough, got {chain}")] InvalidPassThroughChain { chain: ExternalChain }, diff --git a/packages/merkle-map/src/impls/blanket.rs b/packages/merkle-map/src/impls/blanket.rs index 0ad65dcb..6f72a88a 100644 --- a/packages/merkle-map/src/impls/blanket.rs +++ b/packages/merkle-map/src/impls/blanket.rs @@ -29,7 +29,7 @@ impl MerkleDeserializeRaw for T { Err(MerkleSerialError::UnexpectedVersion { highest_supported, actual: version, - type_name: std::any::type_name::().to_string(), + type_name: std::any::type_name::(), offset: deserializer.get_position(), }) } else { diff --git a/packages/merkle-map/src/types.rs b/packages/merkle-map/src/types.rs index 42b698c6..3c0a9bfe 100644 --- a/packages/merkle-map/src/types.rs +++ b/packages/merkle-map/src/types.rs @@ -141,7 +141,7 @@ pub enum MerkleSerialError { UnexpectedVersion { highest_supported: usize, actual: usize, - type_name: String, + type_name: &'static str, offset: usize, }, From 5311a7a6378b99350f3d6ca7d8e4c16c4eb66dc2 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Wed, 21 Jan 2026 16:15:52 -0600 Subject: [PATCH 09/12] refactor boxing --- packages/kolme/src/api_server.rs | 2 +- packages/kolme/src/core/execute.rs | 35 ++++++++++++++++------- packages/kolme/src/core/types.rs | 4 +-- packages/kolme/src/core/types/accounts.rs | 30 ++++++++++--------- packages/kolme/src/core/types/error.rs | 12 ++++---- packages/kolme/src/gossip/websockets.rs | 2 +- packages/kolme/src/listener/cosmos.rs | 6 ++-- packages/kolme/src/listener/solana.rs | 2 +- packages/kolme/src/pass_through.rs | 9 ++---- packages/kolme/src/processor.rs | 14 ++++----- packages/kolme/src/submitter/solana.rs | 31 +++++++++++++------- 11 files changed, 81 insertions(+), 66 deletions(-) diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index d5271008..0aaf37d9 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -469,7 +469,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke latest: &mut tokio::sync::watch::Receiver>>>, ) -> Result>, KolmeError> { loop { - latest.changed().await.map_err(KolmeError::from)?; + latest.changed().await?; if let Some(latest) = latest.borrow().clone().as_ref() { break Ok(latest.clone()); } diff --git a/packages/kolme/src/core/execute.rs b/packages/kolme/src/core/execute.rs index 71908c5d..68368ea8 100644 --- a/packages/kolme/src/core/execute.rs +++ b/packages/kolme/src/core/execute.rs @@ -93,7 +93,7 @@ pub enum KolmeExecuteError { }, #[error(transparent)] - Accounts(#[from] Box), + Accounts(#[from] AccountsError), #[error("Cannot report on an action when no pending actions are present")] NoPendingActionsToReport, @@ -108,6 +108,12 @@ pub enum KolmeExecuteError { 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, @@ -339,7 +345,10 @@ impl ExecutionContext<'_, App> { .listeners .contains(&self.pubkey) { - return Err(KolmeExecuteError::InvalidListenerPubkey.into()); + return Err(KolmeError::NotInValidatorSet { + signer: Box::new(self.pubkey), + role: ValidatorRole::Listener, + }); } let state = self.framework_state.chains.get_mut(chain)?; @@ -476,7 +485,7 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .mint(account_id, asset_config.asset_id, amount) - .map_err(|e| KolmeError::Accounts(Box::new(e)))?; + .map_err(KolmeError::Accounts)?; self.framework_state .chains .get_mut(chain)? @@ -744,7 +753,7 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .burn(source, asset_id, amount_dec) - .map_err(|e| KolmeError::Accounts(Box::new(e)))?; + .map_err(KolmeError::Accounts)?; self.framework_state .chains .get_mut(chain)? @@ -786,7 +795,7 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .mint(recipient, asset_id, amount) - .map_err(|e| KolmeError::Accounts(Box::new(e)))?; + .map_err(KolmeError::Accounts)?; Ok(()) } @@ -802,7 +811,7 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .burn(owner, asset_id, amount) - .map_err(|e| KolmeError::Accounts(Box::new(e)))?; + .map_err(KolmeError::Accounts)?; Ok(()) } @@ -872,7 +881,7 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .add_pubkey_to_account_error_overlap(self.get_sender_id(), *key) - .map_err(|e| KolmeExecuteError::Accounts(Box::new(e)))?; + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::RemovePublicKey { key } => { if key == &self.signing_key { @@ -885,19 +894,19 @@ impl ExecutionContext<'_, App> { self.framework_state .accounts .remove_pubkey_from_account(self.get_sender_id(), *key) - .map_err(|e| KolmeExecuteError::Accounts(Box::new(e)))?; + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::AddWallet { wallet } => { self.framework_state .accounts .add_wallet_to_account(self.get_sender_id(), wallet) - .map_err(|e| KolmeExecuteError::Accounts(Box::new(e)))?; + .map_err(KolmeExecuteError::Accounts)?; } AuthMessage::RemoveWallet { wallet } => { self.framework_state .accounts .remove_wallet_from_account(self.get_sender_id(), wallet) - .map_err(|e| KolmeExecuteError::Accounts(Box::new(e)))?; + .map_err(KolmeExecuteError::Accounts)?; } } Ok(()) @@ -924,7 +933,11 @@ impl ExecutionContext<'_, App> { if !set.remove(&sender) { return Err(KolmeError::NotInValidatorSet { signer: Box::new(sender), - role: if is_approver { "approver" } else { "listener" }.to_string(), + role: if is_approver { + ValidatorRole::Approver + } else { + ValidatorRole::Listener + }, }); } set.insert(replacement); diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 10399ce8..80b1c3c8 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -1808,10 +1808,10 @@ impl ExecAction { #[cfg(feature = "solana")] fn serialize_solana_payload(payload: &shared::solana::Payload) -> Result { - let len = borsh::object_length(&payload).map_err(KolmeError::from)?; + let len = borsh::object_length(&payload)?; let mut buf = Vec::with_capacity(len); - borsh::BorshSerialize::serialize(&payload, &mut buf).map_err(KolmeError::from)?; + borsh::BorshSerialize::serialize(&payload, &mut buf)?; let payload = base64::engine::general_purpose::STANDARD.encode(&buf); diff --git a/packages/kolme/src/core/types/accounts.rs b/packages/kolme/src/core/types/accounts.rs index edb345cc..76e81edf 100644 --- a/packages/kolme/src/core/types/accounts.rs +++ b/packages/kolme/src/core/types/accounts.rs @@ -24,7 +24,7 @@ pub enum AccountsError { }, #[error("Pubkey {key} already in use")] - PubkeyAlreadyInUse { key: PublicKey }, + PubkeyAlreadyInUse { key: Box }, #[error("Wallet {wallet} already in use")] WalletAlreadyInUse { wallet: Wallet }, @@ -33,7 +33,7 @@ pub enum AccountsError { "Cannot remove pubkey {key} from account {id}, it's actually connected to {actual_id}" )] PubkeyAccountMismatch { - key: PublicKey, + key: Box, id: AccountId, actual_id: AccountId, }, @@ -51,7 +51,7 @@ pub enum AccountsError { "New account for pubkey {pubkey} expects an initial nonce of {expected}, received {actual}" )] InvalidInitialNonce { - pubkey: PublicKey, + pubkey: Box, expected: AccountNonce, actual: AccountNonce, }, @@ -60,7 +60,7 @@ pub enum AccountsError { AccountNotFound { account_id: AccountId }, #[error("Pubkey {key} not found")] - PubkeyNotFound { key: PublicKey }, + PubkeyNotFound { key: Box }, #[error("Wallet {wallet} not found")] WalletNotFound { wallet: Wallet }, @@ -197,7 +197,7 @@ impl Accounts { key: PublicKey, ) -> Result<(), AccountsError> { if self.pubkeys.contains_key(&key) { - return Err(AccountsError::PubkeyAlreadyInUse { key }); + return Err(AccountsError::PubkeyAlreadyInUse { key: Box::new(key) }); } let account = self .accounts @@ -278,9 +278,13 @@ impl Accounts { let (_, actual_id) = self .pubkeys .remove(&key) - .ok_or(AccountsError::PubkeyNotFound { key })?; + .ok_or(AccountsError::PubkeyNotFound { key: Box::new(key) })?; if id != actual_id { - return Err(AccountsError::PubkeyAccountMismatch { key, 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); @@ -360,13 +364,11 @@ impl Accounts { self.pubkeys.insert(pubkey, account_id); account.pubkeys.insert(pubkey); if nonce != account.next_nonce { - return Err(KolmeError::Accounts(Box::new( - AccountsError::InvalidInitialNonce { - pubkey, - expected: account.next_nonce, - actual: 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) diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index 5f1255e2..eec54c53 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -22,11 +22,11 @@ pub enum KolmeError { BridgeAlreadyDeployed { chain: ExternalChain }, #[error( - "Signing public key {signer} is not a member of the {role} set and cannot self-replace" + "Signing public key {signer} is not a member of the {role:?} set and cannot self-replace" )] NotInValidatorSet { signer: Box, - role: String, + role: ValidatorRole, }, #[error("Signing public key {signer} is not the current processor and cannot self-replace")] @@ -171,10 +171,10 @@ pub enum KolmeError { TxAlreadyInBlock(BlockHeight), #[error("Failed to serialize Solana payload to Borsh")] - SolanaPayloadSerializationError(std::io::Error), + SolanaPayloadSerializationError(#[source] std::io::Error), #[error("Failed to build Solana initialization transaction")] - SolanaInitTxBuildFailed(std::io::Error), + SolanaInitTxBuildFailed(#[source] std::io::Error), #[error("Failed to create Solana pubsub client")] SolanaPubsubError(#[from] solana_client::nonblocking::pubsub_client::PubsubClientError), @@ -222,7 +222,7 @@ pub enum KolmeError { Asset(#[from] AssetError), #[error("Accounts error")] - Accounts(#[from] Box), + Accounts(#[from] AccountsError), #[error("Validator set error")] ValidatorSet(#[from] ValidatorSetError), @@ -333,7 +333,7 @@ pub enum TransactionError { #[error("Core error: {0}")] CoreError(String), - #[error("Conflicting block in DB")] + #[error("Block with height {height} in database with different hash {existing}, trying to add {adding}")] ConflictingBlockInDb { height: u64, adding: Sha256Hash, diff --git a/packages/kolme/src/gossip/websockets.rs b/packages/kolme/src/gossip/websockets.rs index 0b425125..dba0b8c5 100644 --- a/packages/kolme/src/gossip/websockets.rs +++ b/packages/kolme/src/gossip/websockets.rs @@ -281,7 +281,7 @@ impl WebSocketWrapper for WebSocket { payload: GossipMessage, _: &str, ) -> Result<(), KolmeError> { - let payload = serde_json::to_string(&payload).map_err(KolmeError::from)?; + let payload = serde_json::to_string(&payload)?; self.send(axum::extract::ws::Message::text(payload)).await?; Ok(()) } diff --git a/packages/kolme/src/listener/cosmos.rs b/packages/kolme/src/listener/cosmos.rs index 6373202b..48fefccd 100644 --- a/packages/kolme/src/listener/cosmos.rs +++ b/packages/kolme/src/listener/cosmos.rs @@ -73,12 +73,10 @@ async fn listen_once( .query(&QueryMsg::GetEvent { id: *next_bridge_event_id, }) - .await - .map_err(KolmeError::from)? + .await? { GetEventResp::Found { message } => { - let message = - serde_json::from_slice::(&message).map_err(KolmeError::from)?; + let message = serde_json::from_slice::(&message)?; let message = to_kolme_message::(message, chain.into(), *next_bridge_event_id); diff --git a/packages/kolme/src/listener/solana.rs b/packages/kolme/src/listener/solana.rs index 09a7b2f1..6951155c 100644 --- a/packages/kolme/src/listener/solana.rs +++ b/packages/kolme/src/listener/solana.rs @@ -222,7 +222,7 @@ async fn catch_up( continue; } - let sig = Signature::from_str(&tx.signature).map_err(KolmeError::from)?; + let sig = Signature::from_str(&tx.signature)?; let tx = client .get_transaction(&sig, UiTransactionEncoding::Binary) .await? diff --git a/packages/kolme/src/pass_through.rs b/packages/kolme/src/pass_through.rs index 41f13a4c..652621e1 100644 --- a/packages/kolme/src/pass_through.rs +++ b/packages/kolme/src/pass_through.rs @@ -101,7 +101,7 @@ pub async fn execute( }) .send() .await?; - resp.error_for_status().map_err(KolmeError::from)?; + resp.error_for_status()?; Ok("no-tx-hash-for-pass-through".to_string()) } @@ -158,11 +158,8 @@ pub async fn listen( let (mut ws, _) = connect_async(&ws_url).await.unwrap(); loop { - let message = ws - .next() - .await - .ok_or(KolmeError::WebSocketClosed)? - .map_err(KolmeError::from)?; + 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 bb7b42cb..b3fc8493 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -121,7 +121,7 @@ impl Processor { 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).map_err(KolmeError::from)?; + let secret = self.get_correct_secret(&kolme)?; let signed = self .kolme .read() @@ -194,9 +194,7 @@ impl Processor { error: e.clone(), }; let failed = TaggedJson::new(failed)?; - let key = self - .get_correct_secret(&self.kolme.read()) - .map_err(KolmeError::from)?; + let key = self.get_correct_secret(&self.kolme.read())?; failed.sign(key) })(); @@ -322,7 +320,7 @@ impl Processor { // approve an action, it produces a new block, which will allow us to check if we // need to approve anything else. let kolme = self.kolme.read(); - let secret = self.get_correct_secret(&kolme).map_err(KolmeError::from)?; + let secret = self.get_correct_secret(&kolme)?; let Some((action_id, action)) = kolme.get_next_bridge_action(chain)? else { return Ok(()); @@ -353,9 +351,7 @@ impl Processor { return Ok(()); } - let processor = secret - .sign_recoverable(&action.payload) - .map_err(KolmeError::from)?; + let processor = secret.sign_recoverable(&action.payload)?; let tx = kolme.create_signed_transaction( secret, @@ -386,7 +382,7 @@ impl Processor { }; let json = TaggedJson::new(latest)?; let kolme = self.kolme.read(); - let secret = self.get_correct_secret(&kolme).map_err(KolmeError::from)?; + let secret = self.get_correct_secret(&kolme)?; let signed = json.sign(secret)?; self.kolme.update_latest_block(Arc::new(signed)); Ok(()) diff --git a/packages/kolme/src/submitter/solana.rs b/packages/kolme/src/submitter/solana.rs index 5a702bb9..5fdcd0d4 100644 --- a/packages/kolme/src/submitter/solana.rs +++ b/packages/kolme/src/submitter/solana.rs @@ -5,7 +5,7 @@ use kolme_solana_bridge_client::{ ComputeBudgetInstruction, }; use shared::solana::{InitializeIxData, Payload, SignedAction, SignedMsgIxData}; -use std::error::Error; +use solana_rpc_client_api::client_error::ErrorKind; use std::str::FromStr; use super::*; @@ -22,7 +22,7 @@ pub async fn instantiate( 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(KolmeError::from)?; + let tx = init_tx(program_pubkey, blockhash, keypair, &data)?; client.send_and_confirm_transaction(&tx).await?; @@ -86,19 +86,28 @@ pub async fn execute( keypair, &data, &metas, - ) - .map_err(KolmeError::from)?; + )?; 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.source() - .and_then(|s| s.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) } } From aa981dd975e52d2d342cbd46026a27eb2dffdeaf Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Wed, 21 Jan 2026 17:05:12 -0600 Subject: [PATCH 10/12] refactor KolmeApiError --- packages/kolme/src/api_server.rs | 111 +++++++++++++++---------- packages/kolme/src/core/kolme.rs | 7 +- packages/kolme/src/core/kolme/store.rs | 4 +- packages/kolme/src/core/types.rs | 16 +++- packages/kolme/src/core/types/error.rs | 6 -- 5 files changed, 89 insertions(+), 55 deletions(-) diff --git a/packages/kolme/src/api_server.rs b/packages/kolme/src/api_server.rs index 0aaf37d9..243b90a3 100644 --- a/packages/kolme/src/api_server.rs +++ b/packages/kolme/src/api_server.rs @@ -8,6 +8,7 @@ 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}; @@ -54,6 +55,46 @@ pub enum KolmeApiError { #[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 { @@ -194,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, @@ -244,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 { - return Err(KolmeError::from(KolmeApiError::BlockNotFound(block))); + return Err(KolmeApiError::BlockNotFound(block)); }; let framework_hash = signed_block.block.as_inner().framework_state; @@ -262,7 +303,7 @@ 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().ok_or(KolmeApiError::NoBlocksInChain)?; @@ -280,14 +321,12 @@ 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. - return Err(KolmeError::from(KolmeApiError::ChainVersionNotFound { + return Err(KolmeApiError::ChainVersionNotFound { requested: chain_version.to_string(), earliest: response.chain_version, - })); + }); } - end_block = middle_block - .prev() - .ok_or(KolmeError::from(KolmeApiError::UnderflowInPrev))?; + 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`. @@ -295,13 +334,13 @@ async fn find_block_height( start_block = middle_block.next(); } version_compare::Cmp::Ne => { - return Err(KolmeError::from(KolmeApiError::VersionComparisonFailed)); + return Err(KolmeApiError::VersionComparisonFailed); } } } - Err(KolmeError::from( - KolmeApiError::BlockNotFoundOnChainVersion(chain_version.to_string()), + Err(KolmeApiError::BlockNotFoundOnChainVersion( + chain_version.to_string(), )) } @@ -310,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; @@ -318,36 +357,29 @@ 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).ok_or(KolmeError::from( - KolmeApiError::InvalidChainVersion(response.chain_version.to_owned()), - ))?; + 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() - .ok_or(KolmeError::from(KolmeApiError::UnderflowInPrev))?; + 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() - .ok_or(KolmeError::from(KolmeApiError::UnderflowInPrev))?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } } match first_block { Some(block_height) => Ok(block_height), - None => Err(KolmeError::from(KolmeApiError::FirstBlockNotFound( - chain_version.to_string(), - ))), + None => Err(KolmeApiError::FirstBlockNotFound(chain_version.to_string())), } } @@ -356,11 +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() - .ok_or(KolmeError::from(KolmeApiError::NoBlocksInChain))?; + let latest_block = next_height.prev().ok_or(KolmeApiError::NoBlocksInChain)?; let mut end_block = latest_block; let mut last_block = None; @@ -368,10 +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).ok_or(KolmeError::from( - KolmeApiError::InvalidChainVersion(response.chain_version.to_owned()), - ))?; + 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); @@ -380,17 +409,13 @@ async fn find_last_block( if middle_block.is_start() { break; } - end_block = middle_block - .prev() - .ok_or(KolmeError::from(KolmeApiError::UnderflowInPrev))?; + end_block = middle_block.prev().ok_or(KolmeApiError::UnderflowInPrev)?; } else { start_block = middle_block.next(); } } - last_block.ok_or_else(|| { - KolmeError::from(KolmeApiError::LastBlockNotFound(chain_version.to_string())) - }) + last_block.ok_or_else(|| KolmeApiError::LastBlockNotFound(chain_version.to_string())) } #[derive(serde::Deserialize)] @@ -414,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?; @@ -467,7 +492,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke async fn get_next_latest( latest: &mut tokio::sync::watch::Receiver>>>, - ) -> Result>, KolmeError> { + ) -> Result>, KolmeApiError> { loop { latest.changed().await?; if let Some(latest) = latest.borrow().clone().as_ref() { @@ -489,7 +514,7 @@ async fn handle_websocket(kolme: Kolme, mut socket: WebSocke block.map(|block| Action::Raw(RawMessage::Block(block))) } 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))), + latest = get_next_latest(&mut latest) => latest.map(|latest| Action::Raw(RawMessage::Latest(latest))).map_err(KolmeError::from), }; let action = match action { diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index b77dcdb9..34b3aa22 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -1283,11 +1283,14 @@ impl Kolme { pub async fn get_block( &self, height: BlockHeight, - ) -> Result>>, KolmeError> { + ) -> 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) } diff --git a/packages/kolme/src/core/kolme/store.rs b/packages/kolme/src/core/kolme/store.rs index 7a9be0fc..b9166422 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -142,12 +142,12 @@ impl KolmeStore { pub async fn load_block( &self, height: BlockHeight, - ) -> Result>>, KolmeError> { + ) -> 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 { diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 80b1c3c8..817b0b41 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -796,6 +796,15 @@ impl TryFrom for AccountNonce { } } +#[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, @@ -818,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, }); diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index eec54c53..ba1c2bbb 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -188,12 +188,6 @@ pub enum KolmeError { #[error("Error deserializing Solana bridge message from logs: {details}")] InvalidSolanaBridgeLogMessage { details: String }, - #[error("Start height {start} is greater than {end} height")] - InvalidBlockHeight { - start: BlockHeight, - end: BlockHeight, - }, - #[error(transparent)] Transaction(#[from] TransactionError), From 9546ab9fe24f8ec0fbdaeabcba81bbbaec9d15bd Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Thu, 22 Jan 2026 17:28:04 -0600 Subject: [PATCH 11/12] refactor transactionerror --- packages/kolme/src/core/kolme.rs | 50 +++++++++++--------------- packages/kolme/src/core/kolme/store.rs | 4 +-- packages/kolme/src/core/types.rs | 2 +- packages/kolme/src/core/types/error.rs | 27 ++++++++------ packages/kolme/src/processor.rs | 20 +++++------ 5 files changed, 51 insertions(+), 52 deletions(-) diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 34b3aa22..1f1c33aa 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -337,7 +337,7 @@ impl Kolme { async fn propose_and_await_transaction_inner( &self, tx: Arc>, - ) -> Result>, TransactionError> { + ) -> Result>, KolmeError> { let mut new_block = self.subscribe_new_block(); let mut failed_tx = self.subscribe_failed_txs(); let txhash = tx.hash(); @@ -354,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()); + break Err(KolmeError::other(failed.message.as_inner().error.clone())); } } @@ -407,7 +407,7 @@ impl Kolme { match self.propose_and_await_transaction_inner(tx).await { Ok(block) => return Ok(block), Err(e) => { - if let TransactionError::InvalidNonce { + if let KolmeError::InvalidNonce { pubkey: _, account_id: _, expected, @@ -425,7 +425,7 @@ impl Kolme { continue; } } - return Err(crate::KolmeError::Transaction(e)); + return Err(e); } } } @@ -587,7 +587,7 @@ impl Kolme { .store .save(&framework_state) .await - .map_err(|e| TransactionError::StoreError(e.to_string()))?; + .map_err(TransactionError::StoreError)?; let expected_fw = signed_block.0.message.as_inner().framework_state; if framework_state_hash != expected_fw { @@ -595,8 +595,7 @@ impl Kolme { KolmeCoreError::FrameworkStateHash { expected: expected_fw, actual: framework_state_hash, - } - .to_string(), + }, )); } @@ -605,17 +604,14 @@ impl Kolme { .store .save(&app_state) .await - .map_err(|e| TransactionError::StoreError(e.to_string()))?; + .map_err(TransactionError::StoreError)?; 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(), - )); + return Err(TransactionError::CoreError(KolmeCoreError::AppStateHash { + expected: expected_app, + actual: app_state_hash, + })); } let logs_hash = self @@ -623,17 +619,14 @@ impl Kolme { .store .save(&logs) .await - .map_err(|e| TransactionError::StoreError(e.to_string()))?; + .map_err(TransactionError::StoreError)?; 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(), - )); + return Err(TransactionError::CoreError(KolmeCoreError::LogsHash { + expected: expected_logs, + actual: logs_hash, + })); } self.inner @@ -645,7 +638,7 @@ impl Kolme { block: signed_block.clone(), }) .await - .map_err(|e| TransactionError::StoreError(e.to_string()))?; + .map_err(TransactionError::StoreError)?; self.inner.mempool.add_signed_block(signed_block.clone()); if let Some(tx) = self.inner.landed_txs.get() { @@ -677,7 +670,7 @@ impl Kolme { if self .get_next_to_archive() .await - .map_err(|e| TransactionError::StoreError(e.to_string()))? + .map_err(TransactionError::StoreError)? == height { if let Err(e) = self.inner.store.archive_block(height).await { @@ -1342,13 +1335,12 @@ impl Kolme { } /// Obtains the latest block synced by the Archiver, if it exists - pub async fn get_latest_archived_block(&self) -> Result, KolmeError> { + pub async fn get_latest_archived_block(&self) -> Result, KolmeStoreError> { Ok(self .inner .store .get_latest_archived_block_height() - .await - .map_err(|e| KolmeCoreError::GetLatestArchivedBlockFailed { source: e })? + .await? .map(BlockHeight)) } @@ -1356,7 +1348,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? diff --git a/packages/kolme/src/core/kolme/store.rs b/packages/kolme/src/core/kolme/store.rs index b9166422..1ed20893 100644 --- a/packages/kolme/src/core/kolme/store.rs +++ b/packages/kolme/src/core/kolme/store.rs @@ -186,7 +186,7 @@ impl KolmeStore { pub(super) async fn add_block( &self, block: StorableBlock>, - ) -> Result<(), KolmeError> { + ) -> Result<(), KolmeStoreError> { let insertion_result = self.inner.add_block(&block).await; match insertion_result { Err(KolmeStoreError::MatchingBlockAlreadyInserted { .. }) | Ok(_) => { @@ -198,7 +198,7 @@ impl KolmeStore { Ok(()) } - Err(e) => Err(e.into()), + Err(e) => Err(e), } } diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 817b0b41..2880c700 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -1841,7 +1841,7 @@ pub struct FailedTransaction { pub txhash: TxHash, /// Block height we attempted to generate. pub proposed_height: BlockHeight, - pub error: TransactionError, + pub error: String, } impl Display for FailedTransaction { diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index ba1c2bbb..a0fa58c2 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -312,20 +312,26 @@ impl From> for KolmeError { ProposeTransactionError::InBlock(block) => KolmeError::TxAlreadyInBlock(block.height()), - ProposeTransactionError::Failed(failed) => { - KolmeError::Transaction(failed.message.as_inner().error.clone()) - } + ProposeTransactionError::Failed(failed) => KolmeError::Transaction( + TransactionError::Other(failed.message.as_inner().error.clone()), + ), } } } -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(thiserror::Error, Debug)] pub enum TransactionError { - #[error("Store error: {0}")] - StoreError(String), + #[error("{0}")] + Other(String), - #[error("Core error: {0}")] - CoreError(String), + #[error(transparent)] + StoreError(#[from] KolmeStoreError), + + #[error(transparent)] + CoreError(#[from] KolmeCoreError), + + #[error(transparent)] + MerkleError(#[from] MerkleSerialError), #[error("Block with height {height} in database with different hash {existing}, trying to add {adding}")] ConflictingBlockInDb { @@ -365,7 +371,8 @@ impl From for TransactionError { KolmeError::ExecutedHeightMismatch { expected, actual } => { TransactionError::ExecutedHeightMismatch { expected, actual } } - KolmeError::StoreError(e) => TransactionError::StoreError(e.to_string()), + KolmeError::StoreError(e) => TransactionError::StoreError(e), + KolmeError::CoreError(e) => TransactionError::CoreError(e), KolmeError::InvalidNonce { pubkey, account_id, @@ -377,7 +384,7 @@ impl From for TransactionError { expected, actual, }, - _ => TransactionError::CoreError(err.to_string()), + _ => TransactionError::Other(err.to_string()), } } } diff --git a/packages/kolme/src/processor.rs b/packages/kolme/src/processor.rs index b3fc8493..9a03884b 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -112,7 +112,7 @@ impl Processor { let pubkey = &kolme.get_framework_state().get_validator_set().processor; self.secrets.get(pubkey).ok_or_else(|| { let pubkeys = self.secrets.keys().collect::>(); - TransactionError::CoreError(format!( + TransactionError::Other(format!( "Current processor pubkey is {pubkey}, but we don't have the matching secret key, we have: {pubkeys:?}" )) }) @@ -191,7 +191,7 @@ impl Processor { let failed = FailedTransaction { txhash, proposed_height, - error: e.clone(), + error: e.to_string(), }; let failed = TaggedJson::new(failed)?; let key = self.get_correct_secret(&self.kolme.read())?; @@ -236,10 +236,10 @@ impl Processor { if kolme .get_tx_height(txhash) .await - .map_err(|e| TransactionError::CoreError(e.to_string()))? + .map_err(TransactionError::StoreError)? .is_some() { - return Err(TransactionError::StoreError(format!( + return Err(TransactionError::Other(format!( "TxAlreadyInDb: {}", txhash.0 ))); @@ -282,22 +282,22 @@ impl Processor { height: proposed_height, parent: kolme.get_current_block_hash(), framework_state: merkle_map::api::serialize(&framework_state) - .map_err(|e| TransactionError::CoreError(e.to_string()))? + .map_err(TransactionError::MerkleError)? .hash(), app_state: merkle_map::api::serialize(&app_state) - .map_err(|e| TransactionError::CoreError(e.to_string()))? + .map_err(TransactionError::MerkleError)? .hash(), loads, logs: merkle_map::api::serialize(&logs) - .map_err(|e| TransactionError::CoreError(e.to_string()))? + .map_err(TransactionError::MerkleError)? .hash(), }; - let block = TaggedJson::new(approved_block) - .map_err(|e| TransactionError::CoreError(e.to_string()))?; + 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::CoreError(e.to_string()))?, + .map_err(|e| TransactionError::Other(e.to_string()))?, )); Ok(ExecutedBlock { signed_block, From aab096da9ecd7bafbe1c5d176b94060182bccee9 Mon Sep 17 00:00:00 2001 From: anakinzhed Date: Fri, 23 Jan 2026 11:31:49 -0600 Subject: [PATCH 12/12] FailedTransaction using TransactionError --- packages/kolme/src/core/kolme.rs | 43 +++++++++++++++----------- packages/kolme/src/core/types.rs | 2 +- packages/kolme/src/core/types/error.rs | 25 ++++++++------- packages/kolme/src/processor.rs | 10 +++--- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/packages/kolme/src/core/kolme.rs b/packages/kolme/src/core/kolme.rs index 1f1c33aa..0e0c3b15 100644 --- a/packages/kolme/src/core/kolme.rs +++ b/packages/kolme/src/core/kolme.rs @@ -337,7 +337,7 @@ impl Kolme { async fn propose_and_await_transaction_inner( &self, tx: Arc>, - ) -> Result>, KolmeError> { + ) -> Result>, TransactionError> { let mut new_block = self.subscribe_new_block(); let mut failed_tx = self.subscribe_failed_txs(); let txhash = tx.hash(); @@ -354,7 +354,7 @@ impl Kolme { } Err(ProposeTransactionError::Failed(failed)) => { debug_assert_eq!(failed.message.as_inner().txhash, txhash); - break Err(KolmeError::other(failed.message.as_inner().error.clone())); + break Err(failed.message.as_inner().error.clone()); } } @@ -407,7 +407,7 @@ impl Kolme { match self.propose_and_await_transaction_inner(tx).await { Ok(block) => return Ok(block), Err(e) => { - if let KolmeError::InvalidNonce { + if let TransactionError::InvalidNonce { pubkey: _, account_id: _, expected, @@ -425,7 +425,7 @@ impl Kolme { continue; } } - return Err(e); + return Err(KolmeError::Transaction(e)); } } } @@ -587,7 +587,7 @@ impl Kolme { .store .save(&framework_state) .await - .map_err(TransactionError::StoreError)?; + .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 { @@ -595,7 +595,8 @@ impl Kolme { KolmeCoreError::FrameworkStateHash { expected: expected_fw, actual: framework_state_hash, - }, + } + .to_string(), )); } @@ -604,14 +605,17 @@ impl Kolme { .store .save(&app_state) .await - .map_err(TransactionError::StoreError)?; + .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, - })); + return Err(TransactionError::CoreError( + KolmeCoreError::AppStateHash { + expected: expected_app, + actual: app_state_hash, + } + .to_string(), + )); } let logs_hash = self @@ -619,14 +623,17 @@ impl Kolme { .store .save(&logs) .await - .map_err(TransactionError::StoreError)?; + .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, - })); + return Err(TransactionError::CoreError( + KolmeCoreError::LogsHash { + expected: expected_logs, + actual: logs_hash, + } + .to_string(), + )); } self.inner @@ -638,7 +645,7 @@ impl Kolme { block: signed_block.clone(), }) .await - .map_err(TransactionError::StoreError)?; + .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() { @@ -670,7 +677,7 @@ impl Kolme { if self .get_next_to_archive() .await - .map_err(TransactionError::StoreError)? + .map_err(|e| TransactionError::StoreError(e.to_string()))? == height { if let Err(e) = self.inner.store.archive_block(height).await { diff --git a/packages/kolme/src/core/types.rs b/packages/kolme/src/core/types.rs index 2880c700..817b0b41 100644 --- a/packages/kolme/src/core/types.rs +++ b/packages/kolme/src/core/types.rs @@ -1841,7 +1841,7 @@ pub struct FailedTransaction { pub txhash: TxHash, /// Block height we attempted to generate. pub proposed_height: BlockHeight, - pub error: String, + pub error: TransactionError, } impl Display for FailedTransaction { diff --git a/packages/kolme/src/core/types/error.rs b/packages/kolme/src/core/types/error.rs index a0fa58c2..6e660dc3 100644 --- a/packages/kolme/src/core/types/error.rs +++ b/packages/kolme/src/core/types/error.rs @@ -312,26 +312,26 @@ impl From> for KolmeError { ProposeTransactionError::InBlock(block) => KolmeError::TxAlreadyInBlock(block.height()), - ProposeTransactionError::Failed(failed) => KolmeError::Transaction( - TransactionError::Other(failed.message.as_inner().error.clone()), - ), + ProposeTransactionError::Failed(failed) => { + KolmeError::Transaction(failed.message.as_inner().error.clone()) + } } } } -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum TransactionError { #[error("{0}")] Other(String), - #[error(transparent)] - StoreError(#[from] KolmeStoreError), + #[error("KolmeStoreError: {0}")] + StoreError(String), - #[error(transparent)] - CoreError(#[from] KolmeCoreError), + #[error("KolmeCoreError: {0}")] + CoreError(String), - #[error(transparent)] - MerkleError(#[from] MerkleSerialError), + #[error("KolmeMerkleSerialError: {0}")] + MerkleError(String), #[error("Block with height {height} in database with different hash {existing}, trying to add {adding}")] ConflictingBlockInDb { @@ -371,8 +371,9 @@ impl From for TransactionError { KolmeError::ExecutedHeightMismatch { expected, actual } => { TransactionError::ExecutedHeightMismatch { expected, actual } } - KolmeError::StoreError(e) => TransactionError::StoreError(e), - KolmeError::CoreError(e) => TransactionError::CoreError(e), + 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, diff --git a/packages/kolme/src/processor.rs b/packages/kolme/src/processor.rs index 9a03884b..4f700b91 100644 --- a/packages/kolme/src/processor.rs +++ b/packages/kolme/src/processor.rs @@ -191,7 +191,7 @@ impl Processor { let failed = FailedTransaction { txhash, proposed_height, - error: e.to_string(), + error: e.clone(), }; let failed = TaggedJson::new(failed)?; let key = self.get_correct_secret(&self.kolme.read())?; @@ -236,7 +236,7 @@ impl Processor { if kolme .get_tx_height(txhash) .await - .map_err(TransactionError::StoreError)? + .map_err(|e| TransactionError::StoreError(e.to_string()))? .is_some() { return Err(TransactionError::Other(format!( @@ -282,14 +282,14 @@ impl Processor { height: proposed_height, parent: kolme.get_current_block_hash(), framework_state: merkle_map::api::serialize(&framework_state) - .map_err(TransactionError::MerkleError)? + .map_err(|e| TransactionError::MerkleError(e.to_string()))? .hash(), app_state: merkle_map::api::serialize(&app_state) - .map_err(TransactionError::MerkleError)? + .map_err(|e| TransactionError::MerkleError(e.to_string()))? .hash(), loads, logs: merkle_map::api::serialize(&logs) - .map_err(TransactionError::MerkleError)? + .map_err(|e| TransactionError::MerkleError(e.to_string()))? .hash(), }; let block =