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 challenged_before: bool,
58 },
59 /// This duty is sent after a kickoff is detected and added to the state manager.
60 /// It includes the kickoff data so that the owner can add the relevant txs to the tx sender.
61 AddRelevantTxsToTxSender {
62 kickoff_data: KickoffData,
63 deposit_data: DepositData,
64 },
65 /// -- Kickoff state duties --
66 /// This duty is only sent if a kickoff was challenged.
67 /// This duty is sent after some time (config.time_to_send_watchtower_challenge number of blocks) passes after a kickoff was sent to chain.
68 /// It denotes to the owner that it is time to send a watchtower challenge to the corresponding kickoff.
69 WatchtowerChallenge {
70 kickoff_data: KickoffData,
71 deposit_data: DepositData,
72 },
73 /// This duty is only sent if a kickoff was challenged.
74 /// This duty is sent only after latest blockhash is committed. Latest blockhash is committed after all watchtower challenges are sent
75 /// or timed out so that it is certain no new watchtower challenges can be sent.
76 /// The duty denotes that it is time to start sending operator asserts to the corresponding kickoff.
77 /// It includes the all watchtower challenges and the payout blockhash so that they can be used in the proof.
78 SendOperatorAsserts {
79 kickoff_data: KickoffData,
80 deposit_data: DepositData,
81 watchtower_challenges: HashMap<usize, Transaction>,
82 payout_blockhash: Witness,
83 latest_blockhash: Witness,
84 },
85 /// This duty is only sent if a kickoff was challenged.
86 /// This duty is sent after all asserts and latest blockhash commit are finalized on chain, and all watchtower challenge
87 /// utxos are spent.
88 /// It denotes to the owner that it is time to send a disprove to the corresponding kickoff.
89 /// It includes the operator asserts, operator acks and the payout blockhash so that they can be used in the disprove tx if the proof
90 /// is invalid.
91 VerifierDisprove {
92 kickoff_data: KickoffData,
93 deposit_data: DepositData,
94 operator_asserts: HashMap<usize, Witness>,
95 operator_acks: HashMap<usize, Witness>,
96 payout_blockhash: Witness,
97 latest_blockhash: Witness,
98 },
99 /// This duty is only sent if a kickoff was challenged.
100 /// This duty is sent after every watchtower challenge is either sent or timed out.
101 /// It denotes to the owner that it is time to send a latest blockhash to the corresponding kickoff to be used in the proof.
102 SendLatestBlockhash {
103 kickoff_data: KickoffData,
104 deposit_data: DepositData,
105 latest_blockhash: BlockHash,
106 },
107}
108
109/// Result of handling a duty
110#[derive(Debug, Clone)]
111pub enum DutyResult {
112 /// Duty was handled, no return value is necessary
113 Handled,
114 /// Result of checking if a kickoff contains if a challenge was sent because the kickoff was determined as malicious
115 CheckIfKickoff { challenged: bool },
116}
117
118/// Owner trait with async handling and tx handler creation
119#[async_trait]
120pub trait Owner: Clone + NamedEntity {
121 /// Handle a protocol-related duty
122 async fn handle_duty(
123 &self,
124 dbtx: DatabaseTransaction<'_>,
125 duty: Duty,
126 ) -> Result<DutyResult, BridgeError>;
127
128 /// Create the transactions for an instance of the L1 contract
129 async fn create_txhandlers(
130 &self,
131 dbtx: DatabaseTransaction<'_>,
132 tx_type: TransactionType,
133 contract_context: ContractContext,
134 ) -> Result<BTreeMap<TransactionType, TxHandler>, BridgeError>;
135
136 /// Handle a new finalized block
137 async fn handle_finalized_block(
138 &self,
139 dbtx: DatabaseTransaction<'_>,
140 block_id: u32,
141 block_height: u32,
142 block_cache: Arc<block_cache::BlockCache>,
143 _light_client_proof_wait_interval_secs: Option<u32>,
144 ) -> Result<(), BridgeError>;
145
146 /// Check if a kickoff is relevant for the owner
147 /// For verifiers, all kickoffs are relevant
148 /// For operators, only kickoffs of their own are relevant, which will be checked by a trait fn override
149 fn is_kickoff_relevant_for_owner(&self, _kickoff_data: &KickoffData) -> bool {
150 true
151 }
152}
153
154/// Context for the state machine
155/// Every state can access the context
156#[derive(Debug, Clone)]
157pub struct StateContext<T: Owner> {
158 pub owner: Arc<T>,
159 pub cache: Arc<block_cache::BlockCache>,
160 pub new_round_machines: Vec<InitializedStateMachine<round::RoundStateMachine<T>>>,
161 pub new_kickoff_machines: Vec<InitializedStateMachine<kickoff::KickoffStateMachine<T>>>,
162 pub errors: Vec<Arc<eyre::Report>>,
163 pub config: BridgeConfig,
164 pub owner_type: String,
165 pub shared_dbtx: Arc<Mutex<sqlx::Transaction<'static, sqlx::Postgres>>>,
166}
167
168impl<T: Owner> StateContext<T> {
169 pub fn new(
170 shared_dbtx: Arc<Mutex<sqlx::Transaction<'static, sqlx::Postgres>>>,
171 owner: Arc<T>,
172 cache: Arc<block_cache::BlockCache>,
173 config: BridgeConfig,
174 ) -> Self {
175 // Get the owner type string from the owner instance
176 let owner_type = T::ENTITY_NAME.to_string();
177
178 Self {
179 shared_dbtx,
180 owner,
181 cache,
182 new_round_machines: Vec::new(),
183 new_kickoff_machines: Vec::new(),
184 errors: Vec::new(),
185 config,
186 owner_type,
187 }
188 }
189
190 pub async fn dispatch_duty(&self, duty: Duty) -> Result<DutyResult, BridgeError> {
191 let mut guard = self.shared_dbtx.lock().await;
192 self.owner.handle_duty(&mut guard, duty).await
193 }
194
195 /// Run an async closure and capture any errors in execution.
196 ///
197 /// It will store the error report in the context's `errors` field. The
198 /// errors are later collected by the state manager and reported. This
199 /// ensures that all errors are collected and reported in a single place.
200 /// In general, it's expected that the closure attaches context about the
201 /// state machine to the error report. You may check
202 /// `KickoffStateMachine::wrap_err` and `RoundStateMachine::wrap_err`
203 /// for an example implementation of an error wrapper utility function.
204 ///
205 /// # Parameters
206 /// * `fnc`: An async closure that takes a mutable reference to the state context and returns a result.
207 ///
208 /// # Returns
209 /// * `()`
210 pub async fn capture_error(
211 &mut self,
212 fnc: impl AsyncFnOnce(&mut Self) -> Result<(), eyre::Report>,
213 ) {
214 let result = fnc(self).await;
215 if let Err(e) = result {
216 self.errors.push(e.into());
217 }
218 }
219}