Improve sender interface (#10)
* Refactor tests: add utils.rs for test helper functions * Refactor sending * Add SilentPaymentAddress struct * Use structs instead of tuples as func arguments * Change create_outputs function and remove amounts * Use is_testnet in SilentPaymentAddress struct * Provide pre-computed tweak data to create_outputs function * Git rebase main * Provide ecdh shared secret as argument to create_output * Rename create_outputs -> generate_recipient_pubkeys
This commit is contained in:
parent
cd09955b30
commit
c3b8d65e03
|
@ -0,0 +1,40 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
GenericError(String),
|
||||
InvalidLabel(String),
|
||||
InvalidAddress(String),
|
||||
InvalidSharedSecret(String),
|
||||
Secp256k1Error(secp256k1::Error),
|
||||
OutOfRangeError(secp256k1::scalar::OutOfRangeError),
|
||||
IOError(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for Error {
|
||||
fn from(e: hex::FromHexError) -> Self {
|
||||
Error::InvalidLabel(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bech32::Error> for Error {
|
||||
fn from(e: bech32::Error) -> Self {
|
||||
Error::InvalidAddress(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<secp256k1::Error> for Error {
|
||||
fn from(e: secp256k1::Error) -> Self {
|
||||
Error::Secp256k1Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<secp256k1::scalar::OutOfRangeError> for Error {
|
||||
fn from(e: secp256k1::scalar::OutOfRangeError) -> Self {
|
||||
Error::OutOfRangeError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::IOError(e)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
#![allow(non_snake_case)]
|
||||
mod error;
|
||||
pub mod receiving;
|
||||
pub mod sending;
|
||||
pub mod structs;
|
||||
pub mod utils;
|
||||
mod utils;
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
use bech32::ToBase32;
|
||||
|
||||
use secp256k1::{hashes::Hash, Message, PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
structs::{OutputWithSignature, ScannedOutput},
|
||||
utils::{ser_uint32, Result},
|
||||
error::Error,
|
||||
structs::{Outpoint, OutputWithSignature, ScannedOutput},
|
||||
utils::{hash_outpoints, ser_uint32, Result},
|
||||
};
|
||||
|
||||
pub fn get_receiving_addresses(
|
||||
|
@ -58,7 +62,10 @@ fn create_labeled_silent_payment_address(
|
|||
hrp: Option<&str>,
|
||||
version: Option<u8>,
|
||||
) -> Result<String> {
|
||||
let bytes: [u8; 32] = hex::decode(m)?.as_slice().try_into()?;
|
||||
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();
|
||||
|
@ -104,11 +111,14 @@ pub fn scanning(
|
|||
b_scan: SecretKey,
|
||||
B_spend: PublicKey,
|
||||
A_sum: PublicKey,
|
||||
outpoints_hash: [u8; 32],
|
||||
outpoints: HashSet<Outpoint>,
|
||||
outputs_to_check: Vec<XOnlyPublicKey>,
|
||||
labels: Option<&HashMap<String, String>>,
|
||||
) -> Result<Vec<ScannedOutput>> {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
|
||||
let outpoints_hash = hash_outpoints(&outpoints)?;
|
||||
|
||||
let ecdh_shared_secret = calculate_ecdh_secret(&A_sum, b_scan, outpoints_hash)?;
|
||||
let mut n = 0;
|
||||
let mut wallet: Vec<ScannedOutput> = vec![];
|
||||
|
@ -142,7 +152,10 @@ pub fn scanning(
|
|||
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()?;
|
||||
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 =
|
||||
|
@ -172,7 +185,10 @@ pub fn verify_and_calculate_signatures(
|
|||
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()?;
|
||||
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)?;
|
||||
|
||||
|
|
217
src/sending.rs
217
src/sending.rs
|
@ -1,97 +1,172 @@
|
|||
use bech32::FromBase32;
|
||||
use bech32::{FromBase32, ToBase32};
|
||||
|
||||
use secp256k1::{Parity, PublicKey, Scalar, Secp256k1, SecretKey};
|
||||
use secp256k1::{PublicKey, Secp256k1, SecretKey, XOnlyPublicKey};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::utils::{hash_outpoints, ser_uint32, sha256, Result};
|
||||
use crate::{
|
||||
error::Error,
|
||||
utils::{ser_uint32, sha256, Result},
|
||||
};
|
||||
|
||||
fn get_a_sum_secret_keys(input: &Vec<(SecretKey, bool)>) -> Result<SecretKey> {
|
||||
struct SilentPaymentAddress {
|
||||
version: u8,
|
||||
scan_pubkey: PublicKey,
|
||||
m_pubkey: PublicKey,
|
||||
is_testnet: bool,
|
||||
}
|
||||
|
||||
impl SilentPaymentAddress {
|
||||
pub fn new(
|
||||
scan_pubkey: PublicKey,
|
||||
m_pubkey: PublicKey,
|
||||
is_testnet: bool,
|
||||
version: u8,
|
||||
) -> Result<Self> {
|
||||
if version != 0 {
|
||||
return Err(Error::GenericError(
|
||||
"Can't have other version than 0 for now".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(SilentPaymentAddress {
|
||||
scan_pubkey,
|
||||
m_pubkey,
|
||||
is_testnet,
|
||||
version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SilentPaymentAddress {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(addr: &str) -> Result<Self> {
|
||||
let (hrp, data, _variant) = bech32::decode(&addr)?;
|
||||
|
||||
if data.len() != 107 {
|
||||
return Err(Error::GenericError("Address length is wrong".to_owned()));
|
||||
}
|
||||
|
||||
let version = data[0].to_u8();
|
||||
|
||||
let is_testnet = match hrp.as_str() {
|
||||
"sp" => false,
|
||||
"tsp" => true,
|
||||
_ => {
|
||||
return Err(Error::InvalidAddress(format!(
|
||||
"Wrong prefix, expected \"sp\" or \"tsp\", got \"{}\"",
|
||||
&hrp
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let data = Vec::<u8>::from_base32(&data[1..])?;
|
||||
|
||||
let scan_pubkey = PublicKey::from_slice(&data[..33])?;
|
||||
let m_pubkey = PublicKey::from_slice(&data[33..])?;
|
||||
|
||||
SilentPaymentAddress::new(scan_pubkey, m_pubkey, is_testnet, version.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SilentPaymentAddress {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(addr: String) -> Result<Self> {
|
||||
addr.as_str().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for SilentPaymentAddress {
|
||||
fn into(self) -> String {
|
||||
let hrp = match self.is_testnet {
|
||||
true => "tsp",
|
||||
false => "sp",
|
||||
};
|
||||
|
||||
let version = bech32::u5::try_from_u8(self.version).unwrap();
|
||||
|
||||
let B_scan_bytes = self.scan_pubkey.serialize();
|
||||
let B_m_bytes = self.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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create outputs for a given set of silent payment recipients and their corresponding shared secrets.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `recipients` - A `Vec` of silent payment addresses to be paid.
|
||||
/// * `ecdh_shared_secrets` - A HashMap that maps every scan key to a shared secret created with this scan key.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If successful, the function returns a `Result` wrapping a `HashMap` of silent payment addresses to a `Vec`.
|
||||
/// The `Vec` contains all the outputs that are associated with the silent payment address.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
///
|
||||
/// * The recipients Vec contains a silent payment address with an incorrect format.
|
||||
/// * The ecdh_shared_secrets does not contain a secret for every B_scan that are being paid to.
|
||||
/// * Edge cases are hit during elliptic curve computation (extremely unlikely).
|
||||
pub fn generate_recipient_pubkeys(
|
||||
recipients: Vec<String>,
|
||||
ecdh_shared_secrets: HashMap<PublicKey, PublicKey>,
|
||||
) -> Result<HashMap<String, Vec<XOnlyPublicKey>>> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let mut negated_keys: Vec<SecretKey> = vec![];
|
||||
let mut silent_payment_groups: HashMap<PublicKey, (PublicKey, Vec<SilentPaymentAddress>)> =
|
||||
HashMap::new();
|
||||
for recipient in recipients {
|
||||
let recipient: SilentPaymentAddress = recipient.try_into()?;
|
||||
let B_scan = recipient.scan_pubkey;
|
||||
|
||||
for (key, x_only) in input {
|
||||
let (_, parity) = key.x_only_public_key(&secp);
|
||||
|
||||
if *x_only && parity == Parity::Odd {
|
||||
negated_keys.push(key.negate());
|
||||
if let Some((_, payments)) = silent_payment_groups.get_mut(&B_scan) {
|
||||
payments.push(recipient);
|
||||
} else {
|
||||
negated_keys.push(*key);
|
||||
let ecdh_shared_secret = ecdh_shared_secrets
|
||||
.get(&B_scan)
|
||||
.ok_or(Error::InvalidSharedSecret(
|
||||
"Shared secret for this B_scan not found".to_owned(),
|
||||
))?
|
||||
.to_owned();
|
||||
silent_payment_groups.insert(B_scan, (ecdh_shared_secret, vec![recipient]));
|
||||
}
|
||||
}
|
||||
|
||||
let (head, tail) = negated_keys.split_first().ok_or("Empty input list")?;
|
||||
|
||||
let result: Result<SecretKey> = tail
|
||||
.iter()
|
||||
.fold(Ok(*head), |acc: Result<SecretKey>, &item| {
|
||||
Ok(acc?.add_tweak(&item.into())?)
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn decode_silent_payment_address(addr: &str) -> Result<(PublicKey, PublicKey)> {
|
||||
let (_hrp, data, _variant) = bech32::decode(&addr)?;
|
||||
|
||||
let data = Vec::<u8>::from_base32(&data[1..])?;
|
||||
|
||||
let B_scan = PublicKey::from_slice(&data[..33])?;
|
||||
let B_spend = PublicKey::from_slice(&data[33..])?;
|
||||
|
||||
Ok((B_scan, B_spend))
|
||||
}
|
||||
|
||||
pub fn create_outputs(
|
||||
outpoints: &Vec<(String, u32)>,
|
||||
input_priv_keys: &Vec<(SecretKey, bool)>,
|
||||
recipients: &Vec<(String, f32)>,
|
||||
) -> Result<Vec<HashMap<String, f32>>> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let outpoints_hash = hash_outpoints(outpoints)?;
|
||||
|
||||
let a_sum = get_a_sum_secret_keys(input_priv_keys)?;
|
||||
|
||||
let mut silent_payment_groups: HashMap<PublicKey, Vec<(PublicKey, f32)>> = HashMap::new();
|
||||
for (payment_address, amount) in recipients {
|
||||
let (B_scan, B_m) = decode_silent_payment_address(&payment_address)?;
|
||||
|
||||
if let Some(payments) = silent_payment_groups.get_mut(&B_scan) {
|
||||
payments.push((B_m, *amount));
|
||||
} else {
|
||||
silent_payment_groups.insert(B_scan, vec![(B_m, *amount)]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: Vec<HashMap<String, f32>> = vec![];
|
||||
for (B_scan, B_m_values) in silent_payment_groups.into_iter() {
|
||||
let mut result: HashMap<String, Vec<XOnlyPublicKey>> = HashMap::new();
|
||||
for group in silent_payment_groups.into_values() {
|
||||
let mut n = 0;
|
||||
|
||||
//calculate shared secret
|
||||
let intermediate = B_scan.mul_tweak(&secp, &a_sum.into())?;
|
||||
let scalar = Scalar::from_be_bytes(outpoints_hash)?;
|
||||
let ecdh_shared_secret = intermediate.mul_tweak(&secp, &scalar)?.serialize();
|
||||
let (ecdh_shared_secret, recipients) = group;
|
||||
|
||||
for (B_m, amount) in B_m_values {
|
||||
for recipient in recipients {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.extend_from_slice(&ecdh_shared_secret);
|
||||
bytes.extend_from_slice(&ecdh_shared_secret.serialize());
|
||||
bytes.extend_from_slice(&ser_uint32(n));
|
||||
|
||||
let t_n = sha256(&bytes);
|
||||
|
||||
let G: PublicKey = SecretKey::from_slice(&Scalar::ONE.to_be_bytes())?.public_key(&secp);
|
||||
let res = G.mul_tweak(&secp, &Scalar::from_be_bytes(t_n)?)?;
|
||||
let reskey = res.combine(&B_m)?;
|
||||
let res = SecretKey::from_slice(&t_n)?.public_key(&secp);
|
||||
let reskey = res.combine(&recipient.m_pubkey)?;
|
||||
let (reskey_xonly, _) = reskey.x_only_public_key();
|
||||
|
||||
let mut toAdd: HashMap<String, f32> = HashMap::new();
|
||||
|
||||
toAdd.insert(reskey_xonly.to_string(), amount);
|
||||
|
||||
result.push(toAdd);
|
||||
let entry = result.entry(recipient.into()).or_insert_with(Vec::new);
|
||||
entry.push(reskey_xonly);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn decode_scan_pubkey(silent_payment_address: String) -> Result<PublicKey> {
|
||||
let address: SilentPaymentAddress = silent_payment_address.try_into()?;
|
||||
Ok(address.scan_pubkey)
|
||||
}
|
||||
|
|
|
@ -12,3 +12,9 @@ pub struct OutputWithSignature {
|
|||
pub priv_key_tweak: String,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Outpoint {
|
||||
pub txid: [u8; 32],
|
||||
pub vout: u32,
|
||||
}
|
||||
|
|
26
src/utils.rs
26
src/utils.rs
|
@ -1,9 +1,10 @@
|
|||
use std::io::Write;
|
||||
use std::{collections::HashSet, io::Write};
|
||||
|
||||
use hex::FromHex;
|
||||
use secp256k1::hashes::{sha256, Hash};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
use crate::{error::Error, structs::Outpoint};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn sha256(message: &[u8]) -> [u8; 32] {
|
||||
sha256::Hash::hash(message).to_byte_array()
|
||||
|
@ -13,22 +14,25 @@ pub fn ser_uint32(u: u32) -> Vec<u8> {
|
|||
u.to_be_bytes().into()
|
||||
}
|
||||
|
||||
pub fn hash_outpoints(sending_data: &Vec<(String, u32)>) -> Result<[u8; 32]> {
|
||||
pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> Result<[u8; 32]> {
|
||||
let mut outpoints: Vec<Vec<u8>> = vec![];
|
||||
|
||||
for (txid_str, vout) in sending_data {
|
||||
let mut txid = Vec::from_hex(txid_str)?;
|
||||
txid.reverse();
|
||||
let mut vout_bytes = vout.to_le_bytes().to_vec();
|
||||
txid.append(&mut vout_bytes);
|
||||
outpoints.push(txid);
|
||||
for outpoint in sending_data {
|
||||
let txid = outpoint.txid;
|
||||
let vout = outpoint.vout;
|
||||
|
||||
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)?;
|
||||
engine.write_all(&v).unwrap();
|
||||
}
|
||||
|
||||
Ok(sha256::Hash::from_engine(engine).to_byte_array())
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod input;
|
||||
pub mod structs;
|
||||
pub mod utils;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
#![allow(non_snake_case)]
|
||||
use serde::Deserialize;
|
||||
use serde_json::from_str;
|
||||
use silentpayments::structs::OutputWithSignature;
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::{collections::HashMap, fs::File, io::Read};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TestData {
|
||||
|
@ -54,48 +52,3 @@ pub struct SendingDataGiven {
|
|||
pub struct SendingDataExpected {
|
||||
pub outputs: Vec<(String, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ComparableHashMap {
|
||||
pub inner: HashMap<String, f32>,
|
||||
}
|
||||
|
||||
impl From<HashMap<String, f32>> for ComparableHashMap {
|
||||
fn from(map: HashMap<String, f32>) -> Self {
|
||||
ComparableHashMap { inner: map }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ComparableHashMap {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.inner.len() != other.inner.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.inner.iter().all(|(key, val)| {
|
||||
other
|
||||
.inner
|
||||
.get(key)
|
||||
.map_or(false, |other_val| (val - other_val).abs() < 0.0001)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ComparableHashMap {}
|
||||
|
||||
impl Hash for ComparableHashMap {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let mut keys: Vec<_> = self.inner.keys().collect();
|
||||
keys.sort(); // ensure consistent order
|
||||
for key in keys {
|
||||
key.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_file() -> Vec<TestData> {
|
||||
let mut file = File::open("tests/resources/send_and_receive_test_vectors.json").unwrap();
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).unwrap();
|
||||
from_str(&contents).unwrap()
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
use std::{collections::HashSet, fs::File, io::{Read, Write}, str::FromStr};
|
||||
|
||||
use secp256k1::{PublicKey, SecretKey, XOnlyPublicKey, hashes::{sha256, Hash}, Scalar};
|
||||
use serde_json::from_str;
|
||||
use silentpayments::structs::Outpoint;
|
||||
|
||||
use super::structs::TestData;
|
||||
|
||||
pub fn read_file() -> Vec<TestData> {
|
||||
let mut file = File::open("tests/resources/send_and_receive_test_vectors.json").unwrap();
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).unwrap();
|
||||
from_str(&contents).unwrap()
|
||||
}
|
||||
|
||||
pub fn decode_outpoints(outpoints: &Vec<(String, u32)>) -> HashSet<Outpoint> {
|
||||
outpoints
|
||||
.iter()
|
||||
.map(|(txid_str, vout)| Outpoint {
|
||||
txid: hex::decode(txid_str)
|
||||
.unwrap()
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
vout: *vout,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_priv_keys(input_priv_keys: &Vec<(String, bool)>) -> Vec<(SecretKey, bool)> {
|
||||
input_priv_keys
|
||||
.iter()
|
||||
.map(|(keystr, x_only)| (SecretKey::from_str(&keystr).unwrap(), *x_only))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_input_pub_keys(input_pub_keys: &Vec<String>) -> Vec<PublicKey> {
|
||||
input_pub_keys
|
||||
.iter()
|
||||
.map(|x| match PublicKey::from_str(&x) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
// we always assume even pairing for input public keys if they are omitted
|
||||
let x_only_public_key = XOnlyPublicKey::from_str(&x).unwrap();
|
||||
PublicKey::from_x_only_public_key(x_only_public_key, secp256k1::Parity::Even)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_outputs_to_check(outputs: &Vec<String>) -> Vec<XOnlyPublicKey> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|x| XOnlyPublicKey::from_str(x).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_recipients(recipients: &Vec<(String, f32)>) -> Vec<String> {
|
||||
recipients
|
||||
.iter()
|
||||
.map(|(sp_addr_str, _)| sp_addr_str.to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_a_sum_secret_keys(input: &Vec<(SecretKey, bool)>) -> SecretKey {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
|
||||
let mut negated_keys: Vec<SecretKey> = vec![];
|
||||
|
||||
for (key, is_xonly) in input {
|
||||
let (_, parity) = key.x_only_public_key(&secp);
|
||||
|
||||
if *is_xonly && parity == secp256k1::Parity::Odd {
|
||||
negated_keys.push(key.negate());
|
||||
} else {
|
||||
negated_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let (head, tail) = negated_keys.split_first().unwrap();
|
||||
|
||||
let result: SecretKey = tail
|
||||
.iter()
|
||||
.fold(*head, |acc, &item| acc.add_tweak(&item.into()).unwrap());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn compute_ecdh_shared_secret(a_sum: SecretKey, B_scan: PublicKey, outpoints_hash: Scalar) -> PublicKey {
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
|
||||
let diffie_hellman = B_scan.mul_tweak(&secp, &a_sum.into()).unwrap();
|
||||
diffie_hellman.mul_tweak(&secp, &outpoints_hash).unwrap()
|
||||
}
|
||||
|
||||
pub fn hash_outpoints(sending_data: &HashSet<Outpoint>) -> [u8; 32] {
|
||||
let mut outpoints: Vec<Vec<u8>> = vec![];
|
||||
|
||||
for outpoint in sending_data {
|
||||
let txid = outpoint.txid;
|
||||
let vout = outpoint.vout;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
sha256::Hash::from_engine(engine).to_byte_array()
|
||||
}
|
|
@ -2,28 +2,32 @@
|
|||
mod common;
|
||||
|
||||
use silentpayments::receiving;
|
||||
use silentpayments::sending;
|
||||
use silentpayments::utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
use std::{collections::{HashSet, HashMap}, str::FromStr};
|
||||
|
||||
use secp256k1::{PublicKey, SecretKey, XOnlyPublicKey};
|
||||
use secp256k1::{SecretKey, PublicKey, Scalar};
|
||||
use silentpayments::sending::{decode_scan_pubkey, generate_recipient_pubkeys};
|
||||
|
||||
use crate::{
|
||||
common::input::{self, TestData},
|
||||
common::{
|
||||
structs::TestData,
|
||||
utils::{
|
||||
self, decode_input_pub_keys, decode_outpoints,
|
||||
decode_outputs_to_check, decode_priv_keys, decode_recipients,
|
||||
get_a_sum_secret_keys, hash_outpoints, compute_ecdh_shared_secret,
|
||||
},
|
||||
},
|
||||
receiving::{
|
||||
get_A_sum_public_keys, get_receiving_addresses, scanning,
|
||||
verify_and_calculate_signatures,
|
||||
},
|
||||
sending::create_outputs,
|
||||
utils::hash_outpoints,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_with_test_vectors() {
|
||||
let testdata = input::read_file();
|
||||
let testdata = utils::read_file();
|
||||
|
||||
for test in testdata {
|
||||
process_test_case(test);
|
||||
|
@ -41,18 +45,28 @@ mod tests {
|
|||
let expected_output_addresses: HashSet<String> =
|
||||
expected.iter().map(|(x, _)| x.into()).collect();
|
||||
|
||||
let input_priv_keys: Vec<(SecretKey, bool)> = given
|
||||
.input_priv_keys
|
||||
.iter()
|
||||
.map(|(keystr, x_only)| (SecretKey::from_str(&keystr).unwrap(), *x_only))
|
||||
.collect();
|
||||
let input_priv_keys = decode_priv_keys(&given.input_priv_keys);
|
||||
|
||||
let outputs =
|
||||
create_outputs(&given.outpoints, &input_priv_keys, &given.recipients).unwrap();
|
||||
let outpoints = decode_outpoints(&given.outpoints);
|
||||
|
||||
for map in &outputs {
|
||||
for key in map.keys() {
|
||||
sending_outputs.insert(key.clone());
|
||||
let outpoints_hash = Scalar::from_be_bytes(hash_outpoints(&outpoints)).unwrap();
|
||||
|
||||
let silent_addresses = decode_recipients(&given.recipients);
|
||||
|
||||
let a_sum = get_a_sum_secret_keys(&input_priv_keys);
|
||||
|
||||
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);
|
||||
ecdh_shared_secrets.insert(B_scan, ecdh_shared_secret);
|
||||
}
|
||||
let outputs = generate_recipient_pubkeys(silent_addresses, ecdh_shared_secrets).unwrap();
|
||||
|
||||
for output_pubkeys in &outputs {
|
||||
for pubkey in output_pubkeys.1 {
|
||||
// TODO check if this is always true
|
||||
sending_outputs.insert(hex::encode(pubkey.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,8 +79,9 @@ mod tests {
|
|||
|
||||
let receiving_outputs: HashSet<String> = given.outputs.iter().cloned().collect();
|
||||
|
||||
// assert that the sending outputs generated are equal
|
||||
// to the expected receiving outputs
|
||||
// assert that the generated sending outputs are a subset
|
||||
// of the expected receiving outputs
|
||||
// i.e. all the generated outputs are present
|
||||
assert!(sending_outputs.is_subset(&receiving_outputs));
|
||||
|
||||
let b_scan = SecretKey::from_str(&given.scan_priv_key).unwrap();
|
||||
|
@ -81,32 +96,16 @@ mod tests {
|
|||
let set1: HashSet<_> = receiving_addresses.iter().collect();
|
||||
let set2: HashSet<_> = expected.addresses.iter().collect();
|
||||
|
||||
// check that the receiving addresses generated are equal
|
||||
// to the expected addresses
|
||||
assert_eq!(set1, set2);
|
||||
|
||||
// can be even or odd !
|
||||
let outputs_to_check: Vec<XOnlyPublicKey> = given
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|x| XOnlyPublicKey::from_str(x).unwrap())
|
||||
.collect();
|
||||
let outputs_to_check = decode_outputs_to_check(&given.outputs);
|
||||
|
||||
let outpoints_hash = hash_outpoints(&given.outpoints).unwrap();
|
||||
let outpoints = decode_outpoints(&given.outpoints);
|
||||
|
||||
let input_pub_keys: Vec<PublicKey> = given
|
||||
.input_pub_keys
|
||||
.iter()
|
||||
.map(|x| match PublicKey::from_str(&x) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
// we always assume even pairing for input public keys if they are omitted
|
||||
let x_only_public_key = XOnlyPublicKey::from_str(&x).unwrap();
|
||||
PublicKey::from_x_only_public_key(
|
||||
x_only_public_key,
|
||||
secp256k1::Parity::Even,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let input_pub_keys = decode_input_pub_keys(&given.input_pub_keys);
|
||||
|
||||
let A_sum = get_A_sum_public_keys(&input_pub_keys).unwrap();
|
||||
|
||||
|
@ -115,15 +114,8 @@ mod tests {
|
|||
_ => Some(&given.labels),
|
||||
};
|
||||
|
||||
let mut add_to_wallet = scanning(
|
||||
b_scan,
|
||||
B_spend,
|
||||
A_sum,
|
||||
outpoints_hash,
|
||||
outputs_to_check,
|
||||
labels,
|
||||
)
|
||||
.unwrap();
|
||||
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