oasis_runtime_sdk/
module.rs

1//! Runtime modules.
2use std::{
3    collections::{BTreeMap, BTreeSet},
4    fmt::Debug,
5};
6
7use cbor::Encode as _;
8use impl_trait_for_tuples::impl_for_tuples;
9
10use crate::{
11    context::Context,
12    dispatcher, error,
13    error::Error as _,
14    event, modules,
15    modules::core::types::{MethodHandlerInfo, ModuleInfo},
16    state::CurrentState,
17    storage,
18    storage::Prefix,
19    types::{
20        address::Address,
21        message::MessageResult,
22        transaction::{self, AuthInfo, Call, Transaction, UnverifiedTransaction},
23    },
24};
25
26/// Result of invoking the method handler.
27pub enum DispatchResult<B, R> {
28    Handled(R),
29    Unhandled(B),
30}
31
32impl<B, R> DispatchResult<B, R> {
33    /// Transforms `DispatchResult<B, R>` into `Result<R, E>`, mapping `Handled(r)` to `Ok(r)` and
34    /// `Unhandled(_)` to `Err(err)`.
35    pub fn ok_or<E>(self, err: E) -> Result<R, E> {
36        match self {
37            DispatchResult::Handled(result) => Ok(result),
38            DispatchResult::Unhandled(_) => Err(err),
39        }
40    }
41
42    /// Transforms `DispatchResult<B, R>` into `Result<R, E>`, mapping `Handled(r)` to `Ok(r)` and
43    /// `Unhandled(_)` to `Err(err)` using the provided function.
44    pub fn ok_or_else<E, F: FnOnce() -> E>(self, errf: F) -> Result<R, E> {
45        match self {
46            DispatchResult::Handled(result) => Ok(result),
47            DispatchResult::Unhandled(_) => Err(errf()),
48        }
49    }
50}
51
52/// A variant of `types::transaction::CallResult` but used for dispatch purposes so the dispatch
53/// process can use a different representation.
54///
55/// Specifically, this type is not serializable.
56#[derive(Debug)]
57pub enum CallResult {
58    /// Call has completed successfully.
59    Ok(cbor::Value),
60
61    /// Call has completed with failure.
62    Failed {
63        module: String,
64        code: u32,
65        message: String,
66    },
67
68    /// A fatal error has occurred and the batch must be aborted.
69    Aborted(dispatcher::Error),
70}
71
72impl CallResult {
73    /// Check whether the call result indicates a successful operation or not.
74    pub fn is_success(&self) -> bool {
75        matches!(self, CallResult::Ok(_))
76    }
77
78    #[cfg(any(test, feature = "test"))]
79    pub fn unwrap(self) -> cbor::Value {
80        match self {
81            Self::Ok(v) => v,
82            Self::Failed {
83                module,
84                code,
85                message,
86            } => panic!("{module} reported failure with code {code}: {message}"),
87            Self::Aborted(e) => panic!("tx aborted with error: {e}"),
88        }
89    }
90
91    #[cfg(any(test, feature = "test"))]
92    pub fn unwrap_failed(self) -> (String, u32) {
93        match self {
94            Self::Ok(_) => panic!("call result indicates success"),
95            Self::Failed { module, code, .. } => (module, code),
96            Self::Aborted(e) => panic!("tx aborted with error: {e}"),
97        }
98    }
99}
100
101impl From<CallResult> for transaction::CallResult {
102    fn from(v: CallResult) -> Self {
103        match v {
104            CallResult::Ok(data) => Self::Ok(data),
105            CallResult::Failed {
106                module,
107                code,
108                message,
109            } => Self::Failed {
110                module,
111                code,
112                message,
113            },
114            CallResult::Aborted(err) => Self::Failed {
115                module: err.module_name().to_string(),
116                code: err.code(),
117                message: err.to_string(),
118            },
119        }
120    }
121}
122
123/// A convenience function for dispatching method calls.
124pub fn dispatch_call<C, B, R, E, F>(
125    ctx: &C,
126    body: cbor::Value,
127    f: F,
128) -> DispatchResult<cbor::Value, CallResult>
129where
130    C: Context,
131    B: cbor::Decode,
132    R: cbor::Encode,
133    E: error::Error,
134    F: FnOnce(&C, B) -> Result<R, E>,
135{
136    DispatchResult::Handled((|| {
137        let args = match cbor::from_value(body)
138            .map_err(|err| modules::core::Error::InvalidArgument(err.into()))
139        {
140            Ok(args) => args,
141            Err(err) => return err.into_call_result(),
142        };
143
144        match f(ctx, args) {
145            Ok(value) => CallResult::Ok(cbor::to_value(value)),
146            Err(err) => err.into_call_result(),
147        }
148    })())
149}
150
151/// A convenience function for dispatching queries.
152pub fn dispatch_query<C, B, R, E, F>(
153    ctx: &C,
154    body: cbor::Value,
155    f: F,
156) -> DispatchResult<cbor::Value, Result<cbor::Value, error::RuntimeError>>
157where
158    C: Context,
159    B: cbor::Decode,
160    R: cbor::Encode,
161    E: error::Error,
162    error::RuntimeError: From<E>,
163    F: FnOnce(&C, B) -> Result<R, E>,
164{
165    DispatchResult::Handled((|| {
166        let args = cbor::from_value(body).map_err(|err| -> error::RuntimeError {
167            modules::core::Error::InvalidArgument(err.into()).into()
168        })?;
169        Ok(cbor::to_value(f(ctx, args)?))
170    })())
171}
172
173/// Method handler.
174pub trait MethodHandler {
175    /// Add storage prefixes to prefetch.
176    fn prefetch(
177        _prefixes: &mut BTreeSet<Prefix>,
178        _method: &str,
179        body: cbor::Value,
180        _auth_info: &AuthInfo,
181    ) -> DispatchResult<cbor::Value, Result<(), error::RuntimeError>> {
182        // Default implementation indicates that the call was not handled.
183        DispatchResult::Unhandled(body)
184    }
185
186    /// Dispatch a call.
187    fn dispatch_call<C: Context>(
188        _ctx: &C,
189        _method: &str,
190        body: cbor::Value,
191    ) -> DispatchResult<cbor::Value, CallResult> {
192        // Default implementation indicates that the call was not handled.
193        DispatchResult::Unhandled(body)
194    }
195
196    /// Dispatch a query.
197    fn dispatch_query<C: Context>(
198        _ctx: &C,
199        _method: &str,
200        args: cbor::Value,
201    ) -> DispatchResult<cbor::Value, Result<cbor::Value, error::RuntimeError>> {
202        // Default implementation indicates that the query was not handled.
203        DispatchResult::Unhandled(args)
204    }
205
206    /// Dispatch a message result.
207    fn dispatch_message_result<C: Context>(
208        _ctx: &C,
209        _handler_name: &str,
210        result: MessageResult,
211    ) -> DispatchResult<MessageResult, ()> {
212        // Default implementation indicates that the query was not handled.
213        DispatchResult::Unhandled(result)
214    }
215
216    /// Lists the names of all RPC methods exposed by this module. The result is informational
217    /// only. An empty return vector means that the implementor does not care to list the methods,
218    /// or the implementor is a tuple of modules.
219    fn supported_methods() -> Vec<MethodHandlerInfo> {
220        vec![]
221    }
222
223    /// Checks whether the given query method is tagged as expensive.
224    fn is_expensive_query(_method: &str) -> bool {
225        false
226    }
227
228    /// Checks whether the given query is allowed to access private key manager state.
229    fn is_allowed_private_km_query(_method: &str) -> bool {
230        false
231    }
232
233    /// Checks whether the given call is allowed to be called interactively via read-only
234    /// transactions.
235    fn is_allowed_interactive_call(_method: &str) -> bool {
236        false
237    }
238}
239
240#[impl_for_tuples(30)]
241impl MethodHandler for Tuple {
242    fn prefetch(
243        prefixes: &mut BTreeSet<Prefix>,
244        method: &str,
245        body: cbor::Value,
246        auth_info: &AuthInfo,
247    ) -> DispatchResult<cbor::Value, Result<(), error::RuntimeError>> {
248        // Return on first handler that can handle the method.
249        for_tuples!( #(
250            let body = match Tuple::prefetch(prefixes, method, body, auth_info) {
251                DispatchResult::Handled(result) => return DispatchResult::Handled(result),
252                DispatchResult::Unhandled(body) => body,
253            };
254        )* );
255
256        DispatchResult::Unhandled(body)
257    }
258
259    fn dispatch_call<C: Context>(
260        ctx: &C,
261        method: &str,
262        body: cbor::Value,
263    ) -> DispatchResult<cbor::Value, CallResult> {
264        // Return on first handler that can handle the method.
265        for_tuples!( #(
266            let body = match Tuple::dispatch_call::<C>(ctx, method, body) {
267                DispatchResult::Handled(result) => return DispatchResult::Handled(result),
268                DispatchResult::Unhandled(body) => body,
269            };
270        )* );
271
272        DispatchResult::Unhandled(body)
273    }
274
275    fn dispatch_query<C: Context>(
276        ctx: &C,
277        method: &str,
278        args: cbor::Value,
279    ) -> DispatchResult<cbor::Value, Result<cbor::Value, error::RuntimeError>> {
280        // Return on first handler that can handle the method.
281        for_tuples!( #(
282            let args = match Tuple::dispatch_query::<C>(ctx, method, args) {
283                DispatchResult::Handled(result) => return DispatchResult::Handled(result),
284                DispatchResult::Unhandled(args) => args,
285            };
286        )* );
287
288        DispatchResult::Unhandled(args)
289    }
290
291    fn dispatch_message_result<C: Context>(
292        ctx: &C,
293        handler_name: &str,
294        result: MessageResult,
295    ) -> DispatchResult<MessageResult, ()> {
296        // Return on first handler that can handle the method.
297        for_tuples!( #(
298            let result = match Tuple::dispatch_message_result::<C>(ctx, handler_name, result) {
299                DispatchResult::Handled(result) => return DispatchResult::Handled(result),
300                DispatchResult::Unhandled(result) => result,
301            };
302        )* );
303
304        DispatchResult::Unhandled(result)
305    }
306
307    fn is_expensive_query(method: &str) -> bool {
308        for_tuples!( #(
309            if Tuple::is_expensive_query(method) {
310                return true;
311            }
312        )* );
313        false
314    }
315
316    fn is_allowed_private_km_query(method: &str) -> bool {
317        for_tuples!( #(
318            if Tuple::is_allowed_private_km_query(method) {
319                return true;
320            }
321        )* );
322        false
323    }
324
325    fn is_allowed_interactive_call(method: &str) -> bool {
326        for_tuples!( #(
327            if Tuple::is_allowed_interactive_call(method) {
328                return true;
329            }
330        )* );
331        false
332    }
333}
334
335/// An authentication decision for cases where multiple handlers are available.
336#[derive(Clone, Debug)]
337pub enum AuthDecision {
338    /// Authentication passed, continue with the next authentication handler.
339    Continue,
340    /// Authentication passed, no further authentication handlers should be called.
341    Stop,
342}
343
344/// Transaction handler.
345pub trait TransactionHandler {
346    /// Judge if a raw transaction is good enough to undergo decoding.
347    /// This takes place before even decoding the transaction.
348    fn approve_raw_tx<C: Context>(_ctx: &C, _tx: &[u8]) -> Result<(), modules::core::Error> {
349        // Default implementation doesn't do any checks.
350        Ok(())
351    }
352
353    /// Judge if an unverified transaction is good enough to undergo verification.
354    /// This takes place before even verifying signatures.
355    fn approve_unverified_tx<C: Context>(
356        _ctx: &C,
357        _utx: &UnverifiedTransaction,
358    ) -> Result<(), modules::core::Error> {
359        // Default implementation doesn't do any checks.
360        Ok(())
361    }
362
363    /// Decode a transaction that was sent with module-controlled decoding and verify any
364    /// signatures.
365    ///
366    /// Postcondition: if returning a Transaction, that transaction must pass `validate_basic`.
367    ///
368    /// Returns Ok(Some(_)) if the module is in charge of the encoding scheme identified by _scheme
369    /// or Ok(None) otherwise.
370    fn decode_tx<C: Context>(
371        _ctx: &C,
372        _scheme: &str,
373        _body: &[u8],
374    ) -> Result<Option<Transaction>, modules::core::Error> {
375        // Default implementation is not in charge of any schemes.
376        Ok(None)
377    }
378
379    /// Authenticate a transaction.
380    ///
381    /// Note that any signatures have already been verified.
382    fn authenticate_tx<C: Context>(
383        _ctx: &C,
384        _tx: &Transaction,
385    ) -> Result<AuthDecision, modules::core::Error> {
386        // Default implementation accepts all transactions.
387        Ok(AuthDecision::Continue)
388    }
389
390    /// Perform any action after authentication, within the transaction context.
391    ///
392    /// At this point call format has not yet been decoded so peeking into the call may not be
393    /// possible in case the call is encrypted.
394    fn before_handle_call<C: Context>(_ctx: &C, _call: &Call) -> Result<(), modules::core::Error> {
395        // Default implementation doesn't do anything.
396        Ok(())
397    }
398
399    /// Perform any action after authentication and decoding, within the transaction context.
400    ///
401    /// At this point, the call has been decoded according to the call format,
402    /// and method authorizers have run.
403    fn before_authorized_call_dispatch<C: Context>(
404        _ctx: &C,
405        _call: &Call,
406    ) -> Result<(), modules::core::Error> {
407        // Default implementation doesn't do anything.
408        Ok(())
409    }
410
411    /// Perform any action after call, within the transaction context.
412    ///
413    /// If an error is returned the transaction call fails and updates are rolled back.
414    fn after_handle_call<C: Context>(
415        _ctx: &C,
416        result: CallResult,
417    ) -> Result<CallResult, modules::core::Error> {
418        // Default implementation doesn't do anything.
419        Ok(result)
420    }
421
422    /// Perform any action after dispatching the transaction, in batch context.
423    fn after_dispatch_tx<C: Context>(_ctx: &C, _tx_auth_info: &AuthInfo, _result: &CallResult) {
424        // Default implementation doesn't do anything.
425    }
426}
427
428#[impl_for_tuples(30)]
429impl TransactionHandler for Tuple {
430    fn approve_raw_tx<C: Context>(ctx: &C, tx: &[u8]) -> Result<(), modules::core::Error> {
431        for_tuples!( #( Tuple::approve_raw_tx(ctx, tx)?; )* );
432        Ok(())
433    }
434
435    fn approve_unverified_tx<C: Context>(
436        ctx: &C,
437        utx: &UnverifiedTransaction,
438    ) -> Result<(), modules::core::Error> {
439        for_tuples!( #( Tuple::approve_unverified_tx(ctx, utx)?; )* );
440        Ok(())
441    }
442
443    fn decode_tx<C: Context>(
444        ctx: &C,
445        scheme: &str,
446        body: &[u8],
447    ) -> Result<Option<Transaction>, modules::core::Error> {
448        for_tuples!( #(
449            let decoded = Tuple::decode_tx(ctx, scheme, body)?;
450            if (decoded.is_some()) {
451                return Ok(decoded);
452            }
453        )* );
454        Ok(None)
455    }
456
457    fn authenticate_tx<C: Context>(
458        ctx: &C,
459        tx: &Transaction,
460    ) -> Result<AuthDecision, modules::core::Error> {
461        for_tuples!( #(
462            match Tuple::authenticate_tx(ctx, tx)? {
463                AuthDecision::Stop => return Ok(AuthDecision::Stop),
464                AuthDecision::Continue => {},
465            }
466        )* );
467
468        Ok(AuthDecision::Continue)
469    }
470
471    fn before_handle_call<C: Context>(ctx: &C, call: &Call) -> Result<(), modules::core::Error> {
472        for_tuples!( #( Tuple::before_handle_call(ctx, call)?; )* );
473        Ok(())
474    }
475
476    fn before_authorized_call_dispatch<C: Context>(
477        ctx: &C,
478        call: &Call,
479    ) -> Result<(), modules::core::Error> {
480        for_tuples!( #( Tuple::before_authorized_call_dispatch(ctx, call)?; )* );
481        Ok(())
482    }
483
484    fn after_handle_call<C: Context>(
485        ctx: &C,
486        mut result: CallResult,
487    ) -> Result<CallResult, modules::core::Error> {
488        for_tuples!( #(
489            result = Tuple::after_handle_call(ctx, result)?;
490        )* );
491        Ok(result)
492    }
493
494    fn after_dispatch_tx<C: Context>(ctx: &C, tx_auth_info: &AuthInfo, result: &CallResult) {
495        for_tuples!( #( Tuple::after_dispatch_tx(ctx, tx_auth_info, result); )* );
496    }
497}
498
499/// Fee proxy handler.
500pub trait FeeProxyHandler {
501    /// Resolve the proxy payer for the given transaction. If no payer could be resolved, `None`
502    /// should be returned.
503    fn resolve_payer<C: Context>(
504        ctx: &C,
505        tx: &Transaction,
506    ) -> Result<Option<Address>, modules::core::Error>;
507}
508
509#[impl_for_tuples(30)]
510impl FeeProxyHandler for Tuple {
511    fn resolve_payer<C: Context>(
512        ctx: &C,
513        tx: &Transaction,
514    ) -> Result<Option<Address>, modules::core::Error> {
515        for_tuples!( #(
516            if let Some(payer) = Tuple::resolve_payer(ctx, tx)? {
517                return Ok(Some(payer));
518            }
519        )* );
520
521        Ok(None)
522    }
523}
524
525/// Migration handler.
526pub trait MigrationHandler {
527    /// Genesis state type.
528    ///
529    /// If this state is expensive to compute and not often updated, prefer
530    /// to make the genesis type something like `once_cell::unsync::Lazy<T>`.
531    type Genesis;
532
533    /// Initialize state from genesis or perform a migration.
534    ///
535    /// Should return true in case metadata has been changed.
536    fn init_or_migrate<C: Context>(
537        _ctx: &C,
538        _meta: &mut modules::core::types::Metadata,
539        _genesis: Self::Genesis,
540    ) -> bool {
541        // Default implementation doesn't perform any migrations.
542        false
543    }
544}
545
546#[allow(clippy::type_complexity)]
547#[impl_for_tuples(30)]
548impl MigrationHandler for Tuple {
549    for_tuples!( type Genesis = ( #( Tuple::Genesis ),* ); );
550
551    fn init_or_migrate<C: Context>(
552        ctx: &C,
553        meta: &mut modules::core::types::Metadata,
554        genesis: Self::Genesis,
555    ) -> bool {
556        [for_tuples!( #( Tuple::init_or_migrate(ctx, meta, genesis.Tuple) ),* )]
557            .iter()
558            .any(|x| *x)
559    }
560}
561
562/// Block handler.
563pub trait BlockHandler {
564    /// Perform any common actions at the start of the block (before any transactions have been
565    /// executed).
566    fn begin_block<C: Context>(_ctx: &C) {
567        // Default implementation doesn't do anything.
568    }
569
570    /// Perform any common actions at the end of the block (after all transactions have been
571    /// executed).
572    fn end_block<C: Context>(_ctx: &C) {
573        // Default implementation doesn't do anything.
574    }
575}
576
577#[impl_for_tuples(30)]
578impl BlockHandler for Tuple {
579    fn begin_block<C: Context>(ctx: &C) {
580        for_tuples!( #( Tuple::begin_block(ctx); )* );
581    }
582
583    fn end_block<C: Context>(ctx: &C) {
584        for_tuples!( #( Tuple::end_block(ctx); )* );
585    }
586}
587
588/// Invariant handler.
589pub trait InvariantHandler {
590    /// Check invariants.
591    fn check_invariants<C: Context>(_ctx: &C) -> Result<(), modules::core::Error> {
592        // Default implementation doesn't do anything.
593        Ok(())
594    }
595}
596
597#[impl_for_tuples(30)]
598impl InvariantHandler for Tuple {
599    /// Check the invariants in all modules in the tuple.
600    fn check_invariants<C: Context>(ctx: &C) -> Result<(), modules::core::Error> {
601        for_tuples!( #( Tuple::check_invariants(ctx)?; )* );
602        Ok(())
603    }
604}
605
606/// Info handler.
607pub trait ModuleInfoHandler {
608    /// Reports info about the module (or modules, if `Self` is a tuple).
609    fn module_info<C: Context>(_ctx: &C) -> BTreeMap<String, ModuleInfo>;
610}
611
612impl<M: Module + MethodHandler> ModuleInfoHandler for M {
613    fn module_info<C: Context>(_ctx: &C) -> BTreeMap<String, ModuleInfo> {
614        let mut info = BTreeMap::new();
615        info.insert(
616            Self::NAME.to_string(),
617            ModuleInfo {
618                version: Self::VERSION,
619                params: Self::params().into_cbor_value(),
620                methods: Self::supported_methods(),
621            },
622        );
623        info
624    }
625}
626
627#[impl_for_tuples(30)]
628impl ModuleInfoHandler for Tuple {
629    #[allow(clippy::let_and_return)]
630    fn module_info<C: Context>(ctx: &C) -> BTreeMap<String, ModuleInfo> {
631        let mut merged = BTreeMap::new();
632        for_tuples!( #(
633            merged.extend(Tuple::module_info(ctx));
634        )* );
635        merged
636    }
637}
638
639/// A runtime module.
640pub trait Module {
641    /// Module name.
642    const NAME: &'static str;
643
644    /// Module version.
645    const VERSION: u32 = 1;
646
647    /// Module error type.
648    type Error: error::Error + 'static;
649
650    /// Module event type.
651    type Event: event::Event + 'static;
652
653    /// Module parameters.
654    type Parameters: Parameters + 'static;
655
656    /// Return the module's parameters.
657    fn params() -> Self::Parameters {
658        CurrentState::with_store(|store| {
659            let store = storage::PrefixStore::new(store, &Self::NAME);
660            let store = storage::TypedStore::new(store);
661            store.get(Self::Parameters::STORE_KEY).unwrap_or_default()
662        })
663    }
664
665    /// Set the module's parameters.
666    fn set_params(params: Self::Parameters) {
667        CurrentState::with_store(|store| {
668            let store = storage::PrefixStore::new(store, &Self::NAME);
669            let mut store = storage::TypedStore::new(store);
670            store.insert(Self::Parameters::STORE_KEY, params);
671        });
672    }
673}
674
675/// Parameters for a runtime module.
676pub trait Parameters: Debug + Default + cbor::Encode + cbor::Decode {
677    type Error;
678
679    /// Store key used for storing parameters.
680    const STORE_KEY: &'static [u8] = &[0x00];
681
682    /// Perform basic parameter validation.
683    fn validate_basic(&self) -> Result<(), Self::Error> {
684        // No validation by default.
685        Ok(())
686    }
687}
688
689impl Parameters for () {
690    type Error = std::convert::Infallible;
691}
692
693#[cfg(test)]
694mod test {
695    use super::*;
696    use crate::testing::mock;
697
698    /// An authentication handler that always continues.
699    struct TestAuthContinue;
700    /// An authentication handler that always stops.
701    struct TestAuthStop;
702    /// An authentication handler that always fails.
703    struct TestAuthFail;
704
705    impl super::TransactionHandler for TestAuthContinue {
706        fn authenticate_tx<C: Context>(
707            _ctx: &C,
708            _tx: &Transaction,
709        ) -> Result<AuthDecision, modules::core::Error> {
710            Ok(AuthDecision::Continue)
711        }
712    }
713
714    impl super::TransactionHandler for TestAuthStop {
715        fn authenticate_tx<C: Context>(
716            _ctx: &C,
717            _tx: &Transaction,
718        ) -> Result<AuthDecision, modules::core::Error> {
719            Ok(AuthDecision::Stop)
720        }
721    }
722
723    impl super::TransactionHandler for TestAuthFail {
724        fn authenticate_tx<C: Context>(
725            _ctx: &C,
726            _tx: &Transaction,
727        ) -> Result<AuthDecision, modules::core::Error> {
728            Err(modules::core::Error::NotAuthenticated)
729        }
730    }
731
732    #[test]
733    fn test_authenticate_tx() {
734        let mut mock = mock::Mock::default();
735        let ctx = mock.create_ctx();
736        let tx = mock::transaction();
737
738        // Make sure mock authentication handlers behave as expected.
739        let result = TestAuthContinue::authenticate_tx(&ctx, &tx).unwrap();
740        assert!(matches!(result, AuthDecision::Continue));
741        let result = TestAuthStop::authenticate_tx(&ctx, &tx).unwrap();
742        assert!(matches!(result, AuthDecision::Stop));
743        let _ = TestAuthFail::authenticate_tx(&ctx, &tx).unwrap_err();
744
745        // Make sure that composed variants behave as expected.
746        type Composed1 = (TestAuthContinue, TestAuthContinue, TestAuthContinue);
747        let result = Composed1::authenticate_tx(&ctx, &tx).unwrap();
748        assert!(matches!(result, AuthDecision::Continue));
749
750        type Composed2 = (TestAuthContinue, TestAuthStop, TestAuthContinue);
751        let result = Composed2::authenticate_tx(&ctx, &tx).unwrap();
752        assert!(matches!(result, AuthDecision::Stop));
753
754        type Composed3 = (TestAuthContinue, TestAuthStop, TestAuthFail);
755        let result = Composed3::authenticate_tx(&ctx, &tx).unwrap();
756        assert!(matches!(result, AuthDecision::Stop));
757
758        type Composed4 = (TestAuthFail, TestAuthStop, TestAuthContinue);
759        let _ = Composed4::authenticate_tx(&ctx, &tx).unwrap_err();
760
761        type Composed5 = (TestAuthContinue, TestAuthContinue, TestAuthFail);
762        let _ = Composed5::authenticate_tx(&ctx, &tx).unwrap_err();
763    }
764}