clementine_core/states/
context.rs

1use crate::config::BridgeConfig;
2use crate::database::DatabaseTransaction;
3use crate::deposit::{DepositData, KickoffData};
4use crate::utils::NamedEntity;
5use clementine_primitives::RoundIndex;
6
7use bitcoin::BlockHash;
8use bitcoin::Transaction;
9use bitcoin::Txid;
10use bitcoin::Witness;
11use bitcoin::XOnlyPublicKey;
12use statig::awaitable::InitializedStateMachine;
13use tokio::sync::Mutex;
14use tonic::async_trait;
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use crate::builder::transaction::TxHandler;
20
21use std::collections::BTreeMap;
22
23use crate::builder::transaction::ContractContext;
24
25use clementine_primitives::TransactionType;
26
27use clementine_errors::BridgeError;
28
29use std::collections::HashSet;
30
31use super::block_cache;
32use super::kickoff;
33use super::round;
34
35#[derive(Debug, Clone)]
36/// Duties are notifications that are sent to the owner (verifier or operator) of the state machine to notify them on changes to the current
37/// contract state that require action.
38/// Note that for all kickoff state duties, they are only sent if withdrawal process is still going on, meaning the burn connector and
39/// kickoff finalizer is still on-chain/unspent.
40pub enum Duty {
41    /// -- Round state duties --
42    /// This duty is sent after a new ready to reimburse tx is sent by the corresponding operator.
43    /// used_kickoffs is a set of kickoff indexes that have been used in the previous round.
44    /// If there are unspent kickoffs, the owner can send a unspent kickoff connector tx.
45    NewReadyToReimburse {
46        round_idx: RoundIndex,
47        operator_xonly_pk: XOnlyPublicKey,
48        used_kickoffs: HashSet<usize>,
49    },
50    /// This duty is sent after a kickoff utxo is spent by the operator.
51    /// It includes the txid in which the utxo was spent, so that the owner can verify if this is an actual kickoff sent by operator.
52    /// Witness is also sent as if tx is an actual kickoff, the witness includes payout blockhash.
53    CheckIfKickoff {
54        txid: Txid,
55        block_height: u32,
56        witness: Witness,
57    },
58    /// This duty is sent after a kickoff is detected and the LCP of the block height that contains the kickoff is processed.
59    /// It includes the relevant data so that the owner can check if the kickoff is malicious.
60    /// Only verifiers check if the kickoff is malicious,
61    CheckIfKickoffMalicious {
62        kickoff_witness: Witness,
63        deposit_data: DepositData,
64        kickoff_data: KickoffData,
65        /// challenged_before indicates if another kickoff belonging to the same round was challenged before, because only one challenge is needed per round.
66        challenged_before: bool,
67    },
68    /// This duty is sent when a new kickoff state machine starts.
69    /// It notifies the owner to add necessary transactions for the kickoff to the tx sender.
70    /// For verifiers it doesn't do anything as they don't need to add any transactions unless the kickoff is challenged.
71    /// For operators, this queues transactions like ChallengeTimeout.
72    AddNecessaryTxsForKickoff {
73        kickoff_data: KickoffData,
74        deposit_data: DepositData,
75    },
76    /// This duty is sent after a kickoff is detected to be challenged.
77    /// It includes the kickoff data so that the owner can add the relevant txs to the tx sender.
78    AddRelevantTxsToTxSenderIfChallenged {
79        kickoff_data: KickoffData,
80        deposit_data: DepositData,
81    },
82    /// -- Kickoff state duties --
83    /// This duty is only sent if a kickoff was challenged.
84    /// This duty is sent after some time (config.time_to_send_watchtower_challenge number of blocks) passes after a kickoff was sent to chain.
85    /// It denotes to the owner that it is time to send a watchtower challenge to the corresponding kickoff.
86    WatchtowerChallenge {
87        kickoff_data: KickoffData,
88        deposit_data: DepositData,
89    },
90    /// This duty is only sent if a kickoff was challenged.
91    /// This duty is sent only after latest blockhash is committed. Latest blockhash is committed after all watchtower challenges are sent
92    /// or timed out so that it is certain no new watchtower challenges can be sent.
93    /// The duty denotes that it is time to start sending operator asserts to the corresponding kickoff.
94    /// It includes the all watchtower challenges and the payout blockhash so that they can be used in the proof.
95    SendOperatorAsserts {
96        kickoff_data: KickoffData,
97        deposit_data: DepositData,
98        watchtower_challenges: HashMap<usize, Transaction>,
99        payout_blockhash: Witness,
100        latest_blockhash: Witness,
101    },
102    /// This duty is only sent if a kickoff was challenged.
103    /// This duty is sent after all asserts and latest blockhash commit are finalized on chain, and all watchtower challenge
104    /// utxos are spent.
105    /// It denotes to the owner that it is time to send a disprove to the corresponding kickoff.
106    /// It includes the operator asserts, operator acks and the payout blockhash so that they can be used in the disprove tx if the proof
107    /// is invalid.
108    VerifierDisprove {
109        kickoff_data: KickoffData,
110        deposit_data: DepositData,
111        operator_asserts: HashMap<usize, Witness>,
112        operator_acks: HashMap<usize, Witness>,
113        payout_blockhash: Witness,
114        latest_blockhash: Witness,
115    },
116    /// This duty is only sent if a kickoff was challenged.
117    /// This duty is sent after every watchtower challenge is either sent or timed out.
118    /// It denotes to the owner that it is time to send a latest blockhash to the corresponding kickoff to be used in the proof.
119    SendLatestBlockhash {
120        kickoff_data: KickoffData,
121        deposit_data: DepositData,
122        latest_blockhash: BlockHash,
123    },
124}
125
126/// Result of handling a duty
127#[derive(Debug, Clone)]
128pub enum DutyResult {
129    /// Duty was handled, no return value is necessary
130    Handled,
131    /// Result of checking if a kickoff contains if a challenge was sent because the kickoff was determined as malicious
132    CheckIfKickoffMalicious { challenged: bool },
133}
134
135/// Owner trait with async handling and tx handler creation
136#[async_trait]
137pub trait Owner: Clone + NamedEntity {
138    /// Handle a protocol-related duty
139    async fn handle_duty(
140        &self,
141        dbtx: DatabaseTransaction<'_>,
142        duty: Duty,
143    ) -> Result<DutyResult, BridgeError>;
144
145    /// Create the transactions for an instance of the L1 contract
146    async fn create_txhandlers(
147        &self,
148        dbtx: DatabaseTransaction<'_>,
149        tx_type: TransactionType,
150        contract_context: ContractContext,
151    ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError>;
152
153    /// Check if a kickoff is relevant for the owner
154    /// For verifiers, all kickoffs are relevant
155    /// For operators, only kickoffs of their own are relevant, which will be checked by a trait fn override
156    fn is_kickoff_relevant_for_owner(&self, _kickoff_data: &KickoffData) -> bool {
157        true
158    }
159}
160
161/// Context for the state machine
162/// Every state can access the context
163#[derive(Debug, Clone)]
164pub struct StateContext<T: Owner> {
165    pub owner: Arc<T>,
166    pub cache: Arc<block_cache::BlockCache>,
167    pub new_round_machines: Vec<InitializedStateMachine<round::RoundStateMachine<T>>>,
168    pub new_kickoff_machines: Vec<InitializedStateMachine<kickoff::KickoffStateMachine<T>>>,
169    pub errors: Vec<Arc<eyre::Report>>,
170    pub config: BridgeConfig,
171    pub owner_type: String,
172    pub shared_dbtx: Arc<Mutex<sqlx::Transaction<'static, sqlx::Postgres>>>,
173}
174
175impl<T: Owner> StateContext<T> {
176    pub fn new(
177        shared_dbtx: Arc<Mutex<sqlx::Transaction<'static, sqlx::Postgres>>>,
178        owner: Arc<T>,
179        cache: Arc<block_cache::BlockCache>,
180        config: BridgeConfig,
181    ) -> Self {
182        // Get the owner type string from the owner instance
183        let owner_type = T::ENTITY_NAME.to_string();
184
185        Self {
186            shared_dbtx,
187            owner,
188            cache,
189            new_round_machines: Vec::new(),
190            new_kickoff_machines: Vec::new(),
191            errors: Vec::new(),
192            config,
193            owner_type,
194        }
195    }
196
197    pub async fn dispatch_duty(&self, duty: Duty) -> Result<DutyResult, BridgeError> {
198        let mut guard = self.shared_dbtx.lock().await;
199        self.owner.handle_duty(&mut guard, duty).await
200    }
201
202    /// Run an async closure and capture any errors in execution.
203    ///
204    /// It will store the error report in the context's `errors` field. The
205    /// errors are later collected by the state manager and reported. This
206    /// ensures that all errors are collected and reported in a single place.
207    /// In general, it's expected that the closure attaches context about the
208    /// state machine to the error report.  You may check
209    /// `KickoffStateMachine::wrap_err` and `RoundStateMachine::wrap_err`
210    /// for an example implementation of an error wrapper utility function.
211    ///
212    /// # Parameters
213    /// * `fnc`: An async closure that takes a mutable reference to the state context and returns a result.
214    ///
215    /// # Returns
216    /// * `()`
217    pub async fn capture_error(
218        &mut self,
219        fnc: impl AsyncFnOnce(&mut Self) -> Result<(), eyre::Report>,
220    ) {
221        let result = fnc(self).await;
222        if let Err(e) = result {
223            self.errors.push(e.into());
224        }
225    }
226}