From fa00c8706cb92cee8df87cf026cc5d3a7fd522e9 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:28:42 -0300 Subject: [PATCH 1/2] perf(lib): replace N allocations by 1 in get_smallest_lexicographic_outpoint --- silentpayments/src/lib.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/silentpayments/src/lib.rs b/silentpayments/src/lib.rs index 137f023..bf268b4 100644 --- a/silentpayments/src/lib.rs +++ b/silentpayments/src/lib.rs @@ -65,16 +65,33 @@ pub fn tag_txin(txin: &TxIn, script_pubkey: &ScriptBuf) -> Option { } pub fn get_smallest_lexicographic_outpoint(outpoints: &[OutPoint]) -> [u8; 36] { - outpoints + // Find the outpoint with the smallest lexicographic order + let smallest = outpoints .iter() - .map(|x| { - let mut outpoint_bytes = [0u8; 36]; - outpoint_bytes[..32].copy_from_slice(x.txid.to_raw_hash().as_byte_array()); - outpoint_bytes[32..36].copy_from_slice(&x.vout.to_le_bytes()); - outpoint_bytes + .min_by(|a, b| { + // Compare txids first + let a_txid = a.txid.to_raw_hash(); + let b_txid = b.txid.to_raw_hash(); + + // If txids are different, compare them + match a_txid.as_byte_array().cmp(b_txid.as_byte_array()) { + std::cmp::Ordering::Equal => { + // If txids are equal, compare vouts directly + let a_vout_bytes = a.vout.to_le_bytes(); + let b_vout_bytes = b.vout.to_le_bytes(); + a_vout_bytes.cmp(&b_vout_bytes) + } + other => other, + } }) - .min() - .expect("cannot create silent payment script pubkey without outpoints") + .expect("cannot create silent payment script pubkey without outpoints"); + + // Only allocate the result array once we have the smallest outpoint + let mut result = [0u8; 36]; + result[..32].copy_from_slice(smallest.txid.to_raw_hash().as_byte_array()); + result[32..36].copy_from_slice(&smallest.vout.to_le_bytes()); + + result } pub fn compute_shared_secret(sk: &SecretKey, pk: &PublicKey) -> PublicKey { From 24fcdbfa586a85d7d0ad9e38a5e72152ef1b89b8 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:30:21 -0300 Subject: [PATCH 2/2] test(lib): add unit test for get_smallest_lexicographic_outpoint --- silentpayments/src/lib.rs | 156 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/silentpayments/src/lib.rs b/silentpayments/src/lib.rs index bf268b4..ebc315f 100644 --- a/silentpayments/src/lib.rs +++ b/silentpayments/src/lib.rs @@ -104,3 +104,159 @@ pub fn compute_shared_secret(sk: &SecretKey, pk: &PublicKey) -> PublicKey { PublicKey::from_slice(&ss_bytes).expect("computationally unreachable: can only fail if public key is invalid in the first place or sk is") } + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::*; + use bitcoin::OutPoint; + use bitcoin::Txid; + + #[test] + fn test_get_smallest_outpoint_different_txids_and_vouts() { + let outpoints = vec![ + OutPoint { + txid: Txid::from_slice(&[3u8; 32]).unwrap(), + vout: 2, + }, + OutPoint { + txid: Txid::from_slice(&[2u8; 32]).unwrap(), + vout: 1, + }, + OutPoint { + txid: Txid::from_slice(&[5u8; 32]).unwrap(), + vout: 3, + }, + ]; + + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [2u8; 36]; + expected_bytes[32..36].copy_from_slice(&1u32.to_le_bytes()); + + assert_eq!(result, expected_bytes); + } + + #[test] + #[should_panic(expected = "cannot create silent payment script pubkey without outpoints")] + fn test_get_smallest_outpoint_empty() { + let outpoints: Vec = vec![]; + get_smallest_lexicographic_outpoint(&outpoints); + } + + // Additional test: same txid, different vouts + #[test] + fn test_get_smallest_outpoint_identical_txid_different_vouts() { + let txid = Txid::from_slice(&[0u8; 32]).unwrap(); + let outpoints = vec![ + OutPoint { txid, vout: 10 }, + OutPoint { txid, vout: 2 }, + OutPoint { txid, vout: 5 }, + ]; + + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [0u8; 36]; + expected_bytes[32..36].copy_from_slice(&2u32.to_le_bytes()); + assert_eq!(result, expected_bytes); + } + + #[test] + fn test_get_smallest_outpoint_same_vout_different_txid() { + let outpoints = vec![ + OutPoint { + txid: Txid::from_slice(&[2u8; 32]).unwrap(), + vout: 7, + }, + OutPoint { + txid: Txid::from_slice(&[1u8; 32]).unwrap(), + vout: 7, + }, + OutPoint { + txid: Txid::from_slice(&[3u8; 32]).unwrap(), + vout: 7, + }, + ]; + + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [1u8; 36]; + expected_bytes[32..36].copy_from_slice(&7u32.to_le_bytes()); + assert_eq!(result, expected_bytes); + } + + #[test] + fn test_get_smallest_outpoint_edge_case_max_vout() { + let outpoints = vec![ + OutPoint { + txid: Txid::from_slice(&[1u8; 32]).unwrap(), + vout: u32::MAX, + }, + OutPoint { + txid: Txid::from_slice(&[1u8; 32]).unwrap(), + vout: u32::MIN, + }, + ]; + + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [1u8; 36]; + expected_bytes[..32].copy_from_slice(&[1u8; 32]); + expected_bytes[32..36].copy_from_slice(&0u32.to_le_bytes()); + assert_eq!(result, expected_bytes); + } + + #[test] + fn test_get_smallest_outpoint_txid_takes_precedence() { + let outpoints = vec![ + OutPoint { + txid: Txid::from_slice(&[8u8; 32]).unwrap(), + vout: 0, + }, + OutPoint { + txid: Txid::from_slice(&[5u8; 32]).unwrap(), + vout: 100, + }, + ]; + + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [5u8; 36]; + expected_bytes[32..36].copy_from_slice(&100u32.to_le_bytes()); + assert_eq!(result, expected_bytes); + } + + #[test] + fn test_get_smallest_outpoint_txid_endianness_matters() { + // big endian: 0x[00][00][00][01] + // big endian: 0x[a1][b1][c1][d1] + let mut txid_bytes_be = [0u8; 32]; + txid_bytes_be[0] = 1; + + // little endian: 0x[01][00][00][00] + // little endian: 0x[a2][b2][c2][d2] + let mut txid_bytes_le = [0u8; 32]; + txid_bytes_le[31] = 1; + + let outpoints = vec![ + OutPoint { + txid: Txid::from_slice(&txid_bytes_be).unwrap(), + vout: 1, + }, + OutPoint { + txid: Txid::from_slice(&txid_bytes_le).unwrap(), + vout: 1, + }, + ]; + + // if Txid is big endian then: [a1] < [a2] => expected_bytes = txid_bytes_be + // if Txid is little endian then: [d2] < [d1] => expected_bytes = txid_bytes_le + let result = get_smallest_lexicographic_outpoint(&outpoints); + + let mut expected_bytes = [0u8; 36]; + expected_bytes[31] = 1; + expected_bytes[32..36].copy_from_slice(&1u32.to_le_bytes()); + + assert_eq!(result, expected_bytes); + } +}