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:
cygnet 2023-08-22 21:56:08 +02:00 committed by GitHub
parent bd7962db21
commit 0d28b61730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 471 additions and 275 deletions

View File

@ -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`.

View File

@ -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>;

View File

@ -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)
}

View File

@ -5,7 +5,8 @@ use std::collections::HashMap;
use crate::{
error::Error,
utils::{ser_uint32, sha256, Result},
utils::{ser_uint32, sha256},
Result,
};
struct SilentPaymentAddress {

View File

@ -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,
}

View File

@ -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()))
}
}

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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);
}
}