Skip to content

Commit 9a8c323

Browse files
committed
Fix unexpected confirmed withdrawal bug
1 parent 5eba0b8 commit 9a8c323

4 files changed

Lines changed: 167 additions & 67 deletions

File tree

lib/state/error.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ pub enum Error {
234234
n_reservation_inputs: usize,
235235
n_reservation_outputs: usize,
236236
},
237+
#[error(
238+
"protocol would be insolvent after confirming unexpected withdrawal bundle {} in {}; bundle outpoint {} already spent",
239+
.m6id,
240+
.event_block_hash,
241+
.outpoint,
242+
)]
243+
UnexpectedWithdrawalBundleInsolvency {
244+
event_block_hash: bitcoin::BlockHash,
245+
m6id: M6id,
246+
outpoint: OutPoint,
247+
},
237248
#[error("Unknown withdrawal bundle: {m6id}")]
238249
UnknownWithdrawalBundle { m6id: M6id },
239250
#[error(
@@ -243,6 +254,13 @@ pub enum Error {
243254
event_block_hash: bitcoin::BlockHash,
244255
m6id: M6id,
245256
},
257+
#[error(
258+
"Unknown confirmed withdrawal bundle reconfirmed in {event_block_hash}: {m6id}"
259+
)]
260+
UnknownWithdrawalBundleReconfirmed {
261+
event_block_hash: bitcoin::BlockHash,
262+
m6id: M6id,
263+
},
246264
#[error("utxo double spent")]
247265
UtxoDoubleSpent,
248266
#[error(transparent)]

lib/state/mod.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,6 @@ enum WithdrawalBundleInfo {
5454
},
5555
}
5656

57-
impl WithdrawalBundleInfo {
58-
fn is_known(&self) -> bool {
59-
match self {
60-
Self::Known(_) => true,
61-
Self::Unknown | Self::UnknownConfirmed { .. } => false,
62-
}
63-
}
64-
}
65-
6657
type WithdrawalBundlesDb = DatabaseUnique<
6758
SerdeBincode<M6id>,
6859
SerdeBincode<(
@@ -259,7 +250,8 @@ impl State {
259250
WithdrawalBundleStatus::Confirmed
260251
| WithdrawalBundleStatus::Dropped
261252
| WithdrawalBundleStatus::Pending
262-
| WithdrawalBundleStatus::Submitted => None,
253+
| WithdrawalBundleStatus::Submitted
254+
| WithdrawalBundleStatus::SubmittedUnexpected => None,
263255
})
264256
.unwrap_or_else(|| {
265257
panic!("missing failure status for {latest_failed_m6id}")

lib/state/two_way_peg_data.rs

Lines changed: 143 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ fn connect_withdrawal_bundle_submitted(
175175
};
176176
return Err(err);
177177
}
178-
(_, WithdrawalBundleStatus::Submitted) => {
178+
(
179+
_,
180+
WithdrawalBundleStatus::Submitted
181+
| WithdrawalBundleStatus::SubmittedUnexpected,
182+
) => {
179183
let err =
180184
error::ConnectWithdrawalBundleSubmitted::Resubmitted {
181185
event_block_hash: *event_block_hash,
@@ -242,8 +246,8 @@ fn connect_withdrawal_bundle_submitted(
242246
}
243247
}
244248
bundle_status
245-
.push(WithdrawalBundleStatus::Submitted, block_height)
246-
.expect("push submitted status should be valid");
249+
.push(WithdrawalBundleStatus::SubmittedUnexpected, block_height)
250+
.expect("push submitted unexpected status should be valid");
247251
state
248252
.withdrawal_bundles
249253
.put(rwtxn, &m6id, &(bundle, bundle_status))?
@@ -283,43 +287,90 @@ fn connect_withdrawal_bundle_confirmed(
283287
// Already applied
284288
return Ok(());
285289
}
286-
assert_eq!(
290+
assert!(matches!(
287291
bundle_status.latest().value,
288292
WithdrawalBundleStatus::Submitted
289-
);
290-
// If an unknown bundle is confirmed, all UTXOs older than the
291-
// bundle submission are potentially spent.
292-
// This is only accepted in the case that block height is 0,
293-
// and so no UTXOs could possibly have been double-spent yet.
294-
// In this case, ALL UTXOs are considered spent.
295-
if !bundle.is_known() {
296-
if block_height == 0 {
297-
tracing::warn!(
298-
%event_block_hash,
299-
%m6id,
300-
"Unknown withdrawal bundle confirmed, marking all UTXOs as spent"
301-
);
302-
let raw_utxos: BTreeMap<OutPointKey, _> =
303-
state.utxos.iter(rwtxn)?.collect()?;
304-
let mut utxos: BTreeMap<OutPoint, _> = BTreeMap::new();
305-
for (outpoint_key, output) in &raw_utxos {
306-
let outpoint = OutPoint::from(*outpoint_key);
307-
let spent_output = SpentOutput {
308-
output: output.clone(),
309-
inpoint: InPoint::Withdrawal { m6id },
310-
};
311-
state.stxos.put(rwtxn, outpoint_key, &spent_output)?;
312-
utxos.insert(outpoint, output.clone());
313-
}
314-
state.utxos.clear(rwtxn)?;
315-
bundle =
316-
WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: utxos };
317-
} else {
318-
return Err(Error::UnknownWithdrawalBundleConfirmed {
293+
| WithdrawalBundleStatus::SubmittedUnexpected
294+
));
295+
match &bundle {
296+
WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ } => {
297+
return Err(Error::UnknownWithdrawalBundleReconfirmed {
319298
event_block_hash: *event_block_hash,
320299
m6id,
321300
});
322301
}
302+
WithdrawalBundleInfo::Unknown => {
303+
// If an unknown bundle is confirmed, all UTXOs older than the
304+
// bundle submission are potentially spent.
305+
// This is only accepted in the case that block height is 0,
306+
// and so no UTXOs could possibly have been double-spent yet.
307+
// In this case, ALL UTXOs are considered spent.
308+
if block_height == 0 {
309+
tracing::warn!(
310+
%event_block_hash,
311+
%m6id,
312+
"Unknown withdrawal bundle confirmed, marking all UTXOs as spent"
313+
);
314+
let utxos: BTreeMap<OutPoint, FilledOutput> = state
315+
.utxos
316+
.iter(rwtxn)
317+
.map_err(Error::from)?
318+
.map(|(key, output)| Ok((key.into(), output)))
319+
.collect()?;
320+
for (outpoint, output) in &utxos {
321+
let spent_output = SpentOutput {
322+
output: output.clone(),
323+
inpoint: InPoint::Withdrawal { m6id },
324+
};
325+
state.stxos.put(
326+
rwtxn,
327+
&OutPointKey::from(outpoint),
328+
&spent_output,
329+
)?;
330+
}
331+
state.utxos.clear(rwtxn)?;
332+
bundle = WithdrawalBundleInfo::UnknownConfirmed {
333+
spend_utxos: utxos,
334+
};
335+
} else {
336+
return Err(Error::UnknownWithdrawalBundleConfirmed {
337+
event_block_hash: *event_block_hash,
338+
m6id,
339+
});
340+
}
341+
}
342+
WithdrawalBundleInfo::Known(bundle) => {
343+
if matches!(
344+
bundle_status.latest().value,
345+
WithdrawalBundleStatus::SubmittedUnexpected
346+
) {
347+
// If a previously dropped or failed bundle is confirmed,
348+
// then unless all of the bundle UTXOs can be spent,
349+
// the chain is insolvent, and cannot continue.
350+
tracing::warn!(
351+
%event_block_hash,
352+
%m6id,
353+
"Unexpected withdrawal bundle confirmed, marking bundle UTXOs as spent"
354+
);
355+
for (outpoint, output) in bundle.spend_utxos() {
356+
let outpoint_key = OutPointKey::from(outpoint);
357+
if !state.utxos.delete(rwtxn, &outpoint_key)? {
358+
return Err(
359+
Error::UnexpectedWithdrawalBundleInsolvency {
360+
event_block_hash: *event_block_hash,
361+
m6id,
362+
outpoint: *outpoint,
363+
},
364+
);
365+
}
366+
let spent_output = SpentOutput {
367+
output: output.clone(),
368+
inpoint: InPoint::Withdrawal { m6id },
369+
};
370+
state.stxos.put(rwtxn, &outpoint_key, &spent_output)?;
371+
}
372+
}
373+
}
323374
}
324375
bundle_status
325376
.push(WithdrawalBundleStatus::Confirmed, block_height)
@@ -348,17 +399,21 @@ fn connect_withdrawal_bundle_failed(
348399
// Already applied
349400
return Ok(());
350401
}
351-
assert_eq!(
402+
assert!(matches!(
352403
bundle_status.latest().value,
353404
WithdrawalBundleStatus::Submitted
354-
);
355-
bundle_status
356-
.push(WithdrawalBundleStatus::Failed, block_height)
357-
.expect("Push failed status should be valid");
405+
| WithdrawalBundleStatus::SubmittedUnexpected
406+
));
358407
match &bundle {
359408
WithdrawalBundleInfo::Unknown
360409
| WithdrawalBundleInfo::UnknownConfirmed { .. } => (),
361-
WithdrawalBundleInfo::Known(bundle) => {
410+
WithdrawalBundleInfo::Known(bundle) => 'known: {
411+
if matches!(
412+
bundle_status.latest().value,
413+
WithdrawalBundleStatus::SubmittedUnexpected
414+
) {
415+
break 'known;
416+
}
362417
for (outpoint, output) in bundle.spend_utxos() {
363418
state.stxos.delete(rwtxn, &OutPointKey::from(outpoint))?;
364419
state
@@ -382,6 +437,9 @@ fn connect_withdrawal_bundle_failed(
382437
)?;
383438
}
384439
}
440+
bundle_status
441+
.push(WithdrawalBundleStatus::Failed, block_height)
442+
.expect("Push failed status should be valid");
385443
state
386444
.withdrawal_bundles
387445
.put(rwtxn, &m6id, &(bundle, bundle_status))?;
@@ -569,10 +627,11 @@ fn disconnect_withdrawal_bundle_submitted(
569627
}
570628
};
571629
let (bundle_status, latest_bundle_status) = bundle_status.pop();
572-
assert_eq!(
630+
assert!(matches!(
573631
latest_bundle_status.value,
574632
WithdrawalBundleStatus::Submitted
575-
);
633+
| WithdrawalBundleStatus::SubmittedUnexpected
634+
));
576635
assert_eq!(latest_bundle_status.height, block_height);
577636
match &bundle {
578637
WithdrawalBundleInfo::Unknown
@@ -622,7 +681,11 @@ fn disconnect_withdrawal_bundle_confirmed(
622681
.try_get(rwtxn, &m6id)?
623682
.ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?;
624683
let (prev_bundle_status, latest_bundle_status) = bundle_status.pop();
625-
if latest_bundle_status.value == WithdrawalBundleStatus::Submitted {
684+
if matches!(
685+
latest_bundle_status.value,
686+
WithdrawalBundleStatus::Submitted
687+
| WithdrawalBundleStatus::SubmittedUnexpected
688+
) {
626689
// Already applied
627690
return Ok(());
628691
}
@@ -633,25 +696,41 @@ fn disconnect_withdrawal_bundle_confirmed(
633696
assert_eq!(latest_bundle_status.height, block_height);
634697
let prev_bundle_status = prev_bundle_status
635698
.expect("Pop confirmed bundle status should be valid");
636-
assert_eq!(
699+
assert!(matches!(
637700
prev_bundle_status.latest().value,
638701
WithdrawalBundleStatus::Submitted
639-
);
640-
match bundle {
641-
WithdrawalBundleInfo::Known(_) | WithdrawalBundleInfo::Unknown => (),
702+
| WithdrawalBundleStatus::SubmittedUnexpected
703+
));
704+
match &bundle {
705+
WithdrawalBundleInfo::Known(bundle) => {
706+
if matches!(
707+
prev_bundle_status.latest().value,
708+
WithdrawalBundleStatus::SubmittedUnexpected
709+
) {
710+
for (outpoint, output) in bundle.spend_utxos() {
711+
let outpoint_key = OutPointKey::from(outpoint);
712+
state.utxos.put(rwtxn, &outpoint_key, output)?;
713+
if !state.stxos.delete(rwtxn, &outpoint_key)? {
714+
return Err(Error::NoStxo {
715+
outpoint: *outpoint,
716+
});
717+
};
718+
}
719+
}
720+
}
642721
WithdrawalBundleInfo::UnknownConfirmed { spend_utxos } => {
643722
for (outpoint, output) in spend_utxos {
644-
state.utxos.put(
645-
rwtxn,
646-
&OutPointKey::from(&outpoint),
647-
&output,
648-
)?;
649-
if !state.stxos.delete(rwtxn, &OutPointKey::from(&outpoint))? {
650-
return Err(Error::NoStxo { outpoint });
723+
let outpoint_key = OutPointKey::from(outpoint);
724+
state.utxos.put(rwtxn, &outpoint_key, output)?;
725+
if !state.stxos.delete(rwtxn, &outpoint_key)? {
726+
return Err(Error::NoStxo {
727+
outpoint: *outpoint,
728+
});
651729
};
652730
}
653731
bundle = WithdrawalBundleInfo::Unknown;
654732
}
733+
WithdrawalBundleInfo::Unknown => (),
655734
}
656735
state.withdrawal_bundles.put(
657736
rwtxn,
@@ -681,14 +760,21 @@ fn disconnect_withdrawal_bundle_failed(
681760
assert_eq!(latest_bundle_status.height, block_height);
682761
let prev_bundle_status =
683762
prev_bundle_status.expect("Pop failed bundle status should be valid");
684-
assert_eq!(
763+
assert!(matches!(
685764
prev_bundle_status.latest().value,
686765
WithdrawalBundleStatus::Submitted
687-
);
766+
| WithdrawalBundleStatus::SubmittedUnexpected
767+
));
688768
match &bundle {
689769
WithdrawalBundleInfo::Unknown
690770
| WithdrawalBundleInfo::UnknownConfirmed { .. } => (),
691-
WithdrawalBundleInfo::Known(bundle) => {
771+
WithdrawalBundleInfo::Known(bundle) => 'known: {
772+
if matches!(
773+
prev_bundle_status.latest().value,
774+
WithdrawalBundleStatus::SubmittedUnexpected
775+
) {
776+
break 'known;
777+
}
692778
for (outpoint, output) in bundle.spend_utxos().iter().rev() {
693779
let spent_output = SpentOutput {
694780
output: output.clone(),

types/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ pub enum WithdrawalBundleStatus {
206206
Failed,
207207
Pending,
208208
Submitted,
209+
/// Submitted, but unexpected due to previously being dropped or failing.
210+
/// It may not be possible to account for this withdrawal bundle, if it
211+
/// double-spends UTXOs.
212+
SubmittedUnexpected,
209213
}
210214

211215
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]

0 commit comments

Comments
 (0)