Improve Dispatch Errors (#878)

* better dispatch errors

* dry_run to use same DispatchError

* fix dry_run_fails; use correct transfer amount

* Hide ModuleError impl and avoid pulling details from metadata unless user needs them

* fix tests

* actually fix the tests (hopefully..)

* Add a couple more DispatchError test cases

* Add a comment about where the error was copied from

* Also expose a way to obtain the raw module error data

* Remove redundant variant prefixes

* explicit lifetime on From<str> for clarity

* fmt
This commit is contained in:
James Wilson
2023-03-23 09:50:44 +00:00
committed by GitHub
parent 7c252fccf7
commit b5194be5a2
14 changed files with 673 additions and 474 deletions
+303
View File
@@ -0,0 +1,303 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! A representation of the dispatch error; an error returned when
//! something fails in trying to submit/execute a transaction.
use crate::metadata::{DecodeWithMetadata, Metadata};
use core::fmt::Debug;
use scale_decode::visitor::DecodeAsTypeResult;
use std::borrow::Cow;
/// An error dispatching a transaction.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum DispatchError {
/// Some error occurred.
#[error("Some unknown error occurred.")]
Other,
/// Failed to lookup some data.
#[error("Failed to lookup some data.")]
CannotLookup,
/// A bad origin.
#[error("Bad origin.")]
BadOrigin,
/// A custom error in a module.
#[error("Pallet error: {0}")]
Module(ModuleError),
/// At least one consumer is remaining so the account cannot be destroyed.
#[error("At least one consumer is remaining so the account cannot be destroyed.")]
ConsumerRemaining,
/// There are no providers so the account cannot be created.
#[error("There are no providers so the account cannot be created.")]
NoProviders,
/// There are too many consumers so the account cannot be created.
#[error("There are too many consumers so the account cannot be created.")]
TooManyConsumers,
/// An error to do with tokens.
#[error("Token error: {0}")]
Token(TokenError),
/// An arithmetic error.
#[error("Arithmetic error: {0}")]
Arithmetic(ArithmeticError),
/// The number of transactional layers has been reached, or we are not in a transactional layer.
#[error("Transactional error: {0}")]
Transactional(TransactionalError),
/// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate.
#[error(
"Resources exhausted, e.g. attempt to read/write data which is too large to manipulate."
)]
Exhausted,
/// The state is corrupt; this is generally not going to fix itself.
#[error("The state is corrupt; this is generally not going to fix itself.")]
Corruption,
/// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later.
#[error(
"Some resource (e.g. a preimage) is unavailable right now. This might fix itself later."
)]
Unavailable,
}
/// An error relating to tokens when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum TokenError {
/// Funds are unavailable.
#[error("Funds are unavailable.")]
FundsUnavailable,
/// Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved.
#[error("Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved.")]
OnlyProvider,
/// Account cannot exist with the funds that would be given.
#[error("Account cannot exist with the funds that would be given.")]
BelowMinimum,
/// Account cannot be created.
#[error("Account cannot be created.")]
CannotCreate,
/// The asset in question is unknown.
#[error("The asset in question is unknown.")]
UnknownAsset,
/// Funds exist but are frozen.
#[error("Funds exist but are frozen.")]
Frozen,
/// Operation is not supported by the asset.
#[error("Operation is not supported by the asset.")]
Unsupported,
/// Account cannot be created for a held balance.
#[error("Account cannot be created for a held balance.")]
CannotCreateHold,
/// Withdrawal would cause unwanted loss of account.
#[error("Withdrawal would cause unwanted loss of account.")]
NotExpendable,
}
/// An error relating to arithmetic when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArithmeticError {
/// Underflow.
#[error("Underflow.")]
Underflow,
/// Overflow.
#[error("Overflow.")]
Overflow,
/// Division by zero.
#[error("Division by zero.")]
DivisionByZero,
}
/// An error relating to thr transactional layers when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum TransactionalError {
/// Too many transactional layers have been spawned.
#[error("Too many transactional layers have been spawned.")]
LimitReached,
/// A transactional layer was expected, but does not exist.
#[error("A transactional layer was expected, but does not exist.")]
NoLayer,
}
/// Details about a module error that has occurred.
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub struct ModuleError {
metadata: Metadata,
raw: RawModuleError,
}
impl PartialEq for ModuleError {
fn eq(&self, other: &Self) -> bool {
// A module error is the same if the raw underlying details are the same.
self.raw == other.raw
}
}
impl Eq for ModuleError {}
impl std::fmt::Display for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Ok(details) = self.details() else {
return f.write_str("Unknown pallet error (pallet and error details cannot be retrieved)")
};
let pallet = details.pallet();
let error = details.error();
write!(f, "Pallet error {pallet}::{error}")
}
}
impl ModuleError {
/// Return more details about this error.
pub fn details(&self) -> Result<&crate::metadata::ErrorMetadata, super::Error> {
let error_details = self
.metadata
.error(self.raw.pallet_index, self.raw.error[0])?;
Ok(error_details)
}
/// Return the underlying module error data that was decoded.
pub fn raw(&self) -> RawModuleError {
self.raw
}
}
/// The error details about a module error that has occurred.
///
/// **Note**: Structure used to obtain the underlying bytes of a ModuleError.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RawModuleError {
/// Index of the pallet that the error came from.
pub pallet_index: u8,
/// Raw error bytes.
pub error: [u8; 4],
}
impl RawModuleError {
/// Obtain the error index from the underlying byte data.
pub fn error_index(&self) -> u8 {
// Error index is utilized as the first byte from the error array.
self.error[0]
}
}
impl DispatchError {
/// Attempt to decode a runtime [`DispatchError`].
#[doc(hidden)]
pub fn decode_from<'a>(
bytes: impl Into<Cow<'a, [u8]>>,
metadata: Metadata,
) -> Result<Self, super::Error> {
let bytes = bytes.into();
let dispatch_error_ty_id = match metadata.dispatch_error_ty() {
Some(id) => id,
None => {
tracing::warn!(
"Can't decode error: sp_runtime::DispatchError was not found in Metadata"
);
return Err(super::Error::Unknown(bytes.into_owned()));
}
};
// The aim is to decode our bytes into roughly this shape. This is copied from
// `sp_runtime::DispatchError`; we need the variant names and any inner variant
// names/shapes to line up in order for decoding to be successful.
#[derive(scale_decode::DecodeAsType)]
enum DecodedDispatchError {
Other,
CannotLookup,
BadOrigin,
Module(DecodedModuleErrorBytes),
ConsumerRemaining,
NoProviders,
TooManyConsumers,
Token(TokenError),
Arithmetic(ArithmeticError),
Transactional(TransactionalError),
Exhausted,
Corruption,
Unavailable,
}
// ModuleError is a bit special; we want to support being decoded from either
// a legacy format of 2 bytes, or a newer format of 5 bytes. So, just grab the bytes
// out when decoding to manually work with them.
struct DecodedModuleErrorBytes(Vec<u8>);
struct DecodedModuleErrorBytesVisitor;
impl scale_decode::Visitor for DecodedModuleErrorBytesVisitor {
type Error = scale_decode::Error;
type Value<'scale, 'info> = DecodedModuleErrorBytes;
fn unchecked_decode_as_type<'scale, 'info>(
self,
input: &mut &'scale [u8],
_type_id: scale_decode::visitor::TypeId,
_types: &'info scale_info::PortableRegistry,
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>>
{
DecodeAsTypeResult::Decoded(Ok(DecodedModuleErrorBytes(input.to_vec())))
}
}
impl scale_decode::IntoVisitor for DecodedModuleErrorBytes {
type Visitor = DecodedModuleErrorBytesVisitor;
fn into_visitor() -> Self::Visitor {
DecodedModuleErrorBytesVisitor
}
}
// Decode into our temporary error:
let decoded_dispatch_err = DecodedDispatchError::decode_with_metadata(
&mut &*bytes,
dispatch_error_ty_id,
&metadata,
)?;
// Convert into the outward-facing error, mainly by handling the Module variant.
let dispatch_error = match decoded_dispatch_err {
// Mostly we don't change anything from our decoded to our outward-facing error:
DecodedDispatchError::Other => DispatchError::Other,
DecodedDispatchError::CannotLookup => DispatchError::CannotLookup,
DecodedDispatchError::BadOrigin => DispatchError::BadOrigin,
DecodedDispatchError::ConsumerRemaining => DispatchError::ConsumerRemaining,
DecodedDispatchError::NoProviders => DispatchError::NoProviders,
DecodedDispatchError::TooManyConsumers => DispatchError::TooManyConsumers,
DecodedDispatchError::Token(val) => DispatchError::Token(val),
DecodedDispatchError::Arithmetic(val) => DispatchError::Arithmetic(val),
DecodedDispatchError::Transactional(val) => DispatchError::Transactional(val),
DecodedDispatchError::Exhausted => DispatchError::Exhausted,
DecodedDispatchError::Corruption => DispatchError::Corruption,
DecodedDispatchError::Unavailable => DispatchError::Unavailable,
// But we apply custom logic to transform the module error into the outward facing version:
DecodedDispatchError::Module(module_bytes) => {
let module_bytes = module_bytes.0;
// The old version is 2 bytes; a pallet and error index.
// The new version is 5 bytes; a pallet and error index and then 3 extra bytes.
let raw = if module_bytes.len() == 2 {
RawModuleError {
pallet_index: module_bytes[0],
error: [module_bytes[1], 0, 0, 0],
}
} else if module_bytes.len() == 5 {
RawModuleError {
pallet_index: module_bytes[0],
error: [
module_bytes[1],
module_bytes[2],
module_bytes[3],
module_bytes[4],
],
}
} else {
tracing::warn!("Can't decode error sp_runtime::DispatchError: bytes do not match known shapes");
// Return _all_ of the bytes; every "unknown" return should be consistent.
return Err(super::Error::Unknown(bytes.to_vec()));
};
// And return our outward-facing version:
DispatchError::Module(ModuleError { metadata, raw })
}
};
Ok(dispatch_error)
}
}
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2019-2022 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types representing the errors that can be returned.
mod dispatch_error;
use core::fmt::Debug;
// Re-export dispatch error types:
pub use dispatch_error::{
ArithmeticError, DispatchError, ModuleError, RawModuleError, TokenError, TransactionalError,
};
// Re-expose the errors we use from other crates here:
pub use crate::metadata::{InvalidMetadataError, MetadataError};
pub use scale_decode::Error as DecodeError;
pub use scale_encode::Error as EncodeError;
/// The underlying error enum, generic over the type held by the `Runtime`
/// variant. Prefer to use the [`Error<E>`] and [`Error`] aliases over
/// using this type directly.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Io error.
#[error("Io error: {0}")]
Io(#[from] std::io::Error),
/// Codec error.
#[error("Scale codec error: {0}")]
Codec(#[from] codec::Error),
/// Rpc error.
#[error("Rpc error: {0}")]
Rpc(#[from] RpcError),
/// Serde serialization error
#[error("Serde json error: {0}")]
Serialization(#[from] serde_json::error::Error),
/// Invalid metadata error
#[error("Invalid Metadata: {0}")]
InvalidMetadata(#[from] InvalidMetadataError),
/// Invalid metadata error
#[error("Metadata: {0}")]
Metadata(#[from] MetadataError),
/// Runtime error.
#[error("Runtime error: {0:?}")]
Runtime(#[from] DispatchError),
/// Error decoding to a [`crate::dynamic::Value`].
#[error("Error decoding into dynamic value: {0}")]
Decode(#[from] DecodeError),
/// Error encoding from a [`crate::dynamic::Value`].
#[error("Error encoding from dynamic value: {0}")]
Encode(#[from] EncodeError),
/// Transaction progress error.
#[error("Transaction error: {0}")]
Transaction(#[from] TransactionError),
/// Block related error.
#[error("Block error: {0}")]
Block(#[from] BlockError),
/// An error encoding a storage address.
#[error("Error encoding storage address: {0}")]
StorageAddress(#[from] StorageAddressError),
/// The bytes representing an error that we were unable to decode.
#[error("An error occurred but it could not be decoded: {0:?}")]
Unknown(Vec<u8>),
/// Other error.
#[error("Other error: {0}")]
Other(String),
}
impl<'a> From<&'a str> for Error {
fn from(error: &'a str) -> Self {
Error::Other(error.into())
}
}
impl From<String> for Error {
fn from(error: String) -> Self {
Error::Other(error)
}
}
/// An RPC error. Since we are generic over the RPC client that is used,
/// the error is boxed and could be casted.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RpcError {
// Dev note: We need the error to be safely sent between threads
// for `subscribe_to_block_headers_filling_in_gaps` and friends.
/// Error related to the RPC client.
#[error("RPC error: {0}")]
ClientError(Box<dyn std::error::Error + Send + Sync + 'static>),
/// The RPC subscription dropped.
#[error("RPC error: subscription dropped.")]
SubscriptionDropped,
}
/// Block error
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum BlockError {
/// An error containing the hash of the block that was not found.
#[error("Could not find a block with hash {0} (perhaps it was on a non-finalized fork?)")]
NotFound(String),
}
impl BlockError {
/// Produce an error that a block with the given hash cannot be found.
pub fn not_found(hash: impl AsRef<[u8]>) -> BlockError {
let hash = format!("0x{}", hex::encode(hash));
BlockError::NotFound(hash)
}
}
/// Transaction error.
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum TransactionError {
/// The finality subscription expired (after ~512 blocks we give up if the
/// block hasn't yet been finalized).
#[error("The finality subscription expired")]
FinalitySubscriptionTimeout,
/// The block hash that the transaction was added to could not be found.
/// This is probably because the block was retracted before being finalized.
#[error("The block containing the transaction can no longer be found (perhaps it was on a non-finalized fork?)")]
BlockNotFound,
}
/// Something went wrong trying to encode a storage address.
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StorageAddressError {
/// Storage map type must be a composite type.
#[error("Storage map type must be a composite type")]
MapTypeMustBeTuple,
/// Storage lookup does not have the expected number of keys.
#[error("Storage lookup requires {expected} keys but got {actual} keys")]
WrongNumberOfKeys {
/// The actual number of keys needed, based on the metadata.
actual: usize,
/// The number of keys provided in the storage address.
expected: usize,
},
/// Storage lookup requires a type that wasn't found in the metadata.
#[error("Storage lookup requires type {0} to exist in the metadata, but it was not found")]
TypeNotFound(u32),
/// This storage entry in the metadata does not have the correct number of hashers to fields.
#[error("Storage entry in metadata does not have the correct number of hashers to fields")]
WrongNumberOfHashers {
/// The number of hashers in the metadata for this storage entry.
hashers: usize,
/// The number of fields in the metadata for this storage entry.
fields: usize,
},
}