Skip to content

Commit 1400d32

Browse files
committed
fix: cleanups and more tests
1 parent f998166 commit 1400d32

File tree

5 files changed

+252
-99
lines changed

5 files changed

+252
-99
lines changed

crates/asm/subprotocols/bridge-v1/src/errors.rs

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ use strata_primitives::{
1111
};
1212
use thiserror::Error;
1313

14+
#[derive(Debug, Error)]
15+
pub enum BridgeSubprotocolError {
16+
#[error("failed to parse deposit tx")]
17+
DepositTxParse(#[from] DepositTxParseError),
18+
19+
#[error("failed to process deposit tx")]
20+
DepositTxProcess(#[from] DepositValidationError),
21+
22+
#[error("failed to parse withdrawal fulfillment tx")]
23+
WithdrawalTxParse(#[from] WithdrawalParseError),
24+
25+
#[error("failed to parse withdrawal fulfillment tx")]
26+
WithdrawalTxProcess(#[from] WithdrawalValidationError),
27+
28+
#[error("unsupported tx type {0}")]
29+
UnsupportedTxType(TxType),
30+
}
31+
1432
/// Errors that can occur when validating deposit transactions at the subprotocol level.
1533
///
1634
/// These errors represent state-level validation failures that occur after successful
@@ -40,24 +58,6 @@ pub enum DepositValidationError {
4058
EmptyOperators,
4159
}
4260

43-
#[derive(Debug, Error)]
44-
pub enum BridgeSubprotocolError {
45-
#[error("failed to parse deposit tx")]
46-
DepositTxParse(#[from] DepositTxParseError),
47-
48-
#[error("failed to process deposit tx")]
49-
DepositTxProcess(#[from] DepositValidationError),
50-
51-
#[error("failed to parse withdrawal fulfillment tx")]
52-
WithdrawalTxParse(#[from] WithdrawalParseError),
53-
54-
#[error("failed to parse withdrawal fulfillment tx")]
55-
WithdrawalTxProcess(#[from] WithdrawalValidationError),
56-
57-
#[error("unsupported tx type {0}")]
58-
UnsupportedTxType(TxType),
59-
}
60-
6161
/// Errors that can occur when validating withdrawal fulfillment transactions.
6262
///
6363
/// When these validation errors occur, they are logged and the transaction is skipped.
@@ -105,3 +105,23 @@ pub enum WithdrawalCommandError {
105105
#[error("Deposit amount mismatch {0}")]
106106
DepositWithdrawalAmountMismatch(Mismatch<u64>),
107107
}
108+
109+
/// Error type for OperatorBitmap operations.
110+
#[derive(Clone, Debug, PartialEq, Eq)]
111+
pub enum BitmapError {
112+
/// Attempted to set a bit at an index that would create a gap in the bitmap.
113+
/// Only sequential indices are allowed.
114+
IndexOutOfBounds(OperatorIdx),
115+
}
116+
117+
impl std::fmt::Display for BitmapError {
118+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119+
match self {
120+
BitmapError::IndexOutOfBounds(idx) => {
121+
write!(f, "Index {} is out of bounds for sequential bitmap", idx)
122+
}
123+
}
124+
}
125+
}
126+
127+
impl std::error::Error for BitmapError {}

crates/asm/subprotocols/bridge-v1/src/state/assignment.rs

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,13 @@ impl AssignmentEntry {
142142
let seed_bytes: [u8; 32] = Buf32::from(seed).into();
143143
let mut rng = ChaChaRng::from_seed(seed_bytes);
144144

145-
let empty_bitmap = OperatorBitmap::new_empty(); // No previous assignees at creation
145+
// No previous assignees at creation
146+
let previous_assignees =
147+
OperatorBitmap::new_with_size(deposit_entry.notary_operators_bitmap().len(), false);
146148

147149
let eligible_operators = filter_eligible_operators(
148-
deposit_entry.notary_operators(),
149-
&empty_bitmap,
150+
deposit_entry.notary_operators_bitmap(),
151+
&previous_assignees,
150152
current_active_operators,
151153
);
152154

@@ -162,10 +164,10 @@ impl AssignmentEntry {
162164
let current_assignee = eligible_indices[random_index];
163165

164166
Ok(Self {
165-
deposit_entry,
167+
deposit_entry: deposit_entry.clone(),
166168
withdrawal_cmd,
167169
current_assignee,
168-
previous_assignees: OperatorBitmap::new_empty(),
170+
previous_assignees,
169171
exec_deadline,
170172
})
171173
}
@@ -230,16 +232,19 @@ impl AssignmentEntry {
230232

231233
// Use the already cached bitmap from DepositEntry instead of converting from Vec
232234
let mut eligible_operators = filter_eligible_operators(
233-
self.deposit_entry.notary_operators(),
235+
self.deposit_entry.notary_operators_bitmap(),
234236
&self.previous_assignees,
235237
current_active_operators,
236238
);
237239

238240
if eligible_operators.not_any() {
239241
// If no eligible operators left, clear previous assignees
240-
self.previous_assignees = OperatorBitmap::new_empty();
242+
self.previous_assignees = OperatorBitmap::new_with_size(
243+
self.deposit_entry.notary_operators_bitmap().len(),
244+
false,
245+
);
241246
eligible_operators = filter_eligible_operators(
242-
self.deposit_entry.notary_operators(),
247+
self.deposit_entry.notary_operators_bitmap(),
243248
&self.previous_assignees,
244249
current_active_operators,
245250
);
@@ -388,7 +393,7 @@ impl AssignmentTable {
388393
///
389394
/// ```rust,ignore
390395
/// let current_height = BitcoinBlockHeight::from(1000);
391-
/// let active_operators = OperatorBitmap::new_sequential_active(3);
396+
/// let active_operators = OperatorBitmap::new_with_size(3, true);
392397
/// let seed = L1BlockId::from([0u8; 32]);
393398
///
394399
/// table.reassign_expired_assignments(current_height, &active_operators, seed)?;
@@ -432,7 +437,7 @@ mod tests {
432437
let seed: L1BlockId = arb.generate();
433438

434439
// Use the deposit's notary operators as active operators
435-
let current_active_operators = deposit_entry.notary_operators().clone();
440+
let current_active_operators = deposit_entry.notary_operators_bitmap().clone();
436441

437442
let result = AssignmentEntry::create_with_random_assignment(
438443
deposit_entry.clone(),
@@ -482,21 +487,24 @@ mod tests {
482487
#[test]
483488
fn test_reassign_success() {
484489
let mut arb = ArbitraryGenerator::new();
485-
let deposit_entry: DepositEntry = arb.generate();
490+
491+
// Keep generating deposit entries until we have at least 2 active operators
492+
let deposit_entry: DepositEntry = loop {
493+
let candidate: DepositEntry = arb.generate();
494+
if candidate.notary_operators_bitmap().active_count() >= 2 {
495+
break candidate;
496+
}
497+
};
498+
486499
let withdrawal_cmd: WithdrawalCommand = arb.generate();
487500
let exec_deadline: BitcoinBlockHeight = 100;
488501
let seed1: L1BlockId = arb.generate();
489502
let seed2: L1BlockId = arb.generate();
490503

491504
// Use the deposit's notary operators as active operators
492-
let current_active_operators = deposit_entry.notary_operators().clone();
505+
let current_active_operators = deposit_entry.notary_operators_bitmap().clone();
493506
let new_fee = BitcoinAmount::from_sat(20_000);
494507

495-
// Ensure we have at least 2 operators for reassignment
496-
if current_active_operators.active_count() < 2 {
497-
return; // Skip test if not enough operators
498-
}
499-
500508
let mut assignment = AssignmentEntry::create_with_random_assignment(
501509
deposit_entry,
502510
withdrawal_cmd,
@@ -513,18 +521,10 @@ mod tests {
513521
let result = assignment.reassign(new_fee, seed2, &current_active_operators);
514522
assert!(result.is_ok());
515523

516-
// Verify reassignment - the behavior depends on how many operators are available
517-
if assignment.previous_assignees().len() == 1 {
518-
// Normal case: different operator selected and previous assignee tracked
519-
assert_eq!(assignment.previous_assignees()[0], original_assignee);
520-
assert_ne!(assignment.current_assignee(), original_assignee);
521-
} else {
522-
// Edge case: previous assignees were cleared during reassignment
523-
// This happens when no eligible operators are found initially, forcing
524-
// the reassignment logic to clear previous assignees and retry
525-
assert_eq!(assignment.previous_assignees().len(), 0);
526-
}
527-
assert!(current_active_operators.is_active(assignment.current_assignee()));
524+
// Verify reassignment
525+
assert_eq!(assignment.previous_assignees().len(), 1);
526+
assert_eq!(assignment.previous_assignees()[0], original_assignee);
527+
assert_ne!(assignment.current_assignee(), original_assignee);
528528
}
529529

530530
#[test]
@@ -533,7 +533,7 @@ mod tests {
533533
let mut deposit_entry: DepositEntry = arb.generate();
534534

535535
// Force single operator for this test
536-
let operators = OperatorBitmap::new_sequential_active(1);
536+
let operators = OperatorBitmap::new_with_size(1, true);
537537
deposit_entry = DepositEntry::new(
538538
deposit_entry.idx(),
539539
*deposit_entry.output(),
@@ -548,7 +548,7 @@ mod tests {
548548
let seed1: L1BlockId = arb.generate();
549549
let seed2: L1BlockId = arb.generate();
550550

551-
let current_active_operators = OperatorBitmap::new_sequential_active(1); // Single operator with index 0
551+
let current_active_operators = OperatorBitmap::new_with_size(1, true); // Single operator with index 0
552552

553553
let mut assignment = AssignmentEntry::create_with_random_assignment(
554554
deposit_entry,
@@ -579,7 +579,7 @@ mod tests {
579579
let withdrawal_cmd: WithdrawalCommand = arb.generate();
580580
let exec_deadline: BitcoinBlockHeight = 100;
581581
let seed: L1BlockId = arb.generate();
582-
let current_active_operators = deposit_entry.notary_operators().clone();
582+
let current_active_operators = deposit_entry.notary_operators_bitmap().clone();
583583

584584
let assignment = AssignmentEntry::create_with_random_assignment(
585585
deposit_entry.clone(),
@@ -623,7 +623,7 @@ mod tests {
623623
let deposit_entry1: DepositEntry = arb.generate();
624624
let withdrawal_cmd1: WithdrawalCommand = arb.generate();
625625
let expired_deadline: BitcoinBlockHeight = 100; // Less than current_height
626-
let current_active_operators1 = deposit_entry1.notary_operators().clone();
626+
let current_active_operators1 = deposit_entry1.notary_operators_bitmap().clone();
627627

628628
let expired_assignment = AssignmentEntry::create_with_random_assignment(
629629
deposit_entry1.clone(),
@@ -642,7 +642,7 @@ mod tests {
642642
let deposit_entry2: DepositEntry = arb.generate();
643643
let withdrawal_cmd2: WithdrawalCommand = arb.generate();
644644
let future_deadline: BitcoinBlockHeight = 200; // Greater than current_height
645-
let current_active_operators2 = deposit_entry2.notary_operators().clone();
645+
let current_active_operators2 = deposit_entry2.notary_operators_bitmap().clone();
646646

647647
let future_assignment = AssignmentEntry::create_with_random_assignment(
648648
deposit_entry2.clone(),
@@ -680,7 +680,7 @@ mod tests {
680680
let expired_assignment_after = table.get_assignment(expired_deposit_idx).unwrap();
681681

682682
// The behavior depends on how many eligible operators are available
683-
let deposit1_notary_count = deposit_entry1.notary_operators().active_count();
683+
let deposit1_notary_count = deposit_entry1.notary_operators_bitmap().active_count();
684684
if deposit1_notary_count > 1
685685
&& expired_assignment_after.current_assignee() != original_assignee
686686
{

crates/asm/subprotocols/bridge-v1/src/state/bridge.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ impl BridgeV1State {
192192
) -> Result<(), DepositValidationError> {
193193
// Validate the deposit first
194194
self.validate_deposit(tx, info)?;
195-
let operators = self.operators.current_multisig_bitmap().clone();
196-
let entry = DepositEntry::new(info.deposit_idx, info.outpoint, operators, info.amt)?;
195+
let notary_operators = self.operators.current_multisig_bitmap().clone();
196+
let entry = DepositEntry::new(info.deposit_idx, info.outpoint, notary_operators, info.amt)?;
197197
self.deposits.insert_deposit(entry)?;
198198

199199
Ok(())

crates/asm/subprotocols/bridge-v1/src/state/deposit.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use arbitrary::Arbitrary;
99
use borsh::{BorshDeserialize, BorshSerialize};
1010
use strata_primitives::{
11+
bridge::OperatorIdx,
1112
l1::{BitcoinAmount, OutputRef},
1213
sorted_vec::SortedVec,
1314
};
@@ -55,7 +56,7 @@ pub struct DepositEntry {
5556
/// Any one honest operator from this set can process user withdrawals.
5657
///
5758
/// Uses a memory-efficient bitmap representation instead of storing operator indices.
58-
operators: OperatorBitmap,
59+
notary_operators: OperatorBitmap,
5960

6061
/// Amount of Bitcoin locked in this deposit (in satoshis).
6162
amt: BitcoinAmount,
@@ -95,17 +96,17 @@ impl DepositEntry {
9596
pub fn new(
9697
idx: u32,
9798
output: OutputRef,
98-
operators: OperatorBitmap,
99+
notary_operators: OperatorBitmap,
99100
amt: BitcoinAmount,
100101
) -> Result<Self, DepositValidationError> {
101-
if operators.active_count() == 0 {
102+
if notary_operators.active_count() == 0 {
102103
return Err(DepositValidationError::EmptyOperators);
103104
}
104105

105106
Ok(Self {
106107
deposit_idx: idx,
107108
output,
108-
operators,
109+
notary_operators,
109110
amt,
110111
})
111112
}
@@ -125,8 +126,17 @@ impl DepositEntry {
125126
/// # Returns
126127
///
127128
/// A reference to the operator bitmap that contains the operators active for this deposit.
128-
pub fn notary_operators(&self) -> &OperatorBitmap {
129-
&self.operators
129+
pub fn notary_operators_bitmap(&self) -> &OperatorBitmap {
130+
&self.notary_operators
131+
}
132+
133+
/// Returns the indices of operators that formed the N/N multisig for this deposit.
134+
///
135+
/// # Returns
136+
///
137+
/// Vector containing [`OperatorIdx`] for operators that were active for this deposit.
138+
pub fn notary_operators_indices(&self) -> Vec<OperatorIdx> {
139+
self.notary_operators.to_indices()
130140
}
131141

132142
/// Returns the amount of Bitcoin locked in this deposit.
@@ -143,17 +153,14 @@ impl<'a> Arbitrary<'a> for DepositEntry {
143153
// Generate a random Bitcoin UTXO reference
144154
let output: OutputRef = u.arbitrary()?;
145155

146-
// Generate a random number of notary operators between 1 and 20
147-
let num_operators = u.int_in_range(1..=20)?;
148-
149156
// Create OperatorBitmap directly by setting sequential operators as active
150-
let operators = OperatorBitmap::new_sequential_active(num_operators);
157+
let notary_operators = u.arbitrary()?;
151158

152159
// Generate a random Bitcoin amount (between 1 satoshi and 21 million BTC)
153160
let amount: BitcoinAmount = u.arbitrary()?;
154161

155162
// Create the DepositEntry - this should not fail since we ensure operators is non-empty
156-
Self::new(deposit_idx, output, operators, amount)
163+
Self::new(deposit_idx, output, notary_operators, amount)
157164
.map_err(|_| arbitrary::Error::IncorrectFormat)
158165
}
159166
}

0 commit comments

Comments
 (0)