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 Ok::<(), BridgeError>(())
390 }
391 .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
392 })
393 .await;
394 // check if any action is ready to be started as it could already be ready before a challenge arrives
395 // but we want to make sure kickoff is actually challenged before we start sending actions
396 self.create_matcher_for_latest_blockhash_if_ready(context)
397 .await;
398 self.send_operator_asserts_if_ready(context).await;
399 self.disprove_if_ready(context).await;
400 }
401
402 /// State that is entered when the kickoff is challenged
403 /// It only includes special handling for the TimeToSendWatchtowerChallenge event
404 /// All other events are handled in the kickoff superstate
405 #[state(superstate = "kickoff", entry_action = "on_challenged_entry")]
406 pub(crate) async fn challenged(
407 &mut self,
408 event: &KickoffEvent,
409 context: &mut StateContext<T>,
410 ) -> Response<State> {
411 match event {
412 KickoffEvent::WatchtowerChallengeSent { .. }
413 | KickoffEvent::OperatorAssertSent { .. }
414 | KickoffEvent::OperatorChallengeAckSent { .. }
415 | KickoffEvent::KickoffFinalizerSpent
416 | KickoffEvent::BurnConnectorSpent
417 | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
418 | KickoffEvent::LatestBlockHashSent { .. }
419 | KickoffEvent::TimeToSendLatestBlockhash
420 | KickoffEvent::SavedToDb => Super,
421 KickoffEvent::TimeToSendWatchtowerChallenge => {
422 tracing::debug!(
423 "Reached time to send watchtower challenge for {}",
424 self.kickoff_data
425 );
426 self.send_watchtower_challenge(context).await;
427 Handled
428 }
429 _ => {
430 self.unhandled_event(context, event).await;
431 Handled
432 }
433 }
434 }
435
436 #[superstate]
437 async fn kickoff(
438 &mut self,
439 event: &KickoffEvent,
440 context: &mut StateContext<T>,
441 ) -> Response<State> {
442 tracing::trace!("Received event in kickoff superstate: {:?}", event);
443 match event {
444 // When a watchtower challenge is detected in Bitcoin,
445 // save the full challenge transaction and check if the latest blockhash can be committed
446 // and if the disprove is ready to be sent
447 KickoffEvent::WatchtowerChallengeSent {
448 watchtower_idx,
449 challenge_outpoint,
450 } => {
451 self.spent_watchtower_utxos.insert(*watchtower_idx);
452 let tx = context
453 .cache
454 .get_tx_of_utxo(challenge_outpoint)
455 .expect("Challenge outpoint that got matched should be in block");
456 tracing::info!(
457 "Detected watchtower challenge for watchtower {} for {}",
458 watchtower_idx,
459 self.kickoff_data,
460 );
461 // save challenge witness
462 self.watchtower_challenges
463 .insert(*watchtower_idx, tx.clone());
464 self.create_matcher_for_latest_blockhash_if_ready(context)
465 .await;
466 self.send_operator_asserts_if_ready(context).await;
467 self.disprove_if_ready(context).await;
468 Handled
469 }
470 // When an operator assert is detected in Bitcoin,
471 // save the assert witness (which is the BitVM winternitz commit)
472 // and check if the disprove is ready to be sent
473 KickoffEvent::OperatorAssertSent {
474 assert_idx,
475 assert_outpoint,
476 } => {
477 let witness = context
478 .cache
479 .get_witness_of_utxo(assert_outpoint)
480 .expect("Assert outpoint that got matched should be in block");
481 tracing::info!(
482 "Detected assert {} for kickoff {}",
483 assert_idx,
484 self.kickoff_data,
485 );
486 // save assert witness
487 self.operator_asserts.insert(*assert_idx, witness);
488 self.disprove_if_ready(context).await;
489 Handled
490 }
491 // When an operator challenge ack is detected in Bitcoin,
492 // save the ack witness as the witness includes the revealed preimage that
493 // can be used to disprove if the operator maliciously doesn't include the
494 // watchtower challenge in the BitVM proof
495 KickoffEvent::OperatorChallengeAckSent {
496 watchtower_idx,
497 challenge_ack_outpoint,
498 } => {
499 let witness = context
500 .cache
501 .get_witness_of_utxo(challenge_ack_outpoint)
502 .expect("Challenge ack outpoint that got matched should be in block");
503 // save challenge ack witness
504 self.operator_challenge_acks
505 .insert(*watchtower_idx, witness);
506 tracing::info!(
507 "Detected challenge ack for watchtower {} for {}",
508 watchtower_idx,
509 self.kickoff_data,
510 );
511 self.disprove_if_ready(context).await;
512 Handled
513 }
514 // When the kickoff finalizer is spent in Bitcoin,
515 // the kickoff process is finished and the state machine will transition to the "Closed" state
516 KickoffEvent::KickoffFinalizerSpent => {
517 tracing::info!("Detected kickoff finalizer spent for {}", self.kickoff_data,);
518 Transition(State::closed())
519 }
520 // When the burn connector of the operator is spent in Bitcoin, it means the operator cannot continue with any more kickoffs
521 // (unless burn connector is spent by ready to reimburse tx), so the state machine will transition to the "Closed" state
522 KickoffEvent::BurnConnectorSpent => {
523 tracing::error!(
524 "Burn connector spent before kickoff was finalized for kickoff {:?}",
525 self.kickoff_data
526 );
527 Transition(State::closed())
528 }
529 // When a watchtower challenge timeout is detected in Bitcoin,
530 // set the watchtower utxo as spent and check if the latest blockhash can be committed
531 KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx } => {
532 self.spent_watchtower_utxos.insert(*watchtower_idx);
533 tracing::info!(
534 "Detected watchtower challenge timeout for watchtower {} for {}",
535 watchtower_idx,
536 self.kickoff_data,
537 );
538 self.create_matcher_for_latest_blockhash_if_ready(context)
539 .await;
540 self.send_operator_asserts_if_ready(context).await;
541 self.disprove_if_ready(context).await;
542 Handled
543 }
544 // When the latest blockhash is detected in Bitcoin,
545 // save the witness which includes the blockhash and check if the operator asserts and
546 // disprove tx are ready to be sent
547 KickoffEvent::LatestBlockHashSent {
548 latest_blockhash_outpoint,
549 } => {
550 let witness = context
551 .cache
552 .get_witness_of_utxo(latest_blockhash_outpoint)
553 .expect("Latest blockhash outpoint that got matched should be in block");
554 tracing::info!("Detected latest blockhash for {}", self.kickoff_data,);
555 // save latest blockhash witness
556 self.latest_blockhash = witness;
557 // can start sending asserts as latest blockhash is committed and finalized
558 self.send_operator_asserts_if_ready(context).await;
559 self.disprove_if_ready(context).await;
560 Handled
561 }
562 KickoffEvent::TimeToSendLatestBlockhash => {
563 // tell owner to send latest blockhash tx
564 tracing::debug!(
565 "Reached time to send latest blockhash for {}",
566 self.kickoff_data
567 );
568 self.send_latest_blockhash(context).await;
569 Handled
570 }
571 KickoffEvent::SavedToDb => Handled,
572 _ => {
573 self.unhandled_event(context, event).await;
574 Handled
575 }
576 }
577 }
578
579 /// State that is entered when the kickoff is started
580 /// It will transition to the "Challenged" state if the kickoff is challenged
581 #[state(superstate = "kickoff", entry_action = "on_kickoff_started_entry")]
582 pub(crate) async fn kickoff_started(
583 &mut self,
584 event: &KickoffEvent,
585 context: &mut StateContext<T>,
586 ) -> Response<State> {
587 match event {
588 KickoffEvent::Challenged => {
589 tracing::warn!("Warning: Operator challenged: {}", self.kickoff_data);
590 Transition(State::challenged())
591 }
592 KickoffEvent::WatchtowerChallengeSent { .. }
593 | KickoffEvent::OperatorAssertSent { .. }
594 | KickoffEvent::OperatorChallengeAckSent { .. }
595 | KickoffEvent::KickoffFinalizerSpent
596 | KickoffEvent::BurnConnectorSpent
597 | KickoffEvent::WatchtowerChallengeTimeoutSent { .. }
598 | KickoffEvent::LatestBlockHashSent { .. }
599 | KickoffEvent::TimeToSendLatestBlockhash
600 | KickoffEvent::SavedToDb => Super,
601 _ => {
602 self.unhandled_event(context, event).await;
603 Handled
604 }
605 }
606 }
607
608 /// Adds the default matchers that will be used if the state is "challenged" or "kickoff_started".
609 /// These matchers are used to detect when various transactions in the contract are mined on Bitcoin.
610 async fn add_default_kickoff_matchers(
611 &mut self,
612 context: &mut StateContext<T>,
613 ) -> Result<(), BridgeError> {
614 // First create all transactions for the current deposit
615 let contract_context = ContractContext::new_context_for_kickoff(
616 self.kickoff_data,
617 self.deposit_data.clone(),
618 context.config.protocol_paramset,
619 );
620 let mut txhandlers = {
621 let mut guard = context.shared_dbtx.lock().await;
622 context
623 .owner
624 .create_txhandlers(
625 &mut guard,
626 TransactionType::AllNeededForDeposit,
627 contract_context,
628 )
629 .await?
630 };
631 let kickoff_txhandler =
632 remove_txhandler_from_map(&mut txhandlers, TransactionType::Kickoff)?;
633
634 // add operator asserts
635 let kickoff_txid = *kickoff_txhandler.get_txid();
636 let num_asserts = crate::bitvm_client::ClementineBitVMPublicKeys::number_of_assert_txs();
637 for assert_idx in 0..num_asserts {
638 let mini_assert_vout = UtxoVout::Assert(assert_idx).get_vout();
639 let assert_timeout_txhandler = remove_txhandler_from_map(
640 &mut txhandlers,
641 TransactionType::AssertTimeout(assert_idx),
642 )?;
643 let assert_timeout_txid = assert_timeout_txhandler.get_txid();
644 // Assert transactions can have any txid (there is no enforcement on how the assert utxo is spent, just that
645 // spending assert utxo reveals the BitVM winternitz commit in the utxo's witness)
646 // But assert timeouts are nofn signed transactions with a fixed txid, so we can detect assert transactions
647 // by checking if the assert utxo is spent but not by the assert timeout tx
648 self.matchers.insert(
649 Matcher::SpentUtxoButNotTxid(
650 OutPoint {
651 txid: kickoff_txid,
652 vout: mini_assert_vout,
653 },
654 vec![*assert_timeout_txid],
655 ),
656 KickoffEvent::OperatorAssertSent {
657 assert_outpoint: OutPoint {
658 txid: kickoff_txid,
659 vout: mini_assert_vout,
660 },
661 assert_idx,
662 },
663 );
664 }
665 // add latest blockhash tx sent matcher
666 let latest_blockhash_timeout_txhandler =
667 remove_txhandler_from_map(&mut txhandlers, TransactionType::LatestBlockhashTimeout)?;
668 let latest_blockhash_timeout_txid = latest_blockhash_timeout_txhandler.get_txid();
669 let latest_blockhash_outpoint = OutPoint {
670 txid: kickoff_txid,
671 vout: UtxoVout::LatestBlockhash.get_vout(),
672 };
673 // Same logic as before with assert transaction detection, if latest blockhash utxo is not spent by latest blockhash timeout tx,
674 // it means the latest blockhash is committed on Bitcoin
675 self.matchers.insert(
676 Matcher::SpentUtxoButNotTxid(
677 latest_blockhash_outpoint,
678 vec![*latest_blockhash_timeout_txid],
679 ),
680 KickoffEvent::LatestBlockHashSent {
681 latest_blockhash_outpoint,
682 },
683 );
684 // add watchtower challenges and challenge acks matchers
685 for watchtower_idx in 0..self.deposit_data.get_num_watchtowers() {
686 let watchtower_challenge_vout =
687 UtxoVout::WatchtowerChallenge(watchtower_idx).get_vout();
688 let watchtower_timeout_txhandler = remove_txhandler_from_map(
689 &mut txhandlers,
690 TransactionType::WatchtowerChallengeTimeout(watchtower_idx),
691 )?;
692 let watchtower_timeout_txid = watchtower_timeout_txhandler.get_txid();
693 // matcher in case watchtower challenge timeout is sent
694 self.matchers.insert(
695 Matcher::SentTx(*watchtower_timeout_txid),
696 KickoffEvent::WatchtowerChallengeTimeoutSent { watchtower_idx },
697 );
698 // matcher in case watchtower challenge is sent (watchtower challenge utxo is spent but not by watchtower challenge timeout tx)
699 self.matchers.insert(
700 Matcher::SpentUtxoButNotTxid(
701 OutPoint {
702 txid: kickoff_txid,
703 vout: watchtower_challenge_vout,
704 },
705 vec![*watchtower_timeout_txid],
706 ),
707 KickoffEvent::WatchtowerChallengeSent {
708 watchtower_idx,
709 challenge_outpoint: OutPoint {
710 txid: kickoff_txid,
711 vout: watchtower_challenge_vout,
712 },
713 },
714 );
715 // add operator challenge ack matcher
716 let operator_challenge_ack_vout =
717 UtxoVout::WatchtowerChallengeAck(watchtower_idx).get_vout();
718 let operator_challenge_nack_txhandler = remove_txhandler_from_map(
719 &mut txhandlers,
720 TransactionType::OperatorChallengeNack(watchtower_idx),
721 )?;
722 let operator_challenge_nack_txid = operator_challenge_nack_txhandler.get_txid();
723 // operator challenge ack utxo is spent but not by operator challenge nack tx or watchtower challenge timeout tx
724 self.matchers.insert(
725 Matcher::SpentUtxoButNotTxid(
726 OutPoint {
727 txid: kickoff_txid,
728 vout: operator_challenge_ack_vout,
729 },
730 vec![*operator_challenge_nack_txid, *watchtower_timeout_txid],
731 ),
732 KickoffEvent::OperatorChallengeAckSent {
733 watchtower_idx,
734 challenge_ack_outpoint: OutPoint {
735 txid: kickoff_txid,
736 vout: operator_challenge_ack_vout,
737 },
738 },
739 );
740 }
741
742 // add burn connector tx spent matcher
743 // Burn connector can also be spent in ready to reimburse tx, but before spending burn connector that way,
744 // the kickoff finalizer needs to be spent first, otherwise pre-signed "Kickoff not finalized" tx can be sent by
745 // any verifier, slashing the operator.
746 // If the kickoff finalizer is spent first, the state will be in "Closed" state and all matchers will be deleted.
747 let round_txhandler = remove_txhandler_from_map(&mut txhandlers, TransactionType::Round)?;
748 let round_txid = *round_txhandler.get_txid();
749 self.matchers.insert(
750 Matcher::SpentUtxo(OutPoint {
751 txid: round_txid,
752 vout: UtxoVout::CollateralInRound.get_vout(),
753 }),
754 KickoffEvent::BurnConnectorSpent,
755 );
756 // add kickoff finalizer utxo spent matcher
757 self.matchers.insert(
758 Matcher::SpentUtxo(OutPoint {
759 txid: kickoff_txid,
760 vout: UtxoVout::KickoffFinalizer.get_vout(),
761 }),
762 KickoffEvent::KickoffFinalizerSpent,
763 );
764 // add challenge detector matcher, if challenge utxo is not spent by challenge timeout tx, it means the kickoff is challenged
765 let challenge_timeout_txhandler =
766 remove_txhandler_from_map(&mut txhandlers, TransactionType::ChallengeTimeout)?;
767 let challenge_timeout_txid = challenge_timeout_txhandler.get_txid();
768 self.matchers.insert(
769 Matcher::SpentUtxoButNotTxid(
770 OutPoint {
771 txid: kickoff_txid,
772 vout: UtxoVout::Challenge.get_vout(),
773 },
774 vec![*challenge_timeout_txid],
775 ),
776 KickoffEvent::Challenged,
777 );
778 Ok(())
779 }
780
781 #[action]
782 pub(crate) async fn on_kickoff_started_entry(&mut self, context: &mut StateContext<T>) {
783 context
784 .capture_error(async |context| {
785 {
786 // Add all watchtower challenges and operator asserts to matchers
787 self.add_default_kickoff_matchers(context).await?;
788 Ok::<(), BridgeError>(())
789 }
790 .wrap_err(self.kickoff_meta("on_kickoff_started_entry"))
791 })
792 .await;
793 }
794
795 /// Clears all matchers when the state is "closed".
796 /// This means the state machine will not do any more actions anymore.
797 #[action]
798 #[allow(unused_variables)]
799 pub(crate) async fn on_closed_entry(&mut self, context: &mut StateContext<T>) {
800 self.matchers.clear();
801 }
802
803 #[state(entry_action = "on_closed_entry")]
804 // Terminal state when the kickoff process ends
805 #[allow(unused_variables)]
806 pub(crate) async fn closed(
807 &mut self,
808 event: &KickoffEvent,
809 context: &mut StateContext<T>,
810 ) -> Response<State> {
811 Handled
812 }
813}