Skip to content

Commit 919e73e

Browse files
committed
refactor(lib): replace get_smallest_lexicographic_outpoint by LexMin
The new LexMin is a zero allocation cost struct that allows computing the minimal lexicographic outpoint in the same loop where other logic is contained, avoiding the intermediate step of extracting all the outpoints from their TxIn into a separated collection to sort them.
1 parent 0e65087 commit 919e73e

8 files changed

Lines changed: 162 additions & 104 deletions

File tree

silentpayments/src/lib.rs

Lines changed: 104 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -63,34 +63,62 @@ pub fn tag_txin(txin: &TxIn, script_pubkey: &ScriptBuf) -> Option<SpInputs> {
6363
}
6464
}
6565

66-
pub fn get_smallest_lexicographic_outpoint(outpoints: &[OutPoint]) -> [u8; 36] {
67-
// Find the outpoint with the smallest lexicographic order
68-
let smallest = outpoints
69-
.iter()
70-
.min_by(|a, b| {
71-
// Compare txids first
72-
let a_txid = a.txid.to_raw_hash();
73-
let b_txid = b.txid.to_raw_hash();
74-
75-
// If txids are different, compare them
76-
match a_txid.as_byte_array().cmp(b_txid.as_byte_array()) {
77-
std::cmp::Ordering::Equal => {
78-
// If txids are equal, compare vouts directly
79-
let a_vout_bytes = a.vout.to_le_bytes();
80-
let b_vout_bytes = b.vout.to_le_bytes();
81-
a_vout_bytes.cmp(&b_vout_bytes)
66+
#[derive(Debug)]
67+
pub enum LexMinError {
68+
NoMinOutpoint,
69+
}
70+
71+
impl std::fmt::Display for LexMinError {
72+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73+
match self {
74+
Self::NoMinOutpoint => write!(f, "No minimal outpoint, update at least once"),
75+
}
76+
}
77+
}
78+
79+
#[derive(Default)]
80+
pub struct LexMin<'a> {
81+
current_min: Option<&'a OutPoint>,
82+
}
83+
84+
impl<'a> LexMin<'a> {
85+
pub fn update(&mut self, outpoint: &'a OutPoint) -> &'a OutPoint {
86+
if let Some(min) = self.current_min {
87+
let new_min = std::cmp::min_by(outpoint, min, |a, b| {
88+
// Compare txids first
89+
let a_txid = a.txid.to_raw_hash();
90+
let b_txid = b.txid.to_raw_hash();
91+
92+
// If txids are different, compare them
93+
match a_txid.as_byte_array().cmp(b_txid.as_byte_array()) {
94+
std::cmp::Ordering::Equal => {
95+
// If txids are equal, compare vouts directly
96+
let a_vout_bytes = a.vout.to_le_bytes();
97+
let b_vout_bytes = b.vout.to_le_bytes();
98+
a_vout_bytes.cmp(&b_vout_bytes)
99+
}
100+
other => other,
82101
}
83-
other => other,
84-
}
85-
})
86-
.expect("cannot create silent payment script pubkey without outpoints");
102+
});
103+
self.current_min = Some(new_min);
104+
new_min
105+
} else {
106+
self.current_min = Some(outpoint);
107+
outpoint
108+
}
109+
}
87110

88-
// Only allocate the result array once we have the smallest outpoint
89-
let mut result = [0u8; 36];
90-
result[..32].copy_from_slice(smallest.txid.to_raw_hash().as_byte_array());
91-
result[32..36].copy_from_slice(&smallest.vout.to_le_bytes());
111+
pub fn bytes(&self) -> Result<[u8; 36], LexMinError> {
112+
if let Some(min) = self.current_min {
113+
let mut result = [0u8; 36];
114+
result[..32].copy_from_slice(min.txid.to_raw_hash().as_byte_array());
115+
result[32..36].copy_from_slice(&min.vout.to_le_bytes());
92116

93-
result
117+
Ok(result)
118+
} else {
119+
Err(LexMinError::NoMinOutpoint)
120+
}
121+
}
94122
}
95123

96124
// Do not report coverage for this function as it is a wrapper around external lib function
@@ -110,9 +138,7 @@ pub fn compute_shared_secret(sk: &SecretKey, pk: &PublicKey) -> PublicKey {
110138
#[cfg(test)]
111139
#[cfg_attr(coverage_nightly, coverage(off))]
112140
mod tests {
113-
use super::{
114-
get_smallest_lexicographic_outpoint, tag_txin, Hash, ScriptBuf, SpInputs, TxIn, NUMS_H,
115-
};
141+
use super::{tag_txin, Hash, LexMin, ScriptBuf, SpInputs, TxIn, NUMS_H};
116142
use bitcoin::{
117143
hex::test_hex_unwrap as hex, taproot::TAPROOT_ANNEX_PREFIX, OutPoint, Sequence, Txid,
118144
Witness,
@@ -344,8 +370,9 @@ mod tests {
344370
}
345371

346372
#[test]
347-
fn test_get_smallest_outpoint_different_txids_and_vouts() {
348-
let outpoints = vec![
373+
fn test_lex_min_different_txids_and_vouts() {
374+
let mut lex_min = LexMin::default();
375+
let outpoints = [
349376
OutPoint {
350377
txid: Txid::from_slice(&[3u8; 32]).unwrap(),
351378
vout: 2,
@@ -360,7 +387,11 @@ mod tests {
360387
},
361388
];
362389

363-
let result = get_smallest_lexicographic_outpoint(&outpoints);
390+
for outpoint in outpoints.iter() {
391+
lex_min.update(outpoint);
392+
}
393+
394+
let result = lex_min.bytes().expect("should succeed");
364395

365396
let mut expected_bytes = [2u8; 36];
366397
expected_bytes[32..36].copy_from_slice(&1u32.to_le_bytes());
@@ -369,32 +400,37 @@ mod tests {
369400
}
370401

371402
#[test]
372-
#[should_panic(expected = "cannot create silent payment script pubkey without outpoints")]
373-
fn test_get_smallest_outpoint_empty() {
374-
let outpoints: Vec<OutPoint> = vec![];
375-
get_smallest_lexicographic_outpoint(&outpoints);
403+
fn test_lex_min_no_update() {
404+
let e = LexMin::default().bytes().expect_err("should fail");
405+
assert_eq!("No minimal outpoint, update at least once", e.to_string());
376406
}
377407

378408
// Additional test: same txid, different vouts
379409
#[test]
380-
fn test_get_smallest_outpoint_identical_txid_different_vouts() {
410+
fn test_lex_min_identical_txid_different_vouts() {
411+
let mut lex_min = LexMin::default();
381412
let txid = Txid::from_slice(&[0u8; 32]).unwrap();
382-
let outpoints = vec![
413+
let outpoints = [
383414
OutPoint { txid, vout: 10 },
384415
OutPoint { txid, vout: 2 },
385416
OutPoint { txid, vout: 5 },
386417
];
387418

388-
let result = get_smallest_lexicographic_outpoint(&outpoints);
419+
for outpoint in outpoints.iter() {
420+
lex_min.update(outpoint);
421+
}
422+
423+
let result = lex_min.bytes().expect("should succeed");
389424

390425
let mut expected_bytes = [0u8; 36];
391426
expected_bytes[32..36].copy_from_slice(&2u32.to_le_bytes());
392427
assert_eq!(result, expected_bytes);
393428
}
394429

395430
#[test]
396-
fn test_get_smallest_outpoint_same_vout_different_txid() {
397-
let outpoints = vec![
431+
fn test_lex_min_same_vout_different_txid() {
432+
let mut lex_min = LexMin::default();
433+
let outpoints = [
398434
OutPoint {
399435
txid: Txid::from_slice(&[2u8; 32]).unwrap(),
400436
vout: 7,
@@ -409,16 +445,21 @@ mod tests {
409445
},
410446
];
411447

412-
let result = get_smallest_lexicographic_outpoint(&outpoints);
448+
for outpoint in outpoints.iter() {
449+
lex_min.update(outpoint);
450+
}
451+
452+
let result = lex_min.bytes().expect("should succeed");
413453

414454
let mut expected_bytes = [1u8; 36];
415455
expected_bytes[32..36].copy_from_slice(&7u32.to_le_bytes());
416456
assert_eq!(result, expected_bytes);
417457
}
418458

419459
#[test]
420-
fn test_get_smallest_outpoint_edge_case_max_vout() {
421-
let outpoints = vec![
460+
fn test_lex_min_edge_case_max_vout() {
461+
let mut lex_min = LexMin::default();
462+
let outpoints = [
422463
OutPoint {
423464
txid: Txid::from_slice(&[1u8; 32]).unwrap(),
424465
vout: u32::MAX,
@@ -429,7 +470,11 @@ mod tests {
429470
},
430471
];
431472

432-
let result = get_smallest_lexicographic_outpoint(&outpoints);
473+
for outpoint in outpoints.iter() {
474+
lex_min.update(outpoint);
475+
}
476+
477+
let result = lex_min.bytes().expect("should succeed");
433478

434479
let mut expected_bytes = [1u8; 36];
435480
expected_bytes[..32].copy_from_slice(&[1u8; 32]);
@@ -438,8 +483,9 @@ mod tests {
438483
}
439484

440485
#[test]
441-
fn test_get_smallest_outpoint_txid_takes_precedence() {
442-
let outpoints = vec![
486+
fn test_lex_min_txid_takes_precedence() {
487+
let mut lex_min = LexMin::default();
488+
let outpoints = [
443489
OutPoint {
444490
txid: Txid::from_slice(&[8u8; 32]).unwrap(),
445491
vout: 0,
@@ -450,7 +496,11 @@ mod tests {
450496
},
451497
];
452498

453-
let result = get_smallest_lexicographic_outpoint(&outpoints);
499+
for outpoint in outpoints.iter() {
500+
lex_min.update(outpoint);
501+
}
502+
503+
let result = lex_min.bytes().expect("should succeed");
454504

455505
let mut expected_bytes = [5u8; 36];
456506
expected_bytes[32..36].copy_from_slice(&100u32.to_le_bytes());
@@ -459,6 +509,7 @@ mod tests {
459509

460510
#[test]
461511
fn test_get_smallest_outpoint_txid_endianness_matters() {
512+
let mut lex_min = LexMin::default();
462513
// big endian: 0x[00][00][00][01]
463514
// big endian: 0x[a1][b1][c1][d1]
464515
let mut txid_bytes_be = [0u8; 32];
@@ -469,7 +520,7 @@ mod tests {
469520
let mut txid_bytes_le = [0u8; 32];
470521
txid_bytes_le[31] = 1;
471522

472-
let outpoints = vec![
523+
let outpoints = [
473524
OutPoint {
474525
txid: Txid::from_slice(&txid_bytes_be).unwrap(),
475526
vout: 1,
@@ -480,9 +531,13 @@ mod tests {
480531
},
481532
];
482533

534+
for outpoint in outpoints.iter() {
535+
lex_min.update(outpoint);
536+
}
537+
483538
// if Txid is big endian then: [a1] < [a2] => expected_bytes = txid_bytes_be
484539
// if Txid is little endian then: [d2] < [d1] => expected_bytes = txid_bytes_le
485-
let result = get_smallest_lexicographic_outpoint(&outpoints);
540+
let result = lex_min.bytes().expect("should succeed");
486541

487542
let mut expected_bytes = [0u8; 36];
488543
expected_bytes[31] = 1;

silentpayments/src/receive/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ pub enum SpReceiveError {
66
Secp256k1Error(bitcoin::secp256k1::Error),
77
/// Secp256k1 error
88
SliceError(bitcoin::key::FromSliceError),
9+
/// Cannot derive silent payment output without input prevout outpoints
10+
NoOutpoints(crate::LexMinError),
11+
}
12+
13+
impl From<crate::LexMinError> for SpReceiveError {
14+
fn from(e: crate::LexMinError) -> Self {
15+
Self::NoOutpoints(e)
16+
}
917
}
1018

1119
impl From<bitcoin::secp256k1::Error> for SpReceiveError {
@@ -32,6 +40,7 @@ impl std::fmt::Display for SpReceiveError {
3240
}
3341
SpReceiveError::Secp256k1Error(e) => write!(f, "Silent payment receive error: {e}"),
3442
SpReceiveError::SliceError(e) => write!(f, "Silent payment receive error: {e}"),
43+
Self::NoOutpoints(e) => write!(f, "Silent payment sending error: {e}"),
3544
}
3645
}
3746
}

silentpayments/src/receive/mod.rs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ pub mod scan;
44
pub use self::error::SpReceiveError;
55

66
use crate::{
7-
get_smallest_lexicographic_outpoint,
87
hashes::{InputsHash, SharedSecretHash},
9-
tag_txin, SpInputs,
8+
tag_txin, LexMin, SpInputs,
109
};
1110

1211
use std::collections::BTreeMap;
@@ -231,33 +230,26 @@ pub fn compute_tweak_data(
231230
) -> Result<PublicKey, SpReceiveError> {
232231
let secp = Secp256k1::verification_only();
233232

234-
let input_pubkeys = tx
235-
.input
236-
.clone()
237-
.into_iter()
238-
.zip(prevouts)
239-
.filter_map(|(txin, prevout)| {
240-
// NOTE: Public keys which couldn't be extracted will be ignored
241-
extract_pubkey(txin, &prevout.script_pubkey).map(|(_input_type, key)| key)
242-
})
243-
.collect::<Vec<PublicKey>>();
233+
let mut input_pubkeys = <Vec<PublicKey>>::new();
234+
let mut lex_min = LexMin::default();
235+
for (txin, prevout) in tx.input.iter().zip(prevouts) {
236+
lex_min.update(&txin.previous_output);
237+
// NOTE: Public keys which couldn't be extracted will be ignored
238+
if let Some((_, key)) = extract_pubkey(txin.clone(), &prevout.script_pubkey) {
239+
input_pubkeys.push(key)
240+
}
241+
}
244242

245243
let input_pubkey_refs: Vec<&PublicKey> = input_pubkeys.iter().collect();
246244

247245
#[allow(non_snake_case)]
248246
// NOTE: cannot ignore malicious crafting of transaction with input public keys that cancel
249247
// themselves
250248
let A_sum = PublicKey::combine_keys(&input_pubkey_refs)?;
251-
let outpoints = tx
252-
.input
253-
.iter()
254-
.map(|txin| txin.previous_output)
255-
.collect::<Vec<OutPoint>>();
256-
let smallest_outpoint = get_smallest_lexicographic_outpoint(&outpoints);
257249

258250
let input_hash = {
259251
let mut eng = InputsHash::engine();
260-
eng.input(&smallest_outpoint);
252+
eng.input(&lex_min.bytes()?);
261253
eng.input(&A_sum.serialize());
262254
let hash = InputsHash::from_engine(eng);
263255
// NOTE: Why big endian bytes??? Doesn't matter. Look at: https://github.com/rust-bitcoin/rust-bitcoin/issues/1896

silentpayments/src/send/bip32.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use std::collections::HashMap;
22

33
use crate::{
44
encoding::SilentPaymentCode,
5-
get_smallest_lexicographic_outpoint,
65
send::{
76
create_silentpayment_partial_secret, create_silentpayment_scriptpubkeys, error::SpSendError,
87
},
8+
LexMin,
99
};
1010

1111
use bitcoin::{
@@ -30,10 +30,12 @@ impl XprivSilentPaymentSender {
3030
outputs: &[SilentPaymentCode],
3131
) -> Result<HashMap<SilentPaymentCode, Vec<XOnlyPublicKey>>, SpSendError> {
3232
let secp = Secp256k1::new();
33-
let (outpoints, spks_with_derivations): (Vec<_>, Vec<_>) = inputs.iter().cloned().unzip();
3433

3534
let mut spks_with_keys = <Vec<(ScriptBuf, SecretKey)>>::new();
36-
for (spk, derivation_path) in spks_with_derivations {
35+
let mut lex_min = LexMin::default();
36+
for (outpoint, (spk, derivation_path)) in inputs.iter() {
37+
lex_min.update(outpoint);
38+
3739
let bip32_privkey = self.xpriv.derive_priv(&secp, &derivation_path)?;
3840
let mut internal_privkey = bip32_privkey.private_key;
3941
let (x_only_internal, parity) = internal_privkey.x_only_public_key(&secp);
@@ -49,13 +51,11 @@ impl XprivSilentPaymentSender {
4951
let external_privkey = internal_privkey.add_tweak(&tap_tweak.to_scalar())
5052
.expect("computationally unreachable: can only fail if tap_tweak = -internal_privkey, but tap_tweak is the output of a hash function");
5153

52-
spks_with_keys.push((spk, external_privkey));
54+
spks_with_keys.push((spk.clone(), external_privkey));
5355
}
5456

55-
let smallest_outpoint_bytes = get_smallest_lexicographic_outpoint(&outpoints);
56-
5757
let partial_secret =
58-
create_silentpayment_partial_secret(&smallest_outpoint_bytes, &spks_with_keys)?;
58+
create_silentpayment_partial_secret(&lex_min.bytes()?, &spks_with_keys)?;
5959

6060
create_silentpayment_scriptpubkeys(partial_secret, outputs)
6161
}

0 commit comments

Comments
 (0)