Improve receiver interface (#16)
* Copy recipient code structure from Sosthene00 fork * Move SilentPayment struct to receiving * Provide shared_secret as input argument for scan_for_outputs Also refactors a number of things to enable this change: calculate A_sum and outpoints_hash in the test code instead of the library, remove the obsolete hash_outpoints function from the library move calculate_P_n and calculate_t_n to utils.rs. * Add debug trait implementation for Label * Refactoring * Add get_receiving_address function for getting sp-address per label * Refactor scan_for_outputs and rename to scan_transaction * Provide tweak data to scan_transaction function * Expand README.me
This commit is contained in:
parent
bd7962db21
commit
0d28b61730
31
README.md
31
README.md
|
@ -1,9 +1,32 @@
|
|||
# Silent Payments
|
||||
|
||||
This repo is a rust implementation of BIP352: Silent Payments.
|
||||
This BIP is still under development, and this repo is by no means ready for real use yet.
|
||||
At this point, the repo is no more than a rust rewrite of the `reference.py` python reference implementation.
|
||||
A rust implementation of BIP352: Silent Payments.
|
||||
|
||||
Although this library passes all the tests provided in the silent payment BIP,
|
||||
it is still very new, so be careful when using this with real funds.
|
||||
|
||||
There are two parts to this library: a sender part and a recipient part.
|
||||
|
||||
## Sender
|
||||
|
||||
For sending to a silent payment address, you can call the `sender::generate_recipient_pubkeys` function.
|
||||
This function takes a `recipient: Vec<String>` as an argument, where the `String` is a bech32m encoded silent payment address (`sp1q...` or `tsp1q...`).
|
||||
|
||||
This function additionally takes a `ecdh_shared_secrets: HashMap<PublicKey, PublicKey>` argument, which maps a Spend key to a shared secret.
|
||||
Since this shared secret derivation requires secret data, this library expects the user to provide the pre-computed result.
|
||||
|
||||
See the `tests/vector_tests.rs` and `tests/common/utils.rs` files for an example of how to compute the shared secrets.
|
||||
|
||||
## Recipient
|
||||
|
||||
For receiving silent payments. We have use a struct called `recipient::SilentPayment`.
|
||||
After creating this struct with a spending and scanning secret key,
|
||||
you can call the `scan_transaction` function to look for any outputs in a transaction belonging to you.
|
||||
|
||||
The library also supports labels. You can optionally add labels before scanning by using the `add_label` function.
|
||||
|
||||
## Tests
|
||||
|
||||
The `tests/resources` folder contains a copy of the test vectors as of August 4th 2023.
|
||||
|
||||
You can test the code using the test vectors by running `cargo test`
|
||||
You can test the code using the test vectors by running `cargo test`.
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code, non_snake_case)]
|
||||
|
||||
mod error;
|
||||
pub mod receiving;
|
||||
pub mod sending;
|
||||
pub mod structs;
|
||||
mod utils;
|
||||
|
||||
pub use crate::error::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
435
src/receiving.rs
435
src/receiving.rs
|
@ -1,212 +1,299 @@
|
|||
use bech32::ToBase32;
|
||||
|
||||
use secp256k1::{hashes::Hash, Message, PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
structs::{Outpoint, OutputWithSignature, ScannedOutput},
|
||||
utils::{hash_outpoints, ser_uint32, Result},
|
||||
utils::{calculate_P_n, calculate_t_n, insert_new_key},
|
||||
Error,
|
||||
};
|
||||
use bech32::ToBase32;
|
||||
use secp256k1::{Parity, PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
|
||||
|
||||
pub fn get_receiving_addresses(
|
||||
B_scan: PublicKey,
|
||||
B_spend: PublicKey,
|
||||
labels: &HashMap<String, String>,
|
||||
) -> Result<Vec<String>> {
|
||||
let mut receiving_addresses: Vec<String> = vec![];
|
||||
receiving_addresses.push(encode_silent_payment_address(B_scan, B_spend, None, None)?);
|
||||
for (_, label) in labels {
|
||||
receiving_addresses.push(create_labeled_silent_payment_address(
|
||||
B_scan, B_spend, label, None, None,
|
||||
)?);
|
||||
use crate::Result;
|
||||
|
||||
pub(crate) const NULL_LABEL: Label = Label { s: Scalar::ZERO };
|
||||
|
||||
#[derive(Eq, PartialEq, Clone)]
|
||||
pub struct Label {
|
||||
s: Scalar,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn into_inner(self) -> Scalar {
|
||||
self.s
|
||||
}
|
||||
|
||||
Ok(receiving_addresses)
|
||||
pub fn as_inner(&self) -> &Scalar {
|
||||
&self.s
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
hex::encode(self.as_inner().to_be_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_A_sum_public_keys(
|
||||
input: &Vec<PublicKey>,
|
||||
) -> std::result::Result<PublicKey, secp256k1::Error> {
|
||||
let keys_refs: &Vec<&PublicKey> = &input.iter().collect();
|
||||
|
||||
PublicKey::combine_keys(keys_refs)
|
||||
impl fmt::Debug for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_silent_payment_address(
|
||||
B_scan: PublicKey,
|
||||
B_m: PublicKey,
|
||||
hrp: Option<&str>,
|
||||
version: Option<u8>,
|
||||
) -> Result<String> {
|
||||
let hrp = hrp.unwrap_or("sp");
|
||||
let version = bech32::u5::try_from_u8(version.unwrap_or(0))?;
|
||||
|
||||
let B_scan_bytes = B_scan.serialize();
|
||||
let B_m_bytes = B_m.serialize();
|
||||
|
||||
let mut data = [B_scan_bytes, B_m_bytes].concat().to_base32();
|
||||
|
||||
data.insert(0, version);
|
||||
|
||||
Ok(bech32::encode(hrp, data, bech32::Variant::Bech32m)?)
|
||||
impl Hash for Label {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let bytes = self.s.to_be_bytes();
|
||||
bytes.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_labeled_silent_payment_address(
|
||||
B_scan: PublicKey,
|
||||
B_spend: PublicKey,
|
||||
m: &String,
|
||||
hrp: Option<&str>,
|
||||
version: Option<u8>,
|
||||
) -> Result<String> {
|
||||
let bytes: [u8; 32] = hex::decode(m)?
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| Error::GenericError("Wrong byte length".to_owned()))?;
|
||||
|
||||
let scalar = Scalar::from_be_bytes(bytes)?;
|
||||
let secp = Secp256k1::new();
|
||||
let G: PublicKey = SecretKey::from_slice(&Scalar::ONE.to_be_bytes())?.public_key(&secp);
|
||||
let intermediate = G.mul_tweak(&secp, &scalar)?;
|
||||
let B_m = intermediate.combine(&B_spend)?;
|
||||
|
||||
encode_silent_payment_address(B_scan, B_m, hrp, version)
|
||||
impl From<Scalar> for Label {
|
||||
fn from(s: Scalar) -> Self {
|
||||
Label { s }
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_P_n(B_spend: &PublicKey, t_n: [u8; 32]) -> Result<PublicKey> {
|
||||
let secp = Secp256k1::new();
|
||||
impl TryFrom<String> for Label {
|
||||
type Error = Error;
|
||||
|
||||
let G: PublicKey = SecretKey::from_slice(&Scalar::ONE.to_be_bytes())?.public_key(&secp);
|
||||
let intermediate = G.mul_tweak(&secp, &Scalar::from_be_bytes(t_n)?)?;
|
||||
let P_n = intermediate.combine(&B_spend)?;
|
||||
|
||||
Ok(P_n)
|
||||
fn try_from(s: String) -> Result<Label> {
|
||||
Label::try_from(&s[..])
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_t_n(ecdh_shared_secret: &[u8; 33], n: u32) -> [u8; 32] {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.extend_from_slice(ecdh_shared_secret);
|
||||
bytes.extend_from_slice(&ser_uint32(n));
|
||||
crate::utils::sha256(&bytes)
|
||||
impl TryFrom<&str> for Label {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Label> {
|
||||
// Is it valid hex?
|
||||
let bytes = hex::decode(s)?;
|
||||
// Is it 32B long?
|
||||
let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
|
||||
Error::InvalidLabel("Label must be 32 bytes (256 bits) long".to_owned())
|
||||
})?;
|
||||
// Is it on the curve? If yes, push it on our labels list
|
||||
Ok(Label::from(Scalar::from_be_bytes(bytes)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_ecdh_secret(
|
||||
A_sum: &PublicKey,
|
||||
b_scan: SecretKey,
|
||||
outpoints_hash: [u8; 32],
|
||||
) -> Result<[u8; 33]> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let intermediate = A_sum.mul_tweak(&secp, &b_scan.into())?;
|
||||
let scalar = Scalar::from_be_bytes(outpoints_hash)?;
|
||||
let ecdh_shared_secret = intermediate.mul_tweak(&secp, &scalar)?.serialize();
|
||||
|
||||
Ok(ecdh_shared_secret)
|
||||
impl From<Label> for Scalar {
|
||||
fn from(l: Label) -> Self {
|
||||
l.s
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scanning(
|
||||
b_scan: SecretKey,
|
||||
B_spend: PublicKey,
|
||||
A_sum: PublicKey,
|
||||
outpoints: HashSet<Outpoint>,
|
||||
outputs_to_check: Vec<XOnlyPublicKey>,
|
||||
labels: Option<&HashMap<String, String>>,
|
||||
) -> Result<Vec<ScannedOutput>> {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
/// A struct representing a silent payment recipient.
|
||||
/// It can be used to scan for transaction outputs belonging to us by using the scan_transaction function.
|
||||
/// It internally manages labels, which can be added by using the add_label function.
|
||||
#[derive(Debug)]
|
||||
pub struct SilentPayment {
|
||||
version: u8,
|
||||
scan_privkey: SecretKey,
|
||||
spend_privkey: SecretKey,
|
||||
labels: HashMap<PublicKey, Label>,
|
||||
is_testnet: bool,
|
||||
}
|
||||
|
||||
let outpoints_hash = hash_outpoints(&outpoints)?;
|
||||
impl SilentPayment {
|
||||
pub fn new(
|
||||
version: u32,
|
||||
scan_privkey: SecretKey,
|
||||
spend_privkey: SecretKey,
|
||||
is_testnet: bool,
|
||||
) -> Result<Self> {
|
||||
let labels: HashMap<PublicKey, Label> = HashMap::new();
|
||||
|
||||
let ecdh_shared_secret = calculate_ecdh_secret(&A_sum, b_scan, outpoints_hash)?;
|
||||
let mut n = 0;
|
||||
let mut wallet: Vec<ScannedOutput> = vec![];
|
||||
// Check version, we just refuse anything other than 0 for now
|
||||
if version != 0 {
|
||||
return Err(Error::GenericError(
|
||||
"Can't have other version than 0 for now".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut found = true;
|
||||
while found {
|
||||
found = false;
|
||||
let t_n = calculate_t_n(&ecdh_shared_secret, n);
|
||||
let P_n = calculate_P_n(&B_spend, t_n)?;
|
||||
let (P_n_xonly, _) = P_n.x_only_public_key();
|
||||
if outputs_to_check.iter().any(|&output| output.eq(&P_n_xonly)) {
|
||||
let pub_key = hex::encode(P_n_xonly.serialize());
|
||||
let priv_key_tweak = hex::encode(t_n);
|
||||
wallet.push(ScannedOutput {
|
||||
pub_key,
|
||||
priv_key_tweak,
|
||||
});
|
||||
n += 1;
|
||||
found = true;
|
||||
} else if let Some(labels) = labels {
|
||||
let P_n_negated = P_n.negate(&secp);
|
||||
for output in &outputs_to_check {
|
||||
let output_even = output.public_key(secp256k1::Parity::Even);
|
||||
let output_odd = output.public_key(secp256k1::Parity::Odd);
|
||||
Ok(SilentPayment {
|
||||
version: version as u8,
|
||||
scan_privkey,
|
||||
spend_privkey,
|
||||
labels,
|
||||
is_testnet,
|
||||
})
|
||||
}
|
||||
|
||||
let m_G_sub_even = output_even.combine(&P_n_negated)?;
|
||||
let m_G_sub_odd = output_odd.combine(&P_n_negated)?;
|
||||
let keys: Vec<PublicKey> = vec![m_G_sub_even, m_G_sub_odd];
|
||||
for labelkeystr in labels.keys() {
|
||||
let labelkey = PublicKey::from_str(labelkeystr)?;
|
||||
if keys.iter().any(|x| x.eq(&labelkey)) {
|
||||
let P_nm = hex::encode(output.serialize());
|
||||
let label = labels.get(labelkeystr).unwrap();
|
||||
let label_bytes = hex::decode(label)?
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| Error::GenericError("Wrong byte length".to_owned()))?;
|
||||
let label_scalar = Scalar::from_be_bytes(label_bytes)?;
|
||||
let t_n_as_secret_key = SecretKey::from_slice(&t_n)?;
|
||||
let priv_key_tweak =
|
||||
hex::encode(t_n_as_secret_key.add_tweak(&label_scalar)?.secret_bytes());
|
||||
wallet.push(ScannedOutput {
|
||||
pub_key: P_nm,
|
||||
priv_key_tweak,
|
||||
});
|
||||
n += 1;
|
||||
found = true;
|
||||
/// Takes a Label and adds it to the list of labels that this recipient uses.
|
||||
/// Returns a bool on success, `true` if the label was new, `false` if it already existed in our list.
|
||||
pub fn add_label(&mut self, label: Label) -> Result<bool> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let secret = SecretKey::from_slice(&label.as_inner().to_be_bytes())?;
|
||||
let old_value = self.labels.insert(secret.public_key(&secp), label);
|
||||
Ok(old_value.is_none())
|
||||
}
|
||||
|
||||
/// List all currently known labels used by this recipient.
|
||||
pub fn list_labels(&self) -> HashSet<Label> {
|
||||
self.labels.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get the bech32m-encoded silent payment address, optionally for a specific label.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `label` - An `Option` that wraps a reference to a Label. If the Option is None, then no label is being used.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If successful, the function returns a `Result` wrapping a String, which is the bech32m encoded silent payment address.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
///
|
||||
/// * If the label is not known for this recipient.
|
||||
/// * If key addition results in an invalid key.
|
||||
pub fn get_receiving_address(&mut self, label: Option<&Label>) -> Result<String> {
|
||||
let secp = Secp256k1::new();
|
||||
let base_spend_key = self.spend_privkey;
|
||||
let b_m = match label {
|
||||
Some(label) => {
|
||||
if self.labels.values().any(|l| l.eq(label)) {
|
||||
base_spend_key.add_tweak(label.as_inner())?
|
||||
} else {
|
||||
return Err(Error::InvalidLabel("Label not known".to_owned()));
|
||||
}
|
||||
}
|
||||
None => base_spend_key,
|
||||
};
|
||||
|
||||
Ok(self.encode_silent_payment_address(b_m.public_key(&secp)))
|
||||
}
|
||||
|
||||
/// Scans a transaction for outputs belonging to us.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tweak_data` - The tweak data for the transaction as a PublicKey, the result of elliptic-curve multiplication of `outpoints_hash * A`.
|
||||
/// * `pubkeys_to_check` - A `HashSet` of public keys of all (unspent) taproot output of the transaction.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If successful, the function returns a `Result` wrapping a `HashMap` of labels to a set of private keys (since the same label may have been paid multiple times in one transaction). A resulting `HashMap` of length 0 implies none of the outputs are owned by us.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
///
|
||||
/// * One of the public keys to scan can't be parsed into a valid x-only public key.
|
||||
/// * An error occurs during elliptic curve computation. This may happen if a sender is being malicious. (?)
|
||||
pub fn scan_transaction(
|
||||
&self,
|
||||
tweak_data: &PublicKey,
|
||||
pubkeys_to_check: Vec<XOnlyPublicKey>,
|
||||
) -> Result<HashMap<Label, HashSet<SecretKey>>> {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let B_spend = &self.spend_privkey.public_key(&secp);
|
||||
let ecdh_shared_secret = self.calculate_shared_secret(tweak_data)?;
|
||||
|
||||
let mut my_outputs: HashMap<Label, HashSet<SecretKey>> = HashMap::new();
|
||||
let mut n: u32 = 0;
|
||||
while my_outputs.len() == n as usize {
|
||||
let t_n: Scalar = calculate_t_n(&ecdh_shared_secret, n)?;
|
||||
let P_n: PublicKey = calculate_P_n(&B_spend, t_n)?;
|
||||
if pubkeys_to_check
|
||||
.iter()
|
||||
.any(|p| p.eq(&P_n.x_only_public_key().0))
|
||||
{
|
||||
insert_new_key(self.spend_privkey.add_tweak(&t_n)?, &mut my_outputs, None)?;
|
||||
} else if !self.labels.is_empty() {
|
||||
// We subtract P_n from each outputs to check and see if match a public key in our label list
|
||||
'outer: for p in &pubkeys_to_check {
|
||||
let even_output = p.public_key(Parity::Even);
|
||||
let odd_output = p.public_key(Parity::Odd);
|
||||
let even_diff = even_output.combine(&P_n.negate(&secp))?;
|
||||
let odd_diff = odd_output.combine(&P_n.negate(&secp))?;
|
||||
|
||||
for diff in vec![even_diff, odd_diff] {
|
||||
if let Some(label) = self.labels.get(&diff) {
|
||||
insert_new_key(
|
||||
self.spend_privkey.add_tweak(&t_n)?,
|
||||
&mut my_outputs,
|
||||
Some(label),
|
||||
)?;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
Ok(my_outputs)
|
||||
}
|
||||
|
||||
/// Helper function that can be used to calculate the elliptic curce shared secret.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tweak_data` - The tweak data given as a PublicKey, the result of elliptic-curve multiplication of the outpoints_hash and `A_sum` (the sum of all input public keys).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If successful, the function returns a `Result` wrapping an 33-byte array, which is the shared secret that only the sender and the recipient of a silent payment can derive. This result can be used in the scan_for_outputs function.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
///
|
||||
/// * If key multiplication with the scan private key returns an invalid result.
|
||||
fn calculate_shared_secret(&self, tweak_data: &PublicKey) -> Result<[u8; 33]> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let ecdh_shared_secret = tweak_data
|
||||
.mul_tweak(&secp, &self.scan_privkey.into())?
|
||||
.serialize();
|
||||
|
||||
Ok(ecdh_shared_secret)
|
||||
}
|
||||
|
||||
fn encode_silent_payment_address(&self, m_pubkey: PublicKey) -> String {
|
||||
let hrp = match self.is_testnet {
|
||||
false => "sp",
|
||||
true => "tsp",
|
||||
};
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let version = bech32::u5::try_from_u8(self.version).unwrap();
|
||||
|
||||
let B_scan_bytes = self.scan_privkey.public_key(&secp).serialize();
|
||||
let B_m_bytes = m_pubkey.serialize();
|
||||
|
||||
let mut data = [B_scan_bytes, B_m_bytes].concat().to_base32();
|
||||
|
||||
data.insert(0, version);
|
||||
|
||||
bech32::encode(hrp, data, bech32::Variant::Bech32m).unwrap()
|
||||
}
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
pub fn verify_and_calculate_signatures(
|
||||
add_to_wallet: &mut Vec<ScannedOutput>,
|
||||
b_spend: SecretKey,
|
||||
) -> Result<Vec<OutputWithSignature>> {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let msg = Message::from_hashed_data::<secp256k1::hashes::sha256::Hash>(b"message");
|
||||
let aux = secp256k1::hashes::sha256::Hash::hash(b"random auxiliary data").into_inner();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Label;
|
||||
|
||||
let mut res: Vec<OutputWithSignature> = vec![];
|
||||
for output in add_to_wallet {
|
||||
let pubkey = XOnlyPublicKey::from_str(&output.pub_key)?;
|
||||
let tweak: [u8; 32] = hex::decode(&output.priv_key_tweak)?
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| Error::GenericError("Wrong byte length".to_owned()))?;
|
||||
let scalar = Scalar::from_be_bytes(tweak)?;
|
||||
let mut full_priv_key = b_spend.add_tweak(&scalar)?;
|
||||
|
||||
let (_, parity) = full_priv_key.x_only_public_key(&secp);
|
||||
|
||||
if parity == secp256k1::Parity::Odd {
|
||||
full_priv_key = full_priv_key.negate();
|
||||
}
|
||||
|
||||
let sig = secp.sign_schnorr_with_aux_rand(&msg, &full_priv_key.keypair(&secp), &aux);
|
||||
|
||||
secp.verify_schnorr(&sig, &msg, &pubkey)?;
|
||||
|
||||
res.push(OutputWithSignature {
|
||||
pub_key: output.pub_key.to_string(),
|
||||
priv_key_tweak: output.priv_key_tweak.clone(),
|
||||
signature: sig.to_string(),
|
||||
});
|
||||
#[test]
|
||||
fn string_to_label_success() {
|
||||
let s: String =
|
||||
"8e4bbee712779f746337cadf39e8b1eab8e8869dd40f2e3a7281113e858ffc0b".to_owned();
|
||||
Label::try_from(s).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_to_label_failure() {
|
||||
// Invalid characters
|
||||
let s: String = "deadbeef?:{+!&".to_owned();
|
||||
Label::try_from(s).unwrap_err();
|
||||
// Invalid length
|
||||
let s: String = "deadbee".to_owned();
|
||||
Label::try_from(s).unwrap_err();
|
||||
// Not 32B
|
||||
let s: String = "deadbeef".to_owned();
|
||||
Label::try_from(s).unwrap_err();
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ use std::collections::HashMap;
|
|||
|
||||
use crate::{
|
||||
error::Error,
|
||||
utils::{ser_uint32, sha256, Result},
|
||||
utils::{ser_uint32, sha256},
|
||||
Result,
|
||||
};
|
||||
|
||||
struct SilentPaymentAddress {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScannedOutput {
|
||||
pub pub_key: String,
|
||||
pub priv_key_tweak: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||
pub struct OutputWithSignature {
|
||||
pub pub_key: String,
|
||||
pub priv_key_tweak: String,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Outpoint {
|
||||
pub txid: [u8; 32],
|
||||
pub vout: u32,
|
||||
}
|
76
src/utils.rs
76
src/utils.rs
|
@ -1,39 +1,59 @@
|
|||
use std::{collections::HashSet, io::Write};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use secp256k1::hashes::{sha256, Hash};
|
||||
use crate::{
|
||||
receiving::{Label, NULL_LABEL},
|
||||
Error, Result,
|
||||
};
|
||||
use secp256k1::{
|
||||
hashes::{sha256, Hash},
|
||||
PublicKey, Scalar, Secp256k1, SecretKey,
|
||||
};
|
||||
|
||||
use crate::{error::Error, structs::Outpoint};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn sha256(message: &[u8]) -> [u8; 32] {
|
||||
pub(crate) fn sha256(message: &[u8]) -> [u8; 32] {
|
||||
sha256::Hash::hash(message).into_inner()
|
||||
}
|
||||
|
||||
pub fn ser_uint32(u: u32) -> Vec<u8> {
|
||||
pub(crate) fn ser_uint32(u: u32) -> Vec<u8> {
|
||||
u.to_be_bytes().into()
|
||||
}
|
||||
|
||||
pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> Result<[u8; 32]> {
|
||||
let mut outpoints: Vec<Vec<u8>> = vec![];
|
||||
pub(crate) fn calculate_P_n(B_spend: &PublicKey, t_n: Scalar) -> Result<PublicKey> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
for outpoint in sending_data {
|
||||
let txid = outpoint.txid;
|
||||
let vout = outpoint.vout;
|
||||
let P_n = B_spend.add_exp_tweak(&secp, &t_n)?;
|
||||
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.extend_from_slice(&txid);
|
||||
bytes.reverse();
|
||||
bytes.extend_from_slice(&vout.to_le_bytes());
|
||||
outpoints.push(bytes);
|
||||
}
|
||||
outpoints.sort();
|
||||
|
||||
let mut engine = sha256::HashEngine::default();
|
||||
|
||||
for v in outpoints {
|
||||
engine.write_all(&v).unwrap();
|
||||
}
|
||||
|
||||
Ok(sha256::Hash::from_engine(engine).into_inner())
|
||||
Ok(P_n)
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_t_n(ecdh_shared_secret: &[u8; 33], n: u32) -> Result<Scalar> {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.extend_from_slice(ecdh_shared_secret);
|
||||
bytes.extend_from_slice(&ser_uint32(n));
|
||||
|
||||
Ok(Scalar::from_be_bytes(sha256(&bytes))?)
|
||||
}
|
||||
|
||||
pub(crate) fn insert_new_key(
|
||||
mut new_privkey: SecretKey,
|
||||
my_outputs: &mut HashMap<Label, HashSet<SecretKey>>,
|
||||
label: Option<&Label>,
|
||||
) -> Result<()> {
|
||||
let label: &Label = match label {
|
||||
Some(l) => {
|
||||
new_privkey = new_privkey.add_tweak(l.as_inner())?;
|
||||
l
|
||||
}
|
||||
None => &NULL_LABEL,
|
||||
};
|
||||
|
||||
let res = my_outputs
|
||||
.entry(label.to_owned())
|
||||
.or_insert_with(HashSet::new)
|
||||
.insert(new_privkey);
|
||||
|
||||
if res {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::GenericError("Duplicate key found".to_owned()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#![allow(non_snake_case)]
|
||||
use serde::Deserialize;
|
||||
use silentpayments::structs::OutputWithSignature;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -52,3 +51,16 @@ pub struct SendingDataGiven {
|
|||
pub struct SendingDataExpected {
|
||||
pub outputs: Vec<(String, f32)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Outpoint {
|
||||
pub txid: [u8; 32],
|
||||
pub vout: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||
pub struct OutputWithSignature {
|
||||
pub pub_key: String,
|
||||
pub priv_key_tweak: String,
|
||||
pub signature: String,
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ use std::{
|
|||
|
||||
use secp256k1::{
|
||||
hashes::{sha256, Hash},
|
||||
PublicKey, Scalar, SecretKey, XOnlyPublicKey,
|
||||
Message, PublicKey, Scalar, SecretKey, XOnlyPublicKey,
|
||||
};
|
||||
use serde_json::from_str;
|
||||
use silentpayments::structs::Outpoint;
|
||||
|
||||
use super::structs::TestData;
|
||||
use super::structs::{Outpoint, OutputWithSignature, TestData};
|
||||
|
||||
pub fn read_file() -> Vec<TestData> {
|
||||
let mut file = File::open("tests/resources/send_and_receive_test_vectors.json").unwrap();
|
||||
|
@ -94,7 +93,24 @@ pub fn get_a_sum_secret_keys(input: &Vec<(SecretKey, bool)>) -> SecretKey {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn compute_ecdh_shared_secret(
|
||||
pub fn get_A_sum_public_keys(input: &Vec<PublicKey>) -> PublicKey {
|
||||
let keys_refs: &Vec<&PublicKey> = &input.iter().collect();
|
||||
|
||||
PublicKey::combine_keys(keys_refs).unwrap()
|
||||
}
|
||||
|
||||
pub fn calculate_tweak_data_for_recipient(
|
||||
input_pub_keys: &Vec<PublicKey>,
|
||||
outpoints: &HashSet<Outpoint>,
|
||||
) -> PublicKey {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let A_sum = get_A_sum_public_keys(input_pub_keys);
|
||||
let outpoints_hash = hash_outpoints(outpoints);
|
||||
|
||||
A_sum.mul_tweak(&secp, &outpoints_hash).unwrap()
|
||||
}
|
||||
|
||||
pub fn sender_calculate_shared_secret(
|
||||
a_sum: SecretKey,
|
||||
B_scan: PublicKey,
|
||||
outpoints_hash: Scalar,
|
||||
|
@ -105,7 +121,7 @@ pub fn compute_ecdh_shared_secret(
|
|||
diffie_hellman.mul_tweak(&secp, &outpoints_hash).unwrap()
|
||||
}
|
||||
|
||||
pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> [u8; 32] {
|
||||
pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> Scalar {
|
||||
let mut outpoints: Vec<Vec<u8>> = vec![];
|
||||
|
||||
for outpoint in sending_data {
|
||||
|
@ -126,5 +142,37 @@ pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> [u8; 32] {
|
|||
engine.write_all(&v).unwrap();
|
||||
}
|
||||
|
||||
sha256::Hash::from_engine(engine).into_inner()
|
||||
Scalar::from_be_bytes(sha256::Hash::from_engine(engine).into_inner()).unwrap()
|
||||
}
|
||||
|
||||
pub fn verify_and_calculate_signatures(
|
||||
privkeys: Vec<SecretKey>,
|
||||
b_spend: SecretKey,
|
||||
) -> Result<Vec<OutputWithSignature>, secp256k1::Error> {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
|
||||
let msg = Message::from_hashed_data::<secp256k1::hashes::sha256::Hash>(b"message");
|
||||
let aux = secp256k1::hashes::sha256::Hash::hash(b"random auxiliary data").into_inner();
|
||||
|
||||
let mut res: Vec<OutputWithSignature> = vec![];
|
||||
for k in privkeys {
|
||||
let P = k.x_only_public_key(&secp).0;
|
||||
|
||||
// Add the negated b_spend to get only the tweak
|
||||
let tweak = k.add_tweak(&Scalar::from(b_spend.negate()))?;
|
||||
|
||||
// Sign the message with schnorr
|
||||
let sig = secp.sign_schnorr_with_aux_rand(&msg, &k.keypair(&secp), &aux);
|
||||
|
||||
// Verify the message is correct
|
||||
secp.verify_schnorr(&sig, &msg, &P)?;
|
||||
|
||||
// Push result to list
|
||||
res.push(OutputWithSignature {
|
||||
pub_key: P.to_string(),
|
||||
priv_key_tweak: hex::encode(tweak.secret_bytes()),
|
||||
signature: sig.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#![allow(non_snake_case)]
|
||||
mod common;
|
||||
|
||||
use silentpayments::receiving;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
|
@ -10,24 +8,23 @@ mod tests {
|
|||
str::FromStr,
|
||||
};
|
||||
|
||||
use secp256k1::{PublicKey, Scalar, SecretKey};
|
||||
use silentpayments::sending::{decode_scan_pubkey, generate_recipient_pubkeys};
|
||||
use secp256k1::{PublicKey, SecretKey};
|
||||
use silentpayments::{
|
||||
receiving::SilentPayment,
|
||||
sending::{decode_scan_pubkey, generate_recipient_pubkeys},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
structs::TestData,
|
||||
utils::{
|
||||
self, compute_ecdh_shared_secret, decode_input_pub_keys, decode_outpoints,
|
||||
decode_outputs_to_check, decode_priv_keys, decode_recipients,
|
||||
get_a_sum_secret_keys, hash_outpoints,
|
||||
},
|
||||
},
|
||||
receiving::{
|
||||
get_A_sum_public_keys, get_receiving_addresses, scanning,
|
||||
verify_and_calculate_signatures,
|
||||
use crate::common::{
|
||||
structs::TestData,
|
||||
utils::{
|
||||
self, calculate_tweak_data_for_recipient, decode_input_pub_keys, decode_outpoints,
|
||||
decode_outputs_to_check, decode_priv_keys, decode_recipients, get_a_sum_secret_keys,
|
||||
hash_outpoints, sender_calculate_shared_secret, verify_and_calculate_signatures,
|
||||
},
|
||||
};
|
||||
|
||||
const IS_TESTNET: bool = false;
|
||||
|
||||
#[test]
|
||||
fn test_with_test_vectors() {
|
||||
let testdata = utils::read_file();
|
||||
|
@ -52,7 +49,7 @@ mod tests {
|
|||
|
||||
let outpoints = decode_outpoints(&given.outpoints);
|
||||
|
||||
let outpoints_hash = Scalar::from_be_bytes(hash_outpoints(&outpoints)).unwrap();
|
||||
let outpoints_hash = hash_outpoints(&outpoints);
|
||||
|
||||
let silent_addresses = decode_recipients(&given.recipients);
|
||||
|
||||
|
@ -61,7 +58,8 @@ mod tests {
|
|||
let mut ecdh_shared_secrets: HashMap<PublicKey, PublicKey> = HashMap::new();
|
||||
for addr in &silent_addresses {
|
||||
let B_scan = decode_scan_pubkey(addr.to_owned()).unwrap();
|
||||
let ecdh_shared_secret = compute_ecdh_shared_secret(a_sum, B_scan, outpoints_hash);
|
||||
let ecdh_shared_secret =
|
||||
sender_calculate_shared_secret(a_sum, B_scan, outpoints_hash);
|
||||
ecdh_shared_secrets.insert(B_scan, ecdh_shared_secret);
|
||||
}
|
||||
let outputs =
|
||||
|
@ -77,9 +75,9 @@ mod tests {
|
|||
assert_eq!(sending_outputs, expected_output_addresses);
|
||||
}
|
||||
|
||||
for receivingtest in &test_case.receiving {
|
||||
let given = &receivingtest.given;
|
||||
let expected = &receivingtest.expected;
|
||||
for receivingtest in test_case.receiving {
|
||||
let given = receivingtest.given;
|
||||
let mut expected = receivingtest.expected;
|
||||
|
||||
let receiving_outputs: HashSet<String> = given.outputs.iter().cloned().collect();
|
||||
|
||||
|
@ -90,12 +88,29 @@ mod tests {
|
|||
|
||||
let b_scan = SecretKey::from_str(&given.scan_priv_key).unwrap();
|
||||
let b_spend = SecretKey::from_str(&given.spend_priv_key).unwrap();
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
let B_scan: PublicKey = b_scan.public_key(&secp);
|
||||
let B_spend: PublicKey = b_spend.public_key(&secp);
|
||||
|
||||
let receiving_addresses =
|
||||
get_receiving_addresses(B_scan, B_spend, &given.labels).unwrap();
|
||||
let mut sp_receiver = SilentPayment::new(0, b_scan, b_spend, IS_TESTNET).unwrap();
|
||||
|
||||
let outputs_to_check = decode_outputs_to_check(&given.outputs);
|
||||
|
||||
let outpoints = decode_outpoints(&given.outpoints);
|
||||
|
||||
let input_pub_keys = decode_input_pub_keys(&given.input_pub_keys);
|
||||
|
||||
for (_, label) in &given.labels {
|
||||
let label = label[..].try_into().unwrap();
|
||||
sp_receiver.add_label(label).unwrap();
|
||||
}
|
||||
|
||||
let mut receiving_addresses: HashSet<String> = HashSet::new();
|
||||
// get receiving address for no label
|
||||
receiving_addresses.insert(sp_receiver.get_receiving_address(None).unwrap());
|
||||
|
||||
// get receiving addresses for every label
|
||||
let labels = sp_receiver.list_labels();
|
||||
for label in &labels {
|
||||
receiving_addresses.insert(sp_receiver.get_receiving_address(Some(label)).unwrap());
|
||||
}
|
||||
|
||||
let set1: HashSet<_> = receiving_addresses.iter().collect();
|
||||
let set2: HashSet<_> = expected.addresses.iter().collect();
|
||||
|
@ -104,24 +119,30 @@ mod tests {
|
|||
// to the expected addresses
|
||||
assert_eq!(set1, set2);
|
||||
|
||||
// can be even or odd !
|
||||
let outputs_to_check = decode_outputs_to_check(&given.outputs);
|
||||
let tweak_data = calculate_tweak_data_for_recipient(&input_pub_keys, &outpoints);
|
||||
|
||||
let outpoints = decode_outpoints(&given.outpoints);
|
||||
let scanned_outputs_received = sp_receiver
|
||||
.scan_transaction(&tweak_data, outputs_to_check)
|
||||
.unwrap();
|
||||
|
||||
let input_pub_keys = decode_input_pub_keys(&given.input_pub_keys);
|
||||
let privkeys: Vec<SecretKey> = scanned_outputs_received
|
||||
.into_iter()
|
||||
.flat_map(|(_, list)| {
|
||||
let mut ret: Vec<SecretKey> = vec![];
|
||||
for l in list {
|
||||
ret.push(l);
|
||||
}
|
||||
ret
|
||||
})
|
||||
.collect();
|
||||
|
||||
let A_sum = get_A_sum_public_keys(&input_pub_keys).unwrap();
|
||||
let mut res = verify_and_calculate_signatures(privkeys, b_spend).unwrap();
|
||||
|
||||
let labels = match &given.labels.len() {
|
||||
0 => None,
|
||||
_ => Some(&given.labels),
|
||||
};
|
||||
res.sort_by_key(|output| output.pub_key.clone());
|
||||
expected
|
||||
.outputs
|
||||
.sort_by_key(|output| output.pub_key.clone());
|
||||
|
||||
let mut add_to_wallet =
|
||||
scanning(b_scan, B_spend, A_sum, outpoints, outputs_to_check, labels).unwrap();
|
||||
|
||||
let res = verify_and_calculate_signatures(&mut add_to_wallet, b_spend).unwrap();
|
||||
assert_eq!(res, expected.outputs);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue