Skip to main content

bark/persist/
models.rs

1//! Persistence-focused data models.
2//!
3//! This module defines serializable types that mirror core in-memory structures but are tailored
4//! for durable storage and retrieval via a BarkPersister implementation.
5//!
6//! Intent
7//! - Keep storage concerns decoupled from runtime types used by protocol logic.
8//! - Provide stable, serde-friendly representations for database backends.
9//! - Enable forward/backward compatibility when schema migrations occur.
10
11use std::borrow::Cow;
12use std::fmt;
13
14use bitcoin::{Amount, Transaction};
15use bitcoin::secp256k1::{Keypair, PublicKey};
16use lightning_invoice::Bolt11Invoice;
17
18use ark::{Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
19use ark::vtxo::Full;
20use ark::mailbox::MailboxIdentifier;
21use ark::musig::DangerousSecretNonce;
22use ark::tree::signed::{UnlockHash, VtxoTreeSpec};
23use ark::lightning::{Invoice, PaymentHash, Preimage};
24use ark::rounds::RoundSeq;
25use bitcoin_ext::BlockDelta;
26
27use crate::WalletVtxo;
28use crate::exit::{ExitState, ExitTxOrigin, ExitVtxo};
29use crate::movement::MovementId;
30use crate::round::{AttemptState, RoundFlowState, RoundParticipation, RoundState, RoundStateGuard};
31use crate::vtxo::VtxoState;
32
33/// VTXO with state history for persistence.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SerdeVtxo {
36	#[serde(with = "ark::encode::serde")]
37	pub vtxo: Vtxo<Full>,
38	/// VTXO states, sorted from oldest to newest.
39	pub states: Vec<VtxoState>,
40}
41
42#[derive(Debug, thiserror::Error)]
43#[error("vtxo has no state")]
44pub struct MissingStateError;
45
46impl SerdeVtxo {
47	pub fn current_state(&self) -> Option<&VtxoState> {
48		self.states.last()
49	}
50
51	pub fn to_wallet_vtxo(&self) -> Result<WalletVtxo, MissingStateError> {
52		Ok(WalletVtxo {
53			vtxo: self.vtxo.clone(),
54			state: self.current_state().cloned().ok_or(MissingStateError)?,
55		})
56	}
57}
58
59/// VTXO key mapping for persistence.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SerdeVtxoKey {
62	pub index: u32,
63	pub public_key: PublicKey,
64}
65
66/// Identifier for a stored [RoundState].
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct RoundStateId(pub u32);
69
70impl RoundStateId {
71	pub fn to_bytes(&self) -> [u8; 4] {
72		self.0.to_be_bytes()
73	}
74}
75
76impl fmt::Display for RoundStateId {
77	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78	    fmt::Display::fmt(&self.0, f)
79	}
80}
81
82#[allow(unused)]
83pub struct Locked(RoundStateGuard);
84
85pub struct Unlocked;
86
87pub struct StoredRoundState<G = Locked> {
88	id: RoundStateId,
89	state: RoundState,
90	_guard: G
91}
92
93impl<G> StoredRoundState<G> {
94	pub fn id(&self) -> RoundStateId {
95		self.id
96	}
97
98	pub fn state(&self) -> &RoundState {
99		&self.state
100	}
101}
102
103impl StoredRoundState<Unlocked> {
104	pub fn new(id: RoundStateId, state: RoundState) -> Self {
105		Self { id, state, _guard: Unlocked }
106	}
107
108	pub fn lock(self, guard: RoundStateGuard) -> StoredRoundState {
109		StoredRoundState { id: self.id, state: self.state, _guard: Locked(guard) }
110	}
111}
112
113impl StoredRoundState {
114	pub fn state_mut(&mut self) -> &mut RoundState {
115		&mut self.state
116	}
117
118	pub fn unlock(self) -> StoredRoundState<Unlocked> {
119		StoredRoundState { id: self.id, state: self.state, _guard: Unlocked }
120	}
121}
122
123/// Persisted representation of a pending board.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct PendingBoard {
126	/// This is the [bitcoin::Transaction] that has to
127	/// be confirmed onchain for the board to succeed.
128	#[serde(with = "bitcoin_ext::serde::encodable")]
129	pub funding_tx: Transaction,
130	/// The id of VTXOs being boarded.
131	///
132	/// Currently, this is always a vector of length 1
133	pub vtxos: Vec<VtxoId>,
134	/// The amount of the board.
135	#[serde(with = "bitcoin::amount::serde::as_sat")]
136	pub amount: Amount,
137	/// The [MovementId] associated with this board.
138	pub movement_id: MovementId,
139}
140
141/// Persisted representation of a pending offboard.
142///
143/// Created when an offboard swap is performed, tracked until the
144/// offboard transaction confirms on-chain.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct PendingOffboard {
147	/// The [MovementId] associated with this offboard.
148	pub movement_id: MovementId,
149	/// The txid of the offboard transaction.
150	pub offboard_txid: bitcoin::Txid,
151	/// The full signed offboard transaction.
152	pub offboard_tx: Transaction,
153	/// The VTXOs consumed by this offboard.
154	pub vtxo_ids: Vec<VtxoId>,
155	/// The destination address of the offboard.
156	pub destination: String,
157	/// When this pending offboard was created.
158	pub created_at: chrono::DateTime<chrono::Local>,
159}
160
161/// Persisted representation of a lightning send.
162///
163/// Created after the HTLCs from client to server are constructed.
164#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
165pub struct LightningSend {
166	/// The Lightning invoice being paid.
167	pub invoice: Invoice,
168	/// The amount being sent.
169	#[serde(with = "bitcoin::amount::serde::as_sat")]
170	pub amount: Amount,
171	/// The fee paid for making the lightning payment.
172	pub fee: Amount,
173	/// The open HTLCs that are used for this payment.
174	pub htlc_vtxos: Vec<WalletVtxo>,
175	/// The movement associated with this payment.
176	pub movement_id: MovementId,
177	/// The payment preimage, serving as proof of payment.
178	///
179	/// Combined with [`finished_at`](Self::finished_at), determines the payment state:
180	/// - `None` + `finished_at: None` → Pending (in-flight)
181	/// - `None` + `finished_at: Some(_)` → Failed
182	/// - `Some(_)` + `finished_at: Some(_)` → Succeeded
183	pub preimage: Option<Preimage>,
184	/// When the payment reached a terminal state (succeeded or failed).
185	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
186}
187
188/// Persisted representation of an incoming Lightning payment.
189///
190/// Stores the invoice and related cryptographic material (e.g., payment hash and preimage)
191/// and tracks whether the preimage has been revealed.
192///
193/// Note: the record should be removed when the receive is completed or failed.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct LightningReceive {
196	pub payment_hash: PaymentHash,
197	pub payment_preimage: Preimage,
198	pub invoice: Bolt11Invoice,
199	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
200	pub htlc_vtxos: Vec<WalletVtxo>,
201	pub htlc_recv_cltv_delta: BlockDelta,
202	pub movement_id: Option<MovementId>,
203	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
204}
205
206/// Persistable view of an [ExitVtxo].
207///
208/// `StoredExit` is a lightweight data transfer object tailored for storage backends. It captures
209/// the VTXO ID, the current state, and the full history of the unilateral exit.
210#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct StoredExit {
212	/// Identifier of the VTXO being exited.
213	pub vtxo_id: VtxoId,
214	/// Current exit state.
215	pub state: ExitState,
216	/// Historical states for auditability.
217	pub history: Vec<ExitState>,
218}
219
220impl StoredExit {
221	/// Builds a persistable snapshot from an [ExitVtxo].
222	pub fn new(exit: &ExitVtxo) -> Self {
223		Self {
224			vtxo_id: exit.id(),
225			state: exit.state().clone(),
226			history: exit.history().clone(),
227		}
228	}
229}
230
231/// Exit child transaction for persistence.
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SerdeExitChildTx {
234	#[serde(with = "bitcoin_ext::serde::encodable")]
235	pub child_tx: Transaction,
236	pub origin: ExitTxOrigin,
237}
238
239#[derive(Debug, Clone, Deserialize, Serialize)]
240struct SerdeVtxoRequest<'a> {
241	#[serde(with = "bitcoin::amount::serde::as_sat")]
242	amount: Amount,
243	#[serde(with = "ark::encode::serde")]
244	policy: Cow<'a, VtxoPolicy>,
245}
246
247impl<'a> From<&'a VtxoRequest> for SerdeVtxoRequest<'a> {
248	fn from(v: &'a VtxoRequest) -> Self {
249		Self {
250			amount: v.amount,
251			policy: Cow::Borrowed(&v.policy),
252		}
253	}
254}
255
256impl<'a> From<SerdeVtxoRequest<'a>> for VtxoRequest {
257	fn from(v: SerdeVtxoRequest<'a>) -> Self {
258		VtxoRequest {
259			amount: v.amount,
260			policy: v.policy.into_owned(),
261		}
262	}
263}
264
265/// Model for [RoundParticipation]
266#[derive(Debug, Clone, Serialize, Deserialize)]
267struct SerdeRoundParticipation<'a> {
268	#[serde(with = "ark::encode::serde::cow::vec")]
269	inputs: Cow<'a, [Vtxo<Full>]>,
270	outputs: Vec<SerdeVtxoRequest<'a>>,
271	#[serde(default, skip_serializing_if = "Option::is_none")]
272	unblinded_mailbox_id: Option<MailboxIdentifier>,
273}
274
275impl<'a> From<&'a RoundParticipation> for SerdeRoundParticipation<'a> {
276	fn from(v: &'a RoundParticipation) -> Self {
277	    Self {
278			inputs: Cow::Borrowed(&v.inputs),
279			outputs: v.outputs.iter().map(|v| v.into()).collect(),
280			unblinded_mailbox_id: v.unblinded_mailbox_id,
281		}
282	}
283}
284
285impl<'a> From<SerdeRoundParticipation<'a>> for RoundParticipation {
286	fn from(v: SerdeRoundParticipation<'a>) -> Self {
287		Self {
288			inputs: v.inputs.into_owned(),
289			outputs: v.outputs.into_iter().map(|v| v.into()).collect(),
290			unblinded_mailbox_id: v.unblinded_mailbox_id,
291		}
292	}
293}
294
295/// Model for [AttemptState]
296#[derive(Debug, Serialize, Deserialize)]
297enum SerdeAttemptState<'a> {
298	AwaitingAttempt,
299	AwaitingUnsignedVtxoTree {
300		cosign_keys: Cow<'a, [Keypair]>,
301		secret_nonces: Cow<'a, [Vec<DangerousSecretNonce>]>,
302		unlock_hash: UnlockHash,
303	},
304	AwaitingFinishedRound {
305		#[serde(with = "bitcoin_ext::serde::encodable::cow")]
306		unsigned_round_tx: Cow<'a, Transaction>,
307		#[serde(with = "ark::encode::serde")]
308		vtxos_spec: Cow<'a, VtxoTreeSpec>,
309		unlock_hash: UnlockHash,
310	},
311}
312
313impl<'a> From<&'a AttemptState> for SerdeAttemptState<'a> {
314	fn from(state: &'a AttemptState) -> Self {
315		match state {
316			AttemptState::AwaitingAttempt => SerdeAttemptState::AwaitingAttempt,
317			AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
318				SerdeAttemptState::AwaitingUnsignedVtxoTree {
319					cosign_keys: Cow::Borrowed(cosign_keys),
320					secret_nonces: Cow::Borrowed(secret_nonces),
321					unlock_hash: *unlock_hash,
322				}
323			},
324			AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
325				SerdeAttemptState::AwaitingFinishedRound {
326					unsigned_round_tx: Cow::Borrowed(unsigned_round_tx),
327					vtxos_spec: Cow::Borrowed(vtxos_spec),
328					unlock_hash: *unlock_hash,
329				}
330			},
331		}
332	}
333}
334
335impl<'a> From<SerdeAttemptState<'a>> for AttemptState {
336	fn from(state: SerdeAttemptState<'a>) -> Self {
337		match state {
338			SerdeAttemptState::AwaitingAttempt => AttemptState::AwaitingAttempt,
339			SerdeAttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
340				AttemptState::AwaitingUnsignedVtxoTree {
341					cosign_keys: cosign_keys.into_owned(),
342					secret_nonces: secret_nonces.into_owned(),
343					unlock_hash: unlock_hash,
344				}
345			},
346			SerdeAttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
347				AttemptState::AwaitingFinishedRound {
348					unsigned_round_tx: unsigned_round_tx.into_owned(),
349					vtxos_spec: vtxos_spec.into_owned(),
350					unlock_hash: unlock_hash,
351				}
352			},
353		}
354	}
355}
356
357/// Model for [RoundFlowState]
358#[derive(Debug, Serialize, Deserialize)]
359enum SerdeRoundFlowState<'a> {
360	/// We don't do flow and we just wait for the round to finish
361	NonInteractivePending {
362		unlock_hash: UnlockHash,
363	},
364
365	/// Waiting for round to happen
366	InteractivePending,
367	/// Interactive part ongoing
368	InteractiveOngoing {
369		round_seq: RoundSeq,
370		attempt_seq: usize,
371		state: SerdeAttemptState<'a>,
372	},
373
374	/// Interactive part finished, waiting for confirmation
375	Finished {
376		funding_tx: Cow<'a, Transaction>,
377		unlock_hash: UnlockHash,
378	},
379
380	/// Failed during round
381	Failed {
382		error: Cow<'a, str>,
383	},
384
385	/// User canceled round
386	Canceled,
387}
388
389impl<'a> From<&'a RoundFlowState> for SerdeRoundFlowState<'a> {
390	fn from(state: &'a RoundFlowState) -> Self {
391		match state {
392			RoundFlowState::NonInteractivePending { unlock_hash } => {
393				SerdeRoundFlowState::NonInteractivePending {
394					unlock_hash: *unlock_hash,
395				}
396			},
397			RoundFlowState::InteractivePending => SerdeRoundFlowState::InteractivePending,
398			RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
399				SerdeRoundFlowState::InteractiveOngoing {
400					round_seq: *round_seq,
401					attempt_seq: *attempt_seq,
402					state: state.into(),
403				}
404			},
405			RoundFlowState::Finished { funding_tx, unlock_hash } => {
406				SerdeRoundFlowState::Finished {
407					funding_tx: Cow::Borrowed(funding_tx),
408					unlock_hash: *unlock_hash,
409				}
410			},
411			RoundFlowState::Failed { error } => {
412				SerdeRoundFlowState::Failed {
413					error: Cow::Borrowed(error),
414				}
415			},
416			RoundFlowState::Canceled => SerdeRoundFlowState::Canceled,
417		}
418	}
419}
420
421impl<'a> From<SerdeRoundFlowState<'a>> for RoundFlowState {
422	fn from(state: SerdeRoundFlowState<'a>) -> Self {
423		match state {
424			SerdeRoundFlowState::NonInteractivePending { unlock_hash } => {
425				RoundFlowState::NonInteractivePending { unlock_hash }
426			},
427			SerdeRoundFlowState::InteractivePending => RoundFlowState::InteractivePending,
428			SerdeRoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
429				RoundFlowState::InteractiveOngoing {
430					round_seq: round_seq,
431					attempt_seq: attempt_seq,
432					state: state.into(),
433				}
434			},
435			SerdeRoundFlowState::Finished { funding_tx, unlock_hash } => {
436				RoundFlowState::Finished {
437					funding_tx: funding_tx.into_owned(),
438					unlock_hash,
439				}
440			},
441			SerdeRoundFlowState::Failed { error } => {
442				RoundFlowState::Failed {
443					error: error.into_owned(),
444				}
445			},
446			SerdeRoundFlowState::Canceled => RoundFlowState::Canceled,
447		}
448	}
449}
450
451/// Model for [RoundState]
452#[derive(Debug, Serialize, Deserialize)]
453pub struct SerdeRoundState<'a> {
454	done: bool,
455	participation: SerdeRoundParticipation<'a>,
456	movement_id: Option<MovementId>,
457	flow: SerdeRoundFlowState<'a>,
458	#[serde(with = "ark::encode::serde::cow::vec")]
459	new_vtxos: Cow<'a, [Vtxo<Full>]>,
460	sent_forfeit_sigs: bool,
461}
462
463impl<'a> From<&'a RoundState> for SerdeRoundState<'a> {
464	fn from(state: &'a RoundState) -> Self {
465		Self {
466			done: state.done,
467			participation: (&state.participation).into(),
468			movement_id: state.movement_id,
469			flow: (&state.flow).into(),
470			new_vtxos: Cow::Borrowed(&state.new_vtxos),
471			sent_forfeit_sigs: state.sent_forfeit_sigs,
472		}
473	}
474}
475
476impl<'a> From<SerdeRoundState<'a>> for RoundState {
477	fn from(state: SerdeRoundState<'a>) -> Self {
478		Self {
479			done: state.done,
480			participation: state.participation.into(),
481			movement_id: state.movement_id,
482			flow: state.flow.into(),
483			new_vtxos: state.new_vtxos.into_owned(),
484			sent_forfeit_sigs: state.sent_forfeit_sigs,
485		}
486	}
487}
488
489#[cfg(test)]
490mod test {
491	use crate::exit::{ExitState, ExitTxOrigin};
492	use crate::vtxo::VtxoState;
493
494	#[test]
495	/// Each struct stored as JSON in the database should have test to check for backwards compatibility
496	/// Parsing can occur either in convert.rs or this file (query.rs)
497	fn test_serialised_structs() {
498		// Exit state
499		let serialised = r#"{"type":"start","tip_height":119}"#;
500		serde_json::from_str::<ExitState>(serialised).unwrap();
501		let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-input-confirmation","txids":["ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de"]}}]}"#;
502		serde_json::from_str::<ExitState>(serialised).unwrap();
503		let serialised = r#"{"type":"awaiting-delta","tip_height":122,"confirmed_block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","claimable_height":134}"#;
504		serde_json::from_str::<ExitState>(serialised).unwrap();
505		let serialised = r#"{"type":"claimable","tip_height":134,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block":null}"#;
506		serde_json::from_str::<ExitState>(serialised).unwrap();
507		let serialised = r#"{"type":"claim-in-progress","tip_height":134, "claimable_since": "134:6585896bdda6f08d924bf45cc2b16418af56703b3c50930e4dccbc1728d3800a","claim_txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c"}"#;
508		serde_json::from_str::<ExitState>(serialised).unwrap();
509		let serialised = r#"{"type":"claimed","tip_height":134,"txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c","block": "122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}"#;
510		serde_json::from_str::<ExitState>(serialised).unwrap();
511
512		// Exit child tx origins
513		let serialized = r#"{"type":"wallet","confirmed_in":null}"#;
514		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
515		let serialized = r#"{"type":"wallet","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
516		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
517		let serialized = r#"{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}"#;
518		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
519		let serialized = r#"{"type":"block","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
520		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
521
522		// Vtxo state
523		let serialised = r#"{"type": "spendable"}"#;
524		serde_json::from_str::<VtxoState>(serialised).unwrap();
525		let serialised = r#"{"type": "spent"}"#;
526		serde_json::from_str::<VtxoState>(serialised).unwrap();
527		let serialised = r#"{"type": "locked", "movement_id": null}"#;
528		serde_json::from_str::<VtxoState>(serialised).unwrap();
529	}
530}