diff --git a/crates/batcher/src/lib.rs b/crates/batcher/src/lib.rs index a3d0a670e3..58a219b5c6 100644 --- a/crates/batcher/src/lib.rs +++ b/crates/batcher/src/lib.rs @@ -1,3 +1,4 @@ +use aligned_sdk::common::errors::{ReplacementInvalidReason, SubmitError}; use aligned_sdk::communication::serialization::{cbor_deserialize, cbor_serialize}; use config::NonPayingConfig; use connection::{send_message, WsMessageSink}; @@ -530,9 +531,17 @@ impl Batcher { .await } ClientMessage::SubmitProof(msg) => { - self.clone() - .handle_submit_proof_msg(msg, ws_conn_sink) + if let Err(e) = self + .clone() + .handle_submit_proof_msg(msg, ws_conn_sink.clone()) .await + { + let error = SubmitProofResponseMessage::Error(e); + send_message(ws_conn_sink, error).await; + Ok(()) + } else { + Ok(()) + } } } } @@ -608,7 +617,7 @@ impl Batcher { self: Arc, client_msg: Box, ws_conn_sink: WsMessageSink, - ) -> Result<(), Error> { + ) -> Result<(), SubmitError> { let msg_nonce = client_msg.verification_data.nonce; debug!("Received message with nonce: {msg_nonce:?}"); self.metrics.received_proofs.inc(); @@ -618,32 +627,12 @@ impl Batcher { // * ---------------------------------------------------* // All check functions sends the error to the metrics server and logs it - // if they return false - - if !self.msg_chain_id_is_valid(&client_msg, &ws_conn_sink).await { - return Ok(()); - } - - if !self - .msg_batcher_payment_addr_is_valid(&client_msg, &ws_conn_sink) - .await - { - return Ok(()); - } + // if they return SubmitError + self.msg_chain_id_is_valid(&client_msg).await?; + self.msg_batcher_payment_addr_is_valid(&client_msg).await?; + self.msg_proof_size_is_valid(&client_msg).await?; - if !self - .msg_proof_size_is_valid(&client_msg, &ws_conn_sink) - .await - { - return Ok(()); - } - - let Some(addr_in_msg) = self - .msg_signature_is_valid(&client_msg, &ws_conn_sink) - .await - else { - return Ok(()); - }; + let addr_in_msg = self.msg_signature_is_valid(&client_msg).await?; let addr; let signature = client_msg.signature; @@ -673,32 +662,24 @@ impl Batcher { "Verifier for proving system {} is disabled, skipping verification", verification_data.proving_system ); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidProof(ProofInvalidReason::DisabledVerifier( - verification_data.proving_system, - )), - ) - .await; self.metrics.user_error(&[ "disabled_verifier", &format!("{}", verification_data.proving_system), ]); - return Ok(()); + return Err(SubmitError::InvalidProof( + ProofInvalidReason::DisabledVerifier(verification_data.proving_system), + )); } if !zk_utils::verify(verification_data).await { error!("Invalid proof detected. Verification failed"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidProof(ProofInvalidReason::RejectedProof), - ) - .await; self.metrics.user_error(&[ "rejected_proof", &format!("{}", verification_data.proving_system), ]); - return Ok(()); + return Err(SubmitError::InvalidProof( + ProofInvalidReason::VerificationFailed, + )); } } @@ -707,9 +688,7 @@ impl Batcher { // We don't need a batch state lock here, since if the user locks its funds // after the check, some blocks should pass until he can withdraw. // It is safe to do just do this here. - if !self.msg_user_balance_is_locked(&addr, &ws_conn_sink).await { - return Ok(()); - } + self.msg_user_balance_is_locked(&addr).await?; // We acquire the lock first only to query if the user is already present and the lock is dropped. // If it was not present, then the user nonce is queried to the Aligned contract. @@ -728,13 +707,9 @@ impl Batcher { error!( "Failed to get user nonce from Ethereum for address {addr:?}. Error: {e:?}" ); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::EthRpcError, - ) - .await; self.metrics.user_error(&["eth_rpc_error", ""]); - return Ok(()); + // We don't pass the string error as to not leak anything about the rpc + return Err(SubmitError::EthereumProviderError("".to_string())); } }; let user_state = UserState::new(ethereum_user_nonce); @@ -751,13 +726,8 @@ impl Batcher { let Some(user_balance) = self.get_user_balance(&addr).await else { error!("Could not get balance for address {addr:?}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::EthRpcError, - ) - .await; self.metrics.user_error(&["eth_rpc_error", ""]); - return Ok(()); + return Err(SubmitError::EthereumProviderError("".to_string())); }; // For now on until the message is fully processed, the batch state is locked @@ -767,66 +737,34 @@ impl Batcher { let mut batch_state_lock = self.batch_state.lock().await; let msg_max_fee = nonced_verification_data.max_fee; - let Some(user_last_max_fee_limit) = - batch_state_lock.get_user_last_max_fee_limit(&addr).await - else { - std::mem::drop(batch_state_lock); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::AddToBatchError, - ) - .await; - self.metrics.user_error(&["batcher_state_error", ""]); - return Ok(()); - }; - - let Some(user_accumulated_fee) = batch_state_lock.get_user_total_fees_in_queue(&addr).await - else { - std::mem::drop(batch_state_lock); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::AddToBatchError, - ) - .await; - self.metrics.user_error(&["batcher_state_error", ""]); - return Ok(()); - }; + let user_last_max_fee_limit = batch_state_lock + .get_user_last_max_fee_limit(&addr) + .await + .ok_or(SubmitError::BatcherUnexpectedError)?; + let user_accumulated_fee = batch_state_lock + .get_user_total_fees_in_queue(&addr) + .await + .ok_or(SubmitError::BatcherUnexpectedError)?; - if !self.verify_user_has_enough_balance(user_balance, user_accumulated_fee, msg_max_fee) { - std::mem::drop(batch_state_lock); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InsufficientBalance(addr), - ) - .await; + if let Err(e) = + self.verify_user_has_enough_balance(user_balance, user_accumulated_fee, msg_max_fee) + { self.metrics.user_error(&["insufficient_balance", ""]); - return Ok(()); + return Err(e); } - let cached_user_nonce = batch_state_lock.get_user_nonce(&addr).await; - - let Some(expected_nonce) = cached_user_nonce else { - error!("Failed to get cached user nonce: User not found in user states, but it should have been already inserted"); - std::mem::drop(batch_state_lock); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::AddToBatchError, - ) - .await; - self.metrics.user_error(&["batcher_state_error", ""]); - return Ok(()); - }; + let expected_nonce = batch_state_lock + .get_user_nonce(&addr) + .await + .ok_or(SubmitError::BatcherUnexpectedError)?; if expected_nonce < msg_nonce { - std::mem::drop(batch_state_lock); warn!("Invalid nonce for address {addr}, expected nonce: {expected_nonce:?}, received nonce: {msg_nonce:?}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidNonce, - ) - .await; self.metrics.user_error(&["invalid_nonce", ""]); - return Ok(()); + return Err(SubmitError::InvalidNonce { + sent: msg_nonce, + expected: expected_nonce, + }); } // In this case, the message might be a replacement one. If it is valid, @@ -840,7 +778,7 @@ impl Batcher { client_msg.signature, addr, ) - .await; + .await?; return Ok(()); } @@ -850,13 +788,11 @@ impl Batcher { if msg_max_fee > user_last_max_fee_limit { std::mem::drop(batch_state_lock); warn!("Invalid max fee for address {addr}, had fee limit of {user_last_max_fee_limit:?}, sent {msg_max_fee:?}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidMaxFee, - ) - .await; self.metrics.user_error(&["invalid_max_fee", ""]); - return Ok(()); + return Err(SubmitError::InvalidMaxFee { + sent: msg_max_fee, + required: user_last_max_fee_limit, + }); } // * ---------------------------------------------------------------------* @@ -898,11 +834,7 @@ impl Batcher { batch_state_lock.update_user_state_on_entry_removal(&removed_entry); if let Some(removed_entry_ws) = removed_entry.messaging_sink { - send_message( - removed_entry_ws, - SubmitProofResponseMessage::UnderpricedProof, - ) - .await; + send_message(removed_entry_ws, SubmitError::BatchQueueLimitExceeded).await; }; } else { info!( @@ -910,13 +842,7 @@ impl Batcher { nonced_verification_data.nonce, nonced_verification_data.max_fee ); - std::mem::drop(batch_state_lock); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::UnderpricedProof, - ) - .await; - return Ok(()); + return Err(SubmitError::BatchQueueLimitExceeded); } } @@ -935,9 +861,8 @@ impl Batcher { .await { error!("Error while adding entry to batch: {e:?}"); - send_message(ws_conn_sink, SubmitProofResponseMessage::AddToBatchError).await; self.metrics.user_error(&["add_to_batch_error", ""]); - return Ok(()); + return Err(SubmitError::BatcherUnexpectedError); }; info!("Verification data message handled"); @@ -955,9 +880,16 @@ impl Batcher { user_balance: U256, user_accumulated_fee: U256, new_msg_max_fee: U256, - ) -> bool { + ) -> Result<(), SubmitError> { let required_balance: U256 = user_accumulated_fee + new_msg_max_fee; - user_balance >= required_balance + if user_balance >= required_balance { + Ok(()) + } else { + Err(SubmitError::InsufficientBalance { + available: user_balance, + required: required_balance, + }) + } } /// Handles a replacement message @@ -974,33 +906,29 @@ impl Batcher { ws_conn_sink: WsMessageSink, signature: Signature, addr: Address, - ) { + ) -> Result<(), SubmitError> { let replacement_max_fee = nonced_verification_data.max_fee; let nonce = nonced_verification_data.nonce; let Some(entry) = batch_state_lock.get_entry(addr, nonce) else { - std::mem::drop(batch_state_lock); warn!("Invalid nonce for address {addr}. Queue entry with nonce {nonce} not found"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidNonce, - ) - .await; self.metrics.user_error(&["invalid_nonce", ""]); - return; + return Err(SubmitError::InvalidReplacementMessage( + ReplacementInvalidReason::EntryNotFound, + )); }; let original_max_fee = entry.nonced_verification_data.max_fee; if original_max_fee > replacement_max_fee { std::mem::drop(batch_state_lock); warn!("Invalid replacement message for address {addr}, had max fee: {original_max_fee:?}, received fee: {replacement_max_fee:?}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidReplacementMessage, - ) - .await; self.metrics .user_error(&["invalid_replacement_message", ""]); - return; + return Err(SubmitError::InvalidReplacementMessage( + ReplacementInvalidReason::UnderpricedMaxFee { + sent: replacement_max_fee, + min_bump_required: original_max_fee, + }, + )); } info!("Replacing message for address {addr} with nonce {nonce} and max fee {replacement_max_fee}"); @@ -1030,17 +958,15 @@ impl Batcher { } replacement_entry.messaging_sink = Some(ws_conn_sink.clone()); - if !batch_state_lock.replacement_entry_is_valid(&replacement_entry) { - std::mem::drop(batch_state_lock); + if let Err((nonce, max_fee)) = + batch_state_lock.replacement_entry_is_valid(&replacement_entry) + { warn!("Invalid replacement message"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidReplacementMessage, - ) - .await; self.metrics .user_error(&["invalid_replacement_message", ""]); - return; + return Err(SubmitError::InvalidReplacementMessage( + ReplacementInvalidReason::ReplacementConflictWithPendingEntry { nonce, max_fee }, + )); } info!( @@ -1064,14 +990,8 @@ impl Batcher { .update_user_max_fee_limit(&addr, updated_max_fee_limit_in_batch) .is_none() { - std::mem::drop(batch_state_lock); warn!("User state for address {addr:?} was not present in batcher user states, but it should be"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::AddToBatchError, - ) - .await; - return; + return Err(SubmitError::BatcherUnexpectedError); }; // update total_fees_in_queue @@ -1083,14 +1003,11 @@ impl Batcher { ) .is_none() { - std::mem::drop(batch_state_lock); warn!("User state for address {addr:?} was not present in batcher user states, but it should be"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::AddToBatchError, - ) - .await; + return Err(SubmitError::BatcherUnexpectedError); }; + + Ok(()) } async fn disabled_verifiers(&self) -> Result> { @@ -1455,7 +1372,11 @@ impl Batcher { let mut batch_state_lock = self.batch_state.lock().await; for (entry, _) in batch_state_lock.batch_queue.iter() { if let Some(ws_sink) = entry.messaging_sink.as_ref() { - send_message(ws_sink.clone(), SubmitProofResponseMessage::BatchReset).await; + send_message( + ws_sink.clone(), + SubmitProofResponseMessage::Error(SubmitError::ProofQueueFlushed), + ) + .await; } else { warn!("Websocket sink was found empty. This should only happen in tests"); } @@ -1917,20 +1838,14 @@ impl Batcher { async fn msg_signature_is_valid( &self, client_msg: &SubmitProofMessage, - ws_conn_sink: &WsMessageSink, - ) -> Option
{ + ) -> Result { let Ok(addr) = client_msg.verify_signature() else { error!("Signature verification error"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidSignature, - ) - .await; self.metrics.user_error(&["invalid_signature", ""]); - return None; + return Err(SubmitError::InvalidSignature); }; - Some(addr) + Ok(addr) } /// Checks if the proof size + pub inputs is valid (not exceeding max_proof_size) @@ -1939,105 +1854,76 @@ impl Batcher { async fn msg_proof_size_is_valid( &self, client_msg: &SubmitProofMessage, - ws_conn_sink: &WsMessageSink, - ) -> bool { + ) -> Result<(), SubmitError> { let verification_data = match cbor_serialize(&client_msg.verification_data) { Ok(data) => data, // This should never happened, the user sent all his data serialized - Err(_) => { + Err(e) => { error!("Proof serialization error"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::Error("Proof serialization error".to_string()), - ) - .await; self.metrics.user_error(&["proof_serialization_error", ""]); - return false; + return Err(SubmitError::SerializationError(e.to_string())); } }; if verification_data.len() > self.max_proof_size { error!("Proof size exceeds the maximum allowed size."); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::ProofTooLarge, - ) - .await; self.metrics.user_error(&["proof_too_large", ""]); - return false; + return Err(SubmitError::InvalidProof( + ProofInvalidReason::ProofTooLarge { + size: verification_data.len() as u64, + max_allowed: self.max_proof_size as u64, + }, + )); } - true + Ok(()) } /// Checks if the chain id matches the one in the config - /// Returns false, logs the error, - /// and sends it to the metrics server if it doesn't matches + /// Returns SubmitError if chain id is not valid, logs the error, + /// and sends it to the metrics server async fn msg_chain_id_is_valid( &self, client_msg: &SubmitProofMessage, - ws_conn_sink: &WsMessageSink, - ) -> bool { + ) -> Result<(), SubmitError> { let msg_chain_id = client_msg.verification_data.chain_id; if msg_chain_id != self.chain_id { warn!("Received message with incorrect chain id: {msg_chain_id}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidChainId, - ) - .await; self.metrics.user_error(&["invalid_chain_id", ""]); - return false; + return Err(SubmitError::InvalidChainId); } - true + Ok(()) } /// Checks if the message has a valid payment service address - /// Returns false, logs the error, - /// and sends it to the metrics server if it doesn't match + /// Returns SubmitError if payment_service address is not valid, logs the error, + /// and sends it to the metrics server async fn msg_batcher_payment_addr_is_valid( &self, client_msg: &SubmitProofMessage, - ws_conn_sink: &WsMessageSink, - ) -> bool { + ) -> Result<(), SubmitError> { let msg_payment_service_addr = client_msg.verification_data.payment_service_addr; if msg_payment_service_addr != self.payment_service.address() { warn!("Received message with incorrect payment service address: {msg_payment_service_addr}"); - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InvalidPaymentServiceAddress( - msg_payment_service_addr, - self.payment_service.address(), - ), - ) - .await; self.metrics .user_error(&["invalid_payment_service_address", ""]); - return false; + return Err(SubmitError::InvalidPaymentServiceAddress { + expected: self.payment_service.address(), + received: msg_payment_service_addr, + }); } - true + Ok(()) } /// Checks if the user's balance is unlocked - /// Returns false if balance is unlocked, logs the error, + /// Returns SubmitError if balance is unlocked, logs the error, /// and sends it to the metrics server - async fn msg_user_balance_is_locked( - &self, - addr: &Address, - ws_conn_sink: &WsMessageSink, - ) -> bool { + async fn msg_user_balance_is_locked(&self, addr: &Address) -> Result<(), SubmitError> { if self.user_balance_is_unlocked(addr).await { - send_message( - ws_conn_sink.clone(), - SubmitProofResponseMessage::InsufficientBalance(*addr), - ) - .await; - self.metrics.user_error(&["insufficient_balance", ""]); - return false; + return Err(SubmitError::BalanceUnlocked); } - - true + Ok(()) } } diff --git a/crates/batcher/src/types/batch_state.rs b/crates/batcher/src/types/batch_state.rs index 481ca44f74..1f055e872b 100644 --- a/crates/batcher/src/types/batch_state.rs +++ b/crates/batcher/src/types/batch_state.rs @@ -203,7 +203,7 @@ impl BatchState { pub(crate) fn replacement_entry_is_valid( &mut self, replacement_entry: &BatchQueueEntry, - ) -> bool { + ) -> Result<(), (U256, U256)> { let replacement_max_fee = replacement_entry.nonced_verification_data.max_fee; let nonce = replacement_entry.nonced_verification_data.nonce; let sender = replacement_entry.sender; @@ -214,11 +214,18 @@ impl BatchState { ); // it is a valid entry only if there is no entry with the same sender, lower nonce and a lower fee - !self.batch_queue.iter().any(|(entry, _)| { + if let Some((entry, _)) = self.batch_queue.iter().find(|(entry, _)| { entry.sender == sender && entry.nonced_verification_data.nonce < nonce && entry.nonced_verification_data.max_fee < replacement_max_fee - }) + }) { + Err(( + entry.nonced_verification_data.nonce, + entry.nonced_verification_data.max_fee, + )) + } else { + Ok(()) + } } /// Updates or removes a user's state when their latest proof entry is removed from the batch queue. diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 7b7bc65fef..d90c3cc0f6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -504,7 +504,7 @@ async fn main() -> Result<(), AlignedError> { PathBuf::from(&submit_args.batch_inclusion_data_directory_path); std::fs::create_dir_all(&batch_inclusion_data_directory_path).map_err(|e| { - SubmitError::IoError(batch_inclusion_data_directory_path.clone(), e) + SubmitError::IoError(batch_inclusion_data_directory_path.clone(), e.to_string()) })?; let eth_rpc_url = submit_args.eth_rpc_url.clone(); @@ -542,22 +542,22 @@ async fn main() -> Result<(), AlignedError> { wallet = wallet.with_chain_id(chain_id); let nonce = match &submit_args.nonce { - Some(nonce) => U256::from_dec_str(nonce).map_err(|_| SubmitError::InvalidNonce)?, + Some(nonce) => U256::from_dec_str(nonce).expect("A valid nonce number"), None => { get_nonce_from_batcher(submit_args.network.clone().into(), wallet.address()) .await .map_err(|e| match e { aligned_sdk::common::errors::GetNonceError::EthRpcError(e) => { - SubmitError::GetNonceError(e) + SubmitError::EthereumProviderError(e) } aligned_sdk::common::errors::GetNonceError::ConnectionFailed(e) => { - SubmitError::GenericError(e) + SubmitError::WebSocketConnectionError(e) } aligned_sdk::common::errors::GetNonceError::InvalidRequest(e) => { SubmitError::GenericError(e) } aligned_sdk::common::errors::GetNonceError::SerializationError(e) => { - SubmitError::GenericError(e) + SubmitError::SerializationError(e) } aligned_sdk::common::errors::GetNonceError::ProtocolMismatch { current, @@ -612,8 +612,7 @@ async fn main() -> Result<(), AlignedError> { .insert(aligned_verification_data.batch_merkle_root); } Err(e) => { - warn!("Error while submitting proof: {:?}", e); - handle_submit_err(e).await; + warn!("Error while submitting proof: {}", e); return Ok(()); } }; @@ -646,13 +645,16 @@ async fn main() -> Result<(), AlignedError> { VerifyProofOnchain(verify_inclusion_args) => { let batch_inclusion_file = File::open(verify_inclusion_args.batch_inclusion_data.clone()).map_err(|e| { - SubmitError::IoError(verify_inclusion_args.batch_inclusion_data.clone(), e) + SubmitError::IoError( + verify_inclusion_args.batch_inclusion_data.clone(), + e.to_string(), + ) })?; let reader = BufReader::new(batch_inclusion_file); - let aligned_verification_data: AlignedVerificationData = - cbor_deserialize(reader).map_err(SubmitError::SerializationError)?; + let aligned_verification_data: AlignedVerificationData = cbor_deserialize(reader) + .map_err(|e| SubmitError::SerializationError(e.to_string()))?; info!("Verifying response data matches sent proof data..."); let response = verification_layer::is_proof_verified( @@ -677,10 +679,10 @@ async fn main() -> Result<(), AlignedError> { info!("Commitment: {}", hex::encode(vk_commitment)); if let Some(output_file) = args.output_file { let mut file = File::create(output_file.clone()) - .map_err(|e| SubmitError::IoError(output_file.clone(), e))?; + .map_err(|e| SubmitError::IoError(output_file.clone(), e.to_string()))?; file.write_all(hex::encode(vk_commitment).as_bytes()) - .map_err(|e| SubmitError::IoError(output_file.clone(), e))?; + .map_err(|e| SubmitError::IoError(output_file.clone(), e.to_string()))?; } } DepositToBatcher(deposit_to_batcher_args) => { @@ -940,27 +942,8 @@ fn verification_data_from_args(args: &SubmitArgs) -> Result { - error!("Invalid nonce. try again"); - } - SubmitError::ProofQueueFlushed => { - error!("Batch was reset. try resubmitting the proof"); - } - SubmitError::InvalidProof(reason) => error!("Submitted proof is invalid: {}", reason), - SubmitError::InsufficientBalance(sender_address) => { - error!( - "Insufficient balance to pay for the transaction, address: {}", - sender_address - ) - } - _ => {} - } -} - fn read_file(file_name: PathBuf) -> Result, SubmitError> { - std::fs::read(&file_name).map_err(|e| SubmitError::IoError(file_name, e)) + std::fs::read(&file_name).map_err(|e| SubmitError::IoError(file_name, e.to_string())) } fn read_file_option(param_name: &str, file_name: Option) -> Result, SubmitError> { diff --git a/crates/sdk/src/common/errors.rs b/crates/sdk/src/common/errors.rs index 30d1147242..2acdb92ef1 100644 --- a/crates/sdk/src/common/errors.rs +++ b/crates/sdk/src/common/errors.rs @@ -2,13 +2,12 @@ use core::fmt; use ethers::providers::ProviderError; use ethers::signers::WalletError; use ethers::types::transaction::eip712::Eip712Error; -use ethers::types::{SignatureError, H160}; +use ethers::types::{SignatureError, H160, U256}; +use ethers::utils::format_ether; use serde::{Deserialize, Serialize}; +use std::fmt::Display; use std::io; use std::path::PathBuf; -use tokio_tungstenite::tungstenite::protocol::CloseFrame; - -use crate::communication::serialization::SerializationError; use super::types::ProofInvalidReason; @@ -63,52 +62,81 @@ impl fmt::Display for AlignedError { } } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum SubmitError { - WebSocketConnectionError(tokio_tungstenite::tungstenite::Error), - WebSocketClosedUnexpectedlyError(CloseFrame<'static>), - IoError(PathBuf, io::Error), - SerializationError(SerializationError), + // General system-level errors + GenericError(String), // TODO: Replace with specific errors + WebSocketConnectionError(String), + WebSocketClosedUnexpectedlyError(String), + IoError(PathBuf, String), + SerializationError(String), + + // Ethereum and cryptographic errors EthereumProviderError(String), - HexDecodingError(String), WalletSignerError(String), + HexDecodingError(String), + InvalidEthereumAddress(String), + InvalidSignature, + InvalidChainId, + + // User input and validation errors MissingRequiredParameter(String), UnsupportedProvingSystem(String), - InvalidEthereumAddress(String), ProtocolVersionMismatch { current: u16, expected: u16 }, + InvalidNonce { sent: U256, expected: U256 }, + InvalidMaxFee { sent: U256, required: U256 }, + InsufficientBalance { available: U256, required: U256 }, + BalanceUnlocked, + InvalidProof(ProofInvalidReason), + InvalidReplacementMessage(ReplacementInvalidReason), + InvalidPaymentServiceAddress { expected: H160, received: H160 }, + + // Batcher-related errors + ProofQueueFlushed, + InvalidProofInclusionData, + EmptyVerificationDataCommitments, + EmptyVerificationDataList, + BatchQueueLimitExceeded, + BatchSubmissionFailed(String), + BatcherUnexpectedError, + + // Batcher communication and response errors BatchVerifiedEventStreamError(String), BatchVerificationTimeout { timeout_seconds: u64 }, NoResponseFromBatcher, UnexpectedBatcherResponse(String), - EmptyVerificationDataCommitments, - EmptyVerificationDataList, - InvalidNonce, - InvalidMaxFee, - ProofQueueFlushed, - InvalidSignature, - InvalidChainId, - InvalidProof(ProofInvalidReason), - ProofTooLarge, - InvalidReplacementMessage, - InsufficientBalance(H160), - InvalidPaymentServiceAddress(H160, H160), - BatchSubmissionFailed(String), - AddToBatchError, - InvalidProofInclusionData, - GetNonceError(String), - BatchQueueLimitExceededError, - GenericError(String), } -impl From for SubmitError { - fn from(e: tokio_tungstenite::tungstenite::Error) -> Self { - SubmitError::WebSocketConnectionError(e) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ReplacementInvalidReason { + EntryNotFound, + UnderpricedMaxFee { sent: U256, min_bump_required: U256 }, + ReplacementConflictWithPendingEntry { nonce: U256, max_fee: U256 }, +} + +impl Display for ReplacementInvalidReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + Self::EntryNotFound => write!(f, "Entry not found in state"), + Self::UnderpricedMaxFee { + sent, + min_bump_required, + } => write!( + f, + "Max fee does not cover replacement, sent: {}ether, min bump required {}ether", + format_ether(*sent), + format_ether(*min_bump_required), + ), + Self::ReplacementConflictWithPendingEntry { nonce, max_fee } => { + write!(f, "Replacement rejected: a pending entry from the same sender exists with a lower nonce and higher max fee (nonce: {}, fee: {}).", nonce, format_ether(*max_fee)) + } + } } } -impl From for SubmitError { - fn from(e: SerializationError) -> Self { - SubmitError::SerializationError(e) +impl From for SubmitError { + fn from(e: tokio_tungstenite::tungstenite::Error) -> Self { + SubmitError::WebSocketConnectionError(e.to_string()) } } @@ -140,82 +168,154 @@ impl From for SubmitError { impl fmt::Display for SubmitError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + // General System-level Errors + SubmitError::GenericError(e) => write!(f, "An unexpected error occurred: {}", e), SubmitError::WebSocketConnectionError(e) => { - write!(f, "WebSocket connection error: {}", e) + write!(f, "Failed to establish WebSocket connection: {}", e) } SubmitError::WebSocketClosedUnexpectedlyError(close_frame) => { - write!(f, "WebSocket closed unexpectedly: {}", close_frame) + write!( + f, + "WebSocket connection closed unexpectedly: {}", + close_frame + ) + } + + // Serialization Networking Errors + SubmitError::IoError(path, e) => { + write!(f, "Failed to access file '{}': {}", path.display(), e) + } + SubmitError::SerializationError(e) => { + write!(f, "Failed to serialize or deserialize data: {}", e) } - SubmitError::IoError(path, e) => write!(f, "IO error: {}: {}", path.display(), e), - SubmitError::SerializationError(e) => write!(f, "Serialization error: {}", e), - SubmitError::EthereumProviderError(e) => write!(f, "Ethereum provider error: {}", e), - SubmitError::HexDecodingError(e) => write!(f, "Hex decoding error: {}", e), - SubmitError::WalletSignerError(e) => write!(f, "Wallet signer error: {}", e), + + // Ethereum Cryptographic Errors + SubmitError::EthereumProviderError(e) => { + write!(f, "Ethereum provider error: {}", e) + } + SubmitError::HexDecodingError(e) => { + write!(f, "Failed to decode hexadecimal value: {}", e) + } + SubmitError::WalletSignerError(e) => { + write!(f, "Error while signing transaction with wallet: {}", e) + } + SubmitError::InvalidEthereumAddress(address) => { + write!(f, "Invalid Ethereum address provided: {}", address) + } + SubmitError::InvalidSignature => { + write!(f, "Signature verification failed. Please ensure the message was signed correctly.") + } + SubmitError::InvalidChainId => { + write!( + f, + "Chain ID mismatch. Please check you're connected to the correct network." + ) + } + + // User Input Parameter Validation Errors SubmitError::MissingRequiredParameter(param) => { - write!(f, "Missing required parameter: {}", param) + write!(f, "Missing required parameter: '{}'", param) } SubmitError::UnsupportedProvingSystem(proving_system) => { - write!(f, "Unsupported proving system: {}", proving_system) + write!(f, "Unsupported proving system: '{}'", proving_system) } - SubmitError::InvalidEthereumAddress(address) => { - write!(f, "Invalid Ethereum address: {}", address) + SubmitError::ProtocolVersionMismatch { current, expected } => { + write!( + f, + "Protocol version mismatch: current = {}, expected = {}", + current, expected + ) } - SubmitError::ProtocolVersionMismatch { current, expected } => write!( - f, - "Protocol version mismatch: current={}, expected={}", - current, expected - ), + SubmitError::InvalidNonce { sent, expected } => { + write!(f, "Invalid nonce: sent = {}, expected = {}", sent, expected) + } + SubmitError::InvalidMaxFee { sent, required } => { + write!( + f, + "Max fee too low: sent = {} ETH, minimum required = {} ETH", + format_ether(*sent), + format_ether(*required) + ) + } + SubmitError::InsufficientBalance { + available, + required, + } => { + write!( + f, + "Insufficient balance: available = {} ETH, required = {} ETH", + format_ether(*available), + format_ether(*required) + ) + } + SubmitError::BalanceUnlocked => { + write!( + f, + "The balance in the batcher payment contract is currently unlocked." + ) + } + SubmitError::InvalidProof(reason) => { + write!(f, "Invalid proof provided: {}", reason) + } + SubmitError::InvalidReplacementMessage(reason) => { + write!(f, "Invalid replacement request: {}", reason) + } + SubmitError::InvalidPaymentServiceAddress { + received: received_addr, + expected: expected_addr, + } => { + write!( + f, + "Payment service address mismatch: received '{}', expected '{}'", + received_addr, expected_addr + ) + } + + // Batcher-related Errors SubmitError::BatchVerifiedEventStreamError(e) => { - write!(f, "Batch verified event stream error: {}", e) + write!(f, "Error while reading batch verification events: {}", e) } SubmitError::BatchVerificationTimeout { timeout_seconds } => { write!( f, - "Batch verification timed out after {} seconds", + "Timed out waiting for batch verification (after {} seconds).", timeout_seconds ) } - SubmitError::NoResponseFromBatcher => write!(f, "No response received from batcher"), + SubmitError::NoResponseFromBatcher => { + write!(f, "No response received from the batcher.") + } SubmitError::UnexpectedBatcherResponse(response) => { - write!(f, "Unexpected batcher response: {}", response) + write!(f, "Received unexpected response from batcher: {}", response) } SubmitError::EmptyVerificationDataCommitments => { - write!(f, "Verification data commitments are empty") + write!(f, "No verification data commitments were found.") } - SubmitError::EmptyVerificationDataList => write!(f, "Verification data list is empty"), - SubmitError::InvalidNonce => write!(f, "Invalid nonce"), - SubmitError::InvalidMaxFee => write!(f, "Invalid max fee"), - SubmitError::BatchSubmissionFailed(merkle_root) => write!( - f, - "Could not create task with batch merkle root {}", - merkle_root - ), - SubmitError::GenericError(e) => write!(f, "Generic error: {}", e), - SubmitError::InvalidSignature => write!(f, "Invalid Signature"), - SubmitError::InvalidChainId => write!(f, "Invalid chain Id"), - SubmitError::InvalidProof(reason) => write!(f, "Invalid proof {}", reason), - SubmitError::ProofTooLarge => write!(f, "Proof too Large"), - SubmitError::InvalidReplacementMessage => write!(f, "Invalid replacement message"), - SubmitError::InsufficientBalance(addr) => { - write!(f, "Insufficient balance, address: {}", addr) - } - SubmitError::InvalidPaymentServiceAddress(received_addr, expected_addr) => { + SubmitError::EmptyVerificationDataList => { + write!(f, "Verification data list is empty. Nothing to process.") + } + SubmitError::BatchSubmissionFailed(merkle_root) => { write!( f, - "Invalid payment service address, received: {}, expected: {}", - received_addr, expected_addr + "Failed to submit batch with Merkle root '{}'.", + merkle_root + ) + } + SubmitError::ProofQueueFlushed => { + write!( + f, + "Your proof was removed due to a batch reset. Please resubmit." ) } - SubmitError::ProofQueueFlushed => write!(f, "Batch reset"), - SubmitError::AddToBatchError => write!(f, "Error while adding entry to batch"), SubmitError::InvalidProofInclusionData => { - write!(f, "Batcher responded with invalid batch inclusion data. Can't verify your proof was correctly included in the batch.") + write!(f, "Batcher provided invalid inclusion data. Could not confirm your proof was included in the batch.") } - SubmitError::BatchQueueLimitExceededError => { - write!(f, "Error while adding entry to batch, queue limit exeeded.") + SubmitError::BatchQueueLimitExceeded => { + write!(f, "Batch queue is full. Please try again later.") + } + SubmitError::BatcherUnexpectedError => { + write!(f, "An unexpected error occurred in the batcher.") } - - SubmitError::GetNonceError(e) => write!(f, "Error while getting nonce {}", e), } } } @@ -338,13 +438,7 @@ pub enum BalanceError { #[derive(Debug)] pub enum FileError { IoError(PathBuf, io::Error), - SerializationError(SerializationError), -} - -impl From for FileError { - fn from(e: SerializationError) -> Self { - FileError::SerializationError(e) - } + SerializationError(String), } impl From for FileError { diff --git a/crates/sdk/src/common/types.rs b/crates/sdk/src/common/types.rs index f9850910f4..17f5e69264 100644 --- a/crates/sdk/src/common/types.rs +++ b/crates/sdk/src/common/types.rs @@ -18,6 +18,8 @@ use lambdaworks_crypto::merkle_tree::{ use serde::{Deserialize, Serialize}; use sha3::{Digest, Keccak256}; +use crate::common::errors::SubmitError; + use super::constants::{ ALIGNED_PROOF_AGG_SERVICE_ADDRESS_DEVNET, ALIGNED_PROOF_AGG_SERVICE_ADDRESS_HOLESKY, ALIGNED_PROOF_AGG_SERVICE_ADDRESS_HOLESKY_STAGE, ALIGNED_PROOF_AGG_SERVICE_ADDRESS_MAINNET, @@ -313,7 +315,7 @@ impl SubmitProofMessage { verification_data: NoncedVerificationData, wallet: Wallet, ) -> Self { - let signature = wallet + let signature: Signature = wallet .sign_typed_data(&verification_data) .await .expect("Failed to sign the verification data"); @@ -365,7 +367,8 @@ impl AlignedVerificationData { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProofInvalidReason { - RejectedProof, + ProofTooLarge { size: u64, max_allowed: u64 }, + VerificationFailed, VerifierNotSupported, DisabledVerifier(ProvingSystemId), } @@ -373,34 +376,29 @@ pub enum ProofInvalidReason { impl Display for ProofInvalidReason { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { + ProofInvalidReason::ProofTooLarge { size, max_allowed } => write!( + f, + "Proof too large size {} max allowed {}", + size, max_allowed + ), ProofInvalidReason::VerifierNotSupported => write!(f, "Verifier not supported"), ProofInvalidReason::DisabledVerifier(proving_system_id) => { write!(f, "Disabled verifier: {}", proving_system_id) } - ProofInvalidReason::RejectedProof => write!(f, "Proof did not verify"), + ProofInvalidReason::VerificationFailed => write!(f, "Proof did not verify"), } } } +pub enum SubmitProofSuccessResponse { + ProtocolVersion(u16), +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SubmitProofResponseMessage { - BatchInclusionData(BatchInclusionData), ProtocolVersion(u16), - CreateNewTaskError(String, String), //merkle-root, error - InvalidProof(ProofInvalidReason), - BatchReset, - Error(String), - InvalidNonce, - InvalidSignature, - ProofTooLarge, - InvalidMaxFee, - InsufficientBalance(Address), - InvalidChainId, - InvalidReplacementMessage, - AddToBatchError, - EthRpcError, - InvalidPaymentServiceAddress(Address, Address), - UnderpricedProof, + BatchInclusionData(BatchInclusionData), + Error(SubmitError), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/sdk/src/communication/messaging.rs b/crates/sdk/src/communication/messaging.rs index 2a1c100e7d..ec65205ad2 100644 --- a/crates/sdk/src/communication/messaging.rs +++ b/crates/sdk/src/communication/messaging.rs @@ -62,7 +62,7 @@ pub async fn send_messages( Ok(bin) => bin, Err(e) => { error!("Error while serializing message: {:?}", e); - sent_verification_data.push(Err(SubmitError::SerializationError(e))); + sent_verification_data.push(Err(SubmitError::SerializationError(e.to_string()))); return sent_verification_data; } }; @@ -70,7 +70,7 @@ pub async fn send_messages( // Send the message if let Err(e) = ws_write.send(Message::Binary(msg_bin.clone())).await { error!("Error while sending message: {:?}", e); - sent_verification_data.push(Err(SubmitError::WebSocketConnectionError(e))); + sent_verification_data.push(Err(SubmitError::WebSocketConnectionError(e.to_string()))); return sent_verification_data; } @@ -108,7 +108,7 @@ pub async fn receive( warn!("Unexpected WS close"); if let Some(close_msg) = close_frame { aligned_submitted_data.push(Err(SubmitError::WebSocketClosedUnexpectedlyError( - close_msg.to_owned(), + close_msg.to_string(), ))); break; } @@ -174,79 +174,6 @@ async fn handle_batcher_response(msg: Message) -> Result { - error!("Batcher responded with invalid nonce. Funds have not been spent."); - Err(SubmitError::InvalidNonce) - } - Ok(SubmitProofResponseMessage::InvalidSignature) => { - error!("Batcher responded with invalid signature. Funds have not been spent."); - Err(SubmitError::InvalidSignature) - } - Ok(SubmitProofResponseMessage::ProofTooLarge) => { - error!("Batcher responded with proof too large. Funds have not been spent."); - Err(SubmitError::ProofTooLarge) - } - Ok(SubmitProofResponseMessage::InvalidMaxFee) => { - error!("Batcher responded with invalid max fee. Funds have not been spent."); - Err(SubmitError::InvalidMaxFee) - } - Ok(SubmitProofResponseMessage::InsufficientBalance(addr)) => { - error!("Batcher responded with insufficient balance. Funds have not been spent for submittions which had insufficient balance."); - Err(SubmitError::InsufficientBalance(addr)) - } - Ok(SubmitProofResponseMessage::InvalidChainId) => { - error!("Batcher responded with invalid chain id. Funds have not been spent."); - Err(SubmitError::InvalidChainId) - } - Ok(SubmitProofResponseMessage::InvalidReplacementMessage) => { - error!( - "Batcher responded with invalid replacement message. Funds have not been spent." - ); - Err(SubmitError::InvalidReplacementMessage) - } - Ok(SubmitProofResponseMessage::AddToBatchError) => { - error!("Batcher responded with add to batch error. Funds have not been spent."); - Err(SubmitError::AddToBatchError) - } - Ok(SubmitProofResponseMessage::EthRpcError) => { - error!("Batcher experienced Eth RPC connection error. Funds have not been spent."); - Err(SubmitError::EthereumProviderError( - "Batcher experienced Eth RPC connection error. Funds have not been spent." - .to_string(), - )) - } - Ok(SubmitProofResponseMessage::InvalidPaymentServiceAddress( - received_addr, - expected_addr, - )) => { - error!( - "Batcher responded with invalid payment service address: {:?}, expected: {:?}. Funds have not been spent.", - received_addr, expected_addr - ); - Err(SubmitError::InvalidPaymentServiceAddress( - received_addr, - expected_addr, - )) - } - Ok(SubmitProofResponseMessage::InvalidProof(reason)) => { - error!( - "Batcher responded with invalid proof: {}. Funds have not been spent.", - reason - ); - Err(SubmitError::InvalidProof(reason)) - } - Ok(SubmitProofResponseMessage::CreateNewTaskError(merkle_root, error)) => { - error!( - "Batcher responded with create new task error: {}. Funds have not been spent.", - error - ); - Err(SubmitError::BatchSubmissionFailed( - "Could not create task with merkle root ".to_owned() - + &merkle_root - + ", failed with error: " - + &error, - )) - } Ok(SubmitProofResponseMessage::ProtocolVersion(_)) => { error!("Batcher responded with protocol version instead of batch inclusion data. Funds have not been spent."); Err(SubmitError::UnexpectedBatcherResponse( @@ -254,28 +181,8 @@ async fn handle_batcher_response(msg: Message) -> Result { - error!("Batcher responded with batch reset. Funds have not been spent."); - Err(SubmitError::ProofQueueFlushed) - } - Ok(SubmitProofResponseMessage::Error(e)) => { - error!( - "Batcher responded with error: {}. Funds have not been spent.", - e - ); - Err(SubmitError::GenericError(e)) - } - Ok(SubmitProofResponseMessage::UnderpricedProof) => { - error!("Batcher responded with error: queue limit has been exceeded. Funds have not been spent."); - Err(SubmitError::BatchQueueLimitExceededError) - } - Err(e) => { - error!( - "Error while deserializing batch inclusion data: {}. Funds have not been spent.", - e - ); - Err(SubmitError::SerializationError(e)) - } + Ok(SubmitProofResponseMessage::Error(e)) => Err(e), + Err(e) => Err(SubmitError::SerializationError(e.to_string())), } } diff --git a/crates/sdk/src/communication/protocol.rs b/crates/sdk/src/communication/protocol.rs index 8e03801004..85bf532524 100644 --- a/crates/sdk/src/communication/protocol.rs +++ b/crates/sdk/src/communication/protocol.rs @@ -28,7 +28,7 @@ pub async fn check_protocol_version( )); } Err(e) => { - return Err(SubmitError::SerializationError(e)); + return Err(SubmitError::SerializationError(e.to_string())); } } } diff --git a/crates/sdk/src/communication/serialization.rs b/crates/sdk/src/communication/serialization.rs index 1754bb48b2..3e6caeeeed 100644 --- a/crates/sdk/src/communication/serialization.rs +++ b/crates/sdk/src/communication/serialization.rs @@ -1,22 +1,21 @@ use std::io::Read; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use ciborium::de::Error as CiboriumDeError; +use ciborium::ser::Error as CiboriumSerError; +use serde::{de::DeserializeOwned, Serialize}; +use std::io::Error; -pub fn cbor_serialize(value: &T) -> Result, SerializationError> { +pub type CiboriumSerializationError = CiboriumSerError; +pub type CiboriumDeserializationError = CiboriumDeError; + +pub fn cbor_serialize(value: &T) -> Result, CiboriumSerializationError> { let mut buf = Vec::new(); - ciborium::into_writer(value, &mut buf).map_err(|_| SerializationError)?; + ciborium::into_writer(value, &mut buf)?; Ok(buf) } -pub fn cbor_deserialize(buf: R) -> Result { - ciborium::from_reader(buf).map_err(|_| SerializationError) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct SerializationError; - -impl std::fmt::Display for SerializationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Serialization error") - } +pub fn cbor_deserialize( + buf: R, +) -> Result { + ciborium::from_reader(buf) } diff --git a/crates/sdk/src/verification_layer/mod.rs b/crates/sdk/src/verification_layer/mod.rs index 8f3888b11a..7b5b5adadc 100644 --- a/crates/sdk/src/verification_layer/mod.rs +++ b/crates/sdk/src/verification_layer/mod.rs @@ -5,7 +5,7 @@ use crate::{ DEFAULT_MAX_FEE_BATCH_SIZE, GAS_PRICE_PERCENTAGE_MULTIPLIER, INSTANT_MAX_FEE_BATCH_SIZE, PERCENTAGE_DIVIDER, }, - errors::{self, GetNonceError}, + errors::{self, FileError, GetNonceError}, types::{ AlignedVerificationData, ClientMessage, FeeEstimationType, GetNonceResponseMessage, Network, ProvingSystemId, VerificationData, @@ -238,7 +238,11 @@ pub async fn submit_multiple( ) -> Vec> { let (ws_stream, _) = match connect_async(network.get_batcher_url()).await { Ok((ws_stream, response)) => (ws_stream, response), - Err(e) => return vec![Err(errors::SubmitError::WebSocketConnectionError(e))], + Err(e) => { + return vec![Err(errors::SubmitError::WebSocketConnectionError( + e.to_string(), + ))] + } }; debug!("WebSocket handshake has been successfully completed"); @@ -769,7 +773,8 @@ fn save_response_cbor( let batch_inclusion_data_path = batch_inclusion_data_directory_path.join(batch_inclusion_data_file_name); - let data = cbor_serialize(&aligned_verification_data)?; + let data = cbor_serialize(&aligned_verification_data) + .map_err(|e| FileError::SerializationError(e.to_string()))?; let mut file = File::create(batch_inclusion_data_path)?; file.write_all(data.as_slice())?; diff --git a/examples/validating-public-input/aligned-integration/src/main.rs b/examples/validating-public-input/aligned-integration/src/main.rs index fe36364c34..dd9a66cd5f 100644 --- a/examples/validating-public-input/aligned-integration/src/main.rs +++ b/examples/validating-public-input/aligned-integration/src/main.rs @@ -263,7 +263,7 @@ fn save_response( pub_input: &[u8], ) -> Result<(), SubmitError> { std::fs::create_dir_all(&batch_inclusion_data_directory_path) - .map_err(|e| SubmitError::IoError(batch_inclusion_data_directory_path.clone(), e))?; + .map_err(|e| SubmitError::IoError(batch_inclusion_data_directory_path.clone(), e.to_string()))?; let batch_merkle_root = &hex::encode(aligned_verification_data.batch_merkle_root)[..8]; let batch_inclusion_data_file_name = batch_merkle_root.to_owned() @@ -293,9 +293,9 @@ fn save_response( }); let mut file = File::create(&batch_inclusion_data_path) - .map_err(|e| SubmitError::IoError(batch_inclusion_data_path.clone(), e))?; + .map_err(|e| SubmitError::IoError(batch_inclusion_data_path.clone(), e.to_string()))?; file.write_all(serde_json::to_string_pretty(&data).unwrap().as_bytes()) - .map_err(|e| SubmitError::IoError(batch_inclusion_data_path.clone(), e))?; + .map_err(|e| SubmitError::IoError(batch_inclusion_data_path.clone(), e.to_string()))?; let current_dir = env::current_dir().expect("Failed to get current directory"); info!(