use tari_bor::BorError;
use tari_engine_types::{
commit_result::RejectReason,
entity_id_provider::EntityIdProviderError,
id_provider::IdProviderError,
indexed_value::IndexedValueError,
limits,
lock::LockId,
resource_container::ResourceError,
substate::SubstateId,
virtual_substate::VirtualSubstateId,
};
use tari_ootle_common_types::{displayable::Displayable, optional::IsNotFoundError};
use tari_ootle_transaction::{
CheckOrd,
NftCheck,
args::{WorkspaceId, WorkspaceOffsetId},
};
use tari_template_lib::{
args::{CallAction, VaultFreezeFlag},
models::{AddressAllocationId, BucketId, ProofId},
prelude::RistrettoPublicKeyBytes,
types::{
Amount,
ClaimedOutputTombstoneAddress,
ComponentAddress,
NonFungibleId,
ResourceAddress,
ResourceType,
TemplateAddress,
TransactionReceiptAddress,
VaultId,
},
};
use super::workspace::WorkspaceError;
use crate::{
runtime::{ActionIdent, RuntimeModuleError, locking::LockError},
state_store::StateStoreError,
};
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("Encoding error: {0}")]
EncodingError(#[from] BorError),
#[error("Indexed value error: {0}")]
IndexedValueError(#[from] IndexedValueError),
#[error("State storage error: {0}")]
StateStoreError(#[from] StateStoreError),
#[error("Workspace error: {0}")]
WorkspaceError(#[from] WorkspaceError),
#[error("Substate '{id}' not found or is not a transaction input")]
SubstateNotFound { id: SubstateId },
#[error("Root substate '{id}' not found")]
RootSubstateNotFound { id: SubstateId },
#[error("Referenced substate '{id}' not found")]
ReferencedSubstateNotFound { id: SubstateId },
#[error("Non-existent substate '{id}' returned from call")]
NonExistentSubstateReturned { id: SubstateId },
#[error("Substate '{id}' not in scope")]
SubstateOutOfScope { id: SubstateId },
#[error("Substate {id} is not owned by {requested_owner}")]
SubstateNotOwned {
id: SubstateId,
requested_owner: Box<SubstateId>,
},
#[error("Expected lock {lock_id} to lock {expected_type} but it locks {id}")]
LockSubstateMismatch {
lock_id: LockId,
expected_type: &'static str,
id: SubstateId,
},
#[error("Component {component} referenced an unknown substate {id}")]
ComponentReferencedUnknownSubstate {
component: ComponentAddress,
id: SubstateId,
},
#[error("Encountered unknown or out of scope bucket {bucket_id}")]
BucketNotInScope { bucket_id: BucketId },
#[error("Encountered unknown or out of scope proof {proof_id}")]
ProofNotInScope { proof_id: ProofId },
#[error("Encountered unknown or out of scope address allocation {id}")]
AddressAllocationNotInScope { id: AddressAllocationId },
#[error("Encountered unknown or out of scope signer badge with public key {public_key}")]
SignerBadgeNotInScope { public_key: RistrettoPublicKeyBytes },
#[error("Component not found with address '{address}'")]
ComponentNotFound { address: ComponentAddress },
#[error("Layer one commitment not found with address '{address}'")]
LayerOneCommitmentNotFound { address: ClaimedOutputTombstoneAddress },
#[error("Invalid argument {argument}: {reason}")]
InvalidArgument { argument: &'static str, reason: String },
#[error("Invalid number of arguments: expected {expected}, but got {len}")]
InvalidNumberOfArguments { expected: usize, len: usize },
#[error("Invalid amount '{amount}': {reason}")]
InvalidAmount { amount: Amount, reason: String },
#[error("Call frame error: {details}")]
CurrentFrameError { details: String },
#[error("Vault not found with id ({vault_id})")]
VaultNotFound { vault_id: VaultId },
#[error("Vault {vault_id} is frozen for {freeze_flag}")]
VaultFrozen {
vault_id: VaultId,
freeze_flag: VaultFreezeFlag,
},
#[error("Non-fungible token not found with address {resource_address} and id {nft_id}")]
NonFungibleNotFound {
resource_address: ResourceAddress,
nft_id: NonFungibleId,
},
#[error("Invalid op '{op}' on burnt non-fungible {resource_address} id {nf_id}")]
InvalidOpNonFungibleBurnt {
op: &'static str,
resource_address: ResourceAddress,
nf_id: NonFungibleId,
},
#[error("Bucket not found with id {bucket_id}")]
BucketNotFound { bucket_id: BucketId },
#[error("Proof not found with id {proof_id}")]
ProofNotFound { proof_id: ProofId },
#[error("Resource not found with address {resource_address}")]
ResourceNotFound { resource_address: ResourceAddress },
#[error(transparent)]
ResourceError(#[from] ResourceError),
#[error(
"Resource supply would overflow: resource {resource_address}, current supply {current_supply}, amount {amount}"
)]
ResourceSupplyWouldOverflow {
resource_address: ResourceAddress,
current_supply: Amount,
amount: Amount,
},
#[error("Bucket {bucket_id} was dropped but was not empty")]
BucketNotEmpty { bucket_id: BucketId },
#[error("Item at id {id} does not exist on the workspace (existing ids: {})", .existing_ids.display())]
ItemNotOnWorkspace {
id: WorkspaceOffsetId,
existing_ids: Vec<WorkspaceId>,
},
#[error("Attempted to take the last output but there was no previous instruction output")]
NoLastInstructionOutput,
#[error(transparent)]
TransactionCommitError(#[from] TransactionCommitError),
#[error("Transaction generated too many outputs: {0}")]
TooManyOutputs(#[from] IdProviderError),
#[error("Transaction generated too many new entities: {0}")]
TooManyEntities(#[from] EntityIdProviderError),
#[error("Duplicate NFT token id: {token_id}")]
DuplicateNonFungibleId { token_id: NonFungibleId },
#[error("Access Denied: {action_ident}")]
AccessDenied { action_ident: ActionIdent },
#[error("Access Denied: attempt to set state on component {attempted_on} from another component {attempted_by}")]
AccessDeniedSetComponentState {
attempted_on: SubstateId,
attempted_by: Box<SubstateId>,
},
#[error("Resource Auth Hook Denied Access for action {action_ident}: {details}")]
AccessDeniedAuthHook { action_ident: ActionIdent, details: String },
#[error("Access Denied: You must be the owner to perform this action: {action}")]
AccessDeniedOwnerRequired { action: ActionIdent },
#[error("Invalid method address rule for {template_name}: {details}")]
InvalidMethodAccessRule { template_name: String, details: String },
#[error("Runtime module error: {0}")]
ModuleError(#[from] RuntimeModuleError),
#[error("Invalid burn claim proof: {details}")]
InvalidClaimProof { details: String },
#[error("Layer one commitment already claimed with address '{address}'")]
ConfidentialOutputAlreadyClaimed { address: ClaimedOutputTombstoneAddress },
#[error("Template {template_address} not found")]
TemplateNotFound { template_address: TemplateAddress },
#[error("Insufficient fees paid: required {required_fee}, paid {fees_paid}")]
InsufficientFeesPaid { required_fee: u64, fees_paid: u64 },
#[error("No fees paid from stealth transfer: {details}")]
NoFeesPaid { details: String },
#[error("No fee checkpoint")]
NoFeeCheckpoint,
#[error("Failed to load template '{address}': {details}")]
FailedToLoadTemplate { address: TemplateAddress, details: String },
#[error("Transaction Receipt already exists {address}")]
TransactionReceiptAlreadyExists { address: TransactionReceiptAddress },
#[error("Transaction Receipt not found")]
TransactionReceiptNotFound,
#[error("Component already exists {address}")]
ComponentAlreadyExists { address: ComponentAddress },
#[error("Cross-template call function error of function '{function}' on template '{template_address}': {details}")]
CrossTemplateCallFunctionError {
template_address: TemplateAddress,
function: String,
details: String,
},
#[error("Cross-template call failed for method '{method}' on component '{component_address}': {details}")]
CrossTemplateCallMethodError {
component_address: ComponentAddress,
method: String,
details: String,
},
#[error("Virtual substate not found: {address}")]
VirtualSubstateNotFound { address: VirtualSubstateId },
#[error("Invalid return value: {0}")]
InvalidReturnValue(IndexedValueError),
#[error("Attempt to pop auth scope stack but it was empty")]
AuthScopeStackEmpty,
#[error("Invalid deposit of bucket {bucket_id} has locked value amounting to {locked_amount}")]
InvalidOpDepositLockedBucket { bucket_id: BucketId, locked_amount: Amount },
#[error("Duplicate substate {address}")]
DuplicateSubstate { address: SubstateId },
#[error("Substate {id} is orphaned")]
OrphanedSubstate { id: SubstateId },
#[error("{} orphaned substate(s) detected: {}", .substates.len(), .substates.join(", "))]
OrphanedSubstates { substates: Vec<String> },
#[error("Attempted to finalise state but {remaining} call frame(s) remain on the stack")]
CallFrameRemainingOnStack { remaining: usize },
#[error("Duplicate reference to substate {address}")]
DuplicateReference { address: SubstateId },
#[error("Invalid argument: {0}")]
ArgumentValidationError(#[from] ArgumentValidationError),
#[error("Fee payment found in main intent, which is not allowed")]
FeePaymentInMainIntent,
#[error("BUG: [{function}] Invariant error {details}")]
InvariantError { function: &'static str, details: String },
#[error("Lock error: {0}")]
LockError(#[from] LockError),
#[error("{count} substate locks were still active after call")]
DanglingSubstateLocks { count: usize },
#[error("No active call frame")]
NoActiveCallFrame,
#[error("Max call depth {max_depth} exceeded")]
MaxCallDepthExceeded { max_depth: usize },
#[error("{action} can only be called from within a component context")]
NotInComponentContext { action: ActionIdent },
#[error("Duplicate bucket {bucket_id}")]
DuplicateBucket { bucket_id: BucketId },
#[error("Duplicate proof {proof_id}")]
DuplicateProof { proof_id: ProofId },
#[error("Address allocation not found with id {id}")]
AddressAllocationNotFound { id: u32 },
#[error("Address allocation with id {id} was not used")]
AddressAllocationNotUsed { id: u32 },
#[error("Address allocation type mismatch: got {id}, expected: {expected}")]
AddressAllocationTypeMismatch { id: SubstateId, expected: &'static str },
#[error("Allocated address does not have an associated template")]
AddressAllocationNoTemplate,
#[error("Invalid event topic '{topic}': {reason}")]
InvalidEventTopic { topic: String, reason: String },
#[error("Numeric conversion error: {details}")]
NumericConversionError { details: String },
#[error("Auth callback MUST return null, but it returned non-null")]
UnexpectedNonNullInAuthHookReturn,
#[error("Not supported: {details}")]
NotSupported { details: String },
#[error("Assert error: {0}")]
AssertError(#[from] AssertError),
#[error("Limit error: {0}")]
LimitError(#[from] LimitError),
#[error("Cross-template calls are not allowed in this context: {action:?}")]
CrossTemplateCallNotAllowed { action: CallAction },
}
impl RuntimeError {
pub fn to_reject_reason(&self, instruction_idx: Option<usize>) -> RejectReason {
let instruction_prefix = if let Some(ref idx) = instruction_idx {
format_args!("At instruction #{}: ", *idx)
} else {
format_args!("")
};
match self {
Self::SubstateNotFound { id } => {
RejectReason::SubstateNotFound(format!("{instruction_prefix}{id} not found",))
},
Self::RootSubstateNotFound { id } => RejectReason::SubstateNotFound(format!(
"{instruction_prefix}Template referenced root substate but it was not found: {id}"
)),
Self::ReferencedSubstateNotFound { id } => RejectReason::SubstateNotFound(format!(
"{instruction_prefix}Template referenced substate but it was not found: {id}"
)),
Self::InsufficientFeesPaid {
fees_paid,
required_fee,
} => RejectReason::InsufficientFeesPaid(format!(
"{instruction_prefix}Insufficient fees paid: {fees_paid}, required fees: {required_fee}"
)),
Self::FeePaymentInMainIntent => RejectReason::FeePaymentInMainIntent,
err => RejectReason::ExecutionFailure(format!("{instruction_prefix}{err}")),
}
}
}
impl IsNotFoundError for RuntimeError {
fn is_not_found_error(&self) -> bool {
matches!(
self,
RuntimeError::SubstateNotFound { .. } |
RuntimeError::ComponentNotFound { .. } |
RuntimeError::VaultNotFound { .. } |
RuntimeError::BucketNotFound { .. } |
RuntimeError::ResourceNotFound { .. } |
RuntimeError::NonFungibleNotFound { .. } |
RuntimeError::ProofNotFound { .. } |
RuntimeError::VirtualSubstateNotFound { .. }
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum AssertError {
#[error("The workspace value at {key} is not a bucket")]
NotABucket { key: WorkspaceOffsetId },
#[error("Assertion expected bucket to have resource {expected} but has {got}")]
InvalidResource {
expected: ResourceAddress,
got: ResourceAddress,
},
#[error("Assertion expected bucket to have resource type {expected} but has {got}")]
InvalidResourceType { expected: ResourceType, got: ResourceType },
#[error("Assertion failed: expected bucket amount {got} to be {check} {expected}")]
BucketAmountAssertionFail {
expected: Amount,
check: CheckOrd,
got: Amount,
},
#[error("Assertion failed: expected bucket to contain {check} {nft}")]
BucketContainsNonFungiblesAssertionFail { nft: NonFungibleId, check: NftCheck },
#[error("Assertion failed: expected bucket to contain {check} the non-fungibles")]
BucketContainsNonFungiblesAnyAssertionFail { check: NftCheck },
#[error("Value is null but expected non-null")]
ValueIsNull,
}
#[derive(Debug, thiserror::Error)]
pub enum TransactionCommitError {
#[error("{count} dangling buckets remain after transaction execution")]
DanglingBuckets { count: usize },
#[error(
"{count} dangling proofs remain after transaction execution. You may need to add a `DropAllProofsOnWorkspace` \
instruction"
)]
DanglingProofs { count: usize },
#[error("Locked value (amount: {locked_amount}) remaining in vault {vault_id}")]
DanglingLockedValueInVault { vault_id: VaultId, locked_amount: Amount },
#[error("{count} dangling address allocations remain after transaction execution")]
DanglingAddressAllocations { count: usize },
#[error("{count} dangling items in workspace after transaction execution")]
WorkspaceNotEmpty { count: usize },
#[error(transparent)]
StateStoreError(#[from] StateStoreError),
#[error(transparent)]
IdProviderError(#[from] IdProviderError),
}
#[derive(Debug, thiserror::Error)]
pub enum ArgumentValidationError {
#[error("Maximum number of stealth outputs exceeded: max {max_outputs}, got {actual_outputs}")]
MaxStealthOutputsExceeded { max_outputs: usize, actual_outputs: usize },
#[error("Maximum number of stealth inputs exceeded: max {max_inputs}, got {actual_inputs}")]
MaxStealthInputsExceeded { max_inputs: usize, actual_inputs: usize },
#[error("Too many arguments provided. Got {got}, max is {max}")]
TooManyArguments { got: usize, max: usize },
#[error("Blob index {index} out of bounds (transaction has {count} blob(s))")]
BlobIndexOutOfBounds { index: u8, count: usize },
}
#[derive(Debug, thiserror::Error)]
pub enum LimitError {
#[error("Substate size of {size} bytes exceeds the maximum allowed size of {} bytes", limits::ENGINE_LIMITS.max_substate_size)]
SubstateSizeExceeded { size: usize },
#[error("Log entry of {size} bytes exceeds maximum size of {} bytes", limits::ENGINE_LIMITS.max_log_size_bytes)]
LogSizeExceeded { size: usize },
#[error("Exceeded maximum number of logs per transaction: {}", limits::ENGINE_LIMITS.max_logs)]
MaxLogsExceeded,
#[error("Exceeded maximum number of events per transaction: {}", limits::ENGINE_LIMITS.max_events)]
MaxEventsExceeded,
#[error("Requested random bytes length {len} exceeds maximum of {} bytes", limits::ENGINE_LIMITS.max_random_bytes_len)]
MaxRandomBytesLenExceeded { len: usize },
}