Remove the DecoySelection trait

This commit is contained in:
Luke Parker 2024-07-08 00:30:42 -04:00
parent a2c3aba82b
commit d7f7f69738
No known key found for this signature in database
18 changed files with 320 additions and 338 deletions

1
Cargo.lock generated
View file

@ -4932,7 +4932,6 @@ dependencies = [
name = "monero-wallet" name = "monero-wallet"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait",
"curve25519-dalek", "curve25519-dalek",
"dalek-ff-group", "dalek-ff-group",
"flexible-transcript", "flexible-transcript",

View file

@ -137,13 +137,23 @@ impl Commitment {
} }
/// Decoy data, as used for producing Monero's ring signatures. /// Decoy data, as used for producing Monero's ring signatures.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Decoys { pub struct Decoys {
offsets: Vec<u64>, offsets: Vec<u64>,
signer_index: u8, signer_index: u8,
ring: Vec<[EdwardsPoint; 2]>, ring: Vec<[EdwardsPoint; 2]>,
} }
impl core::fmt::Debug for Decoys {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("Decoys")
.field("offsets", &self.offsets)
.field("ring", &self.ring)
.finish_non_exhaustive()
}
}
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
impl Decoys { impl Decoys {
/// Create a new instance of decoy data. /// Create a new instance of decoy data.

View file

@ -18,7 +18,6 @@ workspace = true
[dependencies] [dependencies]
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false } std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
async-trait = { version = "0.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true } thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }

View file

@ -1,19 +1,21 @@
// TODO: Clean this // TODO: Clean this
use std_shims::{vec::Vec, collections::HashSet}; use std_shims::{io, vec::Vec, collections::HashSet};
use zeroize::Zeroize; use zeroize::{Zeroize, ZeroizeOnDrop};
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
use rand_distr::{Distribution, Gamma}; use rand_distr::{Distribution, Gamma};
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use rand_distr::num_traits::Float; use rand_distr::num_traits::Float;
use curve25519_dalek::edwards::EdwardsPoint; use curve25519_dalek::{Scalar, EdwardsPoint};
use crate::{ use crate::{
DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME, DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
primitives::Commitment,
rpc::{RpcError, Rpc}, rpc::{RpcError, Rpc},
output::OutputData,
WalletOutput, WalletOutput,
}; };
@ -138,20 +140,16 @@ async fn select_decoys<R: RngCore + CryptoRng>(
rpc: &impl Rpc, rpc: &impl Rpc,
ring_len: usize, ring_len: usize,
height: usize, height: usize,
inputs: &[WalletOutput], input: &WalletOutput,
fingerprintable_canonical: bool, fingerprintable_canonical: bool,
) -> Result<Vec<Decoys>, RpcError> { ) -> Result<Decoys, RpcError> {
let mut distribution = vec![]; let mut distribution = vec![];
let decoy_count = ring_len - 1; let decoy_count = ring_len - 1;
// Convert the inputs in question to the raw output data // Convert the inputs in question to the raw output data
let mut real = Vec::with_capacity(inputs.len()); let mut real = vec![input.relative_id.index_on_blockchain];
let mut outputs = Vec::with_capacity(inputs.len()); let output = (real[0], [input.key(), input.commitment().calculate()]);
for input in inputs {
real.push(input.relative_id.index_on_blockchain);
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
}
if distribution.len() < height { if distribution.len() < height {
// TODO: verify distribution elems are strictly increasing // TODO: verify distribution elems are strictly increasing
@ -175,16 +173,14 @@ async fn select_decoys<R: RngCore + CryptoRng>(
}; };
let mut used = HashSet::<u64>::new(); let mut used = HashSet::<u64>::new();
for o in &outputs { used.insert(output.0);
used.insert(o.0);
}
// TODO: Create a TX with less than the target amount, as allowed by the protocol // TODO: Create a TX with less than the target amount, as allowed by the protocol
let high = distribution[distribution.len() - DEFAULT_LOCK_WINDOW]; let high = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
// This assumes that each miner TX had one output (as sane) and checks we have sufficient // This assumes that each miner TX had one output (as sane) and checks we have sufficient
// outputs even when excluding them (due to their own timelock requirements) // outputs even when excluding them (due to their own timelock requirements)
if high.saturating_sub(u64::try_from(COINBASE_LOCK_WINDOW).unwrap()) < if high.saturating_sub(u64::try_from(COINBASE_LOCK_WINDOW).unwrap()) <
u64::try_from(inputs.len() * ring_len).unwrap() u64::try_from(ring_len).unwrap()
{ {
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?; Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
} }
@ -201,17 +197,15 @@ async fn select_decoys<R: RngCore + CryptoRng>(
per_second, per_second,
&real, &real,
&mut used, &mut used,
inputs.len() * decoy_count, decoy_count,
fingerprintable_canonical, fingerprintable_canonical,
) )
.await?; .await?;
real.zeroize(); real.zeroize();
let mut res = Vec::with_capacity(inputs.len());
for o in outputs {
// Grab the decoys for this specific output // Grab the decoys for this specific output
let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::<Vec<_>>(); let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::<Vec<_>>();
ring.push(o); ring.push(output);
ring.sort_by(|a, b| a.0.cmp(&b.0)); ring.sort_by(|a, b| a.0.cmp(&b.0));
// Sanity checks are only run when 1000 outputs are available in Monero // Sanity checks are only run when 1000 outputs are available in Monero
@ -229,8 +223,8 @@ async fn select_decoys<R: RngCore + CryptoRng>(
// If it's not, update the bottom half with new values to ensure the median only moves up // If it's not, update the bottom half with new values to ensure the median only moves up
for removed in ring.drain(0 .. (ring_len / 2)).collect::<Vec<_>>() { for removed in ring.drain(0 .. (ring_len / 2)).collect::<Vec<_>>() {
// If we removed the real spend, add it back // If we removed the real spend, add it back
if removed.0 == o.0 { if removed.0 == output.0 {
ring.push(o); ring.push(output);
} else { } else {
// We could not remove this, saving CPU time and removing low values as // We could not remove this, saving CPU time and removing low values as
// possibilities, yet it'd increase the amount of decoys required to create this // possibilities, yet it'd increase the amount of decoys required to create this
@ -263,74 +257,103 @@ async fn select_decoys<R: RngCore + CryptoRng>(
// members // members
} }
res.push( Ok(
Decoys::new( Decoys::new(
offset(&ring.iter().map(|output| output.0).collect::<Vec<_>>()), offset(&ring.iter().map(|output| output.0).collect::<Vec<_>>()),
// Binary searches for the real spend since we don't know where it sorted to // Binary searches for the real spend since we don't know where it sorted to
u8::try_from(ring.partition_point(|x| x.0 < o.0)).unwrap(), u8::try_from(ring.partition_point(|x| x.0 < output.0)).unwrap(),
ring.iter().map(|output| output.1).collect(), ring.iter().map(|output| output.1).collect(),
) )
.unwrap(), .unwrap(),
); )
}
Ok(res)
} }
pub use monero_serai::primitives::Decoys; pub use monero_serai::primitives::Decoys;
// TODO: Remove this trait /// An output with decoys selected.
/// TODO: Document #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
#[cfg(feature = "std")] pub struct OutputWithDecoys {
#[async_trait::async_trait] output: OutputData,
pub trait DecoySelection { decoys: Decoys,
/// Select decoys using the same distribution as Monero. Relies on the monerod RPC }
/// response for an output's unlocked status, minimizing trips to the daemon.
async fn select<R: Send + Sync + RngCore + CryptoRng>( impl OutputWithDecoys {
rng: &mut R, /// Select decoys for this output.
pub async fn new(
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
rpc: &impl Rpc, rpc: &impl Rpc,
ring_len: usize, ring_len: usize,
height: usize, height: usize,
inputs: &[WalletOutput], output: WalletOutput,
) -> Result<Vec<Decoys>, RpcError>; ) -> Result<OutputWithDecoys, RpcError> {
let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?;
Ok(OutputWithDecoys { output: output.data.clone(), decoys })
}
/// If no reorg has occurred and an honest RPC, any caller who passes the same height to this /// Select a set of decoys for this output with a deterministic process.
/// function will use the same distribution to select decoys. It is fingerprintable
/// because a caller using this will not be able to select decoys that are timelocked
/// with a timestamp. Any transaction which includes timestamp timelocked decoys in its
/// rings could not be constructed using this function.
/// ///
/// TODO: upstream change to monerod get_outs RPC to accept a height param for checking /// This function will always output the same set of decoys when called with the same arguments.
/// output's unlocked status and remove all usage of fingerprintable_canonical /// This makes it very useful in multisignature contexts, where instead of having one participant
async fn fingerprintable_canonical_select<R: Send + Sync + RngCore + CryptoRng>( /// select the decoys, everyone can locally select the decoys while coming to the same result.
rng: &mut R, ///
/// The set of decoys selected may be fingerprintable as having been produced by this
/// methodology.
pub async fn fingerprintable_deterministic_new(
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
rpc: &impl Rpc, rpc: &impl Rpc,
ring_len: usize, ring_len: usize,
height: usize, height: usize,
inputs: &[WalletOutput], output: WalletOutput,
) -> Result<Vec<Decoys>, RpcError>; ) -> Result<OutputWithDecoys, RpcError> {
} let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?;
Ok(OutputWithDecoys { output: output.data.clone(), decoys })
#[cfg(feature = "std")]
#[async_trait::async_trait]
impl DecoySelection for Decoys {
async fn select<R: Send + Sync + RngCore + CryptoRng>(
rng: &mut R,
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[WalletOutput],
) -> Result<Vec<Decoys>, RpcError> {
select_decoys(rng, rpc, ring_len, height, inputs, false).await
} }
async fn fingerprintable_canonical_select<R: Send + Sync + RngCore + CryptoRng>( /// The key this output may be spent by.
rng: &mut R, pub fn key(&self) -> EdwardsPoint {
rpc: &impl Rpc, self.output.key()
ring_len: usize, }
height: usize,
inputs: &[WalletOutput], /// The scalar to add to the private spend key for it to be the discrete logarithm of this
) -> Result<Vec<Decoys>, RpcError> { /// output's key.
select_decoys(rng, rpc, ring_len, height, inputs, true).await pub fn key_offset(&self) -> Scalar {
self.output.key_offset
}
/// The commitment this output created.
pub fn commitment(&self) -> &Commitment {
&self.output.commitment
}
/// The decoys this output selected.
pub fn decoys(&self) -> &Decoys {
&self.decoys
}
/// Write the OutputWithDecoys.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
self.output.write(w)?;
self.decoys.write(w)
}
/// Serialize the OutputWithDecoys to a `Vec<u8>`.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(128);
self.write(&mut serialized).unwrap();
serialized
}
/// Read an OutputWithDecoys.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
} }
} }

View file

@ -35,15 +35,8 @@ pub use output::WalletOutput;
mod scan; mod scan;
pub use scan::{Scanner, GuaranteedScanner}; pub use scan::{Scanner, GuaranteedScanner};
#[cfg(feature = "std")]
mod decoys; mod decoys;
#[cfg(not(feature = "std"))] pub use decoys::OutputWithDecoys;
mod decoys {
pub use monero_serai::primitives::Decoys;
/// TODO: Document/remove
pub trait DecoySelection {}
}
pub use decoys::{DecoySelection, Decoys};
/// Structs and functionality for sending transactions. /// Structs and functionality for sending transactions.
pub mod send; pub mod send;

View file

@ -52,21 +52,15 @@ impl AbsoluteId {
/// An output's relative ID. /// An output's relative ID.
/// ///
/// This id defined as the block which contains the transaction creating the output and the /// This is defined as the output's index on the blockchain.
/// output's index on the blockchain.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct RelativeId { pub(crate) struct RelativeId {
pub(crate) block: [u8; 32],
pub(crate) index_on_blockchain: u64, pub(crate) index_on_blockchain: u64,
} }
impl core::fmt::Debug for RelativeId { impl core::fmt::Debug for RelativeId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish()
.debug_struct("RelativeId")
.field("block", &hex::encode(self.block))
.field("index_on_blockchain", &self.index_on_blockchain)
.finish()
} }
} }
@ -76,7 +70,6 @@ impl RelativeId {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.block)?;
w.write_all(&self.index_on_blockchain.to_le_bytes()) w.write_all(&self.index_on_blockchain.to_le_bytes())
} }
@ -85,18 +78,16 @@ impl RelativeId {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<Self> { fn read<R: Read>(r: &mut R) -> io::Result<Self> {
Ok(RelativeId { block: read_bytes(r)?, index_on_blockchain: read_u64(r)? }) Ok(RelativeId { index_on_blockchain: read_u64(r)? })
} }
} }
/// The data within an output as necessary to spend an output, and the output's additional /// The data within an output, as necessary to spend the output.
/// timelock.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct OutputData { pub(crate) struct OutputData {
pub(crate) key: EdwardsPoint, pub(crate) key: EdwardsPoint,
pub(crate) key_offset: Scalar, pub(crate) key_offset: Scalar,
pub(crate) commitment: Commitment, pub(crate) commitment: Commitment,
pub(crate) additional_timelock: Timelock,
} }
impl core::fmt::Debug for OutputData { impl core::fmt::Debug for OutputData {
@ -106,33 +97,55 @@ impl core::fmt::Debug for OutputData {
.field("key", &hex::encode(self.key.compress().0)) .field("key", &hex::encode(self.key.compress().0))
.field("key_offset", &hex::encode(self.key_offset.to_bytes())) .field("key_offset", &hex::encode(self.key_offset.to_bytes()))
.field("commitment", &self.commitment) .field("commitment", &self.commitment)
.field("additional_timelock", &self.additional_timelock)
.finish() .finish()
} }
} }
impl OutputData { impl OutputData {
// Write the OutputData. /// The key this output may be spent by.
pub(crate) fn key(&self) -> EdwardsPoint {
self.key
}
/// The scalar to add to the private spend key for it to be the discrete logarithm of this
/// output's key.
pub(crate) fn key_offset(&self) -> Scalar {
self.key_offset
}
/// The commitment this output created.
pub(crate) fn commitment(&self) -> &Commitment {
&self.commitment
}
/// Write the OutputData.
/// ///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.key.compress().to_bytes())?; w.write_all(&self.key.compress().to_bytes())?;
w.write_all(&self.key_offset.to_bytes())?; w.write_all(&self.key_offset.to_bytes())?;
self.commitment.write(w)?; self.commitment.write(w)
self.additional_timelock.write(w)
} }
/*
/// Serialize the OutputData to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(32 + 32 + 40);
self.write(&mut res).unwrap();
res
}
*/
/// Read an OutputData. /// Read an OutputData.
/// ///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<OutputData> { pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
Ok(OutputData { Ok(OutputData {
key: read_point(r)?, key: read_point(r)?,
key_offset: read_scalar(r)?, key_offset: read_scalar(r)?,
commitment: Commitment::read(r)?, commitment: Commitment::read(r)?,
additional_timelock: Timelock::read(r)?,
}) })
} }
} }
@ -140,6 +153,7 @@ impl OutputData {
/// The metadata for an output. /// The metadata for an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] #[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct Metadata { pub(crate) struct Metadata {
pub(crate) additional_timelock: Timelock,
pub(crate) subaddress: Option<SubaddressIndex>, pub(crate) subaddress: Option<SubaddressIndex>,
pub(crate) payment_id: Option<PaymentId>, pub(crate) payment_id: Option<PaymentId>,
pub(crate) arbitrary_data: Vec<Vec<u8>>, pub(crate) arbitrary_data: Vec<Vec<u8>>,
@ -149,6 +163,7 @@ impl core::fmt::Debug for Metadata {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt fmt
.debug_struct("Metadata") .debug_struct("Metadata")
.field("additional_timelock", &self.additional_timelock)
.field("subaddress", &self.subaddress) .field("subaddress", &self.subaddress)
.field("payment_id", &self.payment_id) .field("payment_id", &self.payment_id)
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>()) .field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
@ -162,6 +177,8 @@ impl Metadata {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.additional_timelock.write(w)?;
if let Some(subaddress) = self.subaddress { if let Some(subaddress) = self.subaddress {
w.write_all(&[1])?; w.write_all(&[1])?;
w.write_all(&subaddress.account().to_le_bytes())?; w.write_all(&subaddress.account().to_le_bytes())?;
@ -190,6 +207,8 @@ impl Metadata {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<Metadata> { fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
let additional_timelock = Timelock::read(r)?;
let subaddress = match read_byte(r)? { let subaddress = match read_byte(r)? {
0 => None, 0 => None,
1 => Some( 1 => Some(
@ -200,6 +219,7 @@ impl Metadata {
}; };
Ok(Metadata { Ok(Metadata {
additional_timelock,
subaddress, subaddress,
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None }, payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
arbitrary_data: { arbitrary_data: {
@ -214,7 +234,7 @@ impl Metadata {
} }
} }
/// A received output. /// A scanned output and all associated data.
/// ///
/// This struct contains all data necessary to spend this output, or handle it as a payment. /// This struct contains all data necessary to spend this output, or handle it as a payment.
/// ///
@ -244,11 +264,6 @@ impl WalletOutput {
self.absolute_id.index_in_transaction self.absolute_id.index_in_transaction
} }
/// The block containing the transaction which created this output.
pub fn block(&self) -> [u8; 32] {
self.relative_id.block
}
/// The index of the output on the blockchain. /// The index of the output on the blockchain.
pub fn index_on_blockchain(&self) -> u64 { pub fn index_on_blockchain(&self) -> u64 {
self.relative_id.index_on_blockchain self.relative_id.index_on_blockchain
@ -256,18 +271,18 @@ impl WalletOutput {
/// The key this output may be spent by. /// The key this output may be spent by.
pub fn key(&self) -> EdwardsPoint { pub fn key(&self) -> EdwardsPoint {
self.data.key self.data.key()
} }
/// The scalar to add to the private spend key for it to be the discrete logarithm of this /// The scalar to add to the private spend key for it to be the discrete logarithm of this
/// output's key. /// output's key.
pub fn key_offset(&self) -> Scalar { pub fn key_offset(&self) -> Scalar {
self.data.key_offset self.data.key_offset()
} }
/// The commitment this output created. /// The commitment this output created.
pub fn commitment(&self) -> &Commitment { pub fn commitment(&self) -> &Commitment {
&self.data.commitment self.data.commitment()
} }
/// The additional timelock this output is subject to. /// The additional timelock this output is subject to.
@ -276,7 +291,7 @@ impl WalletOutput {
/// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This /// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This
/// function only returns the additional timelock. /// function only returns the additional timelock.
pub fn additional_timelock(&self) -> Timelock { pub fn additional_timelock(&self) -> Timelock {
self.data.additional_timelock self.metadata.additional_timelock
} }
/// The index of the subaddress this output was identified as sent to. /// The index of the subaddress this output was identified as sent to.

View file

@ -107,7 +107,6 @@ impl InternalScanner {
fn scan_transaction( fn scan_transaction(
&self, &self,
block_hash: [u8; 32],
tx_start_index_on_blockchain: u64, tx_start_index_on_blockchain: u64,
tx: &Transaction, tx: &Transaction,
) -> Result<Timelocked, RpcError> { ) -> Result<Timelocked, RpcError> {
@ -224,16 +223,15 @@ impl InternalScanner {
index_in_transaction: o.try_into().unwrap(), index_in_transaction: o.try_into().unwrap(),
}, },
relative_id: RelativeId { relative_id: RelativeId {
block: block_hash,
index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(), index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(),
}, },
data: OutputData { data: OutputData { key: output_key, key_offset, commitment },
key: output_key, metadata: Metadata {
key_offset,
commitment,
additional_timelock: tx.prefix().additional_timelock, additional_timelock: tx.prefix().additional_timelock,
subaddress,
payment_id,
arbitrary_data: extra.data(),
}, },
metadata: Metadata { subaddress, payment_id, arbitrary_data: extra.data() },
}); });
// Break to prevent public keys from being included multiple times, triggering multiple // Break to prevent public keys from being included multiple times, triggering multiple
@ -253,8 +251,6 @@ impl InternalScanner {
)))?; )))?;
} }
let block_hash = block.hash();
// We obtain all TXs in full // We obtain all TXs in full
let mut txs = vec![block.miner_transaction.clone()]; let mut txs = vec![block.miner_transaction.clone()];
txs.extend(rpc.get_transactions(&block.transactions).await?); txs.extend(rpc.get_transactions(&block.transactions).await?);
@ -327,7 +323,7 @@ impl InternalScanner {
{ {
let mut this_txs_outputs = vec![]; let mut this_txs_outputs = vec![];
core::mem::swap( core::mem::swap(
&mut self.scan_transaction(block_hash, tx_start_index_on_blockchain, &tx)?.0, &mut self.scan_transaction(tx_start_index_on_blockchain, &tx)?.0,
&mut this_txs_outputs, &mut this_txs_outputs,
); );
res.0.extend(this_txs_outputs); res.0.extend(this_txs_outputs);
@ -379,27 +375,6 @@ impl Scanner {
self.0.register_subaddress(subaddress) self.0.register_subaddress(subaddress)
} }
/*
/// Scan a transaction.
///
/// This takes in the block hash the transaction is contained in. This method is NOT recommended
/// and MUST be used carefully. The node will receive a request for the output indexes of the
/// specified transactions, which may de-anonymize which transactions belong to a user.
pub async fn scan_transaction(
&self,
rpc: &impl Rpc,
block_hash: [u8; 32],
tx: &Transaction,
) -> Result<Timelocked, RpcError> {
// This isn't technically illegal due to a lack of minimum output rules for a while
let Some(tx_start_index_on_blockchain) =
rpc.get_o_indexes(tx.hash()).await?.first().copied() else {
return Ok(Timelocked(vec![]))
};
self.0.scan_transaction(block_hash, tx_start_index_on_blockchain, tx)
}
*/
/// Scan a block. /// Scan a block.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> { pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
self.0.scan(rpc, block).await self.0.scan(rpc, block).await
@ -429,27 +404,6 @@ impl GuaranteedScanner {
self.0.register_subaddress(subaddress) self.0.register_subaddress(subaddress)
} }
/*
/// Scan a transaction.
///
/// This takes in the block hash the transaction is contained in. This method is NOT recommended
/// and MUST be used carefully. The node will receive a request for the output indexes of the
/// specified transactions, which may de-anonymize which transactions belong to a user.
pub async fn scan_transaction(
&self,
rpc: &impl Rpc,
block_hash: [u8; 32],
tx: &Transaction,
) -> Result<Timelocked, RpcError> {
// This isn't technically illegal due to a lack of minimum output rules for a while
let Some(tx_start_index_on_blockchain) =
rpc.get_o_indexes(tx.hash()).await?.first().copied() else {
return Ok(Timelocked(vec![]))
};
self.0.scan_transaction(block_hash, tx_start_index_on_blockchain, tx)
}
*/
/// Scan a block. /// Scan a block.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> { pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
self.0.scan(rpc, block).await self.0.scan(rpc, block).await

View file

@ -17,7 +17,6 @@ use frost::FrostError;
use crate::{ use crate::{
io::*, io::*,
generators::{MAX_COMMITMENTS, hash_to_point}, generators::{MAX_COMMITMENTS, hash_to_point},
primitives::Decoys,
ringct::{ ringct::{
clsag::{ClsagError, ClsagContext, Clsag}, clsag::{ClsagError, ClsagContext, Clsag},
RctType, RctPrunable, RctProofs, RctType, RctPrunable, RctProofs,
@ -26,7 +25,7 @@ use crate::{
extra::MAX_ARBITRARY_DATA_SIZE, extra::MAX_ARBITRARY_DATA_SIZE,
address::{Network, MoneroAddress}, address::{Network, MoneroAddress},
rpc::FeeRate, rpc::FeeRate,
ViewPair, GuaranteedViewPair, WalletOutput, ViewPair, GuaranteedViewPair, OutputWithDecoys,
}; };
mod tx_keys; mod tx_keys;
@ -231,7 +230,7 @@ pub enum SendError {
pub struct SignableTransaction { pub struct SignableTransaction {
rct_type: RctType, rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>, outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<(WalletOutput, Decoys)>, inputs: Vec<OutputWithDecoys>,
payments: Vec<InternalPayment>, payments: Vec<InternalPayment>,
data: Vec<Vec<u8>>, data: Vec<Vec<u8>>,
fee_rate: FeeRate, fee_rate: FeeRate,
@ -252,9 +251,9 @@ impl SignableTransaction {
if self.inputs.is_empty() { if self.inputs.is_empty() {
Err(SendError::NoInputs)?; Err(SendError::NoInputs)?;
} }
for (_, decoys) in &self.inputs { for input in &self.inputs {
// TODO: Add a function for the ring length // TODO: Add a function for the ring length
if decoys.len() != if input.decoys().len() !=
match self.rct_type { match self.rct_type {
RctType::ClsagBulletproof => 11, RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16, RctType::ClsagBulletproofPlus => 16,
@ -314,7 +313,7 @@ impl SignableTransaction {
} }
// Make sure we have enough funds // Make sure we have enough funds
let in_amount = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>(); let in_amount = self.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
let payments_amount = self let payments_amount = self
.payments .payments
.iter() .iter()
@ -356,7 +355,7 @@ impl SignableTransaction {
pub fn new( pub fn new(
rct_type: RctType, rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>, outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<(WalletOutput, Decoys)>, inputs: Vec<OutputWithDecoys>,
payments: Vec<(MoneroAddress, u64)>, payments: Vec<(MoneroAddress, u64)>,
change: Change, change: Change,
data: Vec<Vec<u8>>, data: Vec<Vec<u8>>,
@ -406,11 +405,6 @@ impl SignableTransaction {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
fn write_input<W: io::Write>(input: &(WalletOutput, Decoys), w: &mut W) -> io::Result<()> {
input.0.write(w)?;
input.1.write(w)
}
fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> { fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
match payment { match payment {
InternalPayment::Payment(addr, amount) => { InternalPayment::Payment(addr, amount) => {
@ -433,7 +427,7 @@ impl SignableTransaction {
write_byte(&u8::from(self.rct_type), w)?; write_byte(&u8::from(self.rct_type), w)?;
w.write_all(self.outgoing_view_key.as_slice())?; w.write_all(self.outgoing_view_key.as_slice())?;
write_vec(write_input, &self.inputs, w)?; write_vec(OutputWithDecoys::write, &self.inputs, w)?;
write_vec(write_payment, &self.payments, w)?; write_vec(write_payment, &self.payments, w)?;
write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?; write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
self.fee_rate.write(w) self.fee_rate.write(w)
@ -454,10 +448,6 @@ impl SignableTransaction {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> { pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
fn read_input(r: &mut impl io::Read) -> io::Result<(WalletOutput, Decoys)> {
Ok((WalletOutput::read(r)?, Decoys::read(r)?))
}
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> { fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
String::from_utf8(read_vec(read_byte, r)?) String::from_utf8(read_vec(read_byte, r)?)
.ok() .ok()
@ -484,7 +474,7 @@ impl SignableTransaction {
rct_type: RctType::try_from(read_byte(r)?) rct_type: RctType::try_from(read_byte(r)?)
.map_err(|()| io::Error::other("unsupported/invalid RctType"))?, .map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
outgoing_view_key: Zeroizing::new(read_bytes(r)?), outgoing_view_key: Zeroizing::new(read_bytes(r)?),
inputs: read_vec(read_input, r)?, inputs: read_vec(OutputWithDecoys::read, r)?,
payments: read_vec(read_payment, r)?, payments: read_vec(read_payment, r)?,
data: read_vec(|r| read_vec(read_byte, r), r)?, data: read_vec(|r| read_vec(read_byte, r), r)?,
fee_rate: FeeRate::read(r)?, fee_rate: FeeRate::read(r)?,
@ -522,7 +512,7 @@ impl SignableTransaction {
) -> Result<Transaction, SendError> { ) -> Result<Transaction, SendError> {
// Calculate the key images // Calculate the key images
let mut key_images = vec![]; let mut key_images = vec![];
for (input, _) in &self.inputs { for input in &self.inputs {
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset()); let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() { if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() {
Err(SendError::WrongPrivateKey)?; Err(SendError::WrongPrivateKey)?;
@ -536,12 +526,12 @@ impl SignableTransaction {
// Prepare the CLSAG signatures // Prepare the CLSAG signatures
let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len()); let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
for (input, decoys) in &tx.intent.inputs { for input in &tx.intent.inputs {
// Re-derive the input key as this will be in a different order // Re-derive the input key as this will be in a different order
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset()); let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
clsag_signs.push(( clsag_signs.push((
input_key, input_key,
ClsagContext::new(decoys.clone(), input.commitment().clone()) ClsagContext::new(input.decoys().clone(), input.commitment().clone())
.map_err(SendError::ClsagError)?, .map_err(SendError::ClsagError)?,
)); ));
} }

View file

@ -65,14 +65,14 @@ impl SignableTransaction {
let mut clsags = vec![]; let mut clsags = vec![];
let mut key_image_generators_and_offsets = vec![]; let mut key_image_generators_and_offsets = vec![];
for (i, (input, decoys)) in self.inputs.iter().enumerate() { for input in &self.inputs {
// Check this is the right set of keys // Check this is the right set of keys
let offset = keys.offset(dfg::Scalar(input.key_offset())); let offset = keys.offset(dfg::Scalar(input.key_offset()));
if offset.group_key().0 != input.key() { if offset.group_key().0 != input.key() {
Err(SendError::WrongPrivateKey)?; Err(SendError::WrongPrivateKey)?;
} }
let context = ClsagContext::new(decoys.clone(), input.commitment().clone()) let context = ClsagContext::new(input.decoys().clone(), input.commitment().clone())
.map_err(SendError::ClsagError)?; .map_err(SendError::ClsagError)?;
let (clsag, clsag_mask_send) = ClsagMultisig::new( let (clsag, clsag_mask_send) = ClsagMultisig::new(
RecommendedTranscript::new(b"Monero Multisignature Transaction"), RecommendedTranscript::new(b"Monero Multisignature Transaction"),
@ -80,7 +80,7 @@ impl SignableTransaction {
); );
key_image_generators_and_offsets.push(( key_image_generators_and_offsets.push((
clsag.key_image_generator(), clsag.key_image_generator(),
keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + self.inputs[i].0.key_offset(), keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + input.key_offset(),
)); ));
clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset))); clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset)));
} }

View file

@ -23,10 +23,10 @@ impl SignableTransaction {
debug_assert_eq!(self.inputs.len(), key_images.len()); debug_assert_eq!(self.inputs.len(), key_images.len());
let mut res = Vec::with_capacity(self.inputs.len()); let mut res = Vec::with_capacity(self.inputs.len());
for ((_, decoys), key_image) in self.inputs.iter().zip(key_images) { for (input, key_image) in self.inputs.iter().zip(key_images) {
res.push(Input::ToKey { res.push(Input::ToKey {
amount: None, amount: None,
key_offsets: decoys.offsets().to_vec(), key_offsets: input.decoys().offsets().to_vec(),
key_image: *key_image, key_image: *key_image,
}); });
} }
@ -299,7 +299,7 @@ impl SignableTransactionWithKeyImages {
} else { } else {
// If we don't have a change output, the difference is the fee // If we don't have a change output, the difference is the fee
let inputs = let inputs =
self.intent.inputs.iter().map(|input| input.0.commitment().amount).sum::<u64>(); self.intent.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
let payments = self let payments = self
.intent .intent
.payments .payments

View file

@ -11,7 +11,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}
use crate::{ use crate::{
primitives::{keccak256, Commitment}, primitives::{keccak256, Commitment},
ringct::EncryptedAmount, ringct::EncryptedAmount,
SharedKeyDerivations, SharedKeyDerivations, OutputWithDecoys,
send::{InternalPayment, SignableTransaction, key_image_sort}, send::{InternalPayment, SignableTransaction, key_image_sort},
}; };
@ -26,7 +26,7 @@ impl SignableTransaction {
// Ensure uniqueness across transactions by binding to a use-once object // Ensure uniqueness across transactions by binding to a use-once object
// The keys for the inputs is binding to their key images, making them use-once // The keys for the inputs is binding to their key images, making them use-once
let mut input_keys = self.inputs.iter().map(|(input, _)| input.key()).collect::<Vec<_>>(); let mut input_keys = self.inputs.iter().map(OutputWithDecoys::key).collect::<Vec<_>>();
// We sort the inputs mid-way through TX construction, so apply our own sort to ensure a // We sort the inputs mid-way through TX construction, so apply our own sort to ensure a
// consistent order // consistent order
// We use the key image sort as it's applicable and well-defined, not because these are key // We use the key image sort as it's applicable and well-defined, not because these are key
@ -208,7 +208,7 @@ impl SignableTransaction {
let amount = match payment { let amount = match payment {
InternalPayment::Payment(_, amount) => *amount, InternalPayment::Payment(_, amount) => *amount,
InternalPayment::Change(_, _) => { InternalPayment::Change(_, _) => {
let inputs = self.inputs.iter().map(|input| input.0.commitment().amount).sum::<u64>(); let inputs = self.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
let payments = self let payments = self
.payments .payments
.iter() .iter()

View file

@ -28,18 +28,17 @@ test!(
// Then make a second tx1 // Then make a second tx1
|rct_type: RctType, rpc: SimpleRequestRpc, mut builder: Builder, addr, state: _| async move { |rct_type: RctType, rpc: SimpleRequestRpc, mut builder: Builder, addr, state: _| async move {
let output_tx0: WalletOutput = state; let output_tx0: WalletOutput = state;
let decoys = Decoys::fingerprintable_canonical_select(
let input = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
ring_len(rct_type), ring_len(rct_type),
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&[output_tx0.clone()], output_tx0.clone(),
) )
.await .await
.unwrap(); .unwrap();
builder.add_input(input);
let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), (rct_type, output_tx0)) (builder.build().unwrap(), (rct_type, output_tx0))
@ -66,17 +65,19 @@ test!(
let mut selected_fresh_decoy = false; let mut selected_fresh_decoy = false;
let mut attempts = 1000; let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 { while !selected_fresh_decoy && attempts > 0 {
let decoys = Decoys::fingerprintable_canonical_select( let decoys = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, // TODO: use a seeded RNG to consistently select the latest output &mut OsRng, // TODO: use a seeded RNG to consistently select the latest output
&rpc, &rpc,
ring_len(rct_type), ring_len(rct_type),
height, height,
&[output_tx0.clone()], output_tx0.clone(),
) )
.await .await
.unwrap(); .unwrap()
.decoys()
.clone();
selected_fresh_decoy = decoys[0].positions().contains(&most_recent_o_index); selected_fresh_decoy = decoys.positions().contains(&most_recent_o_index);
attempts -= 1; attempts -= 1;
} }
@ -107,18 +108,16 @@ test!(
|rct_type: RctType, rpc, mut builder: Builder, addr, output_tx0: WalletOutput| async move { |rct_type: RctType, rpc, mut builder: Builder, addr, output_tx0: WalletOutput| async move {
let rpc: SimpleRequestRpc = rpc; let rpc: SimpleRequestRpc = rpc;
let decoys = Decoys::select( let input = OutputWithDecoys::new(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
ring_len(rct_type), ring_len(rct_type),
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&[output_tx0.clone()], output_tx0.clone(),
) )
.await .await
.unwrap(); .unwrap();
builder.add_input(input);
let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), (rct_type, output_tx0)) (builder.build().unwrap(), (rct_type, output_tx0))
@ -145,17 +144,19 @@ test!(
let mut selected_fresh_decoy = false; let mut selected_fresh_decoy = false;
let mut attempts = 1000; let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 { while !selected_fresh_decoy && attempts > 0 {
let decoys = Decoys::select( let decoys = OutputWithDecoys::new(
&mut OsRng, // TODO: use a seeded RNG to consistently select the latest output &mut OsRng, // TODO: use a seeded RNG to consistently select the latest output
&rpc, &rpc,
ring_len(rct_type), ring_len(rct_type),
height, height,
&[output_tx0.clone()], output_tx0.clone(),
) )
.await .await
.unwrap(); .unwrap()
.decoys()
.clone();
selected_fresh_decoy = decoys[0].positions().contains(&most_recent_o_index); selected_fresh_decoy = decoys.positions().contains(&most_recent_o_index);
attempts -= 1; attempts -= 1;
} }

View file

@ -1,11 +1,10 @@
use zeroize::{Zeroize, Zeroizing}; use zeroize::{Zeroize, Zeroizing};
use monero_wallet::{ use monero_wallet::{
primitives::Decoys,
ringct::RctType, ringct::RctType,
rpc::FeeRate, rpc::FeeRate,
address::MoneroAddress, address::MoneroAddress,
WalletOutput, OutputWithDecoys,
send::{Change, SendError, SignableTransaction}, send::{Change, SendError, SignableTransaction},
extra::MAX_ARBITRARY_DATA_SIZE, extra::MAX_ARBITRARY_DATA_SIZE,
}; };
@ -15,7 +14,7 @@ use monero_wallet::{
pub struct SignableTransactionBuilder { pub struct SignableTransactionBuilder {
rct_type: RctType, rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>, outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<(WalletOutput, Decoys)>, inputs: Vec<OutputWithDecoys>,
payments: Vec<(MoneroAddress, u64)>, payments: Vec<(MoneroAddress, u64)>,
change: Change, change: Change,
data: Vec<Vec<u8>>, data: Vec<Vec<u8>>,
@ -40,12 +39,12 @@ impl SignableTransactionBuilder {
} }
} }
pub fn add_input(&mut self, input: (WalletOutput, Decoys)) -> &mut Self { pub fn add_input(&mut self, input: OutputWithDecoys) -> &mut Self {
self.inputs.push(input); self.inputs.push(input);
self self
} }
#[allow(unused)] #[allow(unused)]
pub fn add_inputs(&mut self, inputs: &[(WalletOutput, Decoys)]) -> &mut Self { pub fn add_inputs(&mut self, inputs: &[OutputWithDecoys]) -> &mut Self {
self.inputs.extend(inputs.iter().cloned()); self.inputs.extend(inputs.iter().cloned());
self self
} }

View file

@ -198,13 +198,10 @@ macro_rules! test {
}; };
use monero_wallet::{ use monero_wallet::{
primitives::Decoys,
ringct::RctType, ringct::RctType,
rpc::FeePriority, rpc::FeePriority,
address::Network, address::Network,
ViewPair, ViewPair, Scanner, OutputWithDecoys,
DecoySelection,
Scanner,
send::{Change, SignableTransaction, Eventuality}, send::{Change, SignableTransaction, Eventuality},
}; };
@ -300,16 +297,14 @@ macro_rules! test {
let temp = Box::new({ let temp = Box::new({
let mut builder = builder.clone(); let mut builder = builder.clone();
let decoys = Decoys::fingerprintable_canonical_select( let input = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
ring_len(rct_type), ring_len(rct_type),
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&[miner_tx.clone()], miner_tx,
) ).await.unwrap();
.await builder.add_input(input);
.unwrap();
builder.add_input((miner_tx, decoys.first().unwrap().clone()));
let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await; let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await;
let fee_rate = tx.fee_rate().clone(); let fee_rate = tx.fee_rate().clone();

View file

@ -4,8 +4,8 @@ use rand_core::OsRng;
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
primitives::Decoys, ringct::RctType, transaction::Transaction, rpc::Rpc, ringct::RctType, transaction::Transaction, rpc::Rpc, address::SubaddressIndex, extra::Extra,
address::SubaddressIndex, extra::Extra, WalletOutput, DecoySelection, WalletOutput, OutputWithDecoys,
}; };
mod runner; mod runner;
@ -18,19 +18,19 @@ async fn add_inputs(
outputs: Vec<WalletOutput>, outputs: Vec<WalletOutput>,
builder: &mut SignableTransactionBuilder, builder: &mut SignableTransactionBuilder,
) { ) {
let decoys = Decoys::fingerprintable_canonical_select( for output in outputs {
builder.add_input(
OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
rpc, rpc,
ring_len(rct_type), ring_len(rct_type),
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&outputs, output,
) )
.await .await
.unwrap(); .unwrap(),
);
let inputs = outputs.into_iter().zip(decoys).collect::<Vec<_>>(); }
builder.add_inputs(&inputs);
} }
test!( test!(

View file

@ -20,7 +20,7 @@ use monero_wallet::{
block::Block, block::Block,
rpc::{FeeRate, RpcError, Rpc}, rpc::{FeeRate, RpcError, Rpc},
address::{Network as MoneroNetwork, SubaddressIndex}, address::{Network as MoneroNetwork, SubaddressIndex},
ViewPair, GuaranteedViewPair, WalletOutput, GuaranteedScanner, DecoySelection, Decoys, ViewPair, GuaranteedViewPair, WalletOutput, OutputWithDecoys, GuaranteedScanner,
send::{ send::{
SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine, SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine,
}, },
@ -322,15 +322,16 @@ impl Monero {
_ => panic!("Monero hard forked and the processor wasn't updated for it"), _ => panic!("Monero hard forked and the processor wasn't updated for it"),
}; };
let spendable_outputs = inputs.iter().map(|input| input.0.clone()).collect::<Vec<_>>();
let mut transcript = let mut transcript =
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript"); RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
transcript.append_message(b"plan", plan_id); transcript.append_message(b"plan", plan_id);
// All signers need to select the same decoys // All signers need to select the same decoys
// All signers use the same height and a seeded RNG to make sure they do so. // All signers use the same height and a seeded RNG to make sure they do so.
let decoys = Decoys::fingerprintable_canonical_select( let mut inputs_actual = Vec::with_capacity(inputs.len());
for input in inputs {
inputs_actual.push(
OutputWithDecoys::fingerprintable_deterministic_new(
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
&self.rpc, &self.rpc,
// TODO: Have Decoys take RctType // TODO: Have Decoys take RctType
@ -340,12 +341,12 @@ impl Monero {
_ => panic!("selecting decoys for an unsupported RctType"), _ => panic!("selecting decoys for an unsupported RctType"),
}, },
block_number + 1, block_number + 1,
&spendable_outputs, input.0.clone(),
) )
.await .await
.map_err(map_rpc_err)?; .map_err(map_rpc_err)?,
);
let inputs = spendable_outputs.into_iter().zip(decoys).collect::<Vec<_>>(); }
// Monero requires at least two outputs // Monero requires at least two outputs
// If we only have one output planned, add a dummy payment // If we only have one output planned, add a dummy payment
@ -375,7 +376,7 @@ impl Monero {
rct_type, rct_type,
// Use the plan ID as the outgoing view key // Use the plan ID as the outgoing view key
Zeroizing::new(*plan_id), Zeroizing::new(*plan_id),
inputs.clone(), inputs_actual,
payments, payments,
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())), Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
vec![], vec![],
@ -400,7 +401,7 @@ impl Monero {
SendError::TooMuchArbitraryData | SendError::TooMuchArbitraryData |
SendError::TooLargeTransaction | SendError::TooLargeTransaction |
SendError::WrongPrivateKey => { SendError::WrongPrivateKey => {
panic!("created an Monero invalid transaction: {e}"); panic!("created an invalid Monero transaction: {e}");
} }
SendError::MultiplePaymentIds => { SendError::MultiplePaymentIds => {
panic!("multiple payment IDs despite not supporting integrated addresses"); panic!("multiple payment IDs despite not supporting integrated addresses");
@ -736,10 +737,11 @@ impl Network for Monero {
} }
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap(); let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
let outputs = let mut outputs =
Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock(); Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock();
let output = outputs.swap_remove(0);
let amount = outputs[0].commitment().amount; let amount = output.commitment().amount;
// The dust should always be sufficient for the fee // The dust should always be sufficient for the fee
let fee = Monero::DUST; let fee = Monero::DUST;
@ -749,7 +751,7 @@ impl Network for Monero {
_ => panic!("Monero hard forked and the processor wasn't updated for it"), _ => panic!("Monero hard forked and the processor wasn't updated for it"),
}; };
let decoys = Decoys::fingerprintable_canonical_select( let output = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
&self.rpc, &self.rpc,
match rct_type { match rct_type {
@ -758,19 +760,17 @@ impl Network for Monero {
_ => panic!("selecting decoys for an unsupported RctType"), _ => panic!("selecting decoys for an unsupported RctType"),
}, },
self.rpc.get_height().await.unwrap(), self.rpc.get_height().await.unwrap(),
&outputs, output,
) )
.await .await
.unwrap(); .unwrap();
let inputs = outputs.into_iter().zip(decoys).collect::<Vec<_>>();
let mut outgoing_view_key = Zeroizing::new([0; 32]); let mut outgoing_view_key = Zeroizing::new([0; 32]);
OsRng.fill_bytes(outgoing_view_key.as_mut()); OsRng.fill_bytes(outgoing_view_key.as_mut());
let tx = MSignableTransaction::new( let tx = MSignableTransaction::new(
rct_type, rct_type,
outgoing_view_key, outgoing_view_key,
inputs, vec![output],
vec![(address.into(), amount - fee)], vec![(address.into(), amount - fee)],
Change::fingerprintable(Some(Self::test_address().into())), Change::fingerprintable(Some(Self::test_address().into())),
vec![], vec![],

View file

@ -348,7 +348,7 @@ async fn mint_and_burn_test() {
ringct::RctType, ringct::RctType,
rpc::{FeePriority, Rpc}, rpc::{FeePriority, Rpc},
address::{Network, AddressType, MoneroAddress}, address::{Network, AddressType, MoneroAddress},
ViewPair, Scanner, DecoySelection, Decoys, ViewPair, Scanner, OutputWithDecoys,
send::{Change, SignableTransaction}, send::{Change, SignableTransaction},
}; };
@ -363,23 +363,22 @@ async fn mint_and_burn_test() {
.additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0) .additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0)
.swap_remove(0); .swap_remove(0);
let decoys = Decoys::fingerprintable_canonical_select( let input = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
16, 16,
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&[output.clone()], output.clone(),
) )
.await .await
.unwrap() .unwrap();
.swap_remove(0);
let mut outgoing_view_key = Zeroizing::new([0; 32]); let mut outgoing_view_key = Zeroizing::new([0; 32]);
OsRng.fill_bytes(outgoing_view_key.as_mut()); OsRng.fill_bytes(outgoing_view_key.as_mut());
let tx = SignableTransaction::new( let tx = SignableTransaction::new(
RctType::ClsagBulletproofPlus, RctType::ClsagBulletproofPlus,
outgoing_view_key, outgoing_view_key,
vec![(output, decoys)], vec![input],
vec![( vec![(
MoneroAddress::new( MoneroAddress::new(
Network::Mainnet, Network::Mainnet,

View file

@ -412,7 +412,7 @@ impl Wallet {
ringct::RctType, ringct::RctType,
rpc::{FeePriority, Rpc}, rpc::{FeePriority, Rpc},
address::{Network, AddressType, Address}, address::{Network, AddressType, Address},
Scanner, DecoySelection, Decoys, Scanner, OutputWithDecoys,
send::{Change, SignableTransaction}, send::{Change, SignableTransaction},
}; };
use processor::{additional_key, networks::Monero}; use processor::{additional_key, networks::Monero};
@ -422,30 +422,35 @@ impl Wallet {
// Prepare inputs // Prepare inputs
let current_height = rpc.get_height().await.unwrap(); let current_height = rpc.get_height().await.unwrap();
let mut inputs = vec![]; let mut outputs = vec![];
for block in last_tx.0 .. current_height { for block in last_tx.0 .. current_height {
let block = rpc.get_block_by_number(block).await.unwrap(); let block = rpc.get_block_by_number(block).await.unwrap();
if (block.miner_transaction.hash() == last_tx.1) || if (block.miner_transaction.hash() == last_tx.1) ||
block.transactions.contains(&last_tx.1) block.transactions.contains(&last_tx.1)
{ {
inputs = Scanner::new(view_pair.clone()) outputs = Scanner::new(view_pair.clone())
.scan(&rpc, &block) .scan(&rpc, &block)
.await .await
.unwrap() .unwrap()
.ignore_additional_timelock(); .ignore_additional_timelock();
} }
} }
assert!(!inputs.is_empty()); assert!(!outputs.is_empty());
let mut decoys = Decoys::fingerprintable_canonical_select( let mut inputs = Vec::with_capacity(outputs.len());
for output in outputs {
inputs.push(
OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
16, 16,
rpc.get_height().await.unwrap(), rpc.get_height().await.unwrap(),
&inputs, output,
) )
.await .await
.unwrap(); .unwrap(),
);
}
let to_spend_key = decompress_point(<[u8; 32]>::try_from(to.as_ref()).unwrap()).unwrap(); let to_spend_key = decompress_point(<[u8; 32]>::try_from(to.as_ref()).unwrap()).unwrap();
let to_view_key = additional_key::<Monero>(0); let to_view_key = additional_key::<Monero>(0);
@ -467,7 +472,7 @@ impl Wallet {
let tx = SignableTransaction::new( let tx = SignableTransaction::new(
RctType::ClsagBulletproofPlus, RctType::ClsagBulletproofPlus,
outgoing_view_key, outgoing_view_key,
inputs.drain(..).zip(decoys.drain(..)).collect(), inputs,
vec![(to_addr, AMOUNT)], vec![(to_addr, AMOUNT)],
Change::new(view_pair), Change::new(view_pair),
data, data,