Skip to main content

solana_transaction_error/
lib.rs

1#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![no_std]
4#[cfg(feature = "serde")]
5use serde_derive::{Deserialize, Serialize};
6#[cfg(feature = "frozen-abi")]
7use solana_frozen_abi_macro::{frozen_abi, AbiEnumVisitor, AbiExample, StableAbi, StableAbiSample};
8#[cfg(any(
9    feature = "frozen-abi",
10    not(any(target_os = "solana", target_arch = "bpf"))
11))]
12extern crate std;
13use {core::fmt, solana_instruction_error::InstructionError, solana_sanitize::SanitizeError};
14
15pub type TransactionResult<T> = Result<T, TransactionError>;
16
17/// Reasons a transaction might be rejected.
18#[cfg_attr(
19    feature = "frozen-abi",
20    derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample),
21    frozen_abi(
22        abi_digest = "2W49i91iPDXeXCEet6LbbpvoDJAm1HEDBfGskAmocXuu",
23        abi_serializer = ["bincode", "wincode"],
24        test_roundtrip = "eq_and_wire"
25    )
26)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
29#[cfg_attr(test, derive(strum_macros::EnumIter))]
30#[derive(Debug, PartialEq, Eq, Clone)]
31#[non_exhaustive]
32pub enum TransactionError {
33    /// An account is already being processed in another transaction in a way
34    /// that does not support parallelism
35    AccountInUse,
36
37    /// A `Pubkey` appears twice in the transaction's `account_keys`.  Instructions can reference
38    /// `Pubkey`s more than once but the message must contain a list with no duplicate keys
39    AccountLoadedTwice,
40
41    /// Attempt to debit an account but found no record of a prior credit.
42    AccountNotFound,
43
44    /// Attempt to load a program that does not exist
45    ProgramAccountNotFound,
46
47    /// The from `Pubkey` does not have sufficient balance to pay the fee to schedule the transaction
48    InsufficientFundsForFee,
49
50    /// This account may not be used to pay transaction fees
51    InvalidAccountForFee,
52
53    /// The bank has seen this transaction before. This can occur under normal operation
54    /// when a UDP packet is duplicated, as a user error from a client not updating
55    /// its `recent_blockhash`, or as a double-spend attack.
56    AlreadyProcessed,
57
58    /// The bank has not seen the given `recent_blockhash` or the transaction is too old and
59    /// the `recent_blockhash` has been discarded.
60    BlockhashNotFound,
61
62    /// An error occurred while processing an instruction. The first element of the tuple
63    /// indicates the instruction index in which the error occurred.
64    InstructionError(u8, InstructionError),
65
66    /// Loader call chain is too deep
67    CallChainTooDeep,
68
69    /// Transaction requires a fee but has no signature present
70    MissingSignatureForFee,
71
72    /// Transaction contains an invalid account reference
73    InvalidAccountIndex,
74
75    /// Transaction did not pass signature verification
76    SignatureFailure,
77
78    /// This program may not be used for executing instructions
79    InvalidProgramForExecution,
80
81    /// Transaction failed to sanitize accounts offsets correctly
82    /// implies that account locks are not taken for this TX, and should
83    /// not be unlocked.
84    SanitizeFailure,
85
86    ClusterMaintenance,
87
88    /// Transaction processing left an account with an outstanding borrowed reference
89    AccountBorrowOutstanding,
90
91    /// Transaction would exceed max Block Cost Limit
92    WouldExceedMaxBlockCostLimit,
93
94    /// Transaction version is unsupported
95    UnsupportedVersion,
96
97    /// Transaction loads a writable account that cannot be written
98    InvalidWritableAccount,
99
100    /// Transaction would exceed max account limit within the block
101    WouldExceedMaxAccountCostLimit,
102
103    /// Transaction would exceed account data limit within the block
104    WouldExceedAccountDataBlockLimit,
105
106    /// Transaction locked too many accounts
107    TooManyAccountLocks,
108
109    /// Address lookup table not found
110    AddressLookupTableNotFound,
111
112    /// Attempted to lookup addresses from an account owned by the wrong program
113    InvalidAddressLookupTableOwner,
114
115    /// Attempted to lookup addresses from an invalid account
116    InvalidAddressLookupTableData,
117
118    /// Address table lookup uses an invalid index
119    InvalidAddressLookupTableIndex,
120
121    /// Transaction leaves an account with a lower balance than rent-exempt minimum
122    InvalidRentPayingAccount,
123
124    /// Transaction would exceed max Vote Cost Limit
125    WouldExceedMaxVoteCostLimit,
126
127    /// Transaction would exceed total account data limit
128    WouldExceedAccountDataTotalLimit,
129
130    /// Transaction contains a duplicate instruction that is not allowed
131    DuplicateInstruction(u8),
132
133    /// Transaction results in an account with insufficient funds for rent
134    InsufficientFundsForRent {
135        account_index: u8,
136    },
137
138    /// Transaction exceeded max loaded accounts data size cap
139    MaxLoadedAccountsDataSizeExceeded,
140
141    /// LoadedAccountsDataSizeLimit set for transaction must be greater than 0.
142    InvalidLoadedAccountsDataSizeLimit,
143
144    /// Sanitized transaction differed before/after feature activation. Needs to be resanitized.
145    ResanitizationNeeded,
146
147    /// Program execution is temporarily restricted on an account.
148    ProgramExecutionTemporarilyRestricted {
149        account_index: u8,
150    },
151
152    /// The total balance before the transaction does not equal the total balance after the transaction
153    UnbalancedTransaction,
154
155    /// Program cache hit max limit.
156    ProgramCacheHitMaxLimit,
157
158    /// Commit cancelled internally.
159    CommitCancelled,
160
161    /// Block production bailed out.
162    /// This discards transactions to protect the leader and does not propagate to followers.
163    /// Meaning this explicitly excludes transactions from consensus.
164    BailOut,
165}
166
167impl TransactionError {
168    pub const VARIANTS: [Self; 40] = [
169        Self::AccountInUse,
170        Self::AccountLoadedTwice,
171        Self::AccountNotFound,
172        Self::ProgramAccountNotFound,
173        Self::InsufficientFundsForFee,
174        Self::InvalidAccountForFee,
175        Self::AlreadyProcessed,
176        Self::BlockhashNotFound,
177        Self::InstructionError(0, InstructionError::Custom(0)),
178        Self::CallChainTooDeep,
179        Self::MissingSignatureForFee,
180        Self::InvalidAccountIndex,
181        Self::SignatureFailure,
182        Self::InvalidProgramForExecution,
183        Self::SanitizeFailure,
184        Self::ClusterMaintenance,
185        Self::AccountBorrowOutstanding,
186        Self::WouldExceedMaxBlockCostLimit,
187        Self::UnsupportedVersion,
188        Self::InvalidWritableAccount,
189        Self::WouldExceedMaxAccountCostLimit,
190        Self::WouldExceedAccountDataBlockLimit,
191        Self::TooManyAccountLocks,
192        Self::AddressLookupTableNotFound,
193        Self::InvalidAddressLookupTableOwner,
194        Self::InvalidAddressLookupTableData,
195        Self::InvalidAddressLookupTableIndex,
196        Self::InvalidRentPayingAccount,
197        Self::WouldExceedMaxVoteCostLimit,
198        Self::WouldExceedAccountDataTotalLimit,
199        Self::DuplicateInstruction(0),
200        Self::InsufficientFundsForRent { account_index: 0 },
201        Self::MaxLoadedAccountsDataSizeExceeded,
202        Self::InvalidLoadedAccountsDataSizeLimit,
203        Self::ResanitizationNeeded,
204        Self::ProgramExecutionTemporarilyRestricted { account_index: 0 },
205        Self::UnbalancedTransaction,
206        Self::ProgramCacheHitMaxLimit,
207        Self::CommitCancelled,
208        Self::BailOut,
209    ];
210}
211
212impl core::error::Error for TransactionError {}
213
214impl fmt::Display for TransactionError {
215    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
216        match self {
217            Self::AccountInUse
218             => f.write_str("Account in use"),
219            Self::AccountLoadedTwice
220             => f.write_str("Account loaded twice"),
221            Self::AccountNotFound
222             => f.write_str("Attempt to debit an account but found no record of a prior credit."),
223            Self::ProgramAccountNotFound
224             => f.write_str("Attempt to load a program that does not exist"),
225            Self::InsufficientFundsForFee
226             => f.write_str("Insufficient funds for fee"),
227            Self::InvalidAccountForFee
228             => f.write_str("This account may not be used to pay transaction fees"),
229            Self::AlreadyProcessed
230             => f.write_str("This transaction has already been processed"),
231            Self::BlockhashNotFound
232             => f.write_str("Blockhash not found"),
233            Self::InstructionError(idx, err) =>  write!(f, "Error processing Instruction {idx}: {err}"),
234            Self::CallChainTooDeep
235             => f.write_str("Loader call chain is too deep"),
236            Self::MissingSignatureForFee
237             => f.write_str("Transaction requires a fee but has no signature present"),
238            Self::InvalidAccountIndex
239             => f.write_str("Transaction contains an invalid account reference"),
240            Self::SignatureFailure
241             => f.write_str("Transaction did not pass signature verification"),
242            Self::InvalidProgramForExecution
243             => f.write_str("This program may not be used for executing instructions"),
244            Self::SanitizeFailure
245             => f.write_str("Transaction failed to sanitize accounts offsets correctly"),
246            Self::ClusterMaintenance
247             => f.write_str("Transactions are currently disabled due to cluster maintenance"),
248            Self::AccountBorrowOutstanding
249             => f.write_str("Transaction processing left an account with an outstanding borrowed reference"),
250            Self::WouldExceedMaxBlockCostLimit
251             => f.write_str("Transaction would exceed max Block Cost Limit"),
252            Self::UnsupportedVersion
253             => f.write_str("Transaction version is unsupported"),
254            Self::InvalidWritableAccount
255             => f.write_str("Transaction loads a writable account that cannot be written"),
256            Self::WouldExceedMaxAccountCostLimit
257             => f.write_str("Transaction would exceed max account limit within the block"),
258            Self::WouldExceedAccountDataBlockLimit
259             => f.write_str("Transaction would exceed account data limit within the block"),
260            Self::TooManyAccountLocks
261             => f.write_str("Transaction locked too many accounts"),
262            Self::AddressLookupTableNotFound
263             => f.write_str("Transaction loads an address table account that doesn't exist"),
264            Self::InvalidAddressLookupTableOwner
265             => f.write_str("Transaction loads an address table account with an invalid owner"),
266            Self::InvalidAddressLookupTableData
267             => f.write_str("Transaction loads an address table account with invalid data"),
268            Self::InvalidAddressLookupTableIndex
269             => f.write_str("Transaction address table lookup uses an invalid index"),
270            Self::InvalidRentPayingAccount
271             => f.write_str("Transaction leaves an account with a lower balance than rent-exempt minimum"),
272            Self::WouldExceedMaxVoteCostLimit
273             => f.write_str("Transaction would exceed max Vote Cost Limit"),
274            Self::WouldExceedAccountDataTotalLimit
275             => f.write_str("Transaction would exceed total account data limit"),
276            Self::DuplicateInstruction(idx) =>  write!(f, "Transaction contains a duplicate instruction ({idx}) that is not allowed"),
277            Self::InsufficientFundsForRent {
278                account_index
279            } =>  write!(f,"Transaction results in an account ({account_index}) with insufficient funds for rent"),
280            Self::MaxLoadedAccountsDataSizeExceeded
281             => f.write_str("Transaction exceeded max loaded accounts data size cap"),
282            Self::InvalidLoadedAccountsDataSizeLimit
283             => f.write_str("LoadedAccountsDataSizeLimit set for transaction must be greater than 0."),
284            Self::ResanitizationNeeded
285             => f.write_str("ResanitizationNeeded"),
286            Self::ProgramExecutionTemporarilyRestricted {
287                account_index
288            } =>  write!(f,"Execution of the program referenced by account at index {account_index} is temporarily restricted."),
289            Self::UnbalancedTransaction
290             => f.write_str("Sum of account balances before and after transaction do not match"),
291            Self::ProgramCacheHitMaxLimit
292             => f.write_str("Program cache hit max limit"),
293            Self::CommitCancelled
294             => f.write_str("CommitCancelled"),
295            Self::BailOut
296             => f.write_str("BailOut"),
297        }
298    }
299}
300
301impl From<SanitizeError> for TransactionError {
302    fn from(_: SanitizeError) -> Self {
303        Self::SanitizeFailure
304    }
305}
306
307#[cfg(not(target_os = "solana"))]
308impl From<SanitizeMessageError> for TransactionError {
309    fn from(err: SanitizeMessageError) -> Self {
310        match err {
311            SanitizeMessageError::AddressLoaderError(err) => Self::from(err),
312            _ => Self::SanitizeFailure,
313        }
314    }
315}
316
317#[cfg(not(target_os = "solana"))]
318#[derive(Debug, PartialEq, Eq, Clone)]
319#[cfg_attr(test, derive(strum_macros::EnumIter, Default))]
320#[non_exhaustive]
321pub enum AddressLoaderError {
322    /// Address loading from lookup tables is disabled
323    #[cfg_attr(test, default)]
324    Disabled,
325
326    /// Failed to load slot hashes sysvar
327    SlotHashesSysvarNotFound,
328
329    /// Attempted to lookup addresses from a table that does not exist
330    LookupTableAccountNotFound,
331
332    /// Attempted to lookup addresses from an account owned by the wrong program
333    InvalidAccountOwner,
334
335    /// Attempted to lookup addresses from an invalid account
336    InvalidAccountData,
337
338    /// Address lookup contains an invalid index
339    InvalidLookupIndex,
340}
341
342#[cfg(not(target_os = "solana"))]
343impl AddressLoaderError {
344    pub const VARIANTS: [Self; 6] = [
345        Self::Disabled,
346        Self::SlotHashesSysvarNotFound,
347        Self::LookupTableAccountNotFound,
348        Self::InvalidAccountOwner,
349        Self::InvalidAccountData,
350        Self::InvalidLookupIndex,
351    ];
352}
353
354#[cfg(not(target_os = "solana"))]
355impl core::error::Error for AddressLoaderError {}
356
357#[cfg(not(target_os = "solana"))]
358impl fmt::Display for AddressLoaderError {
359    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
360        match self {
361            Self::Disabled => f.write_str("Address loading from lookup tables is disabled"),
362            Self::SlotHashesSysvarNotFound => f.write_str("Failed to load slot hashes sysvar"),
363            Self::LookupTableAccountNotFound => {
364                f.write_str("Attempted to lookup addresses from a table that does not exist")
365            }
366            Self::InvalidAccountOwner => f.write_str(
367                "Attempted to lookup addresses from an account owned by the wrong program",
368            ),
369            Self::InvalidAccountData => {
370                f.write_str("Attempted to lookup addresses from an invalid account")
371            }
372            Self::InvalidLookupIndex => f.write_str("Address lookup contains an invalid index"),
373        }
374    }
375}
376
377#[cfg(not(target_os = "solana"))]
378impl From<AddressLoaderError> for TransactionError {
379    fn from(err: AddressLoaderError) -> Self {
380        match err {
381            AddressLoaderError::Disabled => Self::UnsupportedVersion,
382            AddressLoaderError::SlotHashesSysvarNotFound => Self::AccountNotFound,
383            AddressLoaderError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound,
384            AddressLoaderError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner,
385            AddressLoaderError::InvalidAccountData => Self::InvalidAddressLookupTableData,
386            AddressLoaderError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex,
387        }
388    }
389}
390
391#[cfg(not(target_os = "solana"))]
392#[derive(PartialEq, Debug, Eq, Clone)]
393#[cfg_attr(test, derive(strum_macros::EnumIter))]
394#[non_exhaustive]
395pub enum SanitizeMessageError {
396    IndexOutOfBounds,
397    ValueOutOfBounds,
398    InvalidValue,
399    AddressLoaderError(AddressLoaderError),
400}
401
402#[cfg(not(target_os = "solana"))]
403impl SanitizeMessageError {
404    pub const VARIANTS: [Self; 4] = [
405        Self::IndexOutOfBounds,
406        Self::ValueOutOfBounds,
407        Self::InvalidValue,
408        Self::AddressLoaderError(AddressLoaderError::Disabled),
409    ];
410}
411
412#[cfg(not(target_os = "solana"))]
413impl core::error::Error for SanitizeMessageError {
414    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
415        match self {
416            Self::IndexOutOfBounds => None,
417            Self::ValueOutOfBounds => None,
418            Self::InvalidValue => None,
419            Self::AddressLoaderError(e) => Some(e),
420        }
421    }
422}
423
424#[cfg(not(target_os = "solana"))]
425impl fmt::Display for SanitizeMessageError {
426    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
427        match self {
428            Self::IndexOutOfBounds => f.write_str("index out of bounds"),
429            Self::ValueOutOfBounds => f.write_str("value out of bounds"),
430            Self::InvalidValue => f.write_str("invalid value"),
431            Self::AddressLoaderError(e) => {
432                write!(f, "{e}")
433            }
434        }
435    }
436}
437#[cfg(not(target_os = "solana"))]
438impl From<AddressLoaderError> for SanitizeMessageError {
439    fn from(source: AddressLoaderError) -> Self {
440        SanitizeMessageError::AddressLoaderError(source)
441    }
442}
443
444#[cfg(not(target_os = "solana"))]
445impl From<SanitizeError> for SanitizeMessageError {
446    fn from(err: SanitizeError) -> Self {
447        match err {
448            SanitizeError::IndexOutOfBounds => Self::IndexOutOfBounds,
449            SanitizeError::ValueOutOfBounds => Self::ValueOutOfBounds,
450            SanitizeError::InvalidValue => Self::InvalidValue,
451        }
452    }
453}
454
455#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
456#[derive(Debug)]
457pub enum TransportError {
458    IoError(std::io::Error),
459    TransactionError(TransactionError),
460    Custom(std::string::String),
461}
462
463#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
464impl core::error::Error for TransportError {
465    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
466        match self {
467            TransportError::IoError(e) => Some(e),
468            TransportError::TransactionError(e) => Some(e),
469            TransportError::Custom(_) => None,
470        }
471    }
472}
473
474#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
475impl fmt::Display for TransportError {
476    fn fmt(&self, f: &mut fmt::Formatter) -> ::core::fmt::Result {
477        match self {
478            Self::IoError(e) => f.write_fmt(format_args!("transport io error: {e}")),
479            Self::TransactionError(e) => {
480                f.write_fmt(format_args!("transport transaction error: {e}"))
481            }
482            Self::Custom(s) => f.write_fmt(format_args!("transport custom error: {s}")),
483        }
484    }
485}
486
487#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
488impl From<std::io::Error> for TransportError {
489    fn from(e: std::io::Error) -> Self {
490        TransportError::IoError(e)
491    }
492}
493
494#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
495impl From<TransactionError> for TransportError {
496    fn from(e: TransactionError) -> Self {
497        TransportError::TransactionError(e)
498    }
499}
500
501#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
502impl TransportError {
503    pub fn unwrap(&self) -> TransactionError {
504        if let TransportError::TransactionError(err) = self {
505            err.clone()
506        } else {
507            panic!("unexpected transport error")
508        }
509    }
510}
511
512#[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
513pub type TransportResult<T> = std::result::Result<T, TransportError>;
514
515#[cfg(test)]
516mod tests {
517    use {
518        super::{AddressLoaderError, SanitizeMessageError, TransactionError},
519        strum::IntoEnumIterator,
520    };
521
522    #[test]
523    fn test_transaction_error_variants_exhaustive() {
524        for variant in TransactionError::iter() {
525            assert!(TransactionError::VARIANTS.contains(&variant));
526        }
527    }
528
529    #[test]
530    fn test_address_loader_error_variants_exhaustive() {
531        for variant in AddressLoaderError::iter() {
532            assert!(AddressLoaderError::VARIANTS.contains(&variant));
533        }
534    }
535
536    #[test]
537    fn test_sanitize_message_error_variants_exhaustive() {
538        for variant in SanitizeMessageError::iter() {
539            assert!(SanitizeMessageError::VARIANTS.contains(&variant));
540        }
541    }
542}