cnidarium_component/
action_handler.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use cnidarium::{StateRead, StateWrite};
6
7#[async_trait]
8/// Defines the interface for handling transaction actions.
9///
10/// Block-wide execution is performed using the [`Component`](crate::Component)
11/// trait.  Per-transaction execution is performed using the `ActionHandler`
12/// trait.
13///
14/// The `ActionHandler` trait has a top-level implementation on [`Transaction`],
15/// which performs any transaction-wide checks and then calls the
16/// `ActionHandler` implementation for each [`Action`](penumbra_sdk_transaction::Action).
17///
18/// The validation logic in the `ActionHandler` trait is split into three phases:
19///
20/// * [`ActionHandler::check_stateless`], which has no access to chain state, only to the [`CheckStatelessContext`];
21/// * [`ActionHandler::check_stateful`], which has read access to a snapshot of state prior to transaction execution;
22/// * [`ActionHandler::execute`], which has write access to the state and read access to its own writes.
23///
24/// All of these methods are asynchronous and fallible; an error at any level
25/// fails the transaction and aborts any in-progress execution.
26///
27/// These methods are described in more detail below, but in general, as much
28/// work as possible should be pushed up the stack, where greater parallelism is
29/// available, with checks performed in `execute` only as a last resort.
30pub trait ActionHandler {
31    /// Context for stateless validity checks, like the transaction containing the action.
32    type CheckStatelessContext: Clone + Send + Sync + 'static;
33    /// Performs all of this action's stateless validity checks in the
34    /// `context` of some [`Transaction`].
35    ///
36    /// This method is `async` to make it easy to perform stateless validity
37    /// checks in parallel, by allowing `ActionHandler` implementations to
38    /// easily spawn tasks internally.
39    ///
40    /// Supplying the `context` means that stateless checks can use
41    /// transaction-wide data like the SCT anchor.
42    ///
43    /// As much work as possible should be done in `check_stateless`, as it can
44    /// be run in parallel across all transactions in a block.
45    async fn check_stateless(&self, context: Self::CheckStatelessContext) -> Result<()>;
46
47    /// Performs those stateful validity checks that can be performed against a
48    /// historical state snapshot.
49    ///
50    /// This method provides read access to a snapshot of the `State` prior to
51    /// transaction execution.  It is intended to be run in parallel across all
52    /// actions within a transaction.
53    ///
54    /// # Warning
55    ///
56    /// Misuse of this method creates TOCTOU vulnerabilities. Checks performed
57    /// in this method must be valid if they are performed against a _prior_
58    /// state, as another action in the same transaction may execute first and
59    /// change the state.
60    ///
61    /// Checks performed in this phase should have a justification for why they
62    /// are safe to run in parallel with other actions in the same transaction,
63    /// and the default behavior should be to perform checks in
64    /// [`ActionHandler::check_and_execute`].
65    ///
66    /// # Invariants
67    ///
68    /// This method should only be called on data that has been checked
69    /// with [`ActionHandler::check_stateless`].  This method can be called
70    /// before [`Component::begin_block`](crate::Component::begin_block).
71    async fn check_historical<S: StateRead + 'static>(&self, _state: Arc<S>) -> Result<()> {
72        // Default behavior: no-op
73        Ok(())
74    }
75
76    /// Attempts to execute this action against the provided `state`.
77    ///
78    /// This method provides read and write access to the `state`. It is
79    /// fallible, so it's possible to perform checks within the `check_and_execute`
80    /// implementation and abort execution on error; the [`StateTransaction`]
81    /// mechanism ensures that all writes are correctly discarded.
82    ///
83    /// Because `execute` must run sequentially, whenever possible, checks
84    /// should be performed in [`ActionHandler::check_stateless`], or (more carefully) in
85    /// [`ActionHandler::check_historical`].  One example of where this is not
86    /// possible (in fact, the motivating example) is for IBC, where a
87    /// transaction may (1) submit a client update and then (2) relay messages
88    /// valid relative to the newly updated state.  In this case, the checks for
89    /// (2) must be performed during execution, as they depend on the state
90    /// changes written while processing the client update.
91    ///
92    /// However, this data flow pattern should be avoided whenever possible.
93    ///
94    /// # Invariants
95    ///
96    /// This method should only be called after an invocation of
97    /// [`ActionHandler::check_historical`] on the same transaction.  This method
98    /// can be called before [`Component::begin_block`](crate::Component::begin_block).
99    async fn check_and_execute<S: StateWrite>(&self, state: S) -> Result<()>;
100}