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