Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 181 additions & 8 deletions silentpayments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,33 @@ pub fn tag_txin(txin: &TxIn, script_pubkey: &ScriptBuf) -> Option<SpInputs> {
}

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 {
Expand All @@ -87,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<OutPoint> = 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);
}
}
Loading