Skip to main content

midnight_ledger/
error.rs

1// This file is part of midnight-ledger.
2// Copyright (C) 2025 Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14use crate::dust::{DustGenerationInfo, DustNullifier, DustRegistration, DustSpend};
15use crate::error::coin::UserAddress;
16use crate::structure::MAX_SUPPLY;
17use crate::structure::{ClaimKind, ContractOperationVersion, Utxo, UtxoOutput, UtxoSpend};
18use base_crypto::cost_model::CostDuration;
19use base_crypto::fab::{Alignment, Value};
20use base_crypto::hash::HashOutput;
21use base_crypto::signatures::VerifyingKey;
22use base_crypto::time::Timestamp;
23use coin_structure::coin::{self, Commitment, Nullifier, PublicAddress, TokenType};
24use coin_structure::contract::ContractAddress;
25use derive_where::derive_where;
26use onchain_runtime::context::Effects;
27use onchain_runtime::error::TranscriptRejected;
28use onchain_runtime::state::EntryPointBuf;
29use onchain_runtime::transcript::Transcript;
30use std::error::Error;
31use std::fmt::{self, Display, Formatter};
32use storage::db::DB;
33use transient_crypto::curve::EmbeddedGroupAffine;
34use transient_crypto::curve::Fr;
35use transient_crypto::merkle_tree::InvalidUpdate;
36use transient_crypto::proofs::{KeyLocation, ProvingError, VerifyingError};
37use zswap::error::MalformedOffer;
38use zswap::{Input, Output};
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum InvariantViolation {
42    NightBalance(u128),
43}
44
45impl Display for InvariantViolation {
46    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47        match self {
48            InvariantViolation::NightBalance(x) => {
49                write!(
50                    f,
51                    "total supply of night is {MAX_SUPPLY}, but the transaction would imply a total of: {x}"
52                )
53            }
54        }
55    }
56}
57
58impl From<InvariantViolation> for SystemTransactionError {
59    fn from(e: InvariantViolation) -> Self {
60        SystemTransactionError::InvariantViolation(e)
61    }
62}
63
64impl<D: DB> From<InvariantViolation> for TransactionInvalid<D> {
65    fn from(e: InvariantViolation) -> Self {
66        TransactionInvalid::InvariantViolation(e)
67    }
68}
69
70#[derive(Debug)]
71pub enum SystemTransactionError {
72    IllegalPayout {
73        claimed_amount: Option<u128>,
74        supply: u128,
75        bridged_amount: Option<u128>,
76        locked: u128,
77    },
78    InsufficientTreasuryFunds {
79        requested: Option<u128>,
80        actual: u128,
81        token_type: TokenType,
82    },
83    CommitmentAlreadyPresent(Commitment),
84    ReplayProtectionFailure(TransactionApplicationError),
85    IllegalReserveDistribution {
86        distributed_amount: u128,
87        reserve_supply: u128,
88    },
89    GenerationInfoAlreadyPresent(GenerationInfoAlreadyPresentError),
90    InvalidBasisPoints(u32),
91    InvariantViolation(InvariantViolation),
92    TreasuryDisabled,
93}
94
95impl Display for SystemTransactionError {
96    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
97        match self {
98            SystemTransactionError::IllegalPayout {
99                claimed_amount: Some(amount),
100                supply,
101                bridged_amount: None,
102                locked: _,
103            } => write!(
104                f,
105                "illegal payout of {amount} native tokens, exceeding remaining supply of {supply}"
106            ),
107            SystemTransactionError::IllegalPayout {
108                claimed_amount: None,
109                supply: _,
110                bridged_amount: Some(amount),
111                locked,
112            } => write!(
113                f,
114                "illegal bridge of {amount} native tokens, exceeding remaining pool of {locked}"
115            ),
116            SystemTransactionError::IllegalPayout {
117                claimed_amount: None,
118                supply,
119                bridged_amount: None,
120                locked,
121            } => write!(
122                f,
123                "illegal bridge or payout of > 2^128 native tokens, exceeding remaining reserve pool of {supply} and/or bridge pool of {locked}"
124            ),
125            SystemTransactionError::IllegalPayout {
126                claimed_amount: Some(amount_claimed),
127                supply,
128                bridged_amount: Some(amount_bridged),
129                locked,
130            } => write!(
131                f,
132                "illegal payout of {amount_claimed} native tokens, exceeding remaining supply of {supply}; illegal bridge of {amount_bridged} native tokens, exceeding remaining pool of {locked}"
133            ),
134            SystemTransactionError::InsufficientTreasuryFunds {
135                requested: Some(requested),
136                actual,
137                token_type,
138            } => write!(
139                f,
140                "insufficient funds in the treasury; {requested} of token {token_type:?} requested, but only {actual} available"
141            ),
142            SystemTransactionError::InsufficientTreasuryFunds {
143                requested: None,
144                actual,
145                token_type,
146            } => write!(
147                f,
148                "insufficient funds in the treasury; > 2^128 of token {token_type:?} requested, but only {actual} available"
149            ),
150            SystemTransactionError::CommitmentAlreadyPresent(cm) => {
151                write!(f, "faerie-gold attempt with commitment {:?}", cm)
152            }
153            SystemTransactionError::ReplayProtectionFailure(e) => {
154                write!(f, "Replay protection violation: {e}")
155            }
156            SystemTransactionError::IllegalReserveDistribution {
157                distributed_amount,
158                reserve_supply,
159            } => {
160                write!(
161                    f,
162                    "illegal distribution of {distributed_amount} reserve tokens, exceeding remaining supply of {reserve_supply}"
163                )
164            }
165            SystemTransactionError::GenerationInfoAlreadyPresent(e) => e.fmt(f),
166            SystemTransactionError::InvalidBasisPoints(bp) => {
167                write!(
168                    f,
169                    "cardano_to_midnight_bridge_fee_basis_points must be less than 10_000, but was set to: {bp}"
170                )
171            }
172            SystemTransactionError::InvariantViolation(e) => e.fmt(f),
173            SystemTransactionError::TreasuryDisabled => write!(
174                f,
175                "invalid attempt to access treasury; the treasury is disabled until governance for it has been agreed"
176            ),
177        }
178    }
179}
180
181impl Error for SystemTransactionError {
182    fn cause(&self) -> Option<&dyn Error> {
183        match self {
184            SystemTransactionError::GenerationInfoAlreadyPresent(e) => Some(e),
185            _ => None,
186        }
187    }
188}
189
190impl From<GenerationInfoAlreadyPresentError> for SystemTransactionError {
191    fn from(err: GenerationInfoAlreadyPresentError) -> SystemTransactionError {
192        SystemTransactionError::GenerationInfoAlreadyPresent(err)
193    }
194}
195
196#[derive(Debug, Clone)]
197#[non_exhaustive]
198pub enum TransactionInvalid<D: DB> {
199    EffectsMismatch {
200        declared: Box<Effects<D>>,
201        actual: Box<Effects<D>>,
202    },
203    ContractAlreadyDeployed(ContractAddress),
204    ContractNotPresent(ContractAddress),
205    Zswap(zswap::error::TransactionInvalid),
206    Transcript(onchain_runtime::error::TranscriptRejected<D>),
207    InsufficientClaimable {
208        requested: u128,
209        claimable: u128,
210        claimant: UserAddress,
211        kind: ClaimKind,
212    },
213    VerifierKeyNotFound(EntryPointBuf, ContractOperationVersion),
214    VerifierKeyAlreadyPresent(EntryPointBuf, ContractOperationVersion),
215    ReplayCounterMismatch(ContractAddress),
216    ReplayProtectionViolation(TransactionApplicationError),
217    BalanceCheckOutOfBounds {
218        token_type: TokenType,
219        current_balance: u128,
220        operation_value: u128,
221        operation: BalanceOperation,
222    },
223    InputNotInUtxos(Box<Utxo>),
224    DustDoubleSpend(DustNullifier),
225    DustDeregistrationNotRegistered(UserAddress),
226    GenerationInfoAlreadyPresent(GenerationInfoAlreadyPresentError),
227    InvariantViolation(InvariantViolation),
228    RewardTooSmall {
229        claimed: u128,
230        minimum: u128,
231    },
232    DivideByZero,
233}
234
235impl<D: DB> Display for TransactionInvalid<D> {
236    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
237        use TransactionInvalid::*;
238        match self {
239            EffectsMismatch { declared, actual } => write!(
240                formatter,
241                "declared effects {declared:?} don't match computed effects {actual:?}"
242            ),
243            ContractNotPresent(addr) => {
244                write!(formatter, "call to non-existant contract {:?}", addr)
245            }
246            ContractAlreadyDeployed(addr) => {
247                write!(formatter, "contract already deployed {:?}", addr)
248            }
249            Zswap(err) => err.fmt(formatter),
250            Transcript(err) => err.fmt(formatter),
251            InsufficientClaimable {
252                requested,
253                claimable,
254                claimant,
255                kind,
256            } => {
257                write!(
258                    formatter,
259                    "insufficient funds for requested {kind} claim: {requested} tokens of Night requested by {claimant:?}; only {claimable} available"
260                )
261            }
262            VerifierKeyNotFound(ep, ver) => write!(
263                formatter,
264                "the verifier key for {ep:?} version {ver:?} was not present"
265            ),
266            VerifierKeyAlreadyPresent(ep, ver) => write!(
267                formatter,
268                "the verifier key for {ep:?} version {ver:?} was already present"
269            ),
270            ReplayCounterMismatch(addr) => write!(
271                formatter,
272                "the signed counter for {addr:?} did not match the expected one; likely replay attack"
273            ),
274            ReplayProtectionViolation(err) => {
275                write!(formatter, "replay protection has been violated: {err:?}")
276            }
277            BalanceCheckOutOfBounds {
278                token_type,
279                current_balance,
280                operation_value,
281                operation,
282            } => {
283                let (reason_str, to_from, op_str) = match operation {
284                    BalanceOperation::Addition => ("overflow", "to", "add"),
285                    BalanceOperation::Subtraction => ("underflow", "from", "subtract"),
286                };
287                write!(
288                    formatter,
289                    "Balance check failed: couldn't {op_str} {operation_value} {to_from} {current_balance} for token {token_type:?}: {reason_str}"
290                )
291            }
292            InputNotInUtxos(utxo) => write!(formatter, "input missing from utxos set: {:?}", utxo),
293            DustDoubleSpend(nullifier) => write!(
294                formatter,
295                "attempted to double spend Dust UTXO with nullifier {nullifier:?}"
296            ),
297            DustDeregistrationNotRegistered(addr) => write!(
298                formatter,
299                "attempted to deregister the Dust address associated with the Night address {addr:?}, but no such registration exists"
300            ),
301            RewardTooSmall { claimed, minimum } => write!(
302                formatter,
303                "claimed reward ({claimed} STARs) below payout threshold ({minimum} STARs)"
304            ),
305            GenerationInfoAlreadyPresent(e) => e.fmt(formatter),
306            InvariantViolation(e) => e.fmt(formatter),
307            DivideByZero => write!(formatter, "attempted to divide by zero"),
308        }
309    }
310}
311
312impl<D: DB> Error for TransactionInvalid<D> {
313    fn cause(&self) -> Option<&dyn Error> {
314        match self {
315            TransactionInvalid::Zswap(e) => Some(e),
316            TransactionInvalid::Transcript(e) => Some(e),
317            TransactionInvalid::ReplayProtectionViolation(e) => Some(e),
318            TransactionInvalid::GenerationInfoAlreadyPresent(e) => Some(e),
319            _ => None,
320        }
321    }
322}
323
324impl<D: DB> From<zswap::error::TransactionInvalid> for TransactionInvalid<D> {
325    fn from(err: zswap::error::TransactionInvalid) -> TransactionInvalid<D> {
326        TransactionInvalid::Zswap(err)
327    }
328}
329
330impl<D: DB> From<onchain_runtime::error::TranscriptRejected<D>> for TransactionInvalid<D> {
331    fn from(err: onchain_runtime::error::TranscriptRejected<D>) -> TransactionInvalid<D> {
332        TransactionInvalid::Transcript(err)
333    }
334}
335
336impl<D: DB> From<GenerationInfoAlreadyPresentError> for TransactionInvalid<D> {
337    fn from(err: GenerationInfoAlreadyPresentError) -> TransactionInvalid<D> {
338        TransactionInvalid::GenerationInfoAlreadyPresent(err)
339    }
340}
341
342#[derive(Debug, PartialEq, Eq)]
343pub enum FeeCalculationError {
344    OutsideTimeToDismiss {
345        time_to_dismiss: CostDuration,
346        allowed_time_to_dismiss: CostDuration,
347        size: u64,
348    },
349    BlockLimitExceeded,
350}
351
352impl Display for FeeCalculationError {
353    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
354        match self {
355            FeeCalculationError::BlockLimitExceeded => write!(
356                formatter,
357                "exceeded block limit in transaction fee computation"
358            ),
359            FeeCalculationError::OutsideTimeToDismiss {
360                time_to_dismiss,
361                allowed_time_to_dismiss,
362                size,
363            } => write!(
364                formatter,
365                "exceeded the maximum time to dismiss for transaction size; this transaction would take {time_to_dismiss:?} to dismiss, but given its size of {size} bytes, it may take at most {allowed_time_to_dismiss:?}"
366            ),
367        }
368    }
369}
370
371impl Error for FeeCalculationError {}
372
373#[derive(Debug)]
374#[non_exhaustive]
375pub enum MalformedContractDeploy {
376    NonZeroBalance(std::collections::BTreeMap<TokenType, u128>),
377    IncorrectChargedState,
378}
379
380impl Display for MalformedContractDeploy {
381    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
382        use MalformedContractDeploy::*;
383        match self {
384            NonZeroBalance(balance) => {
385                let filtered: std::collections::BTreeMap<_, _> =
386                    balance.iter().filter(|&(_, value)| *value > 0).collect();
387                write!(
388                    formatter,
389                    "contract deployment was sent with the non-zero balance members: {:?}",
390                    filtered
391                )
392            }
393            IncorrectChargedState => write!(
394                formatter,
395                "contract deployment contained an incorrectly computed map of charged keys"
396            ),
397        }
398    }
399}
400
401impl Error for MalformedContractDeploy {}
402
403#[derive(Debug)]
404#[non_exhaustive]
405#[allow(clippy::large_enum_variant)]
406pub enum MalformedTransaction<D: DB> {
407    InvalidNetworkId {
408        expected: String,
409        found: String,
410    },
411    VerifierKeyNotSet {
412        address: ContractAddress,
413        operation: EntryPointBuf,
414    },
415    TransactionTooLarge {
416        tx_size: usize,
417        limit: u64,
418    },
419    VerifierKeyTooLarge {
420        actual: u64,
421        limit: u64,
422    },
423    VerifierKeyNotPresent {
424        address: ContractAddress,
425        operation: EntryPointBuf,
426    },
427    ContractNotPresent(ContractAddress),
428    InvalidProof(VerifyingError),
429    BindingCommitmentOpeningInvalid,
430    NotNormalized,
431    FallibleWithoutCheckpoint,
432    IllegallyDeclaredGuaranteed,
433    ClaimReceiveFailed(coin::Commitment),
434    ClaimSpendFailed(coin::Commitment),
435    ClaimNullifierFailed(coin::Nullifier),
436    InvalidSchnorrProof,
437    UnclaimedCoinCom(coin::Commitment),
438    UnclaimedNullifier(coin::Nullifier),
439    Unbalanced(TokenType, i128), // Think this is unused now?
440    Zswap(zswap::error::MalformedOffer),
441    BuiltinDecode(base_crypto::fab::InvalidBuiltinDecode),
442    FeeCalculation(FeeCalculationError),
443    CantMergeTypes,
444    ClaimOverflow,
445    ClaimCoinMismatch,
446    KeyNotInCommittee {
447        address: ContractAddress,
448        key_id: usize,
449    },
450    InvalidCommitteeSignature {
451        address: ContractAddress,
452        key_id: usize,
453    },
454    InvalidDustRegistrationSignature {
455        registration: Box<DustRegistration<(), D>>,
456    },
457    InvalidDustSpendProof {
458        declared_time: Timestamp,
459        dust_spend: Box<DustSpend<(), D>>,
460    },
461    OutOfDustValidityWindow {
462        dust_ctime: Timestamp,
463        validity_start: Timestamp,
464        validity_end: Timestamp,
465    },
466    MultipleDustRegistrationsForKey {
467        key: VerifyingKey,
468    },
469    InsufficientDustForRegistrationFee {
470        registration: Box<DustRegistration<(), D>>,
471        available_dust: u128,
472    },
473    ThresholdMissed {
474        address: ContractAddress,
475        signatures: usize,
476        threshold: usize,
477    },
478    TooManyZswapEntries,
479    MalformedContractDeploy(MalformedContractDeploy),
480    IntentSignatureVerificationFailure,
481    IntentSignatureKeyMismatch,
482    IntentSegmentIdCollision(u16),
483    IntentAtGuaranteedSegmentId,
484    UnsupportedProofVersion {
485        op_version: String,
486    },
487    GuaranteedTranscriptVersion {
488        op_version: String,
489    },
490    FallibleTranscriptVersion {
491        op_version: String,
492    },
493    TransactionApplicationError(TransactionApplicationError),
494    BalanceCheckOutOfBounds {
495        token_type: TokenType,
496        segment: u16,
497        current_balance: i128,
498        operation_value: i128,
499        operation: BalanceOperation,
500    },
501    BalanceCheckConversionFailure {
502        token_type: TokenType,
503        segment: u16,
504        operation_value: u128,
505    },
506    PedersenCheckFailure {
507        expected: Box<EmbeddedGroupAffine>,
508        calculated: Box<EmbeddedGroupAffine>,
509    },
510    BalanceCheckOverspend {
511        token_type: TokenType,
512        segment: u16,
513        overspent_value: i128,
514    },
515    EffectsCheckFailure(EffectsCheckError),
516    DisjointCheckFailure(DisjointCheckError<D>),
517    SequencingCheckFailure(SequencingCheckError),
518    InputsNotSorted(Vec<UtxoSpend>),
519    OutputsNotSorted(Vec<UtxoOutput>),
520    DuplicateInputs(Vec<UtxoSpend>),
521    InputsSignaturesLengthMismatch {
522        inputs: Vec<UtxoSpend>,
523        erased_signatures: Vec<()>,
524    },
525}
526
527#[derive(Clone, Debug)]
528pub enum SequencingCheckError {
529    CallSequencingViolation {
530        call_predecessor: u32,
531        call_successor: u32,
532        call_predecessor_address: ContractAddress,
533        call_successor_address: ContractAddress,
534    },
535    SequencingCorrelationViolation {
536        address_1: ContractAddress,
537        address_2: ContractAddress,
538        call_position_1: u32,
539        call_position_2: u32,
540    },
541    GuaranteedInFallibleContextViolation {
542        caller: u32,
543        callee: u32,
544        caller_address: ContractAddress,
545        callee_address: ContractAddress,
546    },
547    FallibleInGuaranteedContextViolation {
548        caller: u32,
549        callee: u32,
550        caller_address: ContractAddress,
551        callee_address: ContractAddress,
552    },
553    CausalityConstraintViolation {
554        call_predecessor: u32,
555        call_successor: u32,
556        call_predecessor_address: ContractAddress,
557        call_successor_address: ContractAddress,
558        segment_id_predecessor: u16,
559        segment_id_successor: u16,
560    },
561    CallHasEmptyTranscripts {
562        segment_id: u16,
563        addr: ContractAddress,
564        call_index: u32,
565    },
566}
567
568impl std::fmt::Display for SequencingCheckError {
569    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        match self {
571            SequencingCheckError::CallSequencingViolation {
572                call_predecessor: call_position_x,
573                call_successor: call_position_succ_x,
574                call_predecessor_address: call_x_address,
575                call_successor_address: call_succ_x_address,
576            } => {
577                write!(
578                    formatter,
579                    "sequencing violation: Expected call position {call_position_x} (for call at address: {call_x_address:?}) < {call_position_succ_x} (for call at at address: {call_succ_x_address:?}), but this ordering constraint was violated"
580                )
581            }
582            SequencingCheckError::SequencingCorrelationViolation {
583                address_1,
584                address_2,
585                call_position_1,
586                call_position_2,
587            } => {
588                write!(
589                    formatter,
590                    "sequencing correlation violation: The order of addresses ({address_1:?} vs {address_2:?}) does not match the order of call positions ({call_position_1} vs {call_position_2}); expected both orderings to be consistent"
591                )
592            }
593            SequencingCheckError::GuaranteedInFallibleContextViolation {
594                caller: parent_call_id,
595                callee: child_call_id,
596                caller_address: parent_address,
597                callee_address: child_address,
598            } => {
599                write!(
600                    formatter,
601                    "fallible context violation: Call at position {child_call_id} (address: {child_address:?}) contains a guaranteed transcript but is called from a fallible context in call at position {parent_call_id} (address: {parent_address:?})"
602                )
603            }
604            SequencingCheckError::FallibleInGuaranteedContextViolation {
605                caller: parent_call_id,
606                callee: child_call_id,
607                caller_address: parent_address,
608                callee_address: child_address,
609            } => {
610                write!(
611                    formatter,
612                    "guaranteed context violation: Call at position {child_call_id} (address: {child_address:?}) contains a fallible transcript but is called from a guaranteed context in call at position {parent_call_id} (address: {parent_address:?})"
613                )
614            }
615            SequencingCheckError::CausalityConstraintViolation {
616                call_predecessor,
617                call_successor,
618                call_predecessor_address,
619                call_successor_address,
620                segment_id_predecessor,
621                segment_id_successor,
622            } => {
623                write!(
624                    formatter,
625                    "causality violation: Calls must be arranged to ensure causality constraints are met, but found call at segment_id: {segment_id_predecessor} (address: {call_predecessor_address:?}, position: {call_predecessor}) with fallible transcript and call at segment_id: {segment_id_successor} (address: {call_successor_address:?}, position: {call_successor}) with guaranteed transcript"
626                )
627            }
628            SequencingCheckError::CallHasEmptyTranscripts {
629                segment_id,
630                addr,
631                call_index,
632            } => {
633                write!(
634                    formatter,
635                    "call composition violation: Calls cannot have empty guaranteed and fallible transcripts, but found violating call at segment_id: {segment_id} (address: {addr:?}, position: {call_index})"
636                )
637            }
638        }
639    }
640}
641
642impl Error for SequencingCheckError {}
643
644#[derive_where(Clone, Debug)]
645pub enum DisjointCheckError<D: DB> {
646    ShieldedInputsDisjointFailure {
647        shielded_inputs: std::collections::BTreeSet<Input<(), D>>,
648        transient_inputs: std::collections::BTreeSet<Input<(), D>>,
649    },
650    ShieldedOutputsDisjointFailure {
651        shielded_outputs: std::collections::BTreeSet<Output<(), D>>,
652        transient_outputs: std::collections::BTreeSet<Output<(), D>>,
653    },
654    UnshieldedInputsDisjointFailure {
655        unshielded_inputs: std::collections::BTreeSet<UtxoSpend>,
656        offer_inputs: std::collections::BTreeSet<UtxoSpend>,
657    },
658}
659
660impl<D: DB> std::fmt::Display for DisjointCheckError<D> {
661    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
662        match self {
663            DisjointCheckError::ShieldedInputsDisjointFailure {
664                shielded_inputs,
665                transient_inputs,
666            } => {
667                write!(
668                    formatter,
669                    "shielded_inputs and transient_inputs must be disjoint. shielded_inputs: {shielded_inputs:?}, transient_inputs: {transient_inputs:?}"
670                )
671            }
672            DisjointCheckError::ShieldedOutputsDisjointFailure {
673                shielded_outputs,
674                transient_outputs,
675            } => {
676                write!(
677                    formatter,
678                    "shielded_outputs and transient_outputs must be disjoint. shielded_outputs: {shielded_outputs:?}, transient_outputs: {transient_outputs:?}"
679                )
680            }
681            DisjointCheckError::UnshieldedInputsDisjointFailure {
682                unshielded_inputs,
683                offer_inputs,
684            } => {
685                write!(
686                    formatter,
687                    "unshielded_inputs and offer_inputs must be disjoint. unshielded_inputs: {unshielded_inputs:?}, offer_inputs: {offer_inputs:?}"
688                )
689            }
690        }
691    }
692}
693
694impl<D: DB> Error for DisjointCheckError<D> {}
695
696#[derive(Clone, Debug)]
697pub struct SubsetCheckFailure<T> {
698    pub superset: Vec<T>,
699    pub subset: Vec<T>,
700}
701
702#[derive(Clone, Debug)]
703pub enum EffectsCheckError {
704    RealCallsSubsetCheckFailure(SubsetCheckFailure<(u16, (ContractAddress, HashOutput, Fr))>),
705    AllCommitmentsSubsetCheckFailure(SubsetCheckFailure<(u16, Commitment)>),
706    #[allow(clippy::type_complexity)]
707    RealUnshieldedSpendsSubsetCheckFailure(
708        SubsetCheckFailure<((u16, bool), ((TokenType, PublicAddress), u128))>,
709    ),
710    ClaimedUnshieldedSpendsUniquenessFailure(Vec<((u16, Commitment), usize)>),
711    #[allow(clippy::type_complexity)]
712    ClaimedCallsUniquenessFailure(Vec<((u16, (ContractAddress, HashOutput, Fr)), usize)>),
713    NullifiersNEClaimedNullifiers {
714        nullifiers: Vec<(u16, Nullifier, ContractAddress)>,
715        claimed_nullifiers: Vec<(u16, Nullifier, ContractAddress)>,
716    },
717    CommitmentsNEClaimedShieldedReceives {
718        commitments: Vec<(u16, Commitment, ContractAddress)>,
719        claimed_shielded_receives: Vec<(u16, Commitment, ContractAddress)>,
720    },
721}
722
723impl std::fmt::Display for EffectsCheckError {
724    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
725        match self {
726            EffectsCheckError::RealCallsSubsetCheckFailure(e) => write!(
727                formatter,
728                "claimed_calls is not a subset of real_calls. {}",
729                e
730            ),
731            EffectsCheckError::AllCommitmentsSubsetCheckFailure(e) => write!(
732                formatter,
733                "claimed_shielded_spends is not a subset of all_commitments. \n {}",
734                e
735            ),
736            EffectsCheckError::RealUnshieldedSpendsSubsetCheckFailure(e) => write!(
737                formatter,
738                "claimed_unshielded_spends is not a subset of real_unshielded_spends. \n {:?}",
739                e
740            ),
741            EffectsCheckError::ClaimedUnshieldedSpendsUniquenessFailure(items) => {
742                write!(formatter, "non-unique spends found: {:?}", items)
743            }
744            EffectsCheckError::ClaimedCallsUniquenessFailure(items) => {
745                write!(formatter, "non-unique claimed calls found: {:?}", items)
746            }
747            EffectsCheckError::NullifiersNEClaimedNullifiers {
748                nullifiers,
749                claimed_nullifiers,
750            } => write!(
751                formatter,
752                "all contract-associated nullifiers must be claimed by exactly one instance of the same contract in the same segment. nullifiers: {nullifiers:?}, claimed_nullifiers: {claimed_nullifiers:?}",
753            ),
754            EffectsCheckError::CommitmentsNEClaimedShieldedReceives {
755                commitments,
756                claimed_shielded_receives,
757            } => write!(
758                formatter,
759                "all contract-associated commitments must be claimed by exactly one instance of the same contract in the same segment. commitments: {commitments:?}, claimed_shielded_receives: {claimed_shielded_receives:?}"
760            ),
761        }
762    }
763}
764
765impl Error for EffectsCheckError {}
766
767impl<T: std::fmt::Debug> Display for SubsetCheckFailure<T> {
768    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
769        let SubsetCheckFailure { superset, subset } = self;
770        write!(formatter, "subset: {subset:?} \n superset: {superset:?}")
771    }
772}
773
774#[derive(Clone, Copy, Debug)]
775pub enum BalanceOperation {
776    Addition,
777    Subtraction,
778}
779
780#[derive(Clone, Copy, Debug)]
781pub enum BalanceCheckFailureReason {
782    TypeConversionFailure,
783    OutOfBounds,
784}
785
786fn sanitize_network_id(network_id: &str) -> String {
787    let char_not_permitted = |ch: char| !ch.is_ascii_alphanumeric() && ch != '-';
788    network_id.replace(char_not_permitted, "�")
789}
790
791impl<D: DB> Display for MalformedTransaction<D> {
792    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
793        use MalformedTransaction::*;
794        match self {
795            InvalidNetworkId { expected, found } => {
796                let expected = sanitize_network_id(expected);
797                let found = sanitize_network_id(found);
798                write!(
799                    formatter,
800                    "invalid network ID - expect '{expected}' found '{found}'"
801                )
802            }
803            ContractNotPresent(addr) => {
804                write!(formatter, "call to non-existant contract {:?}", addr)
805            }
806            VerifierKeyNotPresent { address, operation } => write!(
807                formatter,
808                "operation {address:?}/{operation:?} does not have a verifier key",
809            ),
810            InvalidProof(err) => {
811                write!(formatter, "failed to verify proof: ")?;
812                err.fmt(formatter)
813            }
814            TransactionTooLarge { tx_size, limit } => write!(
815                formatter,
816                "transaction too large (size: {tx_size}, limit: {limit})"
817            ),
818            VerifierKeyTooLarge { actual, limit } => write!(
819                formatter,
820                "verifier key for operation too large for deserialization (size: {actual}, limit: {limit})"
821            ),
822            VerifierKeyNotSet { address, operation } => write!(
823                formatter,
824                "tried to deploy {:?}/{:?} without a verifier key",
825                address, operation
826            ),
827            BindingCommitmentOpeningInvalid => write!(
828                formatter,
829                "transaction binding commitment was incorrectly opened"
830            ),
831            IllegallyDeclaredGuaranteed => write!(
832                formatter,
833                "guaranteed segment (0) declared in a context where only fallible segments are permitted"
834            ),
835            FallibleWithoutCheckpoint => write!(
836                formatter,
837                "fallible transcript did not start with a checkpoint"
838            ),
839            NotNormalized => write!(formatter, "transaction is not in normal form"),
840            ClaimReceiveFailed(com) => write!(
841                formatter,
842                "failed to claim coin commitment receive for {:?}",
843                com
844            ),
845            ClaimSpendFailed(com) => write!(
846                formatter,
847                "failed to claim coin commitment spend for {:?}",
848                com
849            ),
850            ClaimNullifierFailed(nul) => write!(
851                formatter,
852                "failed to claim coin commitment nullifier for {:?}",
853                nul
854            ),
855            InvalidSchnorrProof => write!(
856                formatter,
857                "failed to verify Fiat-Shamir transformed Schnorr proof"
858            ),
859            UnclaimedCoinCom(com) => write!(
860                formatter,
861                "a contract-owned coin output was left unclaimed: {:?}",
862                com
863            ),
864            UnclaimedNullifier(nul) => write!(
865                formatter,
866                "a contract-owned coin input was unauthorized: {:?}",
867                nul
868            ),
869            Unbalanced(tt, bal) => write!(
870                formatter,
871                "the transaction has negative balance {} in token type {:?}",
872                bal, tt
873            ),
874            Zswap(err) => err.fmt(formatter),
875            BuiltinDecode(err) => err.fmt(formatter),
876            FeeCalculation(err) => err.fmt(formatter),
877            CantMergeTypes => write!(
878                formatter,
879                "attempted to merge transaction types that are not mergable"
880            ),
881            ClaimOverflow => write!(formatter, "claimed coin value overflows deltas"),
882            ClaimCoinMismatch => write!(
883                formatter,
884                "declared coin in ClaimRewards doesn't match real coin"
885            ),
886            KeyNotInCommittee { address, key_id } => write!(
887                formatter,
888                "declared signture for key id {key_id} does not correspond to a committee member for contract {address:?}"
889            ),
890            InvalidCommitteeSignature { address, key_id } => write!(
891                formatter,
892                "signature for key id {key_id} invalid for contract {address:?}"
893            ),
894            InvalidDustRegistrationSignature { registration } => write!(
895                formatter,
896                "failed to verify signature of dust registration: {registration:?}"
897            ),
898            InvalidDustSpendProof {
899                declared_time,
900                dust_spend,
901            } => write!(
902                formatter,
903                "dust spend proof failed to verify; this is just as likely a disagreement on dust state on the declared time ({declared_time:?}) as the proof being invalid: {dust_spend:?}"
904            ),
905            OutOfDustValidityWindow {
906                dust_ctime,
907                validity_start,
908                validity_end,
909            } => write!(
910                formatter,
911                "dust is outside of its validity window (declared time: {dust_ctime:?}, window: [{validity_start:?}, {validity_end:?}])"
912            ),
913            MultipleDustRegistrationsForKey { key } => write!(
914                formatter,
915                "multiple dust registrations for key in the same intent: {key:?}"
916            ),
917            InsufficientDustForRegistrationFee {
918                registration,
919                available_dust,
920            } => write!(
921                formatter,
922                "insufficient dust to cover registration fee allowance: {available_dust} available, {} requested",
923                registration.allow_fee_payment
924            ),
925            ThresholdMissed {
926                address,
927                signatures,
928                threshold,
929            } => write!(
930                formatter,
931                "threshold update for contract {address:?} does not meet required threshold ({signatures}/{threshold} signatures)"
932            ),
933            TooManyZswapEntries => write!(
934                formatter,
935                "excessive Zswap entries exceeding 2^16 safety margin"
936            ),
937            MalformedContractDeploy(mcd) => write!(formatter, "{:?}", mcd),
938            IntentSignatureVerificationFailure => write!(
939                formatter,
940                "signature verification failed for supplied intent"
941            ),
942            IntentSignatureKeyMismatch => write!(
943                formatter,
944                "supplied signing key does not match verifying key"
945            ),
946            IntentSegmentIdCollision(segment_id) => write!(
947                formatter,
948                "key (segment_id) collision during intents merge: {:?}",
949                segment_id
950            ),
951            IntentAtGuaranteedSegmentId => {
952                write!(formatter, "intents are not allowed at segment_id: 0")
953            }
954            UnsupportedProofVersion { op_version } => write!(
955                formatter,
956                "unsupported proof version provided for contract operation: {op_version}"
957            ),
958            GuaranteedTranscriptVersion { op_version } => write!(
959                formatter,
960                "unsupported guaranteed transcript version provided for contract operation: {op_version}"
961            ),
962            FallibleTranscriptVersion { op_version } => write!(
963                formatter,
964                "unsupported fallible transcript version provided for contract operation: {op_version}"
965            ),
966            TransactionApplicationError(transaction_application_error) => write!(
967                formatter,
968                "transaction application error detected during verification: {transaction_application_error}"
969            ),
970            BalanceCheckOutOfBounds {
971                token_type,
972                segment,
973                current_balance,
974                operation_value,
975                operation,
976            } => {
977                let (reason_str, to_from, op_str) = match operation {
978                    BalanceOperation::Addition => ("overflow", "to", "add"),
979                    BalanceOperation::Subtraction => ("underflow", "from", "subtract"),
980                };
981                write!(
982                    formatter,
983                    "Balance check failed: couldn't {op_str} {operation_value} {to_from} {current_balance} for token {token_type:?} in segment {segment}: {reason_str}"
984                )
985            }
986            BalanceCheckConversionFailure {
987                token_type,
988                segment,
989                operation_value,
990            } => {
991                write!(
992                    formatter,
993                    "Balance check failed: couldn't convert {operation_value} to type i128 for token {token_type:?} in segment {segment}"
994                )
995            }
996            PedersenCheckFailure {
997                expected,
998                calculated,
999            } => write!(
1000                formatter,
1001                "binding commitment calculation mismatch: expected {expected:?}, but calculated {calculated:?}"
1002            ),
1003            BalanceCheckOverspend {
1004                token_type,
1005                segment,
1006                overspent_value,
1007            } => {
1008                write!(
1009                    formatter,
1010                    "invalid balance {overspent_value} for token {token_type:?} in segment {segment}; balance must be positive"
1011                )
1012            }
1013            EffectsCheckFailure(effects_check) => effects_check.fmt(formatter),
1014            DisjointCheckFailure(disjoint_check) => disjoint_check.fmt(formatter),
1015            SequencingCheckFailure(sequencing_check) => sequencing_check.fmt(formatter),
1016            InputsNotSorted(utxo_spends) => {
1017                write!(
1018                    formatter,
1019                    "unshielded offer validation error: inputs are not sorted: {:?}",
1020                    utxo_spends
1021                )
1022            }
1023            OutputsNotSorted(utxo_outputs) => {
1024                write!(
1025                    formatter,
1026                    "unshielded offer validation error: outputs are not sorted: {:?}",
1027                    utxo_outputs
1028                )
1029            }
1030            DuplicateInputs(utxo_spends) => {
1031                write!(
1032                    formatter,
1033                    "unshielded offer validation error: found duplicate inputs: {:?}",
1034                    utxo_spends
1035                )
1036            }
1037            InputsSignaturesLengthMismatch {
1038                inputs,
1039                erased_signatures,
1040            } => {
1041                write!(
1042                    formatter,
1043                    "unshielded offer action validation error: mismatch between number of inputs ({}) and signatures ({})",
1044                    inputs.len(),
1045                    erased_signatures.len()
1046                )
1047            }
1048        }
1049    }
1050}
1051
1052impl<D: DB> Error for MalformedTransaction<D> {
1053    fn cause(&self) -> Option<&dyn Error> {
1054        match self {
1055            MalformedTransaction::MalformedContractDeploy(e) => Some(e),
1056            MalformedTransaction::TransactionApplicationError(e) => Some(e),
1057            MalformedTransaction::EffectsCheckFailure(e) => Some(e),
1058            MalformedTransaction::DisjointCheckFailure(e) => Some(e),
1059            MalformedTransaction::SequencingCheckFailure(e) => Some(e),
1060            _ => None,
1061        }
1062    }
1063}
1064
1065impl<D: DB> From<zswap::error::MalformedOffer> for MalformedTransaction<D> {
1066    fn from(err: zswap::error::MalformedOffer) -> MalformedTransaction<D> {
1067        MalformedTransaction::Zswap(err)
1068    }
1069}
1070
1071impl<D: DB> From<base_crypto::fab::InvalidBuiltinDecode> for MalformedTransaction<D> {
1072    fn from(err: base_crypto::fab::InvalidBuiltinDecode) -> MalformedTransaction<D> {
1073        MalformedTransaction::BuiltinDecode(err)
1074    }
1075}
1076
1077impl<D: DB> From<FeeCalculationError> for MalformedTransaction<D> {
1078    fn from(err: FeeCalculationError) -> MalformedTransaction<D> {
1079        MalformedTransaction::FeeCalculation(err)
1080    }
1081}
1082
1083#[derive(Clone, Debug, PartialEq, Eq, Copy)]
1084pub enum TransactionApplicationError {
1085    IntentTtlExpired(Timestamp, Timestamp),
1086    IntentTtlTooFarInFuture(Timestamp, Timestamp),
1087    IntentAlreadyExists,
1088}
1089
1090impl Display for TransactionApplicationError {
1091    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
1092        match self {
1093            TransactionApplicationError::IntentTtlExpired(ttl, current_block) => write!(
1094                formatter,
1095                "Intent TTL has expired. TTL: {:?}, Current block: {:?}",
1096                ttl, current_block
1097            ),
1098            TransactionApplicationError::IntentTtlTooFarInFuture(ttl, max_allowed) => write!(
1099                formatter,
1100                "Intent TTL is too far in the future. TTL: {:?}, Maximum allowed: {:?}",
1101                ttl, max_allowed
1102            ),
1103            TransactionApplicationError::IntentAlreadyExists => write!(
1104                formatter,
1105                "Intent already exists; duplicate intents are not allowed",
1106            ),
1107        }
1108    }
1109}
1110
1111impl Error for TransactionApplicationError {}
1112
1113#[derive(Debug)]
1114#[non_exhaustive]
1115pub enum QueryFailed<D: DB> {
1116    MissingCall,
1117    InvalidContract(ContractAddress),
1118    InvalidInput { value: Value, ty: Alignment },
1119    Runtime(onchain_runtime::error::TranscriptRejected<D>),
1120    Zswap(zswap::error::OfferCreationFailed),
1121}
1122
1123impl<D: DB> Display for QueryFailed<D> {
1124    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
1125        use QueryFailed::*;
1126        match self {
1127            MissingCall => write!(formatter, "attempted to run query prior to starting a call"),
1128            InvalidContract(addr) => write!(formatter, "contract {:?} does not exist", addr),
1129            InvalidInput { value, ty } => write!(
1130                formatter,
1131                "invalid input value {:?} for type {:?}",
1132                value, ty
1133            ),
1134            Runtime(err) => err.fmt(formatter),
1135            Zswap(err) => err.fmt(formatter),
1136        }
1137    }
1138}
1139
1140impl<D: DB> Error for QueryFailed<D> {}
1141
1142impl<D: DB> From<zswap::error::OfferCreationFailed> for QueryFailed<D> {
1143    fn from(err: zswap::error::OfferCreationFailed) -> QueryFailed<D> {
1144        QueryFailed::Zswap(err)
1145    }
1146}
1147
1148#[derive(Debug)]
1149#[non_exhaustive]
1150pub enum TransactionConstructionError {
1151    TransactionEmpty,
1152    UnfinishedCall {
1153        address: ContractAddress,
1154        operation: EntryPointBuf,
1155    },
1156    ProofFailed(ProvingError),
1157    MissingVerifierKey {
1158        address: ContractAddress,
1159        operation: EntryPointBuf,
1160    },
1161}
1162
1163impl Display for TransactionConstructionError {
1164    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
1165        use TransactionConstructionError::*;
1166        match self {
1167            TransactionEmpty => write!(formatter, "attempted to create empty transaction"),
1168            UnfinishedCall { address, operation } => write!(
1169                formatter,
1170                "unfinished call to {:?}/{:?}",
1171                address, operation
1172            ),
1173            ProofFailed(err) => {
1174                err.fmt(formatter)?;
1175                write!(formatter, " -- while assembling transaction")
1176            }
1177            MissingVerifierKey { address, operation } => write!(
1178                formatter,
1179                "attempted to create proof for {:?}/{:?}, which lacks a verifier key",
1180                address, operation,
1181            ),
1182        }
1183    }
1184}
1185
1186impl Error for TransactionConstructionError {}
1187
1188#[derive(Debug)]
1189#[allow(clippy::large_enum_variant)]
1190pub enum TransactionProvingError<D: DB> {
1191    LeftoverEntries {
1192        address: ContractAddress,
1193        entry_point: EntryPointBuf,
1194        entries: Box<Transcript<D>>,
1195    },
1196    RanOutOfEntries {
1197        address: ContractAddress,
1198        entry_point: EntryPointBuf,
1199    },
1200    MissingKeyset(KeyLocation),
1201    Proving(ProvingError),
1202    Tokio(std::io::Error),
1203}
1204
1205impl<D: DB> Display for TransactionProvingError<D> {
1206    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
1207        use TransactionProvingError::*;
1208        match self {
1209            LeftoverEntries {
1210                address,
1211                entry_point,
1212                entries,
1213            } => write!(
1214                formatter,
1215                "too many transcript entries for {:?}/{:?}: {:?} leftover",
1216                address, entry_point, entries
1217            ),
1218            RanOutOfEntries {
1219                address,
1220                entry_point,
1221            } => write!(
1222                formatter,
1223                "ran out of transcript entries for {:?}/{:?}",
1224                address, entry_point
1225            ),
1226            MissingKeyset(keyloc) => write!(
1227                formatter,
1228                "attempted proof, but couldn't find keys with ID {keyloc:?}"
1229            ),
1230            Proving(e) => e.fmt(formatter),
1231            Tokio(e) => e.fmt(formatter),
1232        }
1233    }
1234}
1235
1236impl<D: DB> Error for TransactionProvingError<D> {
1237    fn cause(&self) -> Option<&dyn Error> {
1238        match self {
1239            TransactionProvingError::Tokio(e) => Some(e),
1240            _ => None,
1241        }
1242    }
1243}
1244
1245impl<D: DB> From<ProvingError> for TransactionProvingError<D> {
1246    fn from(err: ProvingError) -> TransactionProvingError<D> {
1247        TransactionProvingError::Proving(err)
1248    }
1249}
1250
1251#[derive(Debug)]
1252pub enum PartitionFailure<D: DB> {
1253    Transcript(TranscriptRejected<D>),
1254    NonForest,
1255    GuaranteedOnlyUnsatisfied,
1256    IllegalSegmentZero,
1257    Merge(MalformedOffer),
1258}
1259
1260impl<D: DB> From<TranscriptRejected<D>> for PartitionFailure<D> {
1261    fn from(err: TranscriptRejected<D>) -> Self {
1262        PartitionFailure::Transcript(err)
1263    }
1264}
1265
1266impl<D: DB> Display for PartitionFailure<D> {
1267    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1268        match self {
1269            PartitionFailure::NonForest => {
1270                write!(f, "call graph was not a forest; cannot partition")
1271            }
1272            PartitionFailure::Transcript(e) => e.fmt(f),
1273            PartitionFailure::GuaranteedOnlyUnsatisfied => write!(
1274                f,
1275                "transaction could not be constructed to satisfy 'guaranteed only' segment specifier: the call was too expensive"
1276            ),
1277            PartitionFailure::IllegalSegmentZero => {
1278                write!(f, "illegal manual specification of segment 0")
1279            }
1280            PartitionFailure::Merge(e) => write!(f, "failed zswap merge: {e}"),
1281        }
1282    }
1283}
1284
1285impl<D: DB> Error for PartitionFailure<D> {
1286    fn cause(&self) -> Option<&dyn Error> {
1287        match self {
1288            PartitionFailure::Transcript(err) => Some(err),
1289            PartitionFailure::Merge(err) => Some(err),
1290            _ => None,
1291        }
1292    }
1293}
1294
1295#[derive(Debug, Clone)]
1296pub struct GenerationInfoAlreadyPresentError(pub DustGenerationInfo);
1297
1298impl Display for GenerationInfoAlreadyPresentError {
1299    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
1300        write!(
1301            formatter,
1302            "attempted to insert new Dust generation info {:?}, but this already exists",
1303            self.0,
1304        )
1305    }
1306}
1307
1308impl Error for GenerationInfoAlreadyPresentError {}
1309
1310#[derive(Clone, Debug)]
1311#[non_exhaustive]
1312pub enum EventReplayError {
1313    NonLinearInsertion {
1314        expected_next: u64,
1315        received: u64,
1316        tree_name: &'static str,
1317    },
1318    DtimeUpdateForUntracked {
1319        updated: u64,
1320        tracked_up_to_index: u64,
1321    },
1322    EventForPastTime {
1323        synced: Timestamp,
1324        event: Timestamp,
1325    },
1326    MerkleTreeUpdate(InvalidUpdate),
1327}
1328
1329impl Display for EventReplayError {
1330    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1331        use EventReplayError::*;
1332        match self {
1333            NonLinearInsertion {
1334                expected_next,
1335                received,
1336                tree_name,
1337            } => write!(
1338                f,
1339                "values inserted non-linearly into {tree_name} tree; expected to insert index {expected_next}, but received {received}."
1340            ),
1341            DtimeUpdateForUntracked {
1342                updated,
1343                tracked_up_to_index,
1344            } => write!(
1345                f,
1346                "attempted to update the dtime of a dust generation entry that isn't tracked; tracking up to index {tracked_up_to_index}, but received an update for {updated}"
1347            ),
1348            EventForPastTime { synced, event } => write!(
1349                f,
1350                "received an event with a timestamp prior to the time already synced to (synced to: {synced:?}, event time: {event:?})"
1351            ),
1352            MerkleTreeUpdate(err) => err.fmt(f),
1353        }
1354    }
1355}
1356
1357impl Error for EventReplayError {
1358    fn cause(&self) -> Option<&dyn Error> {
1359        match self {
1360            EventReplayError::MerkleTreeUpdate(err) => Some(err),
1361            _ => None,
1362        }
1363    }
1364}
1365
1366impl From<InvalidUpdate> for EventReplayError {
1367    fn from(err: InvalidUpdate) -> Self {
1368        EventReplayError::MerkleTreeUpdate(err)
1369    }
1370}
1371
1372#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1373pub struct BlockLimitExceeded;
1374
1375impl Display for BlockLimitExceeded {
1376    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1377        write!(
1378            f,
1379            "exceeded block limit during post-block update declaration"
1380        )
1381    }
1382}
1383
1384impl Error for BlockLimitExceeded {}