mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 04:09:23 +00:00
Introduce a subaddress capable scanner type
This commit is contained in:
@@ -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()];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user