bark/exit/
vtxo.rs

1//! Unilateral exit tracking and progression for individual VTXOs.
2//!
3//! This module defines types that track the lifecycle of a single [Vtxo] exit, including its current
4//! state, onchain transaction IDs, and a history of prior states for auditing and troubleshooting.
5//!
6//! The primary type is [ExitVtxo], which provides an async [`ExitVtxo::progress`] method to advance
7//! the unilateral exit state machine until completion or until the next step actionable step such
8//! as requiring more onchain funds or waiting for a confirmation.
9//!
10//! See [ExitModel] for persisting the state machine in a database.
11
12use bitcoin::{FeeRate, Txid};
13use log::{debug, trace};
14
15use ark::{Vtxo, VtxoId};
16
17use crate::exit::models::{ExitError, ExitState};
18use crate::exit::progress::{ExitStateProgress, ProgressContext, ProgressStep};
19use crate::exit::transaction_manager::ExitTransactionManager;
20use crate::onchain::{ChainSource, ExitUnilaterally};
21use crate::persist::BarkPersister;
22use crate::persist::models::StoredExit;
23
24/// Tracks the exit lifecycle for a single [Vtxo].
25///
26/// An `ExitVtxo` maintains:
27/// - the underlying [Vtxo] being exited,
28/// - the set of related onchain transaction IDs in topographical order,
29/// - the current state [ExitState],
30/// - and a history of prior states for debugging and auditing.
31///
32/// Use [ExitVtxo::progress] to drive the state machine forward. The method is idempotent and will
33/// only persist when a logical state transition occurs.
34#[derive(Debug, Clone)]
35pub struct ExitVtxo {
36	vtxo: Vtxo,
37	txids: Vec<Txid>,
38	state: ExitState,
39	history: Vec<ExitState>,
40}
41
42impl ExitVtxo {
43	/// Create a new instance for the given [Vtxo].
44	///
45	/// - `vtxo`: the [Vtxo] being exited.
46	/// - `txids`: the ID of each transaction which needs broadcasting onchain in topographical
47	///   order.
48	/// - `tip`: current chain tip used to initialize the starting state.
49	pub fn new(vtxo: Vtxo, txids: Vec<Txid>, tip: u32) -> Self {
50		Self {
51			vtxo,
52			txids,
53			state: ExitState::new_start(tip),
54			history: vec![],
55		}
56	}
57
58	/// Reconstruct an `ExitVtxo` from its parts.
59	///
60	/// Useful when loading a tracked exit from storage.
61	pub fn from_parts(
62		vtxo: Vtxo,
63		txids: Vec<Txid>,
64		state: ExitState,
65		history: Vec<ExitState>,
66	) -> Self {
67		ExitVtxo {
68			vtxo,
69			txids,
70			state,
71			history,
72		}
73	}
74
75	/// Returns the ID of the tracked [Vtxo].
76	pub fn id(&self) -> VtxoId {
77		self.vtxo.id()
78	}
79
80	/// Returns the underlying [Vtxo].
81	pub fn vtxo(&self) -> &Vtxo {
82		&self.vtxo
83	}
84
85	/// Returns the current state of the unilateral exit.
86	pub fn state(&self) -> &ExitState {
87		&self.state
88	}
89
90	/// Returns the history of the exit machine in the order that states were observed.
91	pub fn history(&self) -> &Vec<ExitState> {
92		&self.history
93	}
94
95	/// Returns the set of exit-related transaction IDs, these may not be broadcast yet.
96	pub fn txids(&self) -> &Vec<Txid> {
97		&self.txids
98	}
99
100	/// True if the exit is currently [ExitState::Claimable] and can be claimed/spent.
101	pub fn is_claimable(&self) -> bool {
102		matches!(self.state, ExitState::Claimable(..))
103	}
104
105	/// Advances the exit state machine for this [Vtxo].
106	///
107	/// The method:
108	/// - Attempts to transition the unilateral exit state machine.
109	/// - Persists only when a logical state change occurs.
110	///
111	/// Returns:
112	/// - `Ok(())` when no more immediate work is required, such as when we're waiting for a
113	///   confirmation or when the exit is complete.
114	/// - `Err(ExitError)` when an unrecoverable issue occurs, such as requiring more onchain funds
115	///   or if an exit transaction fails to broadcast; if the error includes a newer state, it will
116	///   be committed before returning.
117	///
118	/// Notes:
119	/// - If `fee_rate_override` is `None`, a suitable fee rate will be calculated.
120	pub async fn progress(
121		&mut self,
122		chain_source: &ChainSource,
123		tx_manager: &mut ExitTransactionManager,
124		persister: &dyn BarkPersister,
125		onchain: &mut dyn ExitUnilaterally,
126		fee_rate_override: Option<FeeRate>,
127	) -> anyhow::Result<(), ExitError> {
128		const MAX_ITERATIONS: usize = 100;
129		for _ in 0..MAX_ITERATIONS {
130			let mut context = ProgressContext {
131				vtxo: &self.vtxo,
132				exit_txids: &self.txids,
133				chain_source: &chain_source,
134				fee_rate: fee_rate_override.unwrap_or(chain_source.fee_rates().await.fast),
135				tx_manager,
136			};
137			// Attempt to move to the next state, which may or may not generate a new state
138			trace!("Progressing VTXO {} at height {}", self.id(), chain_source.tip().await.unwrap());
139			match self.state.clone().progress(&mut context, onchain).await {
140				Ok(new_state) => {
141					self.update_state_if_newer(new_state, persister)?;
142					match ProgressStep::from_exit_state(&self.state) {
143						ProgressStep::Continue => debug!("VTXO {} can continue", self.id()),
144						ProgressStep::Done => return Ok(())
145					}
146				},
147				Err(e) => {
148					// We may need to commit a new state before returning an error
149					if let Some(new_state) = e.state {
150						self.update_state_if_newer(new_state, persister)?;
151					}
152					return Err(e.error);
153				}
154			}
155		}
156		debug_assert!(false, "Exceeded maximum iterations for progressing VTXO {}", self.id());
157		Ok(())
158	}
159
160	fn update_state_if_newer(
161		&mut self,
162		new: ExitState,
163		persister: &dyn BarkPersister,
164	) -> anyhow::Result<(), ExitError> {
165		// We don't want to push a new history item unless the state has changed logically
166		if new != self.state {
167			self.history.push(self.state.clone());
168			self.state = new;
169			self.persist(persister)
170		} else {
171			Ok(())
172		}
173	}
174
175	fn persist(&self, persister: &dyn BarkPersister) -> anyhow::Result<(), ExitError> {
176		persister.store_exit_vtxo_entry(&StoredExit::new(self))
177			.map_err(|e| ExitError::DatabaseVtxoStoreFailure {
178				vtxo_id: self.id(), error: e.to_string(),
179			})
180	}
181}