clementine_core/states/
kickoff.rs

1use std::collections::{HashMap, HashSet};
2
3use bitcoin::{OutPoint, Transaction, Witness};
4use eyre::Context;
5use serde_with::serde_as;
6use statig::prelude::*;
7
8use crate::{
9    bitvm_client::ClementineBitVMPublicKeys,
10    builder::transaction::{
11        input::UtxoVout, remove_txhandler_from_map, ContractContext, TransactionType,
12    },
13    deposit::{DepositData, KickoffData},
14    errors::BridgeError,
15};
16
17use super::{
18    block_cache::BlockCache,
19    context::{Duty, StateContext},
20    matcher::{BlockMatcher, Matcher},
21    Owner, StateMachineError,
22};
23
24/// Events that can be dispatched to the kickoff state machine
25/// These event either trigger state transitions or trigger actions of the owner
26#[derive(
27    Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
28)]
29pub enum KickoffEvent {
30    /// Event that is dispatched when the kickoff is challenged
31    /// This will change the state to "Challenged"
32    Challenged,
33    /// Event that is dispatched when a watchtower challenge is detected in Bitcoin
34    WatchtowerChallengeSent {
35        watchtower_idx: usize,
36        challenge_outpoint: OutPoint,
37    },
38    /// Event that is dispatched when an operator BitVM assert is detected in Bitcoin
39    OperatorAssertSent {
40        assert_idx: usize,
41        assert_outpoint: OutPoint,
42    },
43    /// Event that is dispatched when a watchtower challenge timeout is detected in Bitcoin
44    WatchtowerChallengeTimeoutSent { watchtower_idx: usize },
45    /// Event that is dispatched when an operator challenge ack is detected in Bitcoin
46    /// Operator challenge acks are sent by operators to acknowledge watchtower challenges
47    OperatorChallengeAckSent {
48        watchtower_idx: usize,
49        challenge_ack_outpoint: OutPoint,
50    },
51    /// Event that is dispatched when the latest blockhash is detected in Bitcoin
52    LatestBlockHashSent { latest_blockhash_outpoint: OutPoint },
53    /// Event that is dispatched when the kickoff finalizer is spent in Bitcoin
54    /// Irrespective of whether the kickoff is malicious or not, the kickoff process is finished when the kickoff finalizer is spent.
55    KickoffFinalizerSpent,
56    /// Event that is dispatched when the burn connector is spent in Bitcoin
57    BurnConnectorSpent,
58    /// Vvent that is used to indicate that it is time for the owner to send latest blockhash tx.
59    /// Matcher for this event is created after all watchtower challenge utxos are spent.
60    /// Latest blockhash is sent some blocks after all watchtower challenge utxos are spent, so that the total work until the block commiitted
61    /// in latest blockhash is definitely higher than the highest work in valid watchtower challenges.
62    TimeToSendLatestBlockhash,
63    /// Event that is used to indicate that it is time for the owner to send watchtower challenge tx.
64    /// Watchtower challenges are sent after some blocks pass since the kickoff tx, so that the total work in the watchtower challenge is as high as possible.
65    TimeToSendWatchtowerChallenge,
66    /// Special event that is used to indicate that the state machine has been saved to the database and the dirty flag should be reset to false
67    SavedToDb,
68}
69
70/// State machine for tracking a single kickoff process in the protocol.
71///
72/// # Purpose
73/// The `KickoffStateMachine` manages the lifecycle of a single kickoff process, which is created after a kickoff transaction is detected on Bitcoin. It tracks the transactions related to the kickoff and the resulting data.
74///
75/// # States
76/// - `kickoff_started`: The initial state after a kickoff is detected. Waits for further events such as challenges, but still tracks any committed data on Bitcoin (like latest blockhash, operator asserts, watchtower challenges, etc)
77/// - `challenged`: Entered if the kickoff is challenged. Watchtower challenges are only sent if the kickoff is challenged.
78/// - `closed`: Terminal state indicating the kickoff process has ended, either by kickoff finalizer utxo or burn connector utxo being spent.
79///
80/// # Events
81/// - `Challenged`: The kickoff is challenged, transitioning to the `challenged` state.
82/// - `WatchtowerChallengeSent`: A watchtower challenge is detected on Bitcoin, stores the watchtower challenge transaction, and stores the watchtower utxo as spent.
83/// - `OperatorAssertSent`: An operator BitVM assert is detected, stores the witness of the assert utxo.
84/// - `WatchtowerChallengeTimeoutSent`: A watchtower challenge timeout is detected, stores watchtower utxo as spent.
85/// - `OperatorChallengeAckSent`: An operator challenge acknowledgment is detected, stores the witness of the challenge ack utxo, which holds the revealed preimage that can be used to disprove if the operator maliciously doesn't include the watchtower challenge in the BitVM proof. After sending this transaction, the operator is forced to use the corresponding watchtower challenge in its BitVM proof, otherwise it can be disproven.
86/// - `LatestBlockHashSent`: The latest blockhash is committed on Bitcoin, stores the witness of the latest blockhash utxo, which holds the blockhash that should be used by the operator in its BitVM proof.
87/// - `KickoffFinalizerSpent`: The kickoff finalizer is spent, ending the kickoff process, transitions to the `closed` state.
88/// - `BurnConnectorSpent`: The burn connector is spent, ending the kickoff process, transitions to the `closed` state.
89/// - `TimeToSendWatchtowerChallenge`: Time to send a watchtower challenge (used in challenged state), this event notifies the owner to create and send a watchtower challenge tx. Verifiers wait after a kickoff to send a watchtower challenge so that the total work in the watchtower challenge is as high as possible.
90/// - `SavedToDb`: Indicates the state machine has been persisted and resets the dirty flag.
91///
92/// # Behavior
93/// - The state machine maintains a set of matchers to detect relevant Bitcoin transactions and trigger corresponding events.
94/// - It tracks the progress of the kickoff, including challenges, operator actions, and finalization.
95/// - When terminal events occur (e.g., finalizer or burn connector spent), the state machine transitions to `closed` and clears all matchers.
96/// - The state machine interacts with the owner to perform protocol duties (e.g., sending challenges, asserts, or disproves) as required by the protocol logic.
97///
98/// This design ensures that all protocol-critical events related to a kickoff are tracked and handled in a robust, stateful manner, supporting both normal and adversarial scenarios.
99#[serde_as]
100#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub struct KickoffStateMachine<T: Owner> {
102    /// Maps matchers to the resulting kickoff events.
103    #[serde_as(as = "Vec<(_, _)>")]
104    pub(crate) matchers: HashMap<Matcher, KickoffEvent>,
105    /// Indicates if the state machine has unsaved changes that need to be persisted on db.
106    /// dirty flag is set if any matcher matches the current block.
107    /// the flag is set to true in on_transition and on_dispatch
108    /// the flag is set to false after the state machine is saved to db and the event SavedToDb is dispatched
109    pub(crate) dirty: bool,
110    /// The kickoff data associated with the kickoff being tracked.
111    pub(crate) kickoff_data: KickoffData,
112    /// The deposit data that the kickoff tries to withdraw from.
113    pub(crate) deposit_data: DepositData,
114    /// The block height at which the kickoff transaction was mined.
115    pub(crate) kickoff_height: u32,
116    /// The witness for the kickoff transactions input which is a winternitz signature that commits the payout blockhash.
117    pub(crate) payout_blockhash: Witness,
118    /// Marker to indicate if the state machine is in the challenged state.
119    challenged: bool,
120    /// Set of indices of watchtower UTXOs that have already been spent.
121    spent_watchtower_utxos: HashSet<usize>,
122    /// The witness taken from the transaction spending the latest blockhash utxo.
123    latest_blockhash: Witness,
124    /// Saves watchtower challenges with the watchtower index as the key.
125    /// Watchtower challenges are encoded as the output of the watchtower challenge tx.
126    /// (taproot addresses parsed as 32 bytes + OP_RETURN data), in total 144 bytes.
127    watchtower_challenges: HashMap<usize, Transaction>,
128    /// Saves operator asserts with the index of the assert utxo as the key.
129    /// Operator asserts are witnesses that spend the assert utxo's and contain the winternitz signature of the BitVM assertion.
130    operator_asserts: HashMap<usize, Witness>,
131    /// Saves operator challenge acks with the index of the challenge ack utxo as the key.
132    /// Operator challenge acks are witnesses that spend the challenge ack utxo's.
133    /// The witness contains the revealed preimage that can be used to disprove if the operator
134    /// maliciously doesn't include the watchtower challenge in the BitVM proof.
135    operator_challenge_acks: HashMap<usize, Witness>,
136    /// Marker for the generic owner type (phantom data for type safety).
137    /// This is used to ensure that the state machine is generic over the owner type.
138    phantom: std::marker::PhantomData<T>,
139}
140
141impl<T: Owner> BlockMatcher for KickoffStateMachine<T> {
142    type StateEvent = KickoffEvent;
143
144    fn match_block(&self, block: &BlockCache) -> Vec<Self::StateEvent> {
145        self.matchers
146            .iter()
147            .filter_map(|(matcher, kickoff_event)| {
148                matcher.matches(block).map(|ord| (ord, kickoff_event))
149            })
150            .min()
151            .map(|(_, kickoff_event)| kickoff_event)
152            .into_iter()
153            .cloned()
154            .collect()
155    }
156}
157
158impl<T: Owner> KickoffStateMachine<T> {
159    pub fn new(
160        kickoff_data: KickoffData,
161        kickoff_height: u32,
162        deposit_data: DepositData,
163        payout_blockhash: Witness,
164    ) -> Self {
165        Self {
166            kickoff_data,
167            kickoff_height,
168            deposit_data,
169            payout_blockhash,
170            latest_blockhash: Witness::default(),
171            matchers: HashMap::new(),
172            dirty: true,
173            challenged: false,
174            phantom: std::marker::PhantomData,
175            watchtower_challenges: HashMap::new(),
176            operator_asserts: HashMap::new(),
177            spent_watchtower_utxos: HashSet::new(),
178            operator_challenge_acks: HashMap::new(),
179        }
180    }
181}
182
183#[state_machine(
184    initial = "State::kickoff_started()",
185    on_dispatch = "Self::on_dispatch",
186    on_transition = "Self::on_transition",
187    state(derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize))
188)]
189impl<T: Owner> KickoffStateMachine<T> {
190    #[action]
191    pub(crate) fn on_transition(&mut self, state_a: &State, state_b: &State) {
192        tracing::trace!(?self.kickoff_data, ?self.deposit_data, "Transitioning from {:?} to {:?}", state_a, state_b);
193        self.dirty = true;
194    }
195
196    pub fn kickoff_meta(&self, method: &'static str) -> StateMachineError {
197        eyre::eyre!(
198            "Error in kickoff state machine for kickoff {:?} in {}",
199            self.kickoff_data,
200            method
201        )
202        .into()
203    }
204
205    #[action]
206    pub(crate) fn on_dispatch(
207        &mut self,
208        _state: StateOrSuperstate<'_, '_, Self>,
209        evt: &KickoffEvent,
210    ) {
211        if matches!(evt, KickoffEvent::SavedToDb) {
212            self.dirty = false;
213        } else {
214            tracing::trace!(?self.kickoff_data, "Dispatching event {:?}", evt);
215            self.dirty = true;
216
217            // Remove the matcher corresponding to the event.
218            if let Some((matcher, _)) = self.matchers.iter().find(|(_, ev)| ev == &evt) {
219                let matcher = matcher.clone();
220                self.matchers.remove(&matcher);
221            }
222        }
223    }
224
225    /// Checks if the latest blockhash is ready to be committed on Bitcoin.
226    /// The check is done by checking if all watchtower challenge utxos are spent.
227    /// If the check is successful, the a new matcher is created to send latest blockhash tx after finality depth blocks pass from current block height.
228    async fn create_matcher_for_latest_blockhash_if_ready(
229        &mut self,
230        context: &mut StateContext<T>,
231    ) {
232        context
233            .capture_error(async |context| {
234                {
235                    // if all watchtower challenge utxos are spent, its safe to send latest blockhash commit tx
236                    if self.challenged
237                        && self.spent_watchtower_utxos.len()
238                            == self.deposit_data.get_num_watchtowers()
239                    {
240                        // create a matcher to send latest blockhash tx after finality depth blocks pass from current block height
241                        self.matchers.insert(
242                            Matcher::BlockHeight(
243                                context.cache.block_height
244                                    + context.config.protocol_paramset.finality_depth
245                                    - 1,
246                            ),
247                            KickoffEvent::TimeToSendLatestBlockhash,
248                        );
249                    }
250                    Ok::<(), BridgeError>(())
251                }
252                .wrap_err(self.kickoff_meta("on check_if_time_to_commit_latest_blockhash"))
253            })
254            .await;
255    }
256
257    /// Checks if the disprove is ready to be sent on Bitcoin
258    /// The check is done by checking if all operator asserts are received,
259    /// latest blockhash is committed and all watchtower challenge utxos are spent
260    /// If the check is successful, the disprove is sent on Bitcoin
261    async fn disprove_if_ready(&mut self, context: &mut StateContext<T>) {
262        if self.challenged && self.operator_asserts.len() == ClementineBitVMPublicKeys::number_of_assert_txs()
263            && self.latest_blockhash != Witness::default()
264            && self.spent_watchtower_utxos.len() == self.deposit_data.get_num_watchtowers()
265            // check if all operator acks are received, one ack for each watchtower challenge
266            // to make sure we have all preimages required to disprove if operator didn't include 
267            // the watchtower challenge in the BitVM proof
268            && self.watchtower_challenges.keys().all(|idx| self.operator_challenge_acks.contains_key(idx))
269        {
270            self.send_disprove(context).await;
271        }
272    }
273
274    /// Checks if the operator asserts are ready to be sent on Bitcoin
275    /// The check is done by checking if all watchtower challenge utxos are spent and latest blockhash is committed
276    /// If the check is successful, the operator asserts are sent on Bitcoin
277    async fn send_operator_asserts_if_ready(&mut self, context: &mut StateContext<T>) {
278        context
279            .capture_error(async |context| {
280                {
281                    // if all watchtower challenge utxos are spent and latest blockhash is committed, its safe to send asserts
282                    if self.challenged
283                        && self.spent_watchtower_utxos.len()
284                            == self.deposit_data.get_num_watchtowers()
285                        && self.latest_blockhash != Witness::default()
286                    {
287                        context
288                            .dispatch_duty(Duty::SendOperatorAsserts {
289                                kickoff_data: self.kickoff_data,
290                                deposit_data: self.deposit_data.clone(),
291                                watchtower_challenges: self.watchtower_challenges.clone(),
292                                payout_blockhash: self.payout_blockhash.clone(),
293                                latest_blockhash: self.latest_blockhash.clone(),
294                            })
295                            .await?;
296                    }
297                    Ok::<(), BridgeError>(())
298                }
299                .wrap_err(self.kickoff_meta("on send_operator_asserts"))
300            })
301            .await;
302    }
303
304    async fn send_watchtower_challenge(&mut self, context: &mut StateContext<T>) {
305        context
306            .capture_error(async |context| {
307                {
308                    context
309                        .dispatch_duty(Duty::WatchtowerChallenge {
310                            kickoff_data: self.kickoff_data,
311                            deposit_data: self.deposit_data.clone(),
312                        })
313                        .await?;
314                    Ok::<(), BridgeError>(())
315                }
316                .wrap_err(self.kickoff_meta("on send_watchtower_challenge"))
317            })
318            .await;
319    }
320
321    async fn send_disprove(&mut self, context: &mut StateContext<T>) {
322        context
323            .capture_error(async |context| {
324                {
325                    context
326                        .dispatch_duty(Duty::VerifierDisprove {
327                            kickoff_data: self.kickoff_data,
328                            deposit_data: self.deposit_data.clone(),
329                            operator_asserts: self.operator_asserts.clone(),
330                            operator_acks: self.operator_challenge_acks.clone(),
331                            payout_blockhash: self.payout_blockhash.clone(),
332                            latest_blockhash: self.latest_blockhash.clone(),
333                        })
334                        .await?;
335                    Ok::<(), BridgeError>(())
336                }
337                .wrap_err(self.kickoff_meta("on send_disprove"))
338            })
339            .await;
340    }
341
342    async fn send_latest_blockhash(&mut self, context: &mut StateContext<T>) {
343        context
344            .capture_error(async |context| {
345                {
346                    context
347                        .dispatch_duty(Duty::SendLatestBlockhash {
348                            kickoff_data: self.kickoff_data,
349                            deposit_data: self.deposit_data.clone(),
350                            latest_blockhash: context.cache.block.header.block_hash(),
351                        })
352                        .await?;
353                    Ok::<(), BridgeError>(())
354                }
355                .wrap_err(self.kickoff_meta("on send_latest_blockhash"))
356            })
357            .await;
358    }
359
360    async fn unhandled_event(&mut self, context: &mut StateContext<T>, event: &KickoffEvent) {
361        context
362            .capture_error(async |_context| {
363                let event_str = format!("{event:?}");
364                Err(StateMachineError::UnhandledEvent(event_str))
365                    .wrap_err(self.kickoff_meta("kickoff unhandled event"))
366            })
367            .await;
368    }
369
370    /// If the kickoff is challenged, the state machine will add corresponding matchers for
371    /// sending watchtower challenges after some amount of blocks passes since the kickoff was included in Bitcoin.
372    /// Sending watchtower challenges only happen if the kickoff is challenged.
373    /// As sending latest blockhash commit and asserts depend on watchtower challenges/timeouts being sent,
374    /// they will also not be sent if the kickoff is not challenged and kickoff finalizer is spent with ChallengeTimeout,
375    /// which changes the state to "Closed".
376    #[action]
377    pub(crate) async fn on_challenged_entry(&mut self, context: &mut StateContext<T>) {
378        self.challenged = true;
379        context
380            .capture_error(async |context| {
381                {
382                    // create times to send necessary challenge asserts
383                    self.matchers.insert(
384                        Matcher::BlockHeight(
385                            self.kickoff_height
386                                + context.config.time_to_send_watchtower_challenge as u32,
387                        ),
388                        KickoffEvent::TimeToSendWatchtowerChallenge,
389                    );
390                    Ok::<(), BridgeError>(())
391                }
392                .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
393            })
394            .await;
395        // check if any action is ready to be started as it could already be ready before a challenge arrives
396        // but we want to make sure kickoff is actually challenged before we start sending actions
397        self.create_matcher_for_latest_blockhash_if_ready(context)
398            .await;
399        self.send_operator_asserts_if_ready(context).await;
400        self.disprove_if_ready(context).await;
401    }
402
403    /// State that is entered when the kickoff is challenged
404    /// It only includes special handling for the TimeToSendWatchtowerChallenge event
405    /// All other events are handled in the kickoff superstate
406    #[state(superstate = "kickoff", entry_action = "on_challenged_entry")]
407    pub(crate) async fn challenged(
408        &mut self,
409        event: &KickoffEvent,
410        context: &mut StateContext<T>,
411    ) -> Response<State> {
412        match event {
413            KickoffEvent::WatchtowerChallengeSent { .. }
414            | KickoffEvent::OperatorAssertSent { .. }
415            | KickoffEvent::OperatorChallengeAckSent { .. }
416            | KickoffEvent::KickoffFinalizerSpent
417            | KickoffEvent::BurnConnectorSpent
418            | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
419            | KickoffEvent::LatestBlockHashSent { .. }
420            | KickoffEvent::TimeToSendLatestBlockhash
421            | KickoffEvent::SavedToDb => Super,
422            KickoffEvent::TimeToSendWatchtowerChallenge => {
423                self.send_watchtower_challenge(context).await;
424                Handled
425            }
426            _ => {
427                self.unhandled_event(context, event).await;
428                Handled
429            }
430        }
431    }
432
433    #[superstate]
434    async fn kickoff(
435        &mut self,
436        event: &KickoffEvent,
437        context: &mut StateContext<T>,
438    ) -> Response<State> {
439        tracing::trace!("Received event in kickoff superstate: {:?}", event);
440        match event {
441            // When a watchtower challenge is detected in Bitcoin,
442            // save the full challenge transaction and check if the latest blockhash can be committed
443            // and if the disprove is ready to be sent
444            KickoffEvent::WatchtowerChallengeSent {
445                watchtower_idx,
446                challenge_outpoint,
447            } => {
448                self.spent_watchtower_utxos.insert(*watchtower_idx);
449                let tx = context
450                    .cache
451                    .get_tx_of_utxo(challenge_outpoint)
452                    .expect("Challenge outpoint that got matched should be in block");
453                // save challenge witness
454                self.watchtower_challenges
455                    .insert(*watchtower_idx, tx.clone());
456                self.create_matcher_for_latest_blockhash_if_ready(context)
457                    .await;
458                self.send_operator_asserts_if_ready(context).await;
459                self.disprove_if_ready(context).await;
460                Handled
461            }
462            // When an operator assert is detected in Bitcoin,
463            // save the assert witness (which is the BitVM winternitz commit)
464            // and check if the disprove is ready to be sent
465            KickoffEvent::OperatorAssertSent {
466                assert_idx,
467                assert_outpoint,
468            } => {
469                let witness = context
470                    .cache
471                    .get_witness_of_utxo(assert_outpoint)
472                    .expect("Assert outpoint that got matched should be in block");
473                // save assert witness
474                self.operator_asserts.insert(*assert_idx, witness);
475                self.disprove_if_ready(context).await;
476                Handled
477            }
478            // When an operator challenge ack is detected in Bitcoin,
479            // save the ack witness as the witness includes the revealed preimage that
480            // can be used to disprove if the operator maliciously doesn't include the
481            // watchtower challenge in the BitVM proof
482            KickoffEvent::OperatorChallengeAckSent {
483                watchtower_idx,
484                challenge_ack_outpoint,
485            } => {
486                let witness = context
487                    .cache
488                    .get_witness_of_utxo(challenge_ack_outpoint)
489                    .expect("Challenge ack outpoint that got matched should be in block");
490                // save challenge ack witness
491                self.operator_challenge_acks
492                    .insert(*watchtower_idx, witness);
493                self.disprove_if_ready(context).await;
494                Handled
495            }
496            // When the kickoff finalizer is spent in Bitcoin,
497            // the kickoff process is finished and the state machine will transition to the "Closed" state
498            KickoffEvent::KickoffFinalizerSpent => Transition(State::closed()),
499            // When the burn connector of the operator is spent in Bitcoin, it means the operator cannot continue with any more kickoffs
500            // (unless burn connector is spent by ready to reimburse tx), so the state machine will transition to the "Closed" state
501            KickoffEvent::BurnConnectorSpent => {
502                tracing::error!(
503                    "Burn connector spent before kickoff was finalized for kickoff {:?}",
504                    self.kickoff_data
505                );
506                Transition(State::closed())
507            }
508            // When a watchtower challenge timeout is detected in Bitcoin,
509            // set the watchtower utxo as spent and check if the latest blockhash can be committed
510            KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx } => {
511                self.spent_watchtower_utxos.insert(*watchtower_idx);
512                self.create_matcher_for_latest_blockhash_if_ready(context)
513                    .await;
514                self.send_operator_asserts_if_ready(context).await;
515                self.disprove_if_ready(context).await;
516                Handled
517            }
518            // When the latest blockhash is detected in Bitcoin,
519            // save the witness which includes the blockhash and check if the operator asserts and
520            // disprove tx are ready to be sent
521            KickoffEvent::LatestBlockHashSent {
522                latest_blockhash_outpoint,
523            } => {
524                let witness = context
525                    .cache
526                    .get_witness_of_utxo(latest_blockhash_outpoint)
527                    .expect("Latest blockhash outpoint that got matched should be in block");
528                // save latest blockhash witness
529                self.latest_blockhash = witness;
530                // can start sending asserts as latest blockhash is committed and finalized
531                self.send_operator_asserts_if_ready(context).await;
532                self.disprove_if_ready(context).await;
533                Handled
534            }
535            KickoffEvent::TimeToSendLatestBlockhash => {
536                // tell owner to send latest blockhash tx
537                self.send_latest_blockhash(context).await;
538                Handled
539            }
540            KickoffEvent::SavedToDb => Handled,
541            _ => {
542                self.unhandled_event(context, event).await;
543                Handled
544            }
545        }
546    }
547
548    /// State that is entered when the kickoff is started
549    /// It will transition to the "Challenged" state if the kickoff is challenged
550    #[state(superstate = "kickoff", entry_action = "on_kickoff_started_entry")]
551    pub(crate) async fn kickoff_started(
552        &mut self,
553        event: &KickoffEvent,
554        context: &mut StateContext<T>,
555    ) -> Response<State> {
556        match event {
557            KickoffEvent::Challenged => {
558                tracing::warn!("Warning: Operator challenged: {:?}", self.kickoff_data);
559                Transition(State::challenged())
560            }
561            KickoffEvent::WatchtowerChallengeSent { .. }
562            | KickoffEvent::OperatorAssertSent { .. }
563            | KickoffEvent::OperatorChallengeAckSent { .. }
564            | KickoffEvent::KickoffFinalizerSpent
565            | KickoffEvent::BurnConnectorSpent
566            | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
567            | KickoffEvent::LatestBlockHashSent { .. }
568            | KickoffEvent::TimeToSendLatestBlockhash
569            | KickoffEvent::SavedToDb => Super,
570            _ => {
571                self.unhandled_event(context, event).await;
572                Handled
573            }
574        }
575    }
576
577    /// Adds the default matchers that will be used if the state is "challenged" or "kickoff_started".
578    /// These matchers are used to detect when various transactions in the contract are mined on Bitcoin.
579    async fn add_default_kickoff_matchers(
580        &mut self,
581        context: &mut StateContext<T>,
582    ) -> Result<(), BridgeError> {
583        // First create all transactions for the current deposit
584        let contract_context = ContractContext::new_context_for_kickoff(
585            self.kickoff_data,
586            self.deposit_data.clone(),
587            context.config.protocol_paramset,
588        );
589        let mut txhandlers = {
590            let mut guard = context.shared_dbtx.lock().await;
591            context
592                .owner
593                .create_txhandlers(
594                    &mut guard,
595                    TransactionType::AllNeededForDeposit,
596                    contract_context,
597                )
598                .await?
599        };
600        let kickoff_txhandler =
601            remove_txhandler_from_map(&mut txhandlers, TransactionType::Kickoff)?;
602
603        // add operator asserts
604        let kickoff_txid = *kickoff_txhandler.get_txid();
605        let num_asserts = crate::bitvm_client::ClementineBitVMPublicKeys::number_of_assert_txs();
606        for assert_idx in 0..num_asserts {
607            let mini_assert_vout = UtxoVout::Assert(assert_idx).get_vout();
608            let assert_timeout_txhandler = remove_txhandler_from_map(
609                &mut txhandlers,
610                TransactionType::AssertTimeout(assert_idx),
611            )?;
612            let assert_timeout_txid = assert_timeout_txhandler.get_txid();
613            // Assert transactions can have any txid (there is no enforcement on how the assert utxo is spent, just that
614            // spending assert utxo reveals the BitVM winternitz commit in the utxo's witness)
615            // But assert timeouts are nofn signed transactions with a fixed txid, so we can detect assert transactions
616            // by checking if the assert utxo is spent but not by the assert timeout tx
617            self.matchers.insert(
618                Matcher::SpentUtxoButNotTxid(
619                    OutPoint {
620                        txid: kickoff_txid,
621                        vout: mini_assert_vout,
622                    },
623                    vec![*assert_timeout_txid],
624                ),
625                KickoffEvent::OperatorAssertSent {
626                    assert_outpoint: OutPoint {
627                        txid: kickoff_txid,
628                        vout: mini_assert_vout,
629                    },
630                    assert_idx,
631                },
632            );
633        }
634        // add latest blockhash tx sent matcher
635        let latest_blockhash_timeout_txhandler =
636            remove_txhandler_from_map(&mut txhandlers, TransactionType::LatestBlockhashTimeout)?;
637        let latest_blockhash_timeout_txid = latest_blockhash_timeout_txhandler.get_txid();
638        let latest_blockhash_outpoint = OutPoint {
639            txid: kickoff_txid,
640            vout: UtxoVout::LatestBlockhash.get_vout(),
641        };
642        // Same logic as before with assert transaction detection, if latest blockhash utxo is not spent by latest blockhash timeout tx,
643        // it means the latest blockhash is committed on Bitcoin
644        self.matchers.insert(
645            Matcher::SpentUtxoButNotTxid(
646                latest_blockhash_outpoint,
647                vec![*latest_blockhash_timeout_txid],
648            ),
649            KickoffEvent::LatestBlockHashSent {
650                latest_blockhash_outpoint,
651            },
652        );
653        // add watchtower challenges and challenge acks matchers
654        for watchtower_idx in 0..self.deposit_data.get_num_watchtowers() {
655            let watchtower_challenge_vout =
656                UtxoVout::WatchtowerChallenge(watchtower_idx).get_vout();
657            let watchtower_timeout_txhandler = remove_txhandler_from_map(
658                &mut txhandlers,
659                TransactionType::WatchtowerChallengeTimeout(watchtower_idx),
660            )?;
661            let watchtower_timeout_txid = watchtower_timeout_txhandler.get_txid();
662            // matcher in case watchtower challenge timeout is sent
663            self.matchers.insert(
664                Matcher::SentTx(*watchtower_timeout_txid),
665                KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx },
666            );
667            // matcher in case watchtower challenge is sent (watchtower challenge utxo is spent but not by watchtower challenge timeout tx)
668            self.matchers.insert(
669                Matcher::SpentUtxoButNotTxid(
670                    OutPoint {
671                        txid: kickoff_txid,
672                        vout: watchtower_challenge_vout,
673                    },
674                    vec![*watchtower_timeout_txid],
675                ),
676                KickoffEvent::WatchtowerChallengeSent {
677                    watchtower_idx,
678                    challenge_outpoint: OutPoint {
679                        txid: kickoff_txid,
680                        vout: watchtower_challenge_vout,
681                    },
682                },
683            );
684            // add operator challenge ack matcher
685            let operator_challenge_ack_vout =
686                UtxoVout::WatchtowerChallengeAck(watchtower_idx).get_vout();
687            let operator_challenge_nack_txhandler = remove_txhandler_from_map(
688                &mut txhandlers,
689                TransactionType::OperatorChallengeNack(watchtower_idx),
690            )?;
691            let operator_challenge_nack_txid = operator_challenge_nack_txhandler.get_txid();
692            // operator challenge ack utxo is spent but not by operator challenge nack tx or watchtower challenge timeout tx
693            self.matchers.insert(
694                Matcher::SpentUtxoButNotTxid(
695                    OutPoint {
696                        txid: kickoff_txid,
697                        vout: operator_challenge_ack_vout,
698                    },
699                    vec![*operator_challenge_nack_txid, *watchtower_timeout_txid],
700                ),
701                KickoffEvent::OperatorChallengeAckSent {
702                    watchtower_idx,
703                    challenge_ack_outpoint: OutPoint {
704                        txid: kickoff_txid,
705                        vout: operator_challenge_ack_vout,
706                    },
707                },
708            );
709        }
710
711        // add burn connector tx spent matcher
712        // Burn connector can also be spent in ready to reimburse tx, but before spending burn connector that way,
713        // the kickoff finalizer needs to be spent first, otherwise pre-signed "Kickoff not finalized" tx can be sent by
714        // any verifier, slashing the operator.
715        // If the kickoff finalizer is spent first, the state will be in "Closed" state and all matchers will be deleted.
716        let round_txhandler = remove_txhandler_from_map(&mut txhandlers, TransactionType::Round)?;
717        let round_txid = *round_txhandler.get_txid();
718        self.matchers.insert(
719            Matcher::SpentUtxo(OutPoint {
720                txid: round_txid,
721                vout: UtxoVout::CollateralInRound.get_vout(),
722            }),
723            KickoffEvent::BurnConnectorSpent,
724        );
725        // add kickoff finalizer utxo spent matcher
726        self.matchers.insert(
727            Matcher::SpentUtxo(OutPoint {
728                txid: kickoff_txid,
729                vout: UtxoVout::KickoffFinalizer.get_vout(),
730            }),
731            KickoffEvent::KickoffFinalizerSpent,
732        );
733        // add challenge detector matcher, if challenge utxo is not spent by challenge timeout tx, it means the kickoff is challenged
734        let challenge_timeout_txhandler =
735            remove_txhandler_from_map(&mut txhandlers, TransactionType::ChallengeTimeout)?;
736        let challenge_timeout_txid = challenge_timeout_txhandler.get_txid();
737        self.matchers.insert(
738            Matcher::SpentUtxoButNotTxid(
739                OutPoint {
740                    txid: kickoff_txid,
741                    vout: UtxoVout::Challenge.get_vout(),
742                },
743                vec![*challenge_timeout_txid],
744            ),
745            KickoffEvent::Challenged,
746        );
747        Ok(())
748    }
749
750    #[action]
751    pub(crate) async fn on_kickoff_started_entry(&mut self, context: &mut StateContext<T>) {
752        context
753            .capture_error(async |context| {
754                {
755                    // Add all watchtower challenges and operator asserts to matchers
756                    self.add_default_kickoff_matchers(context).await?;
757                    Ok::<(), BridgeError>(())
758                }
759                .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
760            })
761            .await;
762    }
763
764    /// Clears all matchers when the state is "closed".
765    /// This means the state machine will not do any more actions anymore.
766    #[action]
767    #[allow(unused_variables)]
768    pub(crate) async fn on_closed_entry(&mut self, context: &mut StateContext<T>) {
769        self.matchers.clear();
770    }
771
772    #[state(entry_action = "on_closed_entry")]
773    // Terminal state when the kickoff process ends
774    #[allow(unused_variables)]
775    pub(crate) async fn closed(
776        &mut self,
777        event: &KickoffEvent,
778        context: &mut StateContext<T>,
779    ) -> Response<State> {
780        Handled
781    }
782}