Skip to content

Commit 7fa6d16

Browse files
committed
Merge pull request #26 from 'refactor/replace-get-smallest-outpoint-by-lexmin'
2 parents 0e65087 + 919e73e commit 7fa6d16

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)