Introduce a subaddress capable scanner type

This commit is contained in:
Luke Parker
2022-08-22 08:32:09 -04:00
parent 19f5fd8fe9
commit 5751204a98
5 changed files with 152 additions and 67 deletions

View File

@@ -4,15 +4,10 @@ use thiserror::Error;
use zeroize::Zeroize; use zeroize::Zeroize;
use curve25519_dalek::{ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
constants::ED25519_BASEPOINT_TABLE,
edwards::{EdwardsPoint, CompressedEdwardsY},
};
use base58_monero::base58::{encode_check, decode_check}; use base58_monero::base58::{encode_check, decode_check};
use crate::wallet::ViewPair;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum Network { pub enum Network {
Mainnet, Mainnet,
@@ -130,16 +125,6 @@ pub struct Address {
pub view: EdwardsPoint, pub view: EdwardsPoint,
} }
impl ViewPair {
pub fn address(&self, network: Network, kind: AddressType) -> Address {
Address {
meta: AddressMeta { network, kind },
spend: self.spend,
view: &self.view * &ED25519_BASEPOINT_TABLE,
}
}
}
impl ToString for Address { impl ToString for Address {
fn to_string(&self) -> String { fn to_string(&self) -> String {
let mut data = vec![self.meta.to_byte()]; let mut data = vec![self.meta.to_byte()];

View File

@@ -1,6 +1,12 @@
use std::collections::HashMap;
use zeroize::{Zeroize, ZeroizeOnDrop}; use zeroize::{Zeroize, ZeroizeOnDrop};
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
edwards::{EdwardsPoint, CompressedEdwardsY},
};
use crate::{hash, hash_to_scalar, serialize::write_varint, transaction::Input}; use crate::{hash, hash_to_scalar, serialize::write_varint, transaction::Input};
@@ -8,6 +14,7 @@ mod extra;
pub(crate) use extra::{PaymentId, ExtraField, Extra}; pub(crate) use extra::{PaymentId, ExtraField, Extra};
pub mod address; pub mod address;
use address::{Network, AddressType, AddressMeta, Address};
mod scan; mod scan;
pub use scan::SpendableOutput; pub use scan::SpendableOutput;
@@ -87,6 +94,101 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
#[derive(Clone, Zeroize, ZeroizeOnDrop)] #[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct ViewPair { pub struct ViewPair {
pub spend: EdwardsPoint, spend: EdwardsPoint,
pub view: Scalar, view: Scalar,
}
impl ViewPair {
pub fn new(spend: EdwardsPoint, view: Scalar) -> ViewPair {
ViewPair { spend, view }
}
pub(crate) fn subaddress(&self, index: (u32, u32)) -> Scalar {
if index == (0, 0) {
return Scalar::zero();
}
hash_to_scalar(
&[
b"SubAddr\0".as_ref(),
&self.view.to_bytes(),
&index.0.to_le_bytes(),
&index.1.to_le_bytes(),
]
.concat(),
)
}
}
#[derive(Clone)]
pub struct Scanner {
pair: ViewPair,
network: Network,
guaranteed: bool,
pub(crate) subaddresses: HashMap<CompressedEdwardsY, (u32, u32)>,
}
impl Zeroize for Scanner {
fn zeroize(&mut self) {
self.pair.zeroize();
self.network.zeroize();
self.guaranteed.zeroize();
for (mut key, mut value) in self.subaddresses.drain() {
key.zeroize();
value.zeroize();
}
}
}
impl Drop for Scanner {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for Scanner {}
impl Scanner {
pub fn from_view(pair: ViewPair, network: Network, guaranteed: bool) -> Scanner {
let mut subaddresses = HashMap::new();
subaddresses.insert(pair.spend.compress(), (0, 0));
Scanner { pair, network, guaranteed, subaddresses }
}
pub fn address(&self) -> Address {
Address::new(
AddressMeta {
network: self.network,
kind: if self.guaranteed {
AddressType::Featured(false, None, true)
} else {
AddressType::Standard
},
},
self.pair.spend,
&self.pair.view * &ED25519_BASEPOINT_TABLE,
)
}
pub fn subaddress(&mut self, index: (u32, u32)) -> Address {
if index == (0, 0) {
return self.address();
}
let spend = self.pair.spend + (&self.pair.subaddress(index) * &ED25519_BASEPOINT_TABLE);
self.subaddresses.insert(spend.compress(), index);
Address::new(
AddressMeta {
network: self.network,
kind: if self.guaranteed {
AddressType::Featured(true, None, true)
} else {
AddressType::Subaddress
},
},
spend,
self.pair.view * spend,
)
}
} }

View File

@@ -8,9 +8,7 @@ use crate::{
Commitment, Commitment,
serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point}, serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point},
transaction::{Timelock, Transaction}, transaction::{Timelock, Transaction},
wallet::{ wallet::{PaymentId, Extra, Scanner, uniqueness, shared_key, amount_decryption, commitment_mask},
ViewPair, PaymentId, Extra, uniqueness, shared_key, amount_decryption, commitment_mask,
},
}; };
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
@@ -19,9 +17,13 @@ pub struct SpendableOutput {
pub o: u8, pub o: u8,
pub key: EdwardsPoint, pub key: EdwardsPoint,
// Absolute difference between the spend key and the key in this output, inclusive of any
// subaddress offset
pub key_offset: Scalar, pub key_offset: Scalar,
pub commitment: Commitment, pub commitment: Commitment,
// Metadata to know how to process this output
// Does not have to be an Option since the 0 subaddress is the main address // Does not have to be an Option since the 0 subaddress is the main address
pub subaddress: (u32, u32), pub subaddress: (u32, u32),
// Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should // Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should
@@ -92,24 +94,24 @@ impl SpendableOutput {
} }
} }
impl Transaction { impl Scanner {
pub fn scan(&self, view: &ViewPair, guaranteed: bool) -> Timelocked { pub fn scan(&self, tx: &Transaction) -> Timelocked {
let extra = Extra::deserialize(&mut Cursor::new(&self.prefix.extra)); let extra = Extra::deserialize(&mut Cursor::new(&tx.prefix.extra));
let keys; let keys;
let extra = if let Ok(extra) = extra { let extra = if let Ok(extra) = extra {
keys = extra.keys(); keys = extra.keys();
extra extra
} else { } else {
return Timelocked(self.prefix.timelock, vec![]); return Timelocked(tx.prefix.timelock, vec![]);
}; };
let payment_id = extra.payment_id(); let payment_id = extra.payment_id();
let mut res = vec![]; let mut res = vec![];
for (o, output) in self.prefix.outputs.iter().enumerate() { for (o, output) in tx.prefix.outputs.iter().enumerate() {
for key in &keys { for key in &keys {
let (view_tag, key_offset, payment_id_xor) = shared_key( let (view_tag, key_offset, payment_id_xor) = shared_key(
Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed), if self.guaranteed { Some(uniqueness(&tx.prefix.inputs)) } else { None },
&view.view, &self.pair.view,
key, key,
o, o,
); );
@@ -128,7 +130,10 @@ impl Transaction {
} }
// P - shared == spend // P - shared == spend
if (output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)) != view.spend { let subaddress = self
.subaddresses
.get(&(output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)).compress());
if subaddress.is_none() {
continue; continue;
} }
@@ -140,7 +145,7 @@ impl Transaction {
commitment.amount = output.amount; commitment.amount = output.amount;
// Regular transaction // Regular transaction
} else { } else {
let amount = match self.rct_signatures.base.ecdh_info.get(o) { let amount = match tx.rct_signatures.base.ecdh_info.get(o) {
Some(amount) => amount_decryption(*amount, key_offset), Some(amount) => amount_decryption(*amount, key_offset),
// This should never happen, yet it may be possible with miner transactions? // This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case // Using get just decreases the possibility of a panic and lets us move on in that case
@@ -151,18 +156,18 @@ impl Transaction {
commitment = Commitment::new(commitment_mask(key_offset), amount); commitment = Commitment::new(commitment_mask(key_offset), amount);
// If this is a malicious commitment, move to the next output // If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable // Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) { if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) {
break; break;
} }
} }
if commitment.amount != 0 { if commitment.amount != 0 {
res.push(SpendableOutput { res.push(SpendableOutput {
tx: self.hash(), tx: tx.hash(),
o: o.try_into().unwrap(), o: o.try_into().unwrap(),
key: output.key, key: output.key,
key_offset, key_offset: key_offset + self.pair.subaddress(*subaddress.unwrap()),
commitment, commitment,
subaddress: (0, 0), subaddress: (0, 0),
@@ -175,6 +180,6 @@ impl Transaction {
} }
} }
Timelocked(self.prefix.timelock, res) Timelocked(tx.prefix.timelock, res)
} }
} }

View File

@@ -23,11 +23,7 @@ use frost::{
use monero_serai::{ use monero_serai::{
random_scalar, random_scalar,
wallet::{ wallet::{ViewPair, Scanner, address::Network, SignableTransaction},
ViewPair,
address::{Network, AddressType},
SignableTransaction,
},
}; };
mod rpc; mod rpc;
@@ -78,8 +74,9 @@ async fn send_core(test: usize, multisig: bool) {
} }
} }
let view_pair = ViewPair { view, spend: spend_pub }; let view_pair = ViewPair::new(spend_pub, view);
let addr = view_pair.address(Network::Mainnet, AddressType::Standard); let scanner = Scanner::from_view(view_pair, Network::Mainnet, false);
let addr = scanner.address();
let fee = rpc.get_fee().await.unwrap(); let fee = rpc.get_fee().await.unwrap();
@@ -101,7 +98,7 @@ async fn send_core(test: usize, multisig: bool) {
// Grab the largest output available // Grab the largest output available
let output = { let output = {
let mut outputs = tx.as_ref().unwrap().scan(&view_pair, false).ignore_timelock(); let mut outputs = scanner.scan(tx.as_ref().unwrap()).ignore_timelock();
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse()); outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
outputs.swap_remove(0) outputs.swap_remove(0)
}; };
@@ -126,7 +123,7 @@ async fn send_core(test: usize, multisig: bool) {
for i in (start + 1) .. (start + 9) { for i in (start + 1) .. (start + 9) {
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0); let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
let output = tx.scan(&view_pair, false).ignore_timelock().swap_remove(0); let output = scanner.scan(&tx).ignore_timelock().swap_remove(0);
amount += output.commitment.amount; amount += output.commitment.amount;
outputs.push(output); outputs.push(output);
} }

View File

@@ -10,8 +10,8 @@ use monero_serai::{
transaction::Transaction, transaction::Transaction,
rpc::Rpc, rpc::Rpc,
wallet::{ wallet::{
ViewPair, ViewPair, Scanner,
address::{Network, AddressType, Address}, address::{Network, Address},
Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine, Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine,
}, },
}; };
@@ -71,19 +71,23 @@ impl Monero {
Monero { rpc: Rpc::new(url), view: view_key::<Monero>(0).0 } Monero { rpc: Rpc::new(url), view: view_key::<Monero>(0).0 }
} }
fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair { fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner {
ViewPair { spend: spend.0, view: self.view } Scanner::from_view(ViewPair::new(spend.0, self.view), Network::Mainnet, true)
} }
#[cfg(test)] #[cfg(test)]
fn empty_view_pair(&self) -> ViewPair { fn empty_scanner() -> Scanner {
use group::Group; use group::Group;
self.view_pair(dfg::EdwardsPoint::generator()) Scanner::from_view(
ViewPair::new(*dfg::EdwardsPoint::generator(), Scalar::one()),
Network::Mainnet,
false,
)
} }
#[cfg(test)] #[cfg(test)]
fn empty_address(&self) -> Address { fn empty_address() -> Address {
self.empty_view_pair().address(Network::Mainnet, AddressType::Standard) Self::empty_scanner().address()
} }
} }
@@ -113,7 +117,7 @@ impl Coin for Monero {
const MAX_OUTPUTS: usize = 16; const MAX_OUTPUTS: usize = 16;
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { fn address(&self, key: dfg::EdwardsPoint) -> Self::Address {
self.view_pair(key).address(Network::Mainnet, AddressType::Featured(false, None, true)) self.scanner(key).address()
} }
async fn get_height(&self) -> Result<usize, CoinError> { async fn get_height(&self) -> Result<usize, CoinError> {
@@ -125,11 +129,8 @@ impl Coin for Monero {
} }
async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec<Self::Output> { async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec<Self::Output> {
block let scanner = self.scanner(key);
.iter() block.iter().flat_map(|tx| scanner.scan(tx).not_locked()).map(Output::from).collect()
.flat_map(|tx| tx.scan(&self.view_pair(key), true).not_locked())
.map(Output::from)
.collect()
} }
async fn prepare_send( async fn prepare_send(
@@ -199,7 +200,7 @@ impl Coin for Monero {
Some(serde_json::json!({ Some(serde_json::json!({
"method": "generateblocks", "method": "generateblocks",
"params": { "params": {
"wallet_address": self.empty_address().to_string(), "wallet_address": Self::empty_address().to_string(),
"amount_of_blocks": 10 "amount_of_blocks": 10
}, },
})), })),
@@ -219,13 +220,8 @@ impl Coin for Monero {
self.mine_block().await; self.mine_block().await;
} }
let outputs = self let outputs = Self::empty_scanner()
.rpc .scan(&self.rpc.get_block_transactions_possible(height).await.unwrap().swap_remove(0))
.get_block_transactions_possible(height)
.await
.unwrap()
.swap_remove(0)
.scan(&self.empty_view_pair(), false)
.ignore_timelock(); .ignore_timelock();
let amount = outputs[0].commitment.amount; let amount = outputs[0].commitment.amount;
@@ -234,7 +230,7 @@ impl Coin for Monero {
self.rpc.get_protocol().await.unwrap(), self.rpc.get_protocol().await.unwrap(),
outputs, outputs,
vec![(address, amount - fee)], vec![(address, amount - fee)],
Some(self.empty_address()), Some(Self::empty_address()),
self.rpc.get_fee().await.unwrap(), self.rpc.get_fee().await.unwrap(),
) )
.unwrap() .unwrap()