clementine_core/states/
kickoff.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
use std::collections::{HashMap, HashSet};

use bitcoin::{OutPoint, Transaction, Witness};
use eyre::Context;
use serde_with::serde_as;
use statig::prelude::*;

use crate::{
    bitvm_client::ClementineBitVMPublicKeys,
    builder::transaction::{
        input::UtxoVout, remove_txhandler_from_map, ContractContext, TransactionType,
    },
    deposit::{DepositData, KickoffData},
    errors::BridgeError,
};

use super::{
    block_cache::BlockCache,
    context::{Duty, StateContext},
    matcher::{BlockMatcher, Matcher},
    Owner, StateMachineError,
};

/// Events that can be dispatched to the kickoff state machine
/// These event either trigger state transitions or trigger actions of the owner
#[derive(
    Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
)]
pub enum KickoffEvent {
    /// Event that is dispatched when the kickoff is challenged
    /// This will change the state to "Challenged"
    Challenged,
    /// Event that is dispatched when a watchtower challenge is detected in Bitcoin
    WatchtowerChallengeSent {
        watchtower_idx: usize,
        challenge_outpoint: OutPoint,
    },
    /// Event that is dispatched when an operator BitVM assert is detected in Bitcoin
    OperatorAssertSent {
        assert_idx: usize,
        assert_outpoint: OutPoint,
    },
    /// Event that is dispatched when a watchtower challenge timeout is detected in Bitcoin
    WatchtowerChallengeTimeoutSent { watchtower_idx: usize },
    /// Event that is dispatched when an operator challenge ack is detected in Bitcoin
    /// Operator challenge acks are sent by operators to acknowledge watchtower challenges
    OperatorChallengeAckSent {
        watchtower_idx: usize,
        challenge_ack_outpoint: OutPoint,
    },
    /// Event that is dispatched when the latest blockhash is detected in Bitcoin
    LatestBlockHashSent { latest_blockhash_outpoint: OutPoint },
    /// Event that is dispatched when the kickoff finalizer is spent in Bitcoin
    /// Irrespective of whether the kickoff is malicious or not, the kickoff process is finished when the kickoff finalizer is spent.
    KickoffFinalizerSpent,
    /// Event that is dispatched when the burn connector is spent in Bitcoin
    BurnConnectorSpent,
    /// Vvent that is used to indicate that it is time for the owner to send latest blockhash tx.
    /// Matcher for this event is created after all watchtower challenge utxos are spent.
    /// Latest blockhash is sent some blocks after all watchtower challenge utxos are spent, so that the total work until the block commiitted
    /// in latest blockhash is definitely higher than the highest work in valid watchtower challenges.
    TimeToSendLatestBlockhash,
    /// Event that is used to indicate that it is time for the owner to send watchtower challenge tx.
    /// 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.
    TimeToSendWatchtowerChallenge,
    /// 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
    SavedToDb,
}

/// State machine for tracking a single kickoff process in the protocol.
///
/// # Purpose
/// 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.
///
/// # States
/// - `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)
/// - `challenged`: Entered if the kickoff is challenged. Watchtower challenges are only sent if the kickoff is challenged.
/// - `closed`: Terminal state indicating the kickoff process has ended, either by kickoff finalizer utxo or burn connector utxo being spent.
///
/// # Events
/// - `Challenged`: The kickoff is challenged, transitioning to the `challenged` state.
/// - `WatchtowerChallengeSent`: A watchtower challenge is detected on Bitcoin, stores the watchtower challenge transaction, and stores the watchtower utxo as spent.
/// - `OperatorAssertSent`: An operator BitVM assert is detected, stores the witness of the assert utxo.
/// - `WatchtowerChallengeTimeoutSent`: A watchtower challenge timeout is detected, stores watchtower utxo as spent.
/// - `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.
/// - `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.
/// - `KickoffFinalizerSpent`: The kickoff finalizer is spent, ending the kickoff process, transitions to the `closed` state.
/// - `BurnConnectorSpent`: The burn connector is spent, ending the kickoff process, transitions to the `closed` state.
/// - `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.
/// - `SavedToDb`: Indicates the state machine has been persisted and resets the dirty flag.
///
/// # Behavior
/// - The state machine maintains a set of matchers to detect relevant Bitcoin transactions and trigger corresponding events.
/// - It tracks the progress of the kickoff, including challenges, operator actions, and finalization.
/// - When terminal events occur (e.g., finalizer or burn connector spent), the state machine transitions to `closed` and clears all matchers.
/// - The state machine interacts with the owner to perform protocol duties (e.g., sending challenges, asserts, or disproves) as required by the protocol logic.
///
/// 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.
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct KickoffStateMachine<T: Owner> {
    /// Maps matchers to the resulting kickoff events.
    #[serde_as(as = "Vec<(_, _)>")]
    pub(crate) matchers: HashMap<Matcher, KickoffEvent>,
    /// Indicates if the state machine has unsaved changes that need to be persisted on db.
    /// dirty flag is set if any matcher matches the current block.
    /// the flag is set to true in on_transition and on_dispatch
    /// the flag is set to false after the state machine is saved to db and the event SavedToDb is dispatched
    pub(crate) dirty: bool,
    /// The kickoff data associated with the kickoff being tracked.
    pub(crate) kickoff_data: KickoffData,
    /// The deposit data that the kickoff tries to withdraw from.
    pub(crate) deposit_data: DepositData,
    /// The block height at which the kickoff transaction was mined.
    pub(crate) kickoff_height: u32,
    /// The witness for the kickoff transactions input which is a winternitz signature that commits the payout blockhash.
    pub(crate) payout_blockhash: Witness,
    /// Set of indices of watchtower UTXOs that have already been spent.
    spent_watchtower_utxos: HashSet<usize>,
    /// The witness taken from the transaction spending the latest blockhash utxo.
    latest_blockhash: Witness,
    /// Saves watchtower challenges with the watchtower index as the key.
    /// Watchtower challenges are encoded as the output of the watchtower challenge tx.
    /// (taproot addresses parsed as 32 bytes + OP_RETURN data), in total 144 bytes.
    watchtower_challenges: HashMap<usize, Transaction>,
    /// Saves operator asserts with the index of the assert utxo as the key.
    /// Operator asserts are witnesses that spend the assert utxo's and contain the winternitz signature of the BitVM assertion.
    operator_asserts: HashMap<usize, Witness>,
    /// Saves operator challenge acks with the index of the challenge ack utxo as the key.
    /// Operator challenge acks are witnesses that spend the challenge ack utxo's.
    /// The witness contains the revealed preimage that can be used to disprove if the operator
    /// maliciously doesn't include the watchtower challenge in the BitVM proof.
    operator_challenge_acks: HashMap<usize, Witness>,
    /// Marker for the generic owner type (phantom data for type safety).
    /// This is used to ensure that the state machine is generic over the owner type.
    phantom: std::marker::PhantomData<T>,
}

impl<T: Owner> BlockMatcher for KickoffStateMachine<T> {
    type StateEvent = KickoffEvent;

    fn match_block(&self, block: &BlockCache) -> Vec<Self::StateEvent> {
        self.matchers
            .iter()
            .filter_map(|(matcher, kickoff_event)| {
                matcher.matches(block).map(|ord| (ord, kickoff_event))
            })
            .min()
            .map(|(_, kickoff_event)| kickoff_event)
            .into_iter()
            .cloned()
            .collect()
    }
}

impl<T: Owner> KickoffStateMachine<T> {
    pub fn new(
        kickoff_data: KickoffData,
        kickoff_height: u32,
        deposit_data: DepositData,
        payout_blockhash: Witness,
    ) -> Self {
        Self {
            kickoff_data,
            kickoff_height,
            deposit_data,
            payout_blockhash,
            latest_blockhash: Witness::default(),
            matchers: HashMap::new(),
            dirty: true,
            phantom: std::marker::PhantomData,
            watchtower_challenges: HashMap::new(),
            operator_asserts: HashMap::new(),
            spent_watchtower_utxos: HashSet::new(),
            operator_challenge_acks: HashMap::new(),
        }
    }
}

#[state_machine(
    initial = "State::kickoff_started()",
    on_dispatch = "Self::on_dispatch",
    on_transition = "Self::on_transition",
    state(derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize))
)]
impl<T: Owner> KickoffStateMachine<T> {
    #[action]
    pub(crate) fn on_transition(&mut self, state_a: &State, state_b: &State) {
        tracing::trace!(?self.kickoff_data, ?self.deposit_data, "Transitioning from {:?} to {:?}", state_a, state_b);
        self.dirty = true;
    }

    pub fn kickoff_meta(&self, method: &'static str) -> StateMachineError {
        eyre::eyre!(
            "Error in kickoff state machine for kickoff {:?} in {}",
            self.kickoff_data,
            method
        )
        .into()
    }

    #[action]
    pub(crate) fn on_dispatch(
        &mut self,
        _state: StateOrSuperstate<'_, '_, Self>,
        evt: &KickoffEvent,
    ) {
        if matches!(evt, KickoffEvent::SavedToDb) {
            self.dirty = false;
        } else {
            tracing::trace!(?self.kickoff_data, "Dispatching event {:?}", evt);
            self.dirty = true;

            // Remove the matcher corresponding to the event.
            if let Some((matcher, _)) = self.matchers.iter().find(|(_, ev)| ev == &evt) {
                let matcher = matcher.clone();
                self.matchers.remove(&matcher);
            }
        }
    }

    /// Checks if the latest blockhash is ready to be committed on Bitcoin.
    /// The check is done by checking if all watchtower challenge utxos are spent.
    /// 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.
    async fn create_matcher_for_latest_blockhash_if_ready(
        &mut self,
        context: &mut StateContext<T>,
    ) {
        context
            .capture_error(async |context| {
                {
                    // if all watchtower challenge utxos are spent, its safe to send latest blockhash commit tx
                    if self.spent_watchtower_utxos.len() == self.deposit_data.get_num_watchtowers()
                    {
                        // create a matcher to send latest blockhash tx after finality depth blocks pass from current block height
                        self.matchers.insert(
                            Matcher::BlockHeight(
                                context.cache.block_height + context.paramset.finality_depth,
                            ),
                            KickoffEvent::TimeToSendLatestBlockhash,
                        );
                    }
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on check_if_time_to_commit_latest_blockhash"))
            })
            .await;
    }

    /// Checks if the disprove is ready to be sent on Bitcoin
    /// The check is done by checking if all operator asserts are received,
    /// latest blockhash is committed and all watchtower challenge utxos are spent
    /// If the check is successful, the disprove is sent on Bitcoin
    async fn disprove_if_ready(&mut self, context: &mut StateContext<T>) {
        if self.operator_asserts.len() == ClementineBitVMPublicKeys::number_of_assert_txs()
            && self.latest_blockhash != Witness::default()
            && self.spent_watchtower_utxos.len() == self.deposit_data.get_num_watchtowers()
        {
            self.send_disprove(context).await;
        }
    }

    /// Checks if the operator asserts are ready to be sent on Bitcoin
    /// The check is done by checking if all watchtower challenge utxos are spent and latest blockhash is committed
    /// If the check is successful, the operator asserts are sent on Bitcoin
    async fn send_operator_asserts_if_ready(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    // if all watchtower challenge utxos are spent and latest blockhash is committed, its safe to send asserts
                    if self.spent_watchtower_utxos.len() == self.deposit_data.get_num_verifiers()
                        && self.latest_blockhash != Witness::default()
                    {
                        context
                            .owner
                            .handle_duty(Duty::SendOperatorAsserts {
                                kickoff_data: self.kickoff_data,
                                deposit_data: self.deposit_data.clone(),
                                watchtower_challenges: self.watchtower_challenges.clone(),
                                payout_blockhash: self.payout_blockhash.clone(),
                                latest_blockhash: self.latest_blockhash.clone(),
                            })
                            .await?;
                    }
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on send_operator_asserts"))
            })
            .await;
    }

    async fn send_watchtower_challenge(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    context
                        .owner
                        .handle_duty(Duty::WatchtowerChallenge {
                            kickoff_data: self.kickoff_data,
                            deposit_data: self.deposit_data.clone(),
                        })
                        .await?;
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on send_watchtower_challenge"))
            })
            .await;
    }

    async fn send_disprove(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    context
                        .owner
                        .handle_duty(Duty::VerifierDisprove {
                            kickoff_data: self.kickoff_data,
                            deposit_data: self.deposit_data.clone(),
                            operator_asserts: self.operator_asserts.clone(),
                            operator_acks: self.operator_challenge_acks.clone(),
                            payout_blockhash: self.payout_blockhash.clone(),
                            latest_blockhash: self.latest_blockhash.clone(),
                        })
                        .await?;
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on send_disprove"))
            })
            .await;
    }

    async fn send_latest_blockhash(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    context
                        .owner
                        .handle_duty(Duty::SendLatestBlockhash {
                            kickoff_data: self.kickoff_data,
                            deposit_data: self.deposit_data.clone(),
                            latest_blockhash: context
                                .cache
                                .block
                                .as_ref()
                                .ok_or(eyre::eyre!("Block object not found in block cache"))?
                                .header
                                .block_hash(),
                        })
                        .await?;
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on send_latest_blockhash"))
            })
            .await;
    }

    async fn unhandled_event(&mut self, context: &mut StateContext<T>, event: &KickoffEvent) {
        context
            .capture_error(async |_context| {
                let event_str = format!("{:?}", event);
                Err(StateMachineError::UnhandledEvent(event_str))
                    .wrap_err(self.kickoff_meta("kickoff unhandled event"))
            })
            .await;
    }

    /// If the kickoff is challenged, the state machine will add corresponding matchers for
    /// sending watchtower challenges after some amount of blocks passes since the kickoff was included in Bitcoin.
    /// Sending watchtower challenges only happen if the kickoff is challenged.
    /// As sending latest blockhash commit and asserts depend on watchtower challenges/timeouts being sent,
    /// they will also not be sent if the kickoff is not challenged and kickoff finalizer is spent with ChallengeTimeout,
    /// which changes the state to "Closed".
    #[action]
    pub(crate) async fn on_challenged_entry(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    // create times to send necessary challenge asserts
                    self.matchers.insert(
                        Matcher::BlockHeight(
                            self.kickoff_height
                                + context.paramset.time_to_send_watchtower_challenge as u32,
                        ),
                        KickoffEvent::TimeToSendWatchtowerChallenge,
                    );
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
            })
            .await;
    }

    /// State that is entered when the kickoff is challenged
    /// It only includes special handling for the TimeToSendWatchtowerChallenge event
    /// All other events are handled in the kickoff superstate
    #[state(superstate = "kickoff", entry_action = "on_challenged_entry")]
    pub(crate) async fn challenged(
        &mut self,
        event: &KickoffEvent,
        context: &mut StateContext<T>,
    ) -> Response<State> {
        match event {
            KickoffEvent::WatchtowerChallengeSent { .. }
            | KickoffEvent::OperatorAssertSent { .. }
            | KickoffEvent::OperatorChallengeAckSent { .. }
            | KickoffEvent::KickoffFinalizerSpent
            | KickoffEvent::BurnConnectorSpent
            | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
            | KickoffEvent::LatestBlockHashSent { .. }
            | KickoffEvent::TimeToSendLatestBlockhash
            | KickoffEvent::SavedToDb => Super,
            KickoffEvent::TimeToSendWatchtowerChallenge => {
                self.send_watchtower_challenge(context).await;
                Handled
            }
            _ => {
                self.unhandled_event(context, event).await;
                Handled
            }
        }
    }

    #[superstate]
    async fn kickoff(
        &mut self,
        event: &KickoffEvent,
        context: &mut StateContext<T>,
    ) -> Response<State> {
        tracing::trace!("Received event in kickoff superstate: {:?}", event);
        match event {
            // When a watchtower challenge is detected in Bitcoin,
            // save the full challenge transaction and check if the latest blockhash can be committed
            // and if the disprove is ready to be sent
            KickoffEvent::WatchtowerChallengeSent {
                watchtower_idx,
                challenge_outpoint,
            } => {
                self.spent_watchtower_utxos.insert(*watchtower_idx);
                let tx = context
                    .cache
                    .get_tx_of_utxo(challenge_outpoint)
                    .expect("Challenge outpoint that got matched should be in block");
                // save challenge witness
                self.watchtower_challenges
                    .insert(*watchtower_idx, tx.clone());
                self.create_matcher_for_latest_blockhash_if_ready(context)
                    .await;
                self.disprove_if_ready(context).await;
                Handled
            }
            // When an operator assert is detected in Bitcoin,
            // save the assert witness (which is the BitVM winternitz commit)
            // and check if the disprove is ready to be sent
            KickoffEvent::OperatorAssertSent {
                assert_idx,
                assert_outpoint,
            } => {
                let witness = context
                    .cache
                    .get_witness_of_utxo(assert_outpoint)
                    .expect("Assert outpoint that got matched should be in block");
                // save assert witness
                self.operator_asserts.insert(*assert_idx, witness);
                self.disprove_if_ready(context).await;
                Handled
            }
            // When an operator challenge ack is detected in Bitcoin,
            // save the ack witness as the witness includes the revealed preimage that
            // can be used to disprove if the operator maliciously doesn't include the
            // watchtower challenge in the BitVM proof
            KickoffEvent::OperatorChallengeAckSent {
                watchtower_idx,
                challenge_ack_outpoint,
            } => {
                let witness = context
                    .cache
                    .get_witness_of_utxo(challenge_ack_outpoint)
                    .expect("Challenge ack outpoint that got matched should be in block");
                // save challenge ack witness
                self.operator_challenge_acks
                    .insert(*watchtower_idx, witness);
                Handled
            }
            // When the kickoff finalizer is spent in Bitcoin,
            // the kickoff process is finished and the state machine will transition to the "Closed" state
            KickoffEvent::KickoffFinalizerSpent => Transition(State::closed()),
            // When the burn connector of the operator is spent in Bitcoin, it means the operator cannot continue with any more kickoffs
            // (unless burn connector is spent by ready to reimburse tx), so the state machine will transition to the "Closed" state
            KickoffEvent::BurnConnectorSpent => {
                tracing::error!(
                    "Burn connector spent before kickoff was finalized for kickoff {:?}",
                    self.kickoff_data
                );
                Transition(State::closed())
            }
            // When a watchtower challenge timeout is detected in Bitcoin,
            // set the watchtower utxo as spent and check if the latest blockhash can be committed
            KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx } => {
                self.spent_watchtower_utxos.insert(*watchtower_idx);
                self.create_matcher_for_latest_blockhash_if_ready(context)
                    .await;
                Handled
            }
            // When the latest blockhash is detected in Bitcoin,
            // save the witness which includes the blockhash and check if the operator asserts and
            // disprove tx are ready to be sent
            KickoffEvent::LatestBlockHashSent {
                latest_blockhash_outpoint,
            } => {
                let witness = context
                    .cache
                    .get_witness_of_utxo(latest_blockhash_outpoint)
                    .expect("Latest blockhash outpoint that got matched should be in block");
                // save latest blockhash witness
                self.latest_blockhash = witness;
                // can start sending asserts as latest blockhash is committed and finalized
                self.send_operator_asserts_if_ready(context).await;
                self.disprove_if_ready(context).await;
                Handled
            }
            KickoffEvent::TimeToSendLatestBlockhash => {
                // tell owner to send latest blockhash tx
                self.send_latest_blockhash(context).await;
                Handled
            }
            KickoffEvent::SavedToDb => Handled,
            _ => {
                self.unhandled_event(context, event).await;
                Handled
            }
        }
    }

    /// State that is entered when the kickoff is started
    /// It will transition to the "Challenged" state if the kickoff is challenged
    #[state(superstate = "kickoff", entry_action = "on_kickoff_started_entry")]
    pub(crate) async fn kickoff_started(
        &mut self,
        event: &KickoffEvent,
        context: &mut StateContext<T>,
    ) -> Response<State> {
        match event {
            KickoffEvent::Challenged => {
                tracing::warn!("Warning: Operator challenged: {:?}", self.kickoff_data);
                Transition(State::challenged())
            }
            KickoffEvent::WatchtowerChallengeSent { .. }
            | KickoffEvent::OperatorAssertSent { .. }
            | KickoffEvent::OperatorChallengeAckSent { .. }
            | KickoffEvent::KickoffFinalizerSpent
            | KickoffEvent::BurnConnectorSpent
            | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
            | KickoffEvent::LatestBlockHashSent { .. }
            | KickoffEvent::TimeToSendLatestBlockhash
            | KickoffEvent::SavedToDb => Super,
            _ => {
                self.unhandled_event(context, event).await;
                Handled
            }
        }
    }

    /// Adds the default matchers that will be used if the state is "challenged" or "kickoff_started".
    /// These matchers are used to detect when various transactions in the contract are mined on Bitcoin.
    async fn add_default_kickoff_matchers(
        &mut self,
        context: &mut StateContext<T>,
    ) -> Result<(), BridgeError> {
        // First create all transactions for the current deposit
        let contract_context = ContractContext::new_context_for_kickoff(
            self.kickoff_data,
            self.deposit_data.clone(),
            context.paramset,
        );
        let mut txhandlers = context
            .owner
            .create_txhandlers(TransactionType::AllNeededForDeposit, contract_context)
            .await?;
        let kickoff_txhandler =
            remove_txhandler_from_map(&mut txhandlers, TransactionType::Kickoff)?;

        // add operator asserts
        let kickoff_txid = *kickoff_txhandler.get_txid();
        let num_asserts = crate::bitvm_client::ClementineBitVMPublicKeys::number_of_assert_txs();
        for assert_idx in 0..num_asserts {
            let mini_assert_vout = UtxoVout::Assert(assert_idx).get_vout();
            let assert_timeout_txhandler = remove_txhandler_from_map(
                &mut txhandlers,
                TransactionType::AssertTimeout(assert_idx),
            )?;
            let assert_timeout_txid = assert_timeout_txhandler.get_txid();
            // Assert transactions can have any txid (there is no enforcement on how the assert utxo is spent, just that
            // spending assert utxo reveals the BitVM winternitz commit in the utxo's witness)
            // But assert timeouts are nofn signed transactions with a fixed txid, so we can detect assert transactions
            // by checking if the assert utxo is spent but not by the assert timeout tx
            self.matchers.insert(
                Matcher::SpentUtxoButNotTxid(
                    OutPoint {
                        txid: kickoff_txid,
                        vout: mini_assert_vout,
                    },
                    vec![*assert_timeout_txid],
                ),
                KickoffEvent::OperatorAssertSent {
                    assert_outpoint: OutPoint {
                        txid: kickoff_txid,
                        vout: mini_assert_vout,
                    },
                    assert_idx,
                },
            );
        }
        // add latest blockhash tx sent matcher
        let latest_blockhash_timeout_txhandler =
            remove_txhandler_from_map(&mut txhandlers, TransactionType::LatestBlockhashTimeout)?;
        let latest_blockhash_timeout_txid = latest_blockhash_timeout_txhandler.get_txid();
        let latest_blockhash_outpoint = OutPoint {
            txid: kickoff_txid,
            vout: UtxoVout::LatestBlockhash.get_vout(),
        };
        // Same logic as before with assert transaction detection, if latest blockhash utxo is not spent by latest blockhash timeout tx,
        // it means the latest blockhash is committed on Bitcoin
        self.matchers.insert(
            Matcher::SpentUtxoButNotTxid(
                latest_blockhash_outpoint,
                vec![*latest_blockhash_timeout_txid],
            ),
            KickoffEvent::LatestBlockHashSent {
                latest_blockhash_outpoint,
            },
        );
        // add watchtower challenges and challenge acks matchers
        for watchtower_idx in 0..self.deposit_data.get_num_watchtowers() {
            let watchtower_challenge_vout =
                UtxoVout::WatchtowerChallenge(watchtower_idx).get_vout();
            let watchtower_timeout_txhandler = remove_txhandler_from_map(
                &mut txhandlers,
                TransactionType::WatchtowerChallengeTimeout(watchtower_idx),
            )?;
            let watchtower_timeout_txid = watchtower_timeout_txhandler.get_txid();
            // matcher in case watchtower challenge timeout is sent
            self.matchers.insert(
                Matcher::SentTx(*watchtower_timeout_txid),
                KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx },
            );
            // matcher in case watchtower challenge is sent (watchtower challenge utxo is spent but not by watchtower challenge timeout tx)
            self.matchers.insert(
                Matcher::SpentUtxoButNotTxid(
                    OutPoint {
                        txid: kickoff_txid,
                        vout: watchtower_challenge_vout,
                    },
                    vec![*watchtower_timeout_txid],
                ),
                KickoffEvent::WatchtowerChallengeSent {
                    watchtower_idx,
                    challenge_outpoint: OutPoint {
                        txid: kickoff_txid,
                        vout: watchtower_challenge_vout,
                    },
                },
            );
            // add operator challenge ack matcher
            let operator_challenge_ack_vout =
                UtxoVout::WatchtowerChallengeAck(watchtower_idx).get_vout();
            let operator_challenge_nack_txhandler = remove_txhandler_from_map(
                &mut txhandlers,
                TransactionType::OperatorChallengeNack(watchtower_idx),
            )?;
            let operator_challenge_nack_txid = operator_challenge_nack_txhandler.get_txid();
            // operator challenge ack utxo is spent but not by operator challenge nack tx or watchtower challenge timeout tx
            self.matchers.insert(
                Matcher::SpentUtxoButNotTxid(
                    OutPoint {
                        txid: kickoff_txid,
                        vout: operator_challenge_ack_vout,
                    },
                    vec![*operator_challenge_nack_txid, *watchtower_timeout_txid],
                ),
                KickoffEvent::OperatorChallengeAckSent {
                    watchtower_idx,
                    challenge_ack_outpoint: OutPoint {
                        txid: kickoff_txid,
                        vout: operator_challenge_ack_vout,
                    },
                },
            );
        }

        // add burn connector tx spent matcher
        // Burn connector can also be spent in ready to reimburse tx, but before spending burn connector that way,
        // the kickoff finalizer needs to be spent first, otherwise pre-signed "Kickoff not finalized" tx can be sent by
        // any verifier, slashing the operator.
        // If the kickoff finalizer is spent first, the state will be in "Closed" state and all matchers will be deleted.
        let round_txhandler = remove_txhandler_from_map(&mut txhandlers, TransactionType::Round)?;
        let round_txid = *round_txhandler.get_txid();
        self.matchers.insert(
            Matcher::SpentUtxo(OutPoint {
                txid: round_txid,
                vout: UtxoVout::CollateralInRound.get_vout(),
            }),
            KickoffEvent::BurnConnectorSpent,
        );
        // add kickoff finalizer utxo spent matcher
        self.matchers.insert(
            Matcher::SpentUtxo(OutPoint {
                txid: kickoff_txid,
                vout: UtxoVout::KickoffFinalizer.get_vout(),
            }),
            KickoffEvent::KickoffFinalizerSpent,
        );
        // add challenge detector matcher, if challenge utxo is not spent by challenge timeout tx, it means the kickoff is challenged
        let challenge_timeout_txhandler =
            remove_txhandler_from_map(&mut txhandlers, TransactionType::ChallengeTimeout)?;
        let challenge_timeout_txid = challenge_timeout_txhandler.get_txid();
        self.matchers.insert(
            Matcher::SpentUtxoButNotTxid(
                OutPoint {
                    txid: kickoff_txid,
                    vout: UtxoVout::Challenge.get_vout(),
                },
                vec![*challenge_timeout_txid],
            ),
            KickoffEvent::Challenged,
        );
        Ok(())
    }

    #[action]
    pub(crate) async fn on_kickoff_started_entry(&mut self, context: &mut StateContext<T>) {
        context
            .capture_error(async |context| {
                {
                    // Add all watchtower challenges and operator asserts to matchers
                    self.add_default_kickoff_matchers(context).await?;
                    Ok::<(), BridgeError>(())
                }
                .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
            })
            .await;
    }

    /// Clears all matchers when the state is "closed".
    /// This means the state machine will not do any more actions anymore.
    #[action]
    #[allow(unused_variables)]
    pub(crate) async fn on_closed_entry(&mut self, context: &mut StateContext<T>) {
        self.matchers.clear();
    }

    #[state(entry_action = "on_closed_entry")]
    // Terminal state when the kickoff process ends
    #[allow(unused_variables)]
    pub(crate) async fn closed(
        &mut self,
        event: &KickoffEvent,
        context: &mut StateContext<T>,
    ) -> Response<State> {
        Handled
    }
}