diff --git a/substrate/client/basic-authorship/src/basic_authorship.rs b/substrate/client/basic-authorship/src/basic_authorship.rs index ae3e56557fdc2..8e56b2304c50d 100644 --- a/substrate/client/basic-authorship/src/basic_authorship.rs +++ b/substrate/client/basic-authorship/src/basic_authorship.rs @@ -40,7 +40,9 @@ use sp_runtime::{ Digest, ExtrinsicInclusionMode, Percent, SaturatedConversion, }; use std::{marker::PhantomData, pin::Pin, sync::Arc, time}; -use stp_shield::{ShieldApi, ShieldKeystorePtr, ShieldedTransaction}; +use stp_shield::{ + InnerExtrinsicError, ShieldApi, ShieldError, ShieldKeystorePtr, ShieldedTransaction, +}; use prometheus_endpoint::Registry as PrometheusRegistry; use sc_proposer_metrics::{EndProposingReason, MetricsLink as PrometheusMetrics}; @@ -434,6 +436,11 @@ where debug!(target: LOG_TARGET, "Attempting to push transactions from the pool at {:?}.", self.parent_hash); let mut transaction_pushed = false; + let api = self.client.runtime_api(); + let is_shield_api_v2 = api + .has_api_with::, _>(self.parent_hash, |v| v >= 2) + .unwrap_or(false); + let end_reason = loop { let pending_tx = if let Some(pending_tx) = pending_iterator.next() { pending_tx @@ -459,17 +466,36 @@ where let pending_tx_data = (**pending_tx.data()).clone(); let pending_tx_hash = pending_tx.hash().clone(); - let api = self.client.runtime_api(); - - let maybe_shielded_tx = api - .try_decode_shielded_tx(self.parent_hash, pending_tx_data.clone()) - .ok() - .flatten(); + let maybe_decode_result: Option> = + if is_shield_api_v2 { + api.try_decode_shielded_tx(self.parent_hash, pending_tx_data.clone()) + .ok() + .flatten() + } else { + #[allow(deprecated)] + api.try_decode_shielded_tx_before_version_2( + self.parent_hash, + pending_tx_data.clone(), + ) + .ok() + .flatten() + .map(Ok) + }; + log::debug!(target: LOG_TARGET, "Maybe shielded tx: {:?}", maybe_decode_result); + let is_shielded = maybe_decode_result.is_some(); + + // Determine if this is a shielded tx and whether decode succeeded. + // None = not shielded, Some(Ok(tx)) = decoded, Some(Err(e)) = decode failed. + let (shielded_tx, decode_error) = match maybe_decode_result { + None => (None, None), + Some(Ok(tx)) => (Some(tx), None), + Some(Err(e)) => (None, Some(e.clone())), + }; // Skip shielded txs we can't decrypt (wrong key or disabled via env var) without // reporting them as invalid, they stay in the pool for the right block author to - // pick them up. - if let Some(shielded_tx) = &maybe_shielded_tx { + // pick them up. Only skip if decode succeeded. Decode errors are always included. + if let Some(shielded_tx) = &shielded_tx { let using_current_key = api .is_shielded_using_current_key(self.parent_hash, &shielded_tx.key_hash) .unwrap_or(false); @@ -480,7 +506,7 @@ where } } - let pending_tx_data_size = if let Some(shielded_tx) = &maybe_shielded_tx { + let pending_tx_data_size = if let Some(shielded_tx) = &shielded_tx { // The ciphertext length for XChaCha20Poly1305 is the length of the plaintext + 16 // bytes for the tag (source: https://www.rfc-editor.org/rfc/rfc8439#section-2.8) so we need // to subtract it from the ciphertext length to get the plaintext length @@ -524,14 +550,32 @@ where } } - let tx_type = if maybe_shielded_tx.is_some() { "shield wrapper" } else { "normal" }; + let tx_type = if is_shielded { "shield wrapper" } else { "normal" }; trace!(target: LOG_TARGET, "[{:?}] Pushing {} transaction to the block.", pending_tx_hash, tx_type); - match sc_block_builder::BlockBuilder::push(block_builder, pending_tx_data) { + match sc_block_builder::BlockBuilder::push(block_builder, pending_tx_data.clone()) { Ok(()) => { transaction_pushed = true; trace!(target: LOG_TARGET, "[{:?}] Pushed {} transaction to the block.", pending_tx_hash, tx_type); - let Some(shielded_tx) = maybe_shielded_tx else { + if !is_shielded { + continue; + } + + // We need the wrapper tx hash to report the error tx in case of decode failure. + let wrapper_tx_hash = + <::Header as HeaderT>::Hashing::hash_of(&pending_tx_data); + + // If decode failed, include the error tx (only on v2 runtimes). + if let Some(error) = decode_error { + debug!(target: LOG_TARGET, "[{:?}] Failed to decode shielded transaction: {:?}", pending_tx_hash, error); + if is_shield_api_v2 { + self.push_shield_error_tx(&api, block_builder, wrapper_tx_hash, error); + } + continue; + } + + let Some(shielded_tx) = shielded_tx else { + debug!(target: LOG_TARGET, "[{:?}] No shielded transaction after decode error check; not reachable.", pending_tx_hash); continue; }; @@ -543,6 +587,8 @@ where shielded_tx, &mut skipped, soft_deadline, + wrapper_tx_hash, + is_shield_api_v2, ) { break end_reason; } @@ -591,6 +637,8 @@ where shielded_tx: ShieldedTransaction, skipped: &mut usize, soft_deadline: time::Instant, + wrapper_tx_hash: Block::Hash, + is_shield_api_v2: bool, ) -> Result<(), EndProposingReason> { let dec_key_bytes = self .shield_keystore @@ -604,11 +652,33 @@ where return Ok(()); }; - let Some(unshielded_tx_data) = - api.try_unshield_tx(self.parent_hash, dec_key_bytes, shielded_tx).ok().flatten() - else { - debug!(target: LOG_TARGET, "[{:?}] Failed to unshield transaction", shielded_tx_hash); - return Ok(()); + let unshielded_tx_data = if is_shield_api_v2 { + match api.try_unshield_tx(self.parent_hash, dec_key_bytes, shielded_tx) { + Ok(Ok(tx)) => tx, + Ok(Err(shield_error)) => { + debug!(target: LOG_TARGET, "[{:?}] Failed to unshield transaction: {:?}", shielded_tx_hash, shield_error); + self.push_shield_error_tx(api, block_builder, wrapper_tx_hash, shield_error); + return Ok(()); + }, + Err(api_error) => { + debug!(target: LOG_TARGET, "[{:?}] Runtime API error during unshield: {}", shielded_tx_hash, api_error); + return Ok(()); + }, + } + } else { + #[allow(deprecated)] + match api.try_unshield_tx_before_version_2(self.parent_hash, dec_key_bytes, shielded_tx) + { + Ok(Some(tx)) => tx, + Ok(None) => { + debug!(target: LOG_TARGET, "[{:?}] Failed to unshield transaction (v1)", shielded_tx_hash); + return Ok(()); + }, + Err(api_error) => { + debug!(target: LOG_TARGET, "[{:?}] Runtime API error during unshield: {}", shielded_tx_hash, api_error); + return Ok(()); + }, + } }; debug!(target: LOG_TARGET, "[{:?}] Unshielded inner transaction: {:?}", shielded_tx_hash, unshielded_tx_data); @@ -625,12 +695,48 @@ where target: LOG_TARGET, "[{:?}] Invalid unshielded transaction: {} at: {}", shielded_tx_hash, e, self.parent_hash ); + if is_shield_api_v2 { + let error: ShieldError = match e { + ApplyExtrinsicFailed(Validity(ref ve)) => + InnerExtrinsicError::ValidationFailed(format!("{ve:?}").into_bytes()) + .into(), + _ => InnerExtrinsicError::DecodeFailed(format!("{e}").into_bytes()).into(), + }; + self.push_shield_error_tx(api, block_builder, wrapper_tx_hash, error); + } }, } Ok(()) } + /// Build and push an unsigned `report_unshield_error` extrinsic into the block. + fn push_shield_error_tx( + &self, + api: &ApiRef<'_, C::Api>, + block_builder: &mut sc_block_builder::BlockBuilder<'_, Block, C>, + wrapper_tx_hash: Block::Hash, + error: ShieldError, + ) { + let error_tx = + match api.make_unshield_error_extrinsic(self.parent_hash, wrapper_tx_hash, error) { + Ok(tx) => tx, + Err(e) => { + debug!(target: LOG_TARGET, "[{:?}] Failed to build error extrinsic: {}", wrapper_tx_hash, e); + return; + }, + }; + + match sc_block_builder::BlockBuilder::push(block_builder, error_tx) { + Ok(()) => { + debug!(target: LOG_TARGET, "[{:?}] Pushed shield error extrinsic to block", wrapper_tx_hash); + }, + Err(e) => { + debug!(target: LOG_TARGET, "[{:?}] Failed to push shield error extrinsic: {}", wrapper_tx_hash, e); + }, + } + } + fn report_exhausted_resources( &self, skipped: &mut usize, @@ -1351,7 +1457,8 @@ mod tests { ) -> Arc { let mut builder = TestClientBuilder::new(); if let Some(tx) = decode { - builder = builder.add_extra_storage(SHIELD_TEST_DECODE_KEY.to_vec(), tx.encode()); + let value: Result = Ok(tx); + builder = builder.add_extra_storage(SHIELD_TEST_DECODE_KEY.to_vec(), value.encode()); } if let Some(ext) = unshield { builder = builder.add_extra_storage(SHIELD_TEST_UNSHIELD_KEY.to_vec(), ext.encode()); @@ -1442,8 +1549,8 @@ mod tests { .map(|r| r.block) .unwrap(); - // Block should contain only the wrapper (inner unshielding failed gracefully) - assert_eq!(block.extrinsics().len(), 1); + // Block should contain wrapper + error tx (unshielding failed, error reported) + assert_eq!(block.extrinsics().len(), 2); } #[test] diff --git a/substrate/primitives/shield/src/error.rs b/substrate/primitives/shield/src/error.rs new file mode 100644 index 0000000000000..9bee53ef90b8c --- /dev/null +++ b/substrate/primitives/shield/src/error.rs @@ -0,0 +1,177 @@ +//! Error types for the MEV Shield. + +extern crate alloc; + +use alloc::vec::Vec; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use core::fmt; +use scale_info::TypeInfo; + +/// Top-level error returned when processing a shielded transaction fails. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub enum ShieldError { + /// Failed to parse the ciphertext envelope. + Parsing(ParsingError), + /// The wrapper extrinsic itself is invalid. + WrapperExtrinsic(WrapperExtrinsicError), + /// Decryption of the shielded payload failed. + Decryption(DecryptionError), + /// The decrypted inner extrinsic is invalid. + InnerExtrinsic(InnerExtrinsicError), +} + +/// Errors when parsing the ciphertext envelope (`key_hash || kem_len || kem_ct || nonce || +/// aead_ct`). +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub enum ParsingError { + /// Ciphertext is too short to contain the key hash (16 bytes). + TruncatedKeyHash, + /// Ciphertext is too short to contain the KEM ciphertext length field (2 bytes). + TruncatedKemLen, + /// KEM ciphertext length field exceeds the remaining ciphertext bytes. + KemLenExceedsRemaining, + /// Ciphertext is too short to contain the nonce (24 bytes). + TruncatedNonce, + /// Ciphertext has no AEAD payload after the nonce. + MissingAead, +} + +/// Errors when validating the wrapper (`submit_encrypted`) extrinsic. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub enum WrapperExtrinsicError { + /// The wrapper extrinsic failed to decode (e.g. exceeded nesting depth). + /// Contains the debug representation of the decode error. + DecodeFailed(Vec), + /// The wrapper extrinsic failed validation (e.g. bad signature, AncientBirthBlock). + /// Contains the SCALE-encoded `TransactionValidityError`. + CheckFailed(Vec), +} + +/// Errors during ML-KEM-768 + XChaCha20-Poly1305 decryption. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub enum DecryptionError { + /// The decapsulation key bytes are malformed. + InvalidDecapsulationKey, + /// The KEM ciphertext is malformed (wrong length for ML-KEM-768). + InvalidKemCiphertext, + /// ML-KEM-768 decapsulation failed. + DecapsulationFailed, + /// XChaCha20-Poly1305 decryption failed (wrong key or tampered payload). + AeadDecryptionFailed, + /// Decrypted plaintext is empty. + EmptyPlaintext, +} + +/// Errors when processing the decrypted inner extrinsic. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] +pub enum InnerExtrinsicError { + /// Decrypted bytes are not a valid SCALE-encoded extrinsic. + /// Contains the debug representation of the decode error. + DecodeFailed(Vec), + /// The inner extrinsic failed validation (e.g. AncientBirthBlock, BadProof). + /// Contains the SCALE-encoded `TransactionValidityError`. + ValidationFailed(Vec), +} + +// Convenience `From` impls so call sites can use `?` with sub-errors. + +impl From for ShieldError { + fn from(e: ParsingError) -> Self { + ShieldError::Parsing(e) + } +} + +impl From for ShieldError { + fn from(e: WrapperExtrinsicError) -> Self { + ShieldError::WrapperExtrinsic(e) + } +} + +impl From for ShieldError { + fn from(e: DecryptionError) -> Self { + ShieldError::Decryption(e) + } +} + +impl From for ShieldError { + fn from(e: InnerExtrinsicError) -> Self { + ShieldError::InnerExtrinsic(e) + } +} + +// Display impls — human-readable messages for SDK/wallet developers. + +/// Lossy UTF-8 helper for `Vec` payloads (debug strings or opaque bytes). +fn lossy(bytes: &[u8]) -> alloc::string::String { + alloc::string::String::from_utf8_lossy(bytes).into_owned() +} + +impl fmt::Display for ShieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parsing(e) => write!(f, "ciphertext parsing failed: {e}"), + Self::WrapperExtrinsic(e) => write!(f, "wrapper extrinsic invalid: {e}"), + Self::Decryption(e) => write!(f, "decryption failed: {e}"), + Self::InnerExtrinsic(e) => write!(f, "inner extrinsic invalid: {e}"), + } + } +} + +impl fmt::Display for ParsingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TruncatedKeyHash => + write!(f, "ciphertext too short for key hash (need 16 bytes)"), + Self::TruncatedKemLen => + write!(f, "ciphertext too short for KEM length field (need 2 bytes)"), + Self::KemLenExceedsRemaining => + write!(f, "KEM ciphertext length exceeds remaining data"), + Self::TruncatedNonce => write!(f, "ciphertext too short for nonce (need 24 bytes)"), + Self::MissingAead => write!(f, "no AEAD ciphertext after nonce"), + } + } +} + +impl fmt::Display for WrapperExtrinsicError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DecodeFailed(msg) => write!(f, "SCALE decode failed: {}", lossy(msg)), + Self::CheckFailed(encoded) => { + write!(f, "validation check failed (encoded error: 0x")?; + for b in encoded { + write!(f, "{b:02x}")?; + } + write!(f, ")") + }, + } + } +} + +impl fmt::Display for DecryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidDecapsulationKey => write!(f, "decapsulation key bytes are malformed"), + Self::InvalidKemCiphertext => + write!(f, "KEM ciphertext has wrong length for ML-KEM-768"), + Self::DecapsulationFailed => write!(f, "ML-KEM-768 decapsulation failed"), + Self::AeadDecryptionFailed => + write!(f, "XChaCha20-Poly1305 decryption failed (wrong key or tampered payload)"), + Self::EmptyPlaintext => write!(f, "decrypted plaintext is empty"), + } + } +} + +impl fmt::Display for InnerExtrinsicError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DecodeFailed(msg) => write!(f, "SCALE decode failed: {}", lossy(msg)), + Self::ValidationFailed(encoded) => { + write!(f, "validation failed (encoded error: 0x")?; + for b in encoded { + write!(f, "{b:02x}")?; + } + write!(f, ")") + }, + } + } +} diff --git a/substrate/primitives/shield/src/lib.rs b/substrate/primitives/shield/src/lib.rs index 357c42ef7d2b9..15d99d91a60d5 100644 --- a/substrate/primitives/shield/src/lib.rs +++ b/substrate/primitives/shield/src/lib.rs @@ -6,10 +6,12 @@ extern crate alloc; use sp_inherents::InherentIdentifier; use sp_runtime::{traits::ConstU32, BoundedVec}; +mod error; mod keystore; pub mod runtime_api; mod shielded_tx; +pub use error::*; pub use keystore::*; pub use runtime_api::*; pub use shielded_tx::*; diff --git a/substrate/primitives/shield/src/runtime_api.rs b/substrate/primitives/shield/src/runtime_api.rs index 06ae1747f5ab0..b4d30a73f7717 100644 --- a/substrate/primitives/shield/src/runtime_api.rs +++ b/substrate/primitives/shield/src/runtime_api.rs @@ -2,21 +2,42 @@ extern crate alloc; -use crate::ShieldedTransaction; +use crate::{ShieldError, ShieldedTransaction}; use alloc::vec::Vec; use sp_runtime::traits::Block as BlockT; type ExtrinsicOf = ::Extrinsic; sp_api::decl_runtime_apis! { + /// Runtime API for the MEV Shield. + /// + /// V1: original signatures (`Option`-based, no error details). + /// V2: error-aware signatures (`Result`-based with `ShieldError`). + #[api_version(2)] pub trait ShieldApi { /// Try to decode a shielded transaction from an extrinsic. - fn try_decode_shielded_tx(uxt: ExtrinsicOf) -> Option; + /// + /// Returns `None` if this is not a shielded extrinsic (i.e. not a `submit_encrypted` call). + /// Returns `Some(Ok(tx))` if the shielded transaction was decoded successfully. + /// Returns `Some(Err(e))` if it is a shielded extrinsic but decoding failed. + fn try_decode_shielded_tx(uxt: ExtrinsicOf) -> Option>; /// Check if a transaction is shielded using the current key. fn is_shielded_using_current_key(key_hash: &[u8; 16]) -> bool; /// Try to unshield a transaction using a decapsulation key. + fn try_unshield_tx(dec_key_bytes: Vec, shielded_tx: ShieldedTransaction) -> Result, ShieldError>; + + /// Build an unsigned extrinsic that reports an unshield error. + /// The proposer pushes this into the block in place of the failed inner tx. + fn make_unshield_error_extrinsic(wrapper_tx_hash: ::Hash, error: ShieldError) -> ExtrinsicOf; + + /// V1: try to decode a shielded transaction (no error details). + #[changed_in(2)] + fn try_decode_shielded_tx(uxt: ExtrinsicOf) -> Option; + + /// V1: try to unshield a transaction (no error details). + #[changed_in(2)] fn try_unshield_tx(dec_key_bytes: Vec, shielded_tx: ShieldedTransaction) -> Option>; } } diff --git a/substrate/primitives/shield/src/shielded_tx.rs b/substrate/primitives/shield/src/shielded_tx.rs index 79e7d6a7ce4c5..3b3248b77a181 100644 --- a/substrate/primitives/shield/src/shielded_tx.rs +++ b/substrate/primitives/shield/src/shielded_tx.rs @@ -6,10 +6,12 @@ use alloc::vec::Vec; use codec::{Decode, Encode}; use scale_info::TypeInfo; +use crate::ParsingError; + const KEY_HASH_LEN: usize = 16; const NONCE_LEN: usize = 24; -#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ShieldedTransaction { pub key_hash: [u8; KEY_HASH_LEN], pub kem_ct: Vec, @@ -18,30 +20,46 @@ pub struct ShieldedTransaction { } impl ShieldedTransaction { - pub fn parse(ciphertext: &[u8]) -> Option { - let key_hash: [u8; KEY_HASH_LEN] = ciphertext.get(0..KEY_HASH_LEN)?.try_into().ok()?; + pub fn parse(ciphertext: &[u8]) -> Result { + let key_hash: [u8; KEY_HASH_LEN] = ciphertext + .get(0..KEY_HASH_LEN) + .and_then(|s| s.try_into().ok()) + .ok_or(ParsingError::TruncatedKeyHash)?; let mut cursor = KEY_HASH_LEN; - let kem_ct_len_end = cursor.checked_add(2)?; - let kem_ct_len = ciphertext - .get(cursor..kem_ct_len_end)? + let kem_ct_len_end = cursor + .checked_add(2) + .filter(|&end| end <= ciphertext.len()) + .ok_or(ParsingError::TruncatedKemLen)?; + let kem_ct_len: usize = ciphertext[cursor..kem_ct_len_end] .try_into() .map(u16::from_le_bytes) - .ok()? + .map_err(|_| ParsingError::TruncatedKemLen)? .into(); cursor = kem_ct_len_end; - let kem_ct_end = cursor.checked_add(kem_ct_len)?; - let kem_ct = ciphertext.get(cursor..kem_ct_end)?.to_vec(); + let kem_ct_end = cursor + .checked_add(kem_ct_len) + .filter(|&end| end <= ciphertext.len()) + .ok_or(ParsingError::KemLenExceedsRemaining)?; + let kem_ct = ciphertext[cursor..kem_ct_end].to_vec(); cursor = kem_ct_end; - let nonce_end = cursor.checked_add(NONCE_LEN)?; - let nonce = ciphertext.get(cursor..nonce_end)?.try_into().ok()?; + let nonce_end = cursor + .checked_add(NONCE_LEN) + .filter(|&end| end <= ciphertext.len()) + .ok_or(ParsingError::TruncatedNonce)?; + let nonce: [u8; NONCE_LEN] = ciphertext[cursor..nonce_end] + .try_into() + .map_err(|_| ParsingError::TruncatedNonce)?; cursor = nonce_end; - let aead_ct = ciphertext.get(cursor..)?.to_vec(); + let aead_ct = ciphertext[cursor..].to_vec(); + if aead_ct.is_empty() { + return Err(ParsingError::MissingAead); + } - Some(Self { key_hash, kem_ct, aead_ct, nonce }) + Ok(Self { key_hash, kem_ct, aead_ct, nonce }) } } @@ -88,10 +106,7 @@ mod tests { #[test] fn parse_empty_aead_ct() { let ct = build_ciphertext(&DUMMY_KEY_HASH, &DUMMY_KEM_CT, &DUMMY_NONCE, &[]); - let tx = ShieldedTransaction::parse(&ct).expect("should parse with empty aead_ct"); - - assert!(tx.aead_ct.is_empty()); - assert_eq!(tx.kem_ct, DUMMY_KEM_CT); + assert_eq!(ShieldedTransaction::parse(&ct), Err(ParsingError::MissingAead)); } #[test] @@ -104,21 +119,21 @@ mod tests { } #[test] - fn parse_empty_returns_none() { - assert!(ShieldedTransaction::parse(&[]).is_none()); + fn parse_empty_returns_err() { + assert_eq!(ShieldedTransaction::parse(&[]), Err(ParsingError::TruncatedKeyHash),); } #[test] fn parse_truncated_key_hash() { let ct = [0u8; KEY_HASH_LEN - 1]; - assert!(ShieldedTransaction::parse(&ct).is_none()); + assert_eq!(ShieldedTransaction::parse(&ct), Err(ParsingError::TruncatedKeyHash),); } #[test] fn parse_truncated_kem_len() { // key_hash present but only 1 byte for kem_ct_len (needs 2). let ct = [0u8; KEY_HASH_LEN + 1]; - assert!(ShieldedTransaction::parse(&ct).is_none()); + assert_eq!(ShieldedTransaction::parse(&ct), Err(ParsingError::TruncatedKemLen),); } #[test] @@ -128,7 +143,7 @@ mod tests { ct.extend_from_slice(&DUMMY_KEY_HASH); ct.extend_from_slice(&1088u16.to_le_bytes()); ct.extend_from_slice(&[0u8; 10]); - assert!(ShieldedTransaction::parse(&ct).is_none()); + assert_eq!(ShieldedTransaction::parse(&ct), Err(ParsingError::KemLenExceedsRemaining),); } #[test] @@ -139,7 +154,7 @@ mod tests { ct.extend_from_slice(&4u16.to_le_bytes()); ct.extend_from_slice(&[0u8; 4]); // kem_ct ct.extend_from_slice(&[0u8; 20]); // only 20 of 24 nonce bytes - assert!(ShieldedTransaction::parse(&ct).is_none()); + assert_eq!(ShieldedTransaction::parse(&ct), Err(ParsingError::TruncatedNonce),); } #[test] diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index d5186b0e2e042..f5cf27306c31e 100644 --- a/substrate/test-utils/runtime/src/lib.rs +++ b/substrate/test-utils/runtime/src/lib.rs @@ -837,7 +837,7 @@ impl_runtime_apis! { } impl stp_shield::ShieldApi for Runtime { - fn try_decode_shielded_tx(_uxt: ::Extrinsic) -> Option { + fn try_decode_shielded_tx(_uxt: ::Extrinsic) -> Option> { sp_io::storage::get(SHIELD_TEST_DECODE_KEY) .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) } @@ -848,9 +848,17 @@ impl_runtime_apis! { .unwrap_or(true) } - fn try_unshield_tx(_dec_key_bytes: Vec, _shielded_tx: stp_shield::ShieldedTransaction) -> Option<::Extrinsic> { + fn try_unshield_tx(_dec_key_bytes: Vec, _shielded_tx: stp_shield::ShieldedTransaction) -> Result<::Extrinsic, stp_shield::ShieldError> { sp_io::storage::get(SHIELD_TEST_UNSHIELD_KEY) .and_then(|bytes| Decode::decode(&mut &bytes[..]).ok()) + .ok_or(stp_shield::ShieldError::Decryption(stp_shield::DecryptionError::AeadDecryptionFailed)) + } + + fn make_unshield_error_extrinsic(_wrapper_tx_hash: ::Hash, _error: stp_shield::ShieldError) -> ::Extrinsic { + // Use storage_change which passes ValidateUnsigned in the test runtime. + Extrinsic::new_bare(RuntimeCall::SubstrateTest( + substrate_test_pallet::Call::storage_change { key: Vec::new(), value: None }, + )) } } }