Diversify ViewPair/Scanner into ViewPair/GuaranteedViewPair and Scanner/GuaranteedScanner

Also cleans the Scanner impl.
This commit is contained in:
Luke Parker
2024-07-03 13:35:19 -04:00
parent 64e74c52ec
commit daa0f8f7d5
32 changed files with 1458 additions and 1233 deletions

View File

@@ -169,6 +169,9 @@ impl Decoys {
}
/// Write the Decoys.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
write_vec(write_varint, &self.offsets, w)?;
w.write_all(&[self.signer_index])?;
@@ -181,14 +184,22 @@ impl Decoys {
w,
)
}
/// Serialize the Decoys 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 res =
Vec::with_capacity((1 + (2 * self.offsets.len())) + 1 + 1 + (self.ring.len() * 64));
self.write(&mut res).unwrap();
res
}
/// Read a set of Decoys.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn read(r: &mut impl io::Read) -> io::Result<Decoys> {
Decoys::new(
read_vec(read_varint, r)?,

View File

@@ -32,6 +32,38 @@ use monero_serai::{
// src/wallet/wallet2.cpp#L121
const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
/// An error from the RPC.
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum RpcError {
/// An internal error.
#[cfg_attr(feature = "std", error("internal error ({0})"))]
InternalError(String),
/// A connection error with the node.
#[cfg_attr(feature = "std", error("connection error ({0})"))]
ConnectionError(String),
/// The node is invalid per the expected protocol.
#[cfg_attr(feature = "std", error("invalid node ({0})"))]
InvalidNode(String),
/// Requested transactions weren't found.
#[cfg_attr(feature = "std", error("transactions not found"))]
TransactionsNotFound(Vec<[u8; 32]>),
/// The transaction was pruned.
///
/// Pruned transactions are not supported at this time.
#[cfg_attr(feature = "std", error("pruned transaction"))]
PrunedTransaction,
/// A transaction (sent or received) was invalid.
#[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))]
InvalidTransaction([u8; 32]),
/// The returned fee was unusable.
#[cfg_attr(feature = "std", error("unexpected fee response"))]
InvalidFee,
/// The priority intended for use wasn't usable.
#[cfg_attr(feature = "std", error("invalid priority"))]
InvalidPriority,
}
/// A struct containing a fee rate.
///
/// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes.
@@ -147,7 +179,7 @@ struct TransactionsResponse {
#[derive(Deserialize, Debug)]
pub struct OutputResponse {
/// The height of the block this output was added to the chain in.
pub height: u32,
pub height: usize,
/// If the output is unlocked, per the node's local view.
pub unlocked: bool,
/// The output's key.
@@ -158,38 +190,6 @@ pub struct OutputResponse {
pub txid: String,
}
/// An error from the RPC.
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum RpcError {
/// An internal error.
#[cfg_attr(feature = "std", error("internal error ({0})"))]
InternalError(&'static str),
/// A connection error with the node.
#[cfg_attr(feature = "std", error("connection error ({0})"))]
ConnectionError(String),
/// The node is invalid per the expected protocol.
#[cfg_attr(feature = "std", error("invalid node ({0})"))]
InvalidNode(String),
/// Requested transactions weren't found.
#[cfg_attr(feature = "std", error("transactions not found"))]
TransactionsNotFound(Vec<[u8; 32]>),
/// The transaction was pruned.
///
/// Pruned transactions are not supported at this time.
#[cfg_attr(feature = "std", error("pruned transaction"))]
PrunedTransaction,
/// A transaction (sent or received) was invalid.
#[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))]
InvalidTransaction([u8; 32]),
/// The returned fee was unusable.
#[cfg_attr(feature = "std", error("unexpected fee response"))]
InvalidFee,
/// The priority intended for use wasn't usable.
#[cfg_attr(feature = "std", error("invalid priority"))]
InvalidPriority,
}
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string()))
}
@@ -309,10 +309,10 @@ pub trait Rpc: Sync + Clone + Debug {
///
/// The height is defined as the amount of blocks on the blockchain. For a blockchain with only
/// its genesis block, the height will be 1.
async fn get_height(&self) -> Result<u32, RpcError> {
async fn get_height(&self) -> Result<usize, RpcError> {
#[derive(Deserialize, Debug)]
struct HeightResponse {
height: u32,
height: usize,
}
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
}
@@ -397,7 +397,7 @@ pub trait Rpc: Sync + Clone + Debug {
///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block).
async fn get_block_hash(&self, number: u32) -> Result<[u8; 32], RpcError> {
async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
#[derive(Deserialize, Debug)]
struct BlockHeaderResponse {
hash: String,
@@ -436,7 +436,7 @@ pub trait Rpc: Sync + Clone + Debug {
///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block).
async fn get_block_by_number(&self, number: u32) -> Result<Block, RpcError> {
async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> {
#[derive(Deserialize, Debug)]
struct BlockResponse {
blob: String,
@@ -449,16 +449,16 @@ pub trait Rpc: Sync + Clone + Debug {
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
// Make sure this is actually the block for this number
match block.miner_tx.prefix().inputs.first() {
match block.miner_transaction.prefix().inputs.first() {
Some(Input::Gen(actual)) => {
if u32::try_from(*actual) == Ok(number) {
if usize::try_from(*actual) == Ok(number) {
Ok(block)
} else {
Err(RpcError::InvalidNode("different block than requested (number)".to_string()))
}
}
_ => Err(RpcError::InvalidNode(
"block's miner_tx didn't have an input of kind Input::Gen".to_string(),
"block's miner_transaction didn't have an input of kind Input::Gen".to_string(),
)),
}
}
@@ -471,8 +471,8 @@ pub trait Rpc: Sync + Clone + Debug {
/// block's header.
async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
let block = self.get_block(hash).await?;
let mut res = vec![block.miner_tx];
res.extend(self.get_transactions(&block.txs).await?);
let mut res = vec![block.miner_transaction];
res.extend(self.get_transactions(&block.transactions).await?);
Ok(res)
}
@@ -484,7 +484,7 @@ pub trait Rpc: Sync + Clone + Debug {
/// block's header.
async fn get_block_transactions_by_number(
&self,
number: u32,
number: usize,
) -> Result<Vec<Transaction>, RpcError> {
self.get_block_transactions(self.get_block_hash(number).await?).await
}
@@ -647,7 +647,7 @@ pub trait Rpc: Sync + Clone + Debug {
/// Get the output distribution.
///
/// `from` and `to` are heights, not block numbers, and inclusive.
async fn get_output_distribution(&self, from: u32, to: u32) -> Result<Vec<u64>, RpcError> {
async fn get_output_distribution(&self, from: usize, to: usize) -> Result<Vec<u64>, RpcError> {
#[derive(Deserialize, Debug)]
struct Distribution {
distribution: Vec<u64>,
@@ -716,7 +716,7 @@ pub trait Rpc: Sync + Clone + Debug {
async fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: u32,
height: usize,
fingerprintable_canonical: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
let outs: Vec<OutputResponse> = self.get_outs(indexes).await?;
@@ -857,11 +857,11 @@ pub trait Rpc: Sync + Clone + Debug {
&self,
address: &str,
block_count: usize,
) -> Result<(Vec<[u8; 32]>, u32), RpcError> {
) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
#[derive(Debug, Deserialize)]
struct BlocksResponse {
blocks: Vec<String>,
height: u32,
height: usize,
}
let res = self

View File

@@ -47,7 +47,7 @@ impl BlockHeader {
w.write_all(&self.nonce.to_le_bytes())
}
/// Serialize the BlockHeader to a Vec<u8>.
/// Serialize the BlockHeader to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
@@ -72,9 +72,9 @@ pub struct Block {
/// The block's header.
pub header: BlockHeader,
/// The miner's transaction.
pub miner_tx: Transaction,
pub miner_transaction: Transaction,
/// The transactions within this block.
pub txs: Vec<[u8; 32]>,
pub transactions: Vec<[u8; 32]>,
}
impl Block {
@@ -83,7 +83,7 @@ impl Block {
/// This information comes from the Block's miner transaction. If the miner transaction isn't
/// structed as expected, this will return None.
pub fn number(&self) -> Option<u64> {
match &self.miner_tx {
match &self.miner_transaction {
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => {
match prefix.inputs.first() {
Some(Input::Gen(number)) => Some(*number),
@@ -96,15 +96,15 @@ impl Block {
/// Write the Block.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.header.write(w)?;
self.miner_tx.write(w)?;
write_varint(&self.txs.len(), w)?;
for tx in &self.txs {
self.miner_transaction.write(w)?;
write_varint(&self.transactions.len(), w)?;
for tx in &self.transactions {
w.write_all(tx)?;
}
Ok(())
}
/// Serialize the Block to a Vec<u8>.
/// Serialize the Block to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
@@ -117,8 +117,8 @@ impl Block {
/// use the [`Block::hash`] function.
pub fn serialize_pow_hash(&self) -> Vec<u8> {
let mut blob = self.header.serialize();
blob.extend_from_slice(&merkle_root(self.miner_tx.hash(), &self.txs));
write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap();
blob.extend_from_slice(&merkle_root(self.miner_transaction.hash(), &self.transactions));
write_varint(&(1 + u64::try_from(self.transactions.len()).unwrap()), &mut blob).unwrap();
blob
}
@@ -142,8 +142,10 @@ impl Block {
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
Ok(Block {
header: BlockHeader::read(r)?,
miner_tx: Transaction::read(r)?,
txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
miner_transaction: Transaction::read(r)?,
transactions: (0_usize .. read_varint(r)?)
.map(|_| read_bytes(r))
.collect::<Result<_, _>>()?,
})
}
}

View File

@@ -322,7 +322,7 @@ impl RctPrunable {
}
}
/// Serialize the RctPrunable to a Vec<u8>.
/// Serialize the RctPrunable to a `Vec<u8>`.
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized, rct_type).unwrap();
@@ -437,7 +437,7 @@ impl RctProofs {
self.prunable.write(w, rct_type)
}
/// Serialize the RctProofs to a Vec<u8>.
/// Serialize the RctProofs to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();

View File

@@ -49,7 +49,7 @@ impl Input {
}
}
/// Serialize the Input to a Vec<u8>.
/// Serialize the Input to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut res = vec![];
self.write(&mut res).unwrap();
@@ -102,7 +102,7 @@ impl Output {
Ok(())
}
/// Write the Output to a Vec<u8>.
/// Write the Output to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(8 + 1 + 32);
self.write(&mut res).unwrap();
@@ -150,27 +150,33 @@ pub enum Timelock {
}
impl Timelock {
fn from_raw(raw: u64) -> Timelock {
if raw == 0 {
Timelock::None
} else if raw < 500_000_000 {
// TODO: This is trivial to have panic
Timelock::Block(usize::try_from(raw).unwrap())
} else {
Timelock::Time(raw)
/// Write the Timelock.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
Timelock::None => write_varint(&0u8, w),
Timelock::Block(block) => write_varint(block, w),
Timelock::Time(time) => write_varint(time, w),
}
}
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_varint(
&match self {
Timelock::None => 0,
// TODO: Check this unwrap
Timelock::Block(block) => (*block).try_into().unwrap(),
Timelock::Time(time) => *time,
},
w,
)
/// Serialize the Timelock to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(1);
self.write(&mut res).unwrap();
res
}
/// Read a Timelock.
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
let raw = read_varint::<_, u64>(r)?;
Ok(if raw == 0 {
Timelock::None
} else if raw < u64::from(500_000_000u32) {
// TODO: const-assert 32 or 64 bits
Timelock::Block(usize::try_from(raw).expect("timelock (<32 bits) overflowed usize"))
} else {
Timelock::Time(raw)
})
}
}
@@ -194,6 +200,7 @@ impl PartialOrd for Timelock {
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TransactionPrefix {
/// The timelock this transaction uses.
// TODO: Rename to additional timelock?
pub timelock: Timelock,
/// The inputs for this transaction.
pub inputs: Vec<Input>,
@@ -223,7 +230,7 @@ impl TransactionPrefix {
/// This is distinct from Monero in that it won't read the version. The version must be passed
/// in.
pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
let timelock = Timelock::from_raw(read_varint(r)?);
let timelock = Timelock::read(r)?;
let inputs = read_vec(|r| Input::read(r), r)?;
if inputs.is_empty() {
@@ -316,7 +323,7 @@ impl Transaction {
Ok(())
}
/// Write the Transaction to a Vec<u8>.
/// Write the Transaction to a `Vec<u8>`.
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(2048);
self.write(&mut res).unwrap();

View File

@@ -14,7 +14,7 @@ disabled.
- Scanning Monero transactions
- Sending Monero transactions
- Sending Monero transactions with a FROST-inspired threshold multisignature
protocol, orders of magnitude more performant than Monero's own.
protocol, orders of magnitude more performant than Monero's own
### Caveats

View File

@@ -8,7 +8,7 @@ use std_shims::string::ToString;
use zeroize::Zeroize;
use curve25519_dalek::edwards::EdwardsPoint;
use curve25519_dalek::{traits::IsIdentity, EdwardsPoint};
use monero_io::*;
@@ -112,43 +112,6 @@ impl SubaddressIndex {
}
}
/// A specification for an address to be derived.
///
/// This contains all the information an address will embed once derived.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum AddressSpec {
/// A legacy address type.
Legacy,
/// A legacy address with a payment ID embedded.
LegacyIntegrated([u8; 8]),
/// A subaddress.
///
/// This is what SHOULD be used if specific functionality isn't needed.
Subaddress(SubaddressIndex),
/// A featured address.
///
/// Featured Addresses are an unofficial address specification which is meant to be extensible
/// and support a variety of functionality. This functionality includes being a subaddresses AND
/// having a payment ID, along with being immune to the burning bug.
///
/// At this time, support for featured addresses is limited to this crate. There should be no
/// expectation of interoperability.
Featured {
/// The subaddress index to derive this address with.
///
/// If None, no subaddress derivation occurs.
subaddress: Option<SubaddressIndex>,
/// The payment ID to embed in this address.
payment_id: Option<[u8; 8]>,
/// If this address should be guaranteed.
///
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
/// a modified shared-key derivation which eliminates the burning bug.
guaranteed: bool,
},
}
/// Bytes used as prefixes when encoding addresses.
///
/// These distinguish the address's type.
@@ -227,7 +190,7 @@ pub enum Network {
Testnet,
}
/// Error when decoding an address.
/// Errors when decoding an address.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum AddressError {
@@ -257,6 +220,9 @@ pub enum AddressError {
/// The Network embedded within the Address.
actual: Network,
},
/// The view key was of small order despite being in a guaranteed address.
#[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))]
SmallOrderView,
}
/// Bytes used as prefixes when encoding addresses, variable to the network instance.
@@ -375,6 +341,15 @@ pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new
None => panic!("Monero network byte constants conflicted"),
};
/// Errors when creating an address.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum AddressCreationError {
/// The view key was of small order despite being in a guaranteed address.
#[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))]
SmallOrderView,
}
/// A Monero address.
#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
pub struct Address<const ADDRESS_BYTES: u128> {
@@ -429,8 +404,16 @@ impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
/// Create a new address.
pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
Address { network, kind, spend, view }
pub fn new(
network: Network,
kind: AddressType,
spend: EdwardsPoint,
view: EdwardsPoint,
) -> Result<Self, AddressCreationError> {
if kind.is_guaranteed() && view.mul_by_cofactor().is_identity() {
Err(AddressCreationError::SmallOrderView)?;
}
Ok(Address { network, kind, spend, view })
}
/// Parse an address from a String, accepting any network it is.
@@ -472,6 +455,11 @@ impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
Err(AddressError::InvalidLength)?;
}
// If this is a guaranteed address, reject small-order view keys
if kind.is_guaranteed() && view.mul_by_cofactor().is_identity() {
Err(AddressError::SmallOrderView)?;
}
Ok(Address { network, kind, spend, view })
}

View File

@@ -125,7 +125,7 @@ fn featured() {
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
let kind = AddressType::Featured { subaddress, payment_id, guaranteed };
let addr = MoneroAddress::new(network, kind, spend, view);
let addr = MoneroAddress::new(network, kind, spend, view).unwrap();
assert_eq!(addr.to_string().chars().next().unwrap(), first);
assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
@@ -198,6 +198,7 @@ fn featured_vectors() {
spend,
view
)
.unwrap()
.to_string(),
vector.address
);

View File

@@ -9,9 +9,11 @@ use rand_distr::num_traits::Float;
use curve25519_dalek::edwards::EdwardsPoint;
use monero_serai::{DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME};
use monero_rpc::{RpcError, Rpc};
use crate::scan::SpendableOutput;
use crate::{
DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
rpc::{RpcError, Rpc},
WalletOutput,
};
const RECENT_WINDOW: usize = 15;
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
@@ -34,7 +36,7 @@ async fn select_n<'a, R: RngCore + CryptoRng>(
// TODO: consider removing this extra RPC and expect the caller to handle it
if fingerprintable_canonical && (height > rpc.get_height().await?) {
// TODO: Don't use InternalError for the caller's failure
Err(RpcError::InternalError("decoys being requested from too young blocks"))?;
Err(RpcError::InternalError("decoys being requested from too young blocks".to_string()))?;
}
#[cfg(test)]
@@ -52,7 +54,7 @@ async fn select_n<'a, R: RngCore + CryptoRng>(
iters += 1;
// This is cheap and on fresh chains, a lot of rounds may be needed
if iters == 100 {
Err(RpcError::InternalError("hit decoy selection round limit"))?;
Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?;
}
}
@@ -134,7 +136,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
inputs: &[WalletOutput],
fingerprintable_canonical: bool,
) -> Result<Vec<Decoys>, RpcError> {
let mut distribution = vec![];
@@ -145,7 +147,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
let mut real = Vec::with_capacity(inputs.len());
let mut outputs = Vec::with_capacity(inputs.len());
for input in inputs {
real.push(input.global_index);
real.push(input.relative_id.index_on_blockchain);
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
}
@@ -160,7 +162,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
distribution.truncate(height);
if distribution.len() < DEFAULT_LOCK_WINDOW {
Err(RpcError::InternalError("not enough decoy candidates"))?;
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
}
#[allow(clippy::cast_precision_loss)]
@@ -181,7 +183,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
if high.saturating_sub(COINBASE_LOCK_WINDOW as u64) <
u64::try_from(inputs.len() * ring_len).unwrap()
{
Err(RpcError::InternalError("not enough coinbase candidates"))?;
Err(RpcError::InternalError("not enough coinbase candidates".to_string()))?;
}
// Select all decoys for this transaction, assuming we generate a sane transaction
@@ -283,7 +285,7 @@ pub trait DecoySelection {
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
inputs: &[WalletOutput],
) -> Result<Vec<Decoys>, RpcError>;
async fn fingerprintable_canonical_select<R: Send + Sync + RngCore + CryptoRng>(
@@ -291,7 +293,7 @@ pub trait DecoySelection {
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
inputs: &[WalletOutput],
) -> Result<Vec<Decoys>, RpcError>;
}
@@ -305,7 +307,7 @@ impl DecoySelection for Decoys {
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
inputs: &[WalletOutput],
) -> Result<Vec<Decoys>, RpcError> {
select_decoys(rng, rpc, ring_len, height, inputs, false).await
}
@@ -323,7 +325,7 @@ impl DecoySelection for Decoys {
rpc: &impl Rpc,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
inputs: &[WalletOutput],
) -> Result<Vec<Decoys>, RpcError> {
select_decoys(rng, rpc, ring_len, height, inputs, true).await
}

View File

@@ -221,6 +221,8 @@ impl Extra {
buf
}
// TODO: Is this supposed to silently drop trailing gibberish?
#[allow(clippy::unnecessary_wraps)]
pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> {
let mut res = Extra(vec![]);
let mut field;

View File

@@ -3,11 +3,9 @@
// #![deny(missing_docs)] // TODO
#![cfg_attr(not(feature = "std"), no_std)]
use core::ops::Deref;
use zeroize::{Zeroize, Zeroizing};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
use curve25519_dalek::{Scalar, EdwardsPoint};
use monero_serai::{
io::write_varint,
@@ -20,99 +18,34 @@ pub use monero_serai::*;
pub use monero_rpc as rpc;
pub use monero_address as address;
mod view_pair;
pub use view_pair::{ViewPair, GuaranteedViewPair};
pub mod extra;
pub(crate) use extra::{PaymentId, Extra};
pub use monero_address as address;
use address::{Network, AddressType, SubaddressIndex, AddressSpec, MoneroAddress};
pub(crate) mod output;
pub use output::WalletOutput;
pub mod scan;
mod scan;
pub use scan::{Scanner, GuaranteedScanner};
#[cfg(feature = "std")]
pub mod decoys;
mod decoys;
#[cfg(not(feature = "std"))]
pub mod decoys {
mod decoys {
pub use monero_serai::primitives::Decoys;
pub trait DecoySelection {}
}
pub use decoys::{DecoySelection, Decoys};
/// Structs and functionality for sending transactions.
pub mod send;
/* TODO
#[cfg(test)]
mod tests;
*/
/// The private view key and public spend key, enabling scanning transactions.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct ViewPair {
spend: EdwardsPoint,
view: Zeroizing<Scalar>,
}
impl ViewPair {
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> ViewPair {
ViewPair { spend, view }
}
pub fn spend(&self) -> EdwardsPoint {
self.spend
}
pub fn view(&self) -> EdwardsPoint {
self.view.deref() * ED25519_BASEPOINT_TABLE
}
fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar {
keccak256_to_scalar(Zeroizing::new(
[
b"SubAddr\0".as_ref(),
Zeroizing::new(self.view.to_bytes()).as_ref(),
&index.account().to_le_bytes(),
&index.address().to_le_bytes(),
]
.concat(),
))
}
fn subaddress_keys(&self, index: SubaddressIndex) -> (EdwardsPoint, EdwardsPoint) {
let scalar = self.subaddress_derivation(index);
let spend = self.spend + (&scalar * ED25519_BASEPOINT_TABLE);
let view = self.view.deref() * spend;
(spend, view)
}
/// Returns an address with the provided specification.
pub fn address(&self, network: Network, spec: AddressSpec) -> MoneroAddress {
let mut spend = self.spend;
let mut view: EdwardsPoint = self.view.deref() * ED25519_BASEPOINT_TABLE;
// construct the address type
let kind = match spec {
AddressSpec::Legacy => AddressType::Legacy,
AddressSpec::LegacyIntegrated(payment_id) => AddressType::LegacyIntegrated(payment_id),
AddressSpec::Subaddress(index) => {
(spend, view) = self.subaddress_keys(index);
AddressType::Subaddress
}
AddressSpec::Featured { subaddress, payment_id, guaranteed } => {
if let Some(index) = subaddress {
(spend, view) = self.subaddress_keys(index);
}
AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed }
}
};
MoneroAddress::new(network, kind, spend, view)
}
}
pub(crate) fn compact_amount_encryption(amount: u64, key: Scalar) -> [u8; 8] {
let mut amount_mask = b"amount".to_vec();
amount_mask.extend(key.to_bytes());
(amount ^ u64::from_le_bytes(keccak256(amount_mask)[.. 8].try_into().unwrap())).to_le_bytes()
}
#[derive(Clone, PartialEq, Eq, Zeroize)]
struct SharedKeyDerivations {
@@ -194,6 +127,18 @@ impl SharedKeyDerivations {
res
}
fn compact_amount_encryption(&self, amount: u64) -> [u8; 8] {
let mut amount_mask = Zeroizing::new(b"amount".to_vec());
amount_mask.extend(self.shared_key.to_bytes());
let mut amount_mask = keccak256(&amount_mask);
let mut amount_mask_8 = [0; 8];
amount_mask_8.copy_from_slice(&amount_mask[.. 8]);
amount_mask.zeroize();
(amount ^ u64::from_le_bytes(amount_mask_8)).to_le_bytes()
}
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
match enc_amount {
// TODO: Add a test vector for this
@@ -212,7 +157,7 @@ impl SharedKeyDerivations {
}
EncryptedAmount::Compact { amount } => Commitment::new(
self.commitment_mask(),
u64::from_le_bytes(compact_amount_encryption(u64::from_le_bytes(*amount), self.shared_key)),
u64::from_le_bytes(self.compact_amount_encryption(u64::from_le_bytes(*amount))),
),
}
}

View File

@@ -0,0 +1,338 @@
use std_shims::{
io::{self, Read, Write},
vec::Vec,
};
use zeroize::{Zeroize, ZeroizeOnDrop};
use curve25519_dalek::{Scalar, edwards::EdwardsPoint};
use crate::{
io::*, primitives::Commitment, transaction::Timelock, address::SubaddressIndex, extra::PaymentId,
};
/// An absolute output ID, defined as its transaction hash and output index.
///
/// This is not the output's key as multiple outputs may share an output key.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct AbsoluteId {
pub(crate) transaction: [u8; 32],
pub(crate) index_in_transaction: u32,
}
impl core::fmt::Debug for AbsoluteId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("AbsoluteId")
.field("transaction", &hex::encode(self.transaction))
.field("index_in_transaction", &self.index_in_transaction)
.finish()
}
}
impl AbsoluteId {
/// Write the AbsoluteId.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.transaction)?;
w.write_all(&self.index_in_transaction.to_le_bytes())
}
/// Read an AbsoluteId.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u32(r)? })
}
}
/// An output's relative ID.
///
/// This id defined as the block which contains the transaction creating the output and the
/// output's index on the blockchain.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct RelativeId {
pub(crate) block: [u8; 32],
pub(crate) index_on_blockchain: u64,
}
impl core::fmt::Debug for RelativeId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("RelativeId")
.field("block", &hex::encode(self.block))
.field("index_on_blockchain", &self.index_on_blockchain)
.finish()
}
}
impl RelativeId {
/// Write the RelativeId.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
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())
}
/// Read an RelativeId.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<Self> {
Ok(RelativeId { block: read_bytes(r)?, index_on_blockchain: read_u64(r)? })
}
}
/// The data within an output as necessary to spend an output, and the output's additional
/// timelock.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct OutputData {
pub(crate) key: EdwardsPoint,
pub(crate) key_offset: Scalar,
pub(crate) commitment: Commitment,
pub(crate) additional_timelock: Timelock,
}
impl core::fmt::Debug for OutputData {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("OutputData")
.field("key", &hex::encode(self.key.compress().0))
.field("key_offset", &hex::encode(self.key_offset.to_bytes()))
.field("commitment", &self.commitment)
.field("additional_timelock", &self.additional_timelock)
.finish()
}
}
impl OutputData {
// Write the OutputData.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.key.compress().to_bytes())?;
w.write_all(&self.key_offset.to_bytes())?;
// TODO: Commitment::write?
w.write_all(&self.commitment.mask.to_bytes())?;
w.write_all(&self.commitment.amount.to_le_bytes())?;
self.additional_timelock.write(w)
}
/// Read an OutputData.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
Ok(OutputData {
key: read_point(r)?,
key_offset: read_scalar(r)?,
commitment: Commitment::new(read_scalar(r)?, read_u64(r)?),
additional_timelock: Timelock::read(r)?,
})
}
}
/// The metadata for an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub(crate) struct Metadata {
pub(crate) subaddress: Option<SubaddressIndex>,
pub(crate) payment_id: Option<PaymentId>,
pub(crate) arbitrary_data: Vec<Vec<u8>>,
}
impl core::fmt::Debug for Metadata {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("Metadata")
.field("subaddress", &self.subaddress)
.field("payment_id", &self.payment_id)
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
.finish()
}
}
impl Metadata {
/// Write the Metadata.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
if let Some(subaddress) = self.subaddress {
w.write_all(&[1])?;
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())?;
} else {
w.write_all(&[0])?;
}
if let Some(payment_id) = self.payment_id {
w.write_all(&[1])?;
payment_id.write(w)?;
} else {
w.write_all(&[0])?;
}
w.write_all(&u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes())?;
for part in &self.arbitrary_data {
w.write_all(&[u8::try_from(part.len()).unwrap()])?;
w.write_all(part)?;
}
Ok(())
}
/// Read a Metadata.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
let subaddress = match read_byte(r)? {
0 => None,
1 => Some(
SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
.ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
),
_ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?,
};
Ok(Metadata {
subaddress,
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
arbitrary_data: {
let mut data = vec![];
for _ in 0 .. read_u32(r)? {
let len = read_byte(r)?;
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
}
data
},
})
}
}
/// A received output.
///
/// This struct contains all data necessary to spend this output, or handle it as a payment.
///
/// This struct is bound to a specific instance of the blockchain. If the blockchain reorganizes
/// the block this struct is bound to, it MUST be discarded. If any outputs are mutual to both
/// blockchains, scanning the new blockchain will yield those outputs again.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct WalletOutput {
/// The absolute ID for this transaction.
pub(crate) absolute_id: AbsoluteId,
/// The ID for this transaction, relative to the blockchain.
pub(crate) relative_id: RelativeId,
/// The output's data.
pub(crate) data: OutputData,
/// Associated metadata relevant for handling it as a payment.
pub(crate) metadata: Metadata,
}
impl WalletOutput {
/// The hash of the transaction which created this output.
pub fn transaction(&self) -> [u8; 32] {
self.absolute_id.transaction
}
/// The index of the output within the transaction.
pub fn index_in_transaction(&self) -> u32 {
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.
pub fn index_on_blockchain(&self) -> u64 {
self.relative_id.index_on_blockchain
}
/// The key this output may be spent by.
pub fn key(&self) -> EdwardsPoint {
self.data.key
}
/// The scalar to add to the private spend key for it to be the discrete logarithm of this
/// output's key.
pub fn key_offset(&self) -> Scalar {
self.data.key_offset
}
/// The commitment this output created.
pub fn commitment(&self) -> &Commitment {
&self.data.commitment
}
/// The additional timelock this output is subject to.
///
/// All outputs are subject to the '10-block lock', a 10-block window after their inclusion
/// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This
/// function only returns the additional timelock.
pub fn additional_timelock(&self) -> Timelock {
self.data.additional_timelock
}
/// The index of the subaddress this output was identified as sent to.
pub fn subaddress(&self) -> Option<SubaddressIndex> {
self.metadata.subaddress
}
/// The payment ID included with this output.
///
/// This field may be `Some` even if wallet would not return a payment ID. This will happen if
/// the scanned output belongs to the subaddress which spent Monero within the transaction which
/// created the output. If multiple subaddresses spent Monero within this transactions, the key
/// image with the highest index is determined to be the subaddress considered as the one
/// spending.
// TODO: Clarify and cite for point A ("highest index spent key image"??)
pub fn payment_id(&self) -> Option<PaymentId> {
self.metadata.payment_id
}
/// The arbitrary data from the `extra` field of the transaction which created this output.
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
&self.metadata.arbitrary_data
}
/// Write the WalletOutput.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.absolute_id.write(w)?;
self.relative_id.write(w)?;
self.data.write(w)?;
self.metadata.write(w)
}
/// Serialize the WalletOutput 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 a WalletOutput.
///
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization.
pub fn read<R: Read>(r: &mut R) -> io::Result<WalletOutput> {
Ok(WalletOutput {
absolute_id: AbsoluteId::read(r)?,
relative_id: RelativeId::read(r)?,
data: OutputData::read(r)?,
metadata: Metadata::read(r)?,
})
}
}

View File

@@ -1,18 +1,9 @@
use core::ops::Deref;
use std_shims::{
io::{self, Read, Write},
vec::Vec,
string::ToString,
collections::{HashSet, HashMap},
};
use std_shims::{vec::Vec, string::ToString, collections::HashMap};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
Scalar,
edwards::{EdwardsPoint, CompressedEdwardsY},
};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
use monero_rpc::{RpcError, Rpc};
use monero_serai::{
@@ -21,439 +12,145 @@ use monero_serai::{
transaction::{Input, Timelock, Transaction},
block::Block,
};
use crate::{address::SubaddressIndex, ViewPair, PaymentId, Extra, SharedKeyDerivations};
use crate::{
address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra,
SharedKeyDerivations,
};
/// An absolute output ID, defined as its transaction hash and output index.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct AbsoluteId {
pub tx: [u8; 32],
pub o: u32,
}
/// A collection of potentially additionally timelocked outputs.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Timelocked(Vec<WalletOutput>);
impl core::fmt::Debug for AbsoluteId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt.debug_struct("AbsoluteId").field("tx", &hex::encode(self.tx)).field("o", &self.o).finish()
impl Timelocked {
/// Return the outputs which aren't subject to an additional timelock.
#[must_use]
pub fn not_additionally_locked(self) -> Vec<WalletOutput> {
let mut res = vec![];
for output in &self.0 {
if output.additional_timelock() == Timelock::None {
res.push(output.clone());
}
}
impl AbsoluteId {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.tx)?;
w.write_all(&self.o.to_le_bytes())
}
res
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(32 + 4);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
Ok(AbsoluteId { tx: read_bytes(r)?, o: read_u32(r)? })
}
}
/// The data contained with an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct OutputData {
pub key: EdwardsPoint,
/// Absolute difference between the spend key and the key in this output
pub key_offset: Scalar,
pub commitment: Commitment,
}
impl core::fmt::Debug for OutputData {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("OutputData")
.field("key", &hex::encode(self.key.compress().0))
.field("key_offset", &hex::encode(self.key_offset.to_bytes()))
.field("commitment", &self.commitment)
.finish()
}
}
impl OutputData {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.key.compress().to_bytes())?;
w.write_all(&self.key_offset.to_bytes())?;
w.write_all(&self.commitment.mask.to_bytes())?;
w.write_all(&self.commitment.amount.to_le_bytes())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(32 + 32 + 32 + 8);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
Ok(OutputData {
key: read_point(r)?,
key_offset: read_scalar(r)?,
commitment: Commitment::new(read_scalar(r)?, read_u64(r)?),
})
}
}
/// The metadata for an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Metadata {
/// The subaddress this output was sent to.
pub subaddress: Option<SubaddressIndex>,
/// The payment ID included with this output.
/// There are 2 circumstances in which the reference wallet2 ignores the payment ID
/// but the payment ID will be returned here anyway:
/// Return the outputs whose additional timelock unlocks by the specified block/time.
///
/// 1) If the payment ID is tied to an output received by a subaddress account
/// that spent Monero in the transaction (the received output is considered
/// "change" and is not considered a "payment" in this case). If there are multiple
/// spending subaddress accounts in a transaction, the highest index spent key image
/// is used to determine the spending subaddress account.
/// Additional timelocks are almost never used outside of miner transactions, and are
/// increasingly planned for removal. Ignoring non-miner additionally-timelocked outputs is
/// recommended.
///
/// 2) If the payment ID is the unencrypted variant and the block's hf version is
/// v12 or higher (https://github.com/serai-dex/serai/issues/512)
pub payment_id: Option<PaymentId>,
/// Arbitrary data encoded in TX extra.
pub arbitrary_data: Vec<Vec<u8>>,
}
impl core::fmt::Debug for Metadata {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("Metadata")
.field("subaddress", &self.subaddress)
.field("payment_id", &self.payment_id)
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
.finish()
}
}
impl Metadata {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
if let Some(subaddress) = self.subaddress {
w.write_all(&[1])?;
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())?;
} else {
w.write_all(&[0])?;
}
if let Some(payment_id) = self.payment_id {
w.write_all(&[1])?;
payment_id.write(w)?;
} else {
w.write_all(&[0])?;
}
w.write_all(&u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes())?;
for part in &self.arbitrary_data {
w.write_all(&[u8::try_from(part.len()).unwrap()])?;
w.write_all(part)?;
}
Ok(())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(1 + 8 + 1);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
let subaddress = if read_byte(r)? == 1 {
Some(
SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
.ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
)
} else {
None
};
Ok(Metadata {
subaddress,
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
arbitrary_data: {
let mut data = vec![];
for _ in 0 .. read_u32(r)? {
let len = read_byte(r)?;
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
}
data
},
})
}
}
/// A received output, defined as its absolute ID, data, and metadara.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ReceivedOutput {
pub absolute: AbsoluteId,
pub data: OutputData,
pub metadata: Metadata,
}
impl ReceivedOutput {
pub fn key(&self) -> EdwardsPoint {
self.data.key
}
pub fn key_offset(&self) -> Scalar {
self.data.key_offset
}
pub fn commitment(&self) -> Commitment {
self.data.commitment.clone()
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
&self.metadata.arbitrary_data
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.absolute.write(w)?;
self.data.write(w)?;
self.metadata.write(w)
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
Ok(ReceivedOutput {
absolute: AbsoluteId::read(r)?,
data: OutputData::read(r)?,
metadata: Metadata::read(r)?,
})
}
}
/// A spendable output, defined as a received output and its index on the Monero blockchain.
/// This index is dependent on the Monero blockchain and will only be known once the output is
/// included within a block. This may change if there's a reorganization.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct SpendableOutput {
pub output: ReceivedOutput,
pub global_index: u64,
}
impl SpendableOutput {
/// Update the spendable output's global index. This is intended to be called if a
/// re-organization occurred.
pub async fn refresh_global_index(&mut self, rpc: &impl Rpc) -> Result<(), RpcError> {
self.global_index = *rpc
.get_o_indexes(self.output.absolute.tx)
.await?
.get(
usize::try_from(self.output.absolute.o)
.map_err(|_| RpcError::InternalError("output's index didn't fit within a usize"))?,
)
.ok_or(RpcError::InvalidNode(
"node returned output indexes didn't include an index for this output".to_string(),
))?;
Ok(())
}
pub async fn from(rpc: &impl Rpc, output: ReceivedOutput) -> Result<SpendableOutput, RpcError> {
let mut output = SpendableOutput { output, global_index: 0 };
output.refresh_global_index(rpc).await?;
Ok(output)
}
pub fn key(&self) -> EdwardsPoint {
self.output.key()
}
pub fn key_offset(&self) -> Scalar {
self.output.key_offset()
}
pub fn commitment(&self) -> Commitment {
self.output.commitment()
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
self.output.arbitrary_data()
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.output.write(w)?;
w.write_all(&self.global_index.to_le_bytes())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<SpendableOutput> {
Ok(SpendableOutput { output: ReceivedOutput::read(r)?, global_index: read_u64(r)? })
}
}
/// A collection of timelocked outputs, either received or spendable.
#[derive(Zeroize)]
pub struct Timelocked<O: Clone + Zeroize>(Timelock, Vec<O>);
impl<O: Clone + Zeroize> Drop for Timelocked<O> {
fn drop(&mut self) {
self.zeroize();
}
}
impl<O: Clone + Zeroize> ZeroizeOnDrop for Timelocked<O> {}
impl<O: Clone + Zeroize> Timelocked<O> {
pub fn timelock(&self) -> Timelock {
self.0
}
/// Return the outputs if they're not timelocked, or an empty vector if they are.
/// `block` is the block number of the block the additional timelock must be satsified by.
///
/// `time` is represented in seconds since the epoch. Please note Monero uses an on-chain
/// deterministic clock for time which is subject to variance from the real world time. This time
/// argument will be evaluated against Monero's clock, not the local system's clock.
#[must_use]
pub fn not_locked(&self) -> Vec<O> {
if self.0 == Timelock::None {
return self.1.clone();
pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
let mut res = vec![];
for output in &self.0 {
if (output.additional_timelock() <= Timelock::Block(block)) ||
(output.additional_timelock() <= Timelock::Time(time))
{
res.push(output.clone());
}
vec![]
}
res
}
/// Returns None if the Timelocks aren't comparable. Returns Some(vec![]) if none are unlocked.
/// Ignore the timelocks and return all outputs within this container.
#[must_use]
pub fn unlocked(&self, timelock: Timelock) -> Option<Vec<O>> {
// If the Timelocks are comparable, return the outputs if they're now unlocked
if self.0 <= timelock {
Some(self.1.clone())
} else {
None
}
}
#[must_use]
pub fn ignore_timelock(&self) -> Vec<O> {
self.1.clone()
pub fn ignore_additional_timelock(mut self) -> Vec<WalletOutput> {
let mut res = vec![];
core::mem::swap(&mut self.0, &mut res);
res
}
}
/// Transaction scanner.
///
/// This scanner is capable of generating subaddresses, additionally scanning for them once they've
/// been explicitly generated. If the burning bug is attempted, any secondary outputs will be
/// ignored.
#[derive(Clone)]
pub struct Scanner {
struct InternalScanner {
pair: ViewPair,
// Also contains the spend key as None
pub(crate) subaddresses: HashMap<CompressedEdwardsY, Option<SubaddressIndex>>,
pub(crate) burning_bug: Option<HashSet<CompressedEdwardsY>>,
guaranteed: bool,
subaddresses: HashMap<CompressedEdwardsY, Option<SubaddressIndex>>,
}
impl Zeroize for Scanner {
impl Zeroize for InternalScanner {
fn zeroize(&mut self) {
self.pair.zeroize();
self.guaranteed.zeroize();
// These may not be effective, unfortunately
// This may not be effective, unfortunately
for (mut key, mut value) in self.subaddresses.drain() {
key.zeroize();
value.zeroize();
}
if let Some(ref mut burning_bug) = self.burning_bug.take() {
for mut output in burning_bug.drain() {
output.zeroize();
}
}
}
}
impl Drop for Scanner {
impl Drop for InternalScanner {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for InternalScanner {}
impl ZeroizeOnDrop for Scanner {}
impl Scanner {
/// Create a Scanner from a ViewPair.
///
/// burning_bug is a HashSet of used keys, intended to prevent key reuse which would burn funds.
///
/// When an output is successfully scanned, the output key MUST be saved to disk.
///
/// When a new scanner is created, ALL saved output keys must be passed in to be secure.
///
/// If None is passed, a modified shared key derivation is used which is immune to the burning
/// bug (specifically the Guaranteed feature from Featured Addresses).
pub fn from_view(pair: ViewPair, burning_bug: Option<HashSet<CompressedEdwardsY>>) -> Scanner {
impl InternalScanner {
fn new(pair: ViewPair, guaranteed: bool) -> Self {
let mut subaddresses = HashMap::new();
subaddresses.insert(pair.spend.compress(), None);
Scanner { pair, subaddresses, burning_bug }
subaddresses.insert(pair.spend().compress(), None);
Self { pair, guaranteed, subaddresses }
}
/// Register a subaddress.
// There used to be an address function here, yet it wasn't safe. It could generate addresses
// incompatible with the Scanner. While we could return None for that, then we have the issue
// of runtime failures to generate an address.
// Removing that API was the simplest option.
pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
let (spend, _) = self.pair.subaddress_keys(subaddress);
self.subaddresses.insert(spend.compress(), Some(subaddress));
}
}
impl Scanner {
/// Scan a transaction to discover the received outputs.
pub fn scan_transaction(&mut self, tx: &Transaction) -> Timelocked<ReceivedOutput> {
fn scan_transaction(
&self,
block_hash: [u8; 32],
tx_start_index_on_blockchain: u64,
tx: &Transaction,
) -> Result<Timelocked, RpcError> {
// Only scan RCT TXs since we can only spend RCT outputs
if tx.version() != 2 {
return Timelocked(tx.prefix().timelock, vec![]);
return Ok(Timelocked(vec![]));
}
// Read the extra field
let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) else {
return Timelocked(tx.prefix().timelock, vec![]);
return Ok(Timelocked(vec![]));
};
let Some((tx_keys, additional)) = extra.keys() else {
return Timelocked(tx.prefix().timelock, vec![]);
return Ok(Timelocked(vec![]));
};
let payment_id = extra.payment_id();
let mut res = vec![];
for (o, output) in tx.prefix().outputs.iter().enumerate() {
// https://github.com/serai-dex/serai/issues/106
if let Some(burning_bug) = self.burning_bug.as_ref() {
if burning_bug.contains(&output.key) {
continue;
}
}
let Some(output_key) = decompress_point(output.key.to_bytes()) else { continue };
let output_key = decompress_point(output.key.to_bytes());
if output_key.is_none() {
continue;
}
let output_key = output_key.unwrap();
// Monero checks with each TX key and with the additional key for this output
let additional = additional.as_ref().map(|additional| additional.get(o));
for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) {
let key = match key {
Some(Some(key)) => key,
Some(None) => {
// This is non-standard. There were additional keys, yet not one for this output
// This will be None if there's no additional keys, Some(None) if there's additional keys
// yet not one for this output (which is non-standard), and Some(Some(_)) if there's an
// additional key for this output
// https://github.com/monero-project/monero/
// blob/04a1e2875d6e35e27bb21497988a6c822d319c28/
// src/cryptonote_basic/cryptonote_format_utils.cpp#L1062
continue;
}
None => {
break;
}
let additional = additional.as_ref().map(|additional| additional.get(o));
#[allow(clippy::manual_let_else)]
for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) {
// Get the key, or continue if there isn't one
let key = match key {
Some(Some(key)) => key,
Some(None) | None => continue,
};
// Calculate the ECDH
let ecdh = Zeroizing::new(self.pair.view.deref() * key);
let output_derivations = SharedKeyDerivations::output_derivations(
if self.burning_bug.is_none() {
if self.guaranteed {
Some(SharedKeyDerivations::uniqueness(&tx.prefix().inputs))
} else {
None
@@ -462,8 +159,7 @@ impl Scanner {
o,
);
let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh));
// Check the view tag matches, if there is a view tag
if let Some(actual_view_tag) = output.view_tag {
if actual_view_tag != output_derivations.view_tag {
continue;
@@ -471,22 +167,25 @@ impl Scanner {
}
// P - shared == spend
let subaddress = self.subaddresses.get(
&(output_key - (&output_derivations.shared_key * ED25519_BASEPOINT_TABLE)).compress(),
);
if subaddress.is_none() {
let Some(subaddress) = ({
// The output key may be of torsion [0, 8)
// Our subtracting of a prime-order element means any torsion will be preserved
// If someone wanted to malleate output keys with distinct torsions, only one will be
// scanned accordingly (the one which has matching torsion of the spend key)
// TODO: If there's a torsioned spend key, can we spend outputs to it?
let subaddress_spend_key =
output_key - (&output_derivations.shared_key * ED25519_BASEPOINT_TABLE);
self.subaddresses.get(&subaddress_spend_key.compress())
}) else {
continue;
}
let subaddress = *subaddress.unwrap();
// If it has torsion, it'll subtract the non-torsioned shared key to a torsioned key
// We will not have a torsioned key in our HashMap of keys, so we wouldn't identify it as
// ours
// If we did though, it'd enable bypassing the included burning bug protection
assert!(output_key.is_torsion_free());
};
let subaddress = *subaddress;
// The key offset is this shared key
let mut key_offset = output_derivations.shared_key;
if let Some(subaddress) = subaddress {
// And if this was to a subaddress, it's additionally the offset from subaddress spend
// key to the normal spend key
key_offset += self.pair.subaddress_derivation(subaddress);
}
// Since we've found an output to us, get its amount
@@ -498,83 +197,90 @@ impl Scanner {
// Regular transaction
} else {
let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
return Timelocked(tx.prefix().timelock, vec![]);
// Invalid transaction, as of consensus rules at the time of writing this code
Err(RpcError::InvalidNode("non-miner v2 transaction without RCT proofs".to_string()))?
};
commitment = match proofs.base.encrypted_amounts.get(o) {
Some(amount) => output_derivations.decrypt(amount),
// 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
None => break,
// Invalid transaction, as of consensus rules at the time of writing this code
None => Err(RpcError::InvalidNode(
"RCT proofs without an encrypted amount per output".to_string(),
))?,
};
// 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
// Rebuild the commitment to verify it
if Some(&commitment.calculate()) != proofs.base.commitments.get(o) {
break;
continue;
}
}
if commitment.amount != 0 {
res.push(ReceivedOutput {
absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() },
data: OutputData { key: output_key, key_offset, commitment },
// Decrypt the payment ID
let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh));
res.push(WalletOutput {
absolute_id: AbsoluteId {
transaction: tx.hash(),
index_in_transaction: o.try_into().unwrap(),
},
relative_id: RelativeId {
block: block_hash,
index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(),
},
data: OutputData {
key: output_key,
key_offset,
commitment,
additional_timelock: tx.prefix().timelock,
},
metadata: Metadata { subaddress, payment_id, arbitrary_data: extra.data() },
});
if let Some(burning_bug) = self.burning_bug.as_mut() {
burning_bug.insert(output.key);
}
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
}
Timelocked(tx.prefix().timelock, res)
Ok(Timelocked(res))
}
/// Scan a block to obtain its spendable outputs. Its the presence in a block giving these
/// transactions their global index, and this must be batched as asking for the index of specific
/// transactions is a dead giveaway for which transactions you successfully scanned. This
/// function obtains the output indexes for the miner transaction, incrementing from there
/// instead.
pub async fn scan(
&mut self,
rpc: &impl Rpc,
block: &Block,
) -> Result<Vec<Timelocked<SpendableOutput>>, RpcError> {
let mut index = rpc.get_o_indexes(block.miner_tx.hash()).await?[0];
let mut txs = vec![block.miner_tx.clone()];
txs.extend(rpc.get_transactions(&block.txs).await?);
let map = |mut timelock: Timelocked<ReceivedOutput>, index| {
if timelock.1.is_empty() {
None
} else {
Some(Timelocked(
timelock.0,
timelock
.1
.drain(..)
.map(|output| SpendableOutput {
global_index: index + u64::from(output.absolute.o),
output,
})
.collect(),
))
async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
if block.header.hardfork_version > 16 {
Err(RpcError::InternalError(format!(
"scanning a hardfork {} block, when we only support up to 16",
block.header.hardfork_version
)))?;
}
};
let mut res = vec![];
let block_hash = block.hash();
// We get the output indexes for the miner transaction as a reference point
// TODO: Are miner transactions since v2 guaranteed to have an output?
let mut tx_start_index_on_blockchain = *rpc
.get_o_indexes(block.miner_transaction.hash())
.await?
.first()
.ok_or(RpcError::InvalidNode("miner transaction without outputs".to_string()))?;
// We obtain all TXs in full
let mut txs = vec![block.miner_transaction.clone()];
txs.extend(rpc.get_transactions(&block.transactions).await?);
let mut res = Timelocked(vec![]);
for tx in txs {
if let Some(timelock) = map(self.scan_transaction(&tx), index) {
res.push(timelock);
// Push all outputs into our result
{
let mut this_txs_outputs = vec![];
core::mem::swap(
&mut self.scan_transaction(block_hash, tx_start_index_on_blockchain, &tx)?.0,
&mut this_txs_outputs,
);
res.0.extend(this_txs_outputs);
}
index += u64::try_from(
// Update the TX start index for the next TX
tx_start_index_on_blockchain += u64::try_from(
tx.prefix()
.outputs
.iter()
@@ -588,6 +294,120 @@ impl Scanner {
)
.unwrap()
}
// If the block's version is >= 12, drop all unencrypted payment IDs
// TODO: Cite rule
// TODO: What if TX extra had multiple payment IDs embedded?
if block.header.hardfork_version >= 12 {
for output in &mut res.0 {
if matches!(output.metadata.payment_id, Some(PaymentId::Unencrypted(_))) {
output.metadata.payment_id = None;
}
}
}
Ok(res)
}
}
/// A transaction scanner to find outputs received.
///
/// When an output is successfully scanned, the output key MUST be checked against the local
/// database for lack of prior observation. If it was prior observed, that output is an instance
/// of the burning bug (TODO: cite) and MAY be unspendable. Only the prior received output(s) or
/// the newly received output will be spendable (as spending one will burn all of them).
///
/// Once checked, the output key MUST be saved to the local database so future checks can be
/// performed.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Scanner(InternalScanner);
impl Scanner {
/// Create a Scanner from a ViewPair.
pub fn new(pair: ViewPair) -> Self {
Self(InternalScanner::new(pair, false))
}
/// Register a subaddress to scan for.
///
/// Subaddresses must be explicitly registered ahead of time in order to be successfully scanned.
pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
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.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
self.0.scan(rpc, block).await
}
}
/// A transaction scanner to find outputs received which are guaranteed to be spendable.
///
/// 'Guaranteed' outputs, or transactions outputs to the burning bug, are not officially specified
/// by the Monero project. They should only be used if necessary. No support outside of
/// monero-wallet is promised.
///
/// "guaranteed to be spendable" assumes satisfaction of any timelocks in effect.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct GuaranteedScanner(InternalScanner);
impl GuaranteedScanner {
/// Create a GuaranteedScanner from a GuaranteedViewPair.
pub fn new(pair: GuaranteedViewPair) -> Self {
Self(InternalScanner::new(pair.0, true))
}
/// Register a subaddress to scan for.
///
/// Subaddresses must be explicitly registered ahead of time in order to be successfully scanned.
pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
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.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> {
self.0.scan(rpc, block).await
}
}

View File

@@ -9,6 +9,20 @@ use crate::{
};
/// The eventual output of a SignableTransaction.
///
/// If a SignableTransaction is signed and published on-chain, it will create a Transaction
/// identifiable to whoever else has the same SignableTransaction (with the same outgoing view
/// key). This structure enables checking if a Transaction is in fact such an output, as it can.
///
/// Since Monero is a privacy coin without outgoing view keys, this only performs a fuzzy match.
/// The fuzzy match executes over the outputs and associated data necessary to work with the
/// outputs (the transaction randomness, ciphertexts). This transaction does not check if the
/// inputs intended to be spent where actually the inputs spent (as infeasible).
///
/// The transaction randomness does bind to the inputs intended to be spent, so an on-chain
/// transaction will not match for multiple `Eventuality`s unless the `SignableTransaction`s they
/// were built from were in conflict (and their intended transactions cannot simultaneously exist
/// on-chain).
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Eventuality(SignableTransaction);
@@ -19,14 +33,15 @@ impl From<SignableTransaction> for Eventuality {
}
impl Eventuality {
/// Return the extra any TX following this intent would use.
/// Return the `extra` field any transaction following this intent would use.
///
/// This enables building a HashMap of Extra -> Eventuality for efficiently checking if an
/// on-chain transaction may match one of several eventuality.
/// This enables building a HashMap of Extra -> Eventuality for efficiently fetching the
/// `Eventuality` an on-chain transaction may complete.
///
/// This extra is cryptographically bound to the set of outputs intended to be spent as inputs.
/// This means two SignableTransactions for the same set of payments will have distinct extras.
/// This does not guarantee the matched transaction actually spent the intended outputs.
/// This extra is cryptographically bound to the inputs intended to be spent. If the
/// `SignableTransaction`s the `Eventuality`s are built from are not in conflict (their intended
/// transactions can simultaneously exist on-chain), then each extra will only have a single
/// Eventuality associated (barring a cryptographic problem considered hard failing).
pub fn extra(&self) -> Vec<u8> {
self.0.extra()
}
@@ -96,14 +111,26 @@ impl Eventuality {
true
}
/// Write the Eventuality.
///
/// 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.0.write(w)
}
/// Serialize the Eventuality 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> {
self.0.serialize()
}
/// Read a Eventuality.
///
/// 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<Eventuality> {
Ok(Eventuality(SignableTransaction::read(r)?))
}

View File

@@ -20,10 +20,9 @@ use crate::{
},
transaction::Transaction,
extra::MAX_ARBITRARY_DATA_SIZE,
address::{Network, AddressSpec, MoneroAddress},
address::{Network, MoneroAddress},
rpc::FeeRate,
ViewPair,
scan::SpendableOutput,
ViewPair, GuaranteedViewPair, WalletOutput,
};
mod tx_keys;
@@ -33,7 +32,8 @@ pub use eventuality::Eventuality;
#[cfg(feature = "multisig")]
mod multisig;
pub use multisig::TransactionMachine;
#[cfg(feature = "multisig")]
pub use multisig::{TransactionMachine, TransactionSignMachine, TransactionSignatureMachine};
pub(crate) fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering {
x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
@@ -70,18 +70,25 @@ impl Change {
/// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
/// its wallet protocol accordingly.
// TODO: Accept AddressSpec, not `guaranteed: bool`
pub fn new(view: &ViewPair, guaranteed: bool) -> Change {
pub fn new(view: &ViewPair) -> Change {
Change(ChangeEnum::AddressWithView(
// Which network doesn't matter as the derivations will all be the same
// TODO: Support subaddresses
view.legacy_address(Network::Mainnet),
view.view.clone(),
))
}
pub fn guaranteed(view: &GuaranteedViewPair) -> Change {
Change(ChangeEnum::AddressWithView(
view.address(
// Which network doesn't matter as the derivations will all be the same
Network::Mainnet,
if !guaranteed {
AddressSpec::Legacy
} else {
AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true }
},
// TODO: Support subaddresses
None,
None,
),
view.view.clone(),
view.0.view.clone(),
))
}
@@ -183,7 +190,7 @@ pub enum SendError {
pub struct SignableTransaction {
rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<(SpendableOutput, Decoys)>,
inputs: Vec<(WalletOutput, Decoys)>,
payments: Vec<InternalPayment>,
data: Vec<Vec<u8>>,
fee_rate: FeeRate,
@@ -301,7 +308,7 @@ impl SignableTransaction {
pub fn new(
rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<(SpendableOutput, Decoys)>,
inputs: Vec<(WalletOutput, Decoys)>,
payments: Vec<(MoneroAddress, u64)>,
change: Change,
data: Vec<Vec<u8>>,
@@ -341,8 +348,12 @@ impl SignableTransaction {
self.weight_and_fee().1
}
/// Write a SignableTransaction.
///
/// 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<()> {
fn write_input<W: io::Write>(input: &(SpendableOutput, Decoys), 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)
}
@@ -375,15 +386,23 @@ impl SignableTransaction {
self.fee_rate.write(w)
}
/// Serialize the SignableTransaction 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 buf = Vec::with_capacity(256);
self.write(&mut buf).unwrap();
buf
}
/// Read a `SignableTransaction`.
///
/// 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<SignableTransaction> {
fn read_input(r: &mut impl io::Read) -> io::Result<(SpendableOutput, Decoys)> {
Ok((SpendableOutput::read(r)?, Decoys::read(r)?))
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> {

View File

@@ -70,8 +70,8 @@ impl SignableTransaction {
Err(SendError::WrongPrivateKey)?;
}
let context =
ClsagContext::new(decoys.clone(), input.commitment()).map_err(SendError::ClsagError)?;
let context = ClsagContext::new(decoys.clone(), input.commitment().clone())
.map_err(SendError::ClsagError)?;
let (clsag, clsag_mask_send) = ClsagMultisig::new(
RecommendedTranscript::new(b"Monero Multisignature Transaction"),
context,

View File

@@ -10,7 +10,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}
use crate::{
primitives::{keccak256, Commitment},
ringct::EncryptedAmount,
SharedKeyDerivations, compact_amount_encryption,
SharedKeyDerivations,
send::{InternalPayment, SignableTransaction},
};
@@ -34,11 +34,7 @@ fn seeded_rng(
impl SignableTransaction {
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
seeded_rng(
dst,
&self.outgoing_view_key,
self.inputs.iter().map(|(input, _)| input.output.key()),
)
seeded_rng(dst, &self.outgoing_view_key, self.inputs.iter().map(|(input, _)| input.key()))
}
fn has_payments_to_subaddresses(&self) -> bool {
@@ -226,7 +222,7 @@ impl SignableTransaction {
};
let commitment = Commitment::new(shared_key_derivations.commitment_mask(), amount);
let encrypted_amount = EncryptedAmount::Compact {
amount: compact_amount_encryption(amount, shared_key_derivations.shared_key),
amount: shared_key_derivations.compact_amount_encryption(amount),
};
res.push((commitment, encrypted_amount));
}

View File

@@ -1,11 +1,57 @@
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
use monero_serai::io::write_varint;
use crate::{ExtraField, Extra, extra::MAX_TX_EXTRA_PADDING_COUNT};
use crate::{
io::write_varint,
extra::{MAX_TX_EXTRA_PADDING_COUNT, ExtraField, Extra},
};
// Borrowed tests from
// Tests derived from
// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
// tests/unit_tests/test_tx_utils.cpp
// which is licensed
#[rustfmt::skip]
/*
Copyright (c) 2014-2022, The Monero Project
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Parts of the project are originally copyright (c) 2012-2013 The Cryptonote
developers
Parts of the project are originally copyright (c) 2014 The Boolberry
developers, distributed under the MIT licence:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const PUB_KEY_BYTES: [u8; 33] = [
1, 30, 208, 98, 162, 133, 64, 85, 83, 112, 91, 188, 89, 211, 24, 131, 39, 154, 22, 228, 80, 63,

View File

@@ -0,0 +1,157 @@
use core::ops::Deref;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
use crate::{
primitives::keccak256_to_scalar,
address::{Network, AddressType, SubaddressIndex, AddressCreationError, MoneroAddress},
};
/// The pair of keys necessary to scan transactions.
///
/// This is composed of the public spend key and the private view key.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct ViewPair {
spend: EdwardsPoint,
pub(crate) view: Zeroizing<Scalar>,
}
impl ViewPair {
/// Create a new ViewPair.
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Self {
ViewPair { spend, view }
}
/// The public spend key for this ViewPair.
pub fn spend(&self) -> EdwardsPoint {
self.spend
}
/// The public view key for this ViewPair.
pub fn view(&self) -> EdwardsPoint {
self.view.deref() * ED25519_BASEPOINT_TABLE
}
pub(crate) fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar {
keccak256_to_scalar(Zeroizing::new(
[
b"SubAddr\0".as_ref(),
Zeroizing::new(self.view.to_bytes()).as_ref(),
&index.account().to_le_bytes(),
&index.address().to_le_bytes(),
]
.concat(),
))
}
pub(crate) fn subaddress_keys(&self, index: SubaddressIndex) -> (EdwardsPoint, EdwardsPoint) {
let scalar = self.subaddress_derivation(index);
let spend = self.spend + (&scalar * ED25519_BASEPOINT_TABLE);
let view = self.view.deref() * spend;
(spend, view)
}
/// Derive a legacy address from this ViewPair.
///
/// Subaddresses SHOULD be used instead.
pub fn legacy_address(&self, network: Network) -> MoneroAddress {
match MoneroAddress::new(network, AddressType::Legacy, self.spend, self.view()) {
Ok(addr) => addr,
Err(AddressCreationError::SmallOrderView) => {
panic!("small-order view key error despite not making a guaranteed address")
}
}
}
/// Derive a legacy integrated address from this ViewPair.
///
/// Subaddresses SHOULD be used instead.
pub fn legacy_integrated_address(&self, network: Network, payment_id: [u8; 8]) -> MoneroAddress {
match MoneroAddress::new(
network,
AddressType::LegacyIntegrated(payment_id),
self.spend,
self.view(),
) {
Ok(addr) => addr,
Err(AddressCreationError::SmallOrderView) => {
panic!("small-order view key error despite not making a guaranteed address")
}
}
}
/// Derive a subaddress from this ViewPair.
pub fn subaddress(&self, network: Network, subaddress: SubaddressIndex) -> MoneroAddress {
let (spend, view) = self.subaddress_keys(subaddress);
match MoneroAddress::new(network, AddressType::Subaddress, spend, view) {
Ok(addr) => addr,
Err(AddressCreationError::SmallOrderView) => {
panic!("small-order view key error despite not making a guaranteed address")
}
}
}
}
/// The pair of keys necessary to scan outputs immune to the burning bug.
///
/// This is composed of the public spend key and a non-zero private view key.
///
/// 'Guaranteed' outputs, or transactions outputs to the burning bug, are not officially specified
/// by the Monero project. They should only be used if necessary. No support outside of
/// monero-wallet is promised.
#[derive(Clone, Zeroize)]
pub struct GuaranteedViewPair(pub(crate) ViewPair);
impl GuaranteedViewPair {
/// Create a new GuaranteedViewPair.
///
/// This will return None if the view key is of small order (if it's zero).
// Internal doc comment: These scalars are of prime order so 0 is the only small order Scalar
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Option<Self> {
if view.deref() == &Scalar::ZERO {
None?;
}
Some(GuaranteedViewPair(ViewPair::new(spend, view)))
}
/// The public spend key for this GuaranteedViewPair.
pub fn spend(&self) -> EdwardsPoint {
self.0.spend()
}
/// The public view key for this GuaranteedViewPair.
pub fn view(&self) -> EdwardsPoint {
self.0.view()
}
/// Returns an address with the provided specification.
///
/// The returned address will be a featured address with the guaranteed flag set. These should
/// not be presumed to be interoperable with any other software.
pub fn address(
&self,
network: Network,
subaddress: Option<SubaddressIndex>,
payment_id: Option<[u8; 8]>,
) -> MoneroAddress {
let (spend, view) = if let Some(index) = subaddress {
self.0.subaddress_keys(index)
} else {
(self.spend(), self.view())
};
match MoneroAddress::new(
network,
AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed: true },
spend,
view,
) {
Ok(addr) => addr,
Err(AddressCreationError::SmallOrderView) => {
panic!("created a ViewPair with identity as the view key")
}
}
}
}

View File

@@ -15,8 +15,10 @@ test!(
builder.add_payment(addr, 5);
(builder.build().unwrap(), (arbitrary_data,))
},
|_, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data()[0], data.0);
},
@@ -40,8 +42,10 @@ test!(
builder.add_payment(addr, 5);
(builder.build().unwrap(), data)
},
|_, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data(), data);
},
@@ -66,8 +70,10 @@ test!(
builder.add_payment(addr, 5);
(builder.build().unwrap(), data)
},
|_, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data(), vec![data]);
},

View File

@@ -3,7 +3,7 @@ use monero_wallet::{
DEFAULT_LOCK_WINDOW,
transaction::Transaction,
rpc::{OutputResponse, Rpc},
scan::SpendableOutput,
WalletOutput,
};
mod runner;
@@ -16,16 +16,18 @@ test!(
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|rpc: SimpleRequestRpc, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2000000000000);
SpendableOutput::from(&rpc, output).await.unwrap()
output
},
),
(
// Then make a second tx1
|rct_type: RctType, rpc: SimpleRequestRpc, mut builder: Builder, addr, state: _| async move {
let output_tx0: SpendableOutput = state;
let output_tx0: WalletOutput = state;
let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng,
&rpc,
@@ -43,25 +45,26 @@ test!(
(builder.build().unwrap(), (rct_type, output_tx0))
},
// Then make sure DSA selects freshly unlocked output from tx1 as a decoy
|rpc: SimpleRequestRpc, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
|rpc, block, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
use rand_core::OsRng;
let rpc: SimpleRequestRpc = rpc;
let height = rpc.get_height().await.unwrap();
let output_tx1 =
SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0))
.await
.unwrap();
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output_tx1.transaction(), tx.hash());
// Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse =
rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0);
rpc.get_outs(&[output_tx1.index_on_blockchain()]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked);
// Select decoys using spendable output from tx0 as the real, and make sure DSA selects
// the freshly unlocked output from tx1 as a decoy
let (rct_type, output_tx0): (RctType, SpendableOutput) = state;
let (rct_type, output_tx0): (RctType, WalletOutput) = state;
let mut selected_fresh_decoy = false;
let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 {
@@ -75,7 +78,7 @@ test!(
.await
.unwrap();
selected_fresh_decoy = decoys[0].positions().contains(&output_tx1.global_index);
selected_fresh_decoy = decoys[0].positions().contains(&output_tx1.index_on_blockchain());
attempts -= 1;
}
@@ -93,16 +96,18 @@ test!(
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|rpc: SimpleRequestRpc, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2000000000000);
SpendableOutput::from(&rpc, output).await.unwrap()
},
),
(
// Then make a second tx1
|rct_type: RctType, rpc: SimpleRequestRpc, mut builder: Builder, addr, state: _| async move {
let output_tx0: SpendableOutput = state;
|rct_type: RctType, rpc, mut builder: Builder, addr, output_tx0: WalletOutput| async move {
let rpc: SimpleRequestRpc = rpc;
let decoys = Decoys::select(
&mut OsRng,
&rpc,
@@ -120,25 +125,26 @@ test!(
(builder.build().unwrap(), (rct_type, output_tx0))
},
// Then make sure DSA selects freshly unlocked output from tx1 as a decoy
|rpc: SimpleRequestRpc, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
|rpc, block, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
use rand_core::OsRng;
let rpc: SimpleRequestRpc = rpc;
let height = rpc.get_height().await.unwrap();
let output_tx1 =
SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0))
.await
.unwrap();
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output_tx1.transaction(), tx.hash());
// Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse =
rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0);
rpc.get_outs(&[output_tx1.index_on_blockchain()]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked);
// Select decoys using spendable output from tx0 as the real, and make sure DSA selects
// the freshly unlocked output from tx1 as a decoy
let (rct_type, output_tx0): (RctType, SpendableOutput) = state;
let (rct_type, output_tx0): (RctType, WalletOutput) = state;
let mut selected_fresh_decoy = false;
let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 {
@@ -152,7 +158,7 @@ test!(
.await
.unwrap();
selected_fresh_decoy = decoys[0].positions().contains(&output_tx1.global_index);
selected_fresh_decoy = decoys[0].positions().contains(&output_tx1.index_on_blockchain());
attempts -= 1;
}

View File

@@ -21,7 +21,8 @@ test!(
AddressType::Legacy,
ED25519_BASEPOINT_POINT,
ED25519_BASEPOINT_POINT,
),
)
.unwrap(),
1,
);
builder.add_payment(
@@ -30,7 +31,8 @@ test!(
AddressType::LegacyIntegrated([0xaa; 8]),
ED25519_BASEPOINT_POINT,
ED25519_BASEPOINT_POINT,
),
)
.unwrap(),
2,
);
builder.add_payment(
@@ -39,7 +41,8 @@ test!(
AddressType::Subaddress,
ED25519_BASEPOINT_POINT,
ED25519_BASEPOINT_POINT,
),
)
.unwrap(),
3,
);
builder.add_payment(
@@ -48,7 +51,8 @@ test!(
AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true },
ED25519_BASEPOINT_POINT,
ED25519_BASEPOINT_POINT,
),
)
.unwrap(),
4,
);
let tx = builder.build().unwrap();
@@ -59,7 +63,7 @@ test!(
);
(tx, eventuality)
},
|_, mut tx: Transaction, _, eventuality: Eventuality| async move {
|_, _, mut tx: Transaction, _, eventuality: Eventuality| async move {
// 4 explicitly outputs added and one change output
assert_eq!(tx.prefix().outputs.len(), 5);

View File

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

View File

@@ -1,5 +1,5 @@
use core::ops::Deref;
use std_shims::{sync::OnceLock, collections::HashSet};
use std_shims::sync::OnceLock;
use zeroize::Zeroizing;
use rand_core::OsRng;
@@ -12,10 +12,10 @@ use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
ringct::RctType,
transaction::Transaction,
block::Block,
rpc::{Rpc, FeeRate},
ViewPair,
address::{Network, AddressType, AddressSpec, MoneroAddress},
scan::{SpendableOutput, Scanner},
address::{Network, AddressType, MoneroAddress},
ViewPair, GuaranteedViewPair, WalletOutput, Scanner,
};
mod builder;
@@ -41,20 +41,43 @@ pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
AddressType::Legacy,
spend_pub,
view.deref() * ED25519_BASEPOINT_TABLE,
),
)
.unwrap(),
)
}
#[allow(unused)]
pub fn random_guaranteed_address() -> (Scalar, GuaranteedViewPair, MoneroAddress) {
let spend = Scalar::random(&mut OsRng);
let spend_pub = &spend * ED25519_BASEPOINT_TABLE;
let view = Zeroizing::new(Scalar::random(&mut OsRng));
(
spend,
GuaranteedViewPair::new(spend_pub, view.clone()).unwrap(),
MoneroAddress::new(
Network::Mainnet,
AddressType::Legacy,
spend_pub,
view.deref() * ED25519_BASEPOINT_TABLE,
)
.unwrap(),
)
}
// TODO: Support transactions already on-chain
// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions
pub async fn mine_until_unlocked(rpc: &SimpleRequestRpc, addr: &str, tx_hash: [u8; 32]) {
pub async fn mine_until_unlocked(rpc: &SimpleRequestRpc, addr: &str, tx_hash: [u8; 32]) -> Block {
// mine until tx is in a block
let mut height = rpc.get_height().await.unwrap();
let mut found = false;
let mut block = None;
while !found {
let block = rpc.get_block_by_number(height - 1).await.unwrap();
found = match block.txs.iter().find(|&&x| x == tx_hash) {
Some(_) => true,
let inner_block = rpc.get_block_by_number(height - 1).await.unwrap();
found = match inner_block.transactions.iter().find(|&&x| x == tx_hash) {
Some(_) => {
block = Some(inner_block);
true
}
None => {
height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1;
false
@@ -73,22 +96,21 @@ pub async fn mine_until_unlocked(rpc: &SimpleRequestRpc, addr: &str, tx_hash: [u
{
height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1;
}
block.unwrap()
}
// Mines 60 blocks and returns an unlocked miner TX output.
#[allow(dead_code)]
pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> SpendableOutput {
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> WalletOutput {
let mut scanner = Scanner::new(view.clone());
// Mine 60 blocks to unlock a miner TX
let start = rpc.get_height().await.unwrap();
rpc
.generate_blocks(&view.address(Network::Mainnet, AddressSpec::Legacy).to_string(), 60)
.await
.unwrap();
rpc.generate_blocks(&view.legacy_address(Network::Mainnet).to_string(), 60).await.unwrap();
let block = rpc.get_block_by_number(start).await.unwrap();
scanner.scan(rpc, &block).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0)
scanner.scan(rpc, &block).await.unwrap().ignore_additional_timelock().swap_remove(0)
}
/// Make sure the weight and fee match the expected calculation.
@@ -119,6 +141,7 @@ pub async fn rpc() -> SimpleRequestRpc {
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
)
.unwrap()
.to_string();
// Mine 40 blocks to ensure decoy availability
@@ -164,7 +187,6 @@ macro_rules! test {
async_sequential! {
async fn $name() {
use core::{ops::Deref, any::Any};
use std::collections::HashSet;
#[cfg(feature = "multisig")]
use std::collections::HashMap;
@@ -184,10 +206,10 @@ macro_rules! test {
primitives::Decoys,
ringct::RctType,
rpc::FeePriority,
address::{Network, AddressSpec},
address::Network,
ViewPair,
DecoySelection,
scan::Scanner,
Scanner,
send::{Change, SignableTransaction},
};
@@ -226,7 +248,7 @@ macro_rules! test {
let mut outgoing_view = Zeroizing::new([0; 32]);
OsRng.fill_bytes(outgoing_view.as_mut());
let view = ViewPair::new(spend_pub, view_priv.clone());
let addr = view.address(Network::Mainnet, AddressSpec::Legacy);
let addr = view.legacy_address(Network::Mainnet);
let miner_tx = get_miner_tx_output(&rpc, &view).await;
@@ -244,7 +266,6 @@ macro_rules! test {
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
Zeroizing::new(Scalar::random(&mut OsRng))
),
false
),
rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
);
@@ -293,12 +314,12 @@ macro_rules! test {
let fee_rate = tx.fee_rate().clone();
let signed = sign(tx).await;
rpc.publish_transaction(&signed).await.unwrap();
let block =
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
check_weight_and_fee(&tx, fee_rate);
let scanner =
Scanner::from_view(view.clone(), Some(HashSet::new()));
($first_checks)(rpc.clone(), tx, scanner, state).await
let scanner = Scanner::new(view.clone());
($first_checks)(rpc.clone(), block, tx, scanner, state).await
});
#[allow(unused_variables, unused_mut, unused_assignments)]
let mut carried_state: Box<dyn Any> = temp;
@@ -314,6 +335,7 @@ macro_rules! test {
let fee_rate = tx.fee_rate().clone();
let signed = sign(tx).await;
rpc.publish_transaction(&signed).await.unwrap();
let block =
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" {
@@ -323,10 +345,8 @@ macro_rules! test {
}
#[allow(unused_assignments)]
{
let scanner =
Scanner::from_view(view.clone(), Some(HashSet::new()));
carried_state =
Box::new(($checks)(rpc.clone(), tx, scanner, state).await);
let scanner = Scanner::new(view.clone());
carried_state = Box::new(($checks)(rpc.clone(), block, tx, scanner, state).await);
}
)*
}

View File

@@ -1,5 +1,5 @@
use monero_serai::transaction::Transaction;
use monero_wallet::{rpc::Rpc, address::SubaddressIndex, extra::PaymentId};
use monero_wallet::{rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner};
mod runner;
@@ -8,15 +8,16 @@ test!(
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
builder.add_payment(view.address(Network::Mainnet, AddressSpec::Legacy), 5);
let scanner = Scanner::new(view.clone());
builder.add_payment(view.legacy_address(Network::Mainnet), 5);
(builder.build().unwrap(), scanner)
},
|_, tx: Transaction, _, mut state: Scanner| async move {
let output = state.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: Scanner| async move {
let output = state.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
let dummy_payment_id = PaymentId::Encrypted([0u8; 8]);
assert_eq!(output.metadata.payment_id, Some(dummy_payment_id));
assert_eq!(output.payment_id(), Some(dummy_payment_id));
},
),
);
@@ -28,16 +29,18 @@ test!(
let subaddress = SubaddressIndex::new(0, 1).unwrap();
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let mut scanner = Scanner::new(view.clone());
scanner.register_subaddress(subaddress);
builder.add_payment(view.address(Network::Mainnet, AddressSpec::Subaddress(subaddress)), 5);
builder.add_payment(view.subaddress(Network::Mainnet, subaddress), 5);
(builder.build().unwrap(), (scanner, subaddress))
},
|_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move {
let output =
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.subaddress, Some(state.1));
assert_eq!(output.subaddress(), Some(state.1));
},
),
);
@@ -47,160 +50,43 @@ test!(
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let scanner = Scanner::new(view.clone());
let mut payment_id = [0u8; 8];
OsRng.fill_bytes(&mut payment_id);
builder
.add_payment(view.address(Network::Mainnet, AddressSpec::LegacyIntegrated(payment_id)), 5);
builder.add_payment(view.legacy_integrated_address(Network::Mainnet, payment_id), 5);
(builder.build().unwrap(), (scanner, payment_id))
},
|_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move {
let output =
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1)));
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1)));
},
),
);
test!(
scan_featured_standard,
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: false },
),
5,
);
(builder.build().unwrap(), scanner)
},
|_, tx: Transaction, _, mut state: Scanner| async move {
let output = state.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 5);
},
),
);
test!(
scan_featured_subaddress,
scan_guaranteed,
(
|_, mut builder: Builder, _| async move {
let subaddress = SubaddressIndex::new(0, 2).unwrap();
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let view = runner::random_guaranteed_address().1;
let mut scanner = GuaranteedScanner::new(view.clone());
scanner.register_subaddress(subaddress);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: Some(subaddress),
payment_id: None,
guaranteed: false,
},
),
5,
);
builder.add_payment(view.address(Network::Mainnet, None, None), 5);
(builder.build().unwrap(), (scanner, subaddress))
},
|_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.subaddress, Some(state.1));
},
),
);
test!(
scan_featured_integrated,
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let mut payment_id = [0u8; 8];
OsRng.fill_bytes(&mut payment_id);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: None,
payment_id: Some(payment_id),
guaranteed: false,
},
),
5,
);
(builder.build().unwrap(), (scanner, payment_id))
},
|_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1)));
},
),
);
test!(
scan_featured_integrated_subaddress,
(
|_, mut builder: Builder, _| async move {
let subaddress = SubaddressIndex::new(0, 3).unwrap();
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
scanner.register_subaddress(subaddress);
let mut payment_id = [0u8; 8];
OsRng.fill_bytes(&mut payment_id);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: Some(subaddress),
payment_id: Some(payment_id),
guaranteed: false,
},
),
5,
);
(builder.build().unwrap(), (scanner, payment_id, subaddress))
},
|_, tx: Transaction, _, mut state: (Scanner, [u8; 8], SubaddressIndex)| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1)));
assert_eq!(output.metadata.subaddress, Some(state.2));
},
),
);
test!(
scan_guaranteed_standard,
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), None);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true },
),
5,
);
(builder.build().unwrap(), scanner)
},
|_, tx: Transaction, _, mut state: Scanner| async move {
let output = state.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move {
let output =
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.subaddress(), Some(state.1));
},
),
);
@@ -209,29 +95,21 @@ test!(
scan_guaranteed_subaddress,
(
|_, mut builder: Builder, _| async move {
let subaddress = SubaddressIndex::new(1, 0).unwrap();
let subaddress = SubaddressIndex::new(0, 2).unwrap();
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), None);
let view = runner::random_guaranteed_address().1;
let mut scanner = GuaranteedScanner::new(view.clone());
scanner.register_subaddress(subaddress);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: Some(subaddress),
payment_id: None,
guaranteed: true,
},
),
5,
);
builder.add_payment(view.address(Network::Mainnet, Some(subaddress), None), 5);
(builder.build().unwrap(), (scanner, subaddress))
},
|_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move {
let output =
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.subaddress, Some(state.1));
assert_eq!(output.subaddress(), Some(state.1));
},
),
);
@@ -240,63 +118,53 @@ test!(
scan_guaranteed_integrated,
(
|_, mut builder: Builder, _| async move {
let view = runner::random_address().1;
let scanner = Scanner::from_view(view.clone(), None);
let view = runner::random_guaranteed_address().1;
let scanner = GuaranteedScanner::new(view.clone());
let mut payment_id = [0u8; 8];
OsRng.fill_bytes(&mut payment_id);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: None,
payment_id: Some(payment_id),
guaranteed: true,
},
),
5,
);
builder.add_payment(view.address(Network::Mainnet, None, Some(payment_id)), 5);
(builder.build().unwrap(), (scanner, payment_id))
},
|_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move {
let output =
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1)));
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1)));
},
),
);
#[rustfmt::skip]
test!(
scan_guaranteed_integrated_subaddress,
(
|_, mut builder: Builder, _| async move {
let subaddress = SubaddressIndex::new(1, 1).unwrap();
let subaddress = SubaddressIndex::new(0, 3).unwrap();
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), None);
let view = runner::random_guaranteed_address().1;
let mut scanner = GuaranteedScanner::new(view.clone());
scanner.register_subaddress(subaddress);
let mut payment_id = [0u8; 8];
OsRng.fill_bytes(&mut payment_id);
builder.add_payment(
view.address(
Network::Mainnet,
AddressSpec::Featured {
subaddress: Some(subaddress),
payment_id: Some(payment_id),
guaranteed: true,
},
),
5,
);
builder.add_payment(view.address(Network::Mainnet, Some(subaddress), Some(payment_id)), 5);
(builder.build().unwrap(), (scanner, payment_id, subaddress))
},
|_, tx: Transaction, _, mut state: (Scanner, [u8; 8], SubaddressIndex)| async move {
let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0);
|
rpc,
block,
tx: Transaction,
_,
mut state: (GuaranteedScanner, [u8; 8], SubaddressIndex),
| async move {
let output = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1)));
assert_eq!(output.metadata.subaddress, Some(state.2));
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1)));
assert_eq!(output.subaddress(), Some(state.2));
},
),
);

View File

@@ -1,15 +1,11 @@
use std::collections::HashSet;
use rand_core::OsRng;
use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
primitives::Decoys,
ringct::RctType,
transaction::Transaction,
rpc::Rpc,
address::SubaddressIndex,
extra::Extra,
scan::{ReceivedOutput, SpendableOutput},
DecoySelection,
primitives::Decoys, ringct::RctType, transaction::Transaction, rpc::Rpc,
address::SubaddressIndex, extra::Extra, WalletOutput, DecoySelection,
};
mod runner;
@@ -19,25 +15,20 @@ use runner::{SignableTransactionBuilder, ring_len};
async fn add_inputs(
rct_type: RctType,
rpc: &SimpleRequestRpc,
outputs: Vec<ReceivedOutput>,
outputs: Vec<WalletOutput>,
builder: &mut SignableTransactionBuilder,
) {
let mut spendable_outputs = Vec::with_capacity(outputs.len());
for output in outputs {
spendable_outputs.push(SpendableOutput::from(rpc, output).await.unwrap());
}
let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng,
rpc,
ring_len(rct_type),
rpc.get_height().await.unwrap(),
&spendable_outputs,
&outputs,
)
.await
.unwrap();
let inputs = spendable_outputs.into_iter().zip(decoys).collect::<Vec<_>>();
let inputs = outputs.into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
}
@@ -49,8 +40,10 @@ test!(
builder.add_payment(addr, 5);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5);
},
),
@@ -64,8 +57,11 @@ test!(
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].transaction(), tx.hash());
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
assert_eq!(outputs[1].commitment().amount, 2000000000000);
@@ -73,13 +69,15 @@ test!(
},
),
(
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<WalletOutput>| async move {
add_inputs(rct_type, &rpc, outputs, &mut builder).await;
builder.add_payment(addr, 6);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 6);
},
),
@@ -95,15 +93,16 @@ test!(
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|rct_type, rpc: SimpleRequestRpc, _, _, outputs: Vec<ReceivedOutput>| async move {
|rct_type, rpc: SimpleRequestRpc, _, _, outputs: Vec<WalletOutput>| async move {
use monero_wallet::rpc::FeePriority;
let view_priv = Zeroizing::new(Scalar::random(&mut OsRng));
@@ -115,7 +114,7 @@ test!(
let mut builder = SignableTransactionBuilder::new(
rct_type,
outgoing_view,
Change::new(&change_view, false),
Change::new(&change_view),
rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
);
add_inputs(rct_type, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
@@ -125,23 +124,23 @@ test!(
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
Zeroizing::new(Scalar::random(&mut OsRng)),
);
builder.add_payment(
sub_view
.address(Network::Mainnet, AddressSpec::Subaddress(SubaddressIndex::new(0, 1).unwrap())),
1,
);
builder
.add_payment(sub_view.subaddress(Network::Mainnet, SubaddressIndex::new(0, 1).unwrap()), 1);
(builder.build().unwrap(), (change_view, sub_view))
},
|_, tx: Transaction, _, views: (ViewPair, ViewPair)| async move {
|rpc, block, tx: Transaction, _, views: (ViewPair, ViewPair)| async move {
// Make sure the change can pick up its output
let mut change_scanner = Scanner::from_view(views.0, Some(HashSet::new()));
assert!(change_scanner.scan_transaction(&tx).not_locked().len() == 1);
let mut change_scanner = Scanner::new(views.0);
assert!(
change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().len() == 1
);
// Make sure the subaddress can pick up its output
let mut sub_scanner = Scanner::from_view(views.1, Some(HashSet::new()));
let mut sub_scanner = Scanner::new(views.1);
sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap());
let sub_outputs = sub_scanner.scan_transaction(&tx).not_locked();
let sub_outputs = sub_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert!(sub_outputs.len() == 1);
assert_eq!(sub_outputs[0].transaction(), tx.hash());
assert_eq!(sub_outputs[0].commitment().amount, 1);
// Make sure only one R was included in TX extra
@@ -162,21 +161,24 @@ test!(
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 2000000000000);
outputs
},
),
(
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<WalletOutput>| async move {
add_inputs(rct_type, &rpc, outputs, &mut builder).await;
builder.add_payment(addr, 2);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output =
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2);
},
),
@@ -189,15 +191,16 @@ test!(
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
|rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec<WalletOutput>| async move {
add_inputs(rct_type, &rpc, outputs, &mut builder).await;
for i in 0 .. 15 {
@@ -205,8 +208,8 @@ test!(
}
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut scanned_tx = scanner.scan_transaction(&tx).not_locked();
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut scanned_tx = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
let mut output_amounts = HashSet::new();
for i in 0 .. 15 {
@@ -214,10 +217,11 @@ test!(
}
for _ in 0 .. 15 {
let output = scanned_tx.swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
let amount = output.commitment().amount;
assert!(output_amounts.contains(&amount));
output_amounts.remove(&amount);
assert!(output_amounts.remove(&amount));
}
assert_eq!(output_amounts.len(), 0);
},
),
);
@@ -229,38 +233,36 @@ test!(
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|rct_type: RctType, rpc, mut builder: Builder, _, outputs: Vec<ReceivedOutput>| async move {
|rct_type: RctType, rpc, mut builder: Builder, _, outputs: Vec<WalletOutput>| async move {
add_inputs(rct_type, &rpc, outputs, &mut builder).await;
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let mut scanner = Scanner::new(view.clone());
let mut subaddresses = vec![];
for i in 0 .. 15 {
let subaddress = SubaddressIndex::new(0, i + 1).unwrap();
scanner.register_subaddress(subaddress);
builder.add_payment(
view.address(Network::Mainnet, AddressSpec::Subaddress(subaddress)),
u64::from(i + 1),
);
builder.add_payment(view.subaddress(Network::Mainnet, subaddress), u64::from(i + 1));
subaddresses.push(subaddress);
}
(builder.build().unwrap(), (scanner, subaddresses))
},
|_, tx: Transaction, _, mut state: (Scanner, Vec<SubaddressIndex>)| async move {
|rpc, block, tx: Transaction, _, mut state: (Scanner, Vec<SubaddressIndex>)| async move {
use std::collections::HashMap;
let mut scanned_tx = state.0.scan_transaction(&tx).not_locked();
let mut scanned_tx = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked();
let mut output_amounts_by_subaddress = HashMap::new();
for i in 0 .. 15 {
@@ -268,13 +270,15 @@ test!(
}
for _ in 0 .. 15 {
let output = scanned_tx.swap_remove(0);
assert_eq!(output.transaction(), tx.hash());
let amount = output.commitment().amount;
assert!(output_amounts_by_subaddress.contains_key(&amount));
assert_eq!(output.metadata.subaddress, Some(output_amounts_by_subaddress[&amount]));
output_amounts_by_subaddress.remove(&amount);
assert_eq!(
output.subaddress().unwrap(),
output_amounts_by_subaddress.remove(&amount).unwrap()
);
}
assert_eq!(output_amounts_by_subaddress.len(), 0);
},
),
);
@@ -286,15 +290,16 @@ test!(
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|rct_type, rpc: SimpleRequestRpc, _, addr, outputs: Vec<ReceivedOutput>| async move {
|rct_type, rpc: SimpleRequestRpc, _, addr, outputs: Vec<WalletOutput>| async move {
use monero_wallet::rpc::FeePriority;
let mut outgoing_view = Zeroizing::new([0; 32]);
@@ -311,8 +316,11 @@ test!(
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[1].transaction(), tx.hash());
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 10000);
assert_eq!(outputs[1].commitment().amount, 50000);

View File

@@ -1,5 +1,3 @@
use std::collections::HashSet;
use rand_core::{OsRng, RngCore};
use serde::Deserialize;
@@ -9,13 +7,20 @@ use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
transaction::Transaction,
rpc::Rpc,
address::{Network, AddressSpec, SubaddressIndex, MoneroAddress},
address::{Network, SubaddressIndex, MoneroAddress},
extra::{MAX_TX_EXTRA_NONCE_SIZE, Extra, PaymentId},
scan::Scanner,
Scanner,
};
mod runner;
#[derive(Clone, Copy, PartialEq, Eq)]
enum AddressSpec {
Legacy,
LegacyIntegrated([u8; 8]),
Subaddress(SubaddressIndex),
}
#[derive(Deserialize, Debug)]
struct EmptyResponse {}
@@ -70,7 +75,13 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) {
// make an addr
let (_, view_pair, _) = runner::random_address();
let addr = view_pair.address(Network::Mainnet, spec);
let addr = match spec {
AddressSpec::Legacy => view_pair.legacy_address(Network::Mainnet),
AddressSpec::LegacyIntegrated(payment_id) => {
view_pair.legacy_integrated_address(Network::Mainnet, payment_id)
}
AddressSpec::Subaddress(index) => view_pair.subaddress(Network::Mainnet, index),
};
// refresh & make a tx
let _: EmptyResponse = wallet_rpc.json_rpc_call("refresh", None).await.unwrap();
@@ -97,33 +108,34 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) {
// .unwrap();
// unlock it
runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await;
let block = runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await;
// Create the scanner
let mut scanner = Scanner::from_view(view_pair, Some(HashSet::new()));
let mut scanner = Scanner::new(view_pair);
if let AddressSpec::Subaddress(index) = spec {
scanner.register_subaddress(index);
}
// Retrieve it and scan it
let tx = daemon_rpc.get_transaction(tx_hash).await.unwrap();
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
let output =
scanner.scan(&daemon_rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx_hash);
// TODO: Needs https://github.com/monero-project/monero/pull/9260
// runner::check_weight_and_fee(&tx, fee_rate);
match spec {
AddressSpec::Subaddress(index) => {
assert_eq!(output.metadata.subaddress, Some(index));
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8])));
assert_eq!(output.subaddress(), Some(index));
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted([0u8; 8])));
}
AddressSpec::LegacyIntegrated(payment_id) => {
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(payment_id)));
assert_eq!(output.metadata.subaddress, None);
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(payment_id)));
assert_eq!(output.subaddress(), None);
}
AddressSpec::Legacy | AddressSpec::Featured { .. } => {
assert_eq!(output.metadata.subaddress, None);
assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8])));
AddressSpec::Legacy => {
assert_eq!(output.subaddress(), None);
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted([0u8; 8])));
}
}
assert_eq!(output.commitment().amount, 1000000000000);
@@ -176,7 +188,7 @@ test!(
.add_payment(MoneroAddress::from_str(Network::Mainnet, &wallet_rpc_addr).unwrap(), 1000000);
(builder.build().unwrap(), wallet_rpc)
},
|_, tx: Transaction, _, data: SimpleRequestRpc| async move {
|_, _, tx: Transaction, _, data: SimpleRequestRpc| async move {
// confirm receipt
let _: EmptyResponse = data.json_rpc_call("refresh", None).await.unwrap();
let transfer: TransfersResponse = data
@@ -210,7 +222,7 @@ test!(
.add_payment(MoneroAddress::from_str(Network::Mainnet, &addr.address).unwrap(), 1000000);
(builder.build().unwrap(), (wallet_rpc, addr.account_index))
},
|_, tx: Transaction, _, data: (SimpleRequestRpc, u32)| async move {
|_, _, tx: Transaction, _, data: (SimpleRequestRpc, u32)| async move {
// confirm receipt
let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap();
let transfer: TransfersResponse = data
@@ -262,7 +274,7 @@ test!(
]);
(builder.build().unwrap(), (wallet_rpc, daemon_rpc, addrs.address_index))
},
|_, tx: Transaction, _, data: (SimpleRequestRpc, SimpleRequestRpc, u32)| async move {
|_, _, tx: Transaction, _, data: (SimpleRequestRpc, SimpleRequestRpc, u32)| async move {
// confirm receipt
let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap();
let transfer: TransfersResponse = data
@@ -307,7 +319,7 @@ test!(
builder.add_payment(MoneroAddress::from_str(Network::Mainnet, &addr).unwrap(), 1000000);
(builder.build().unwrap(), (wallet_rpc, payment_id))
},
|_, tx: Transaction, _, data: (SimpleRequestRpc, [u8; 8])| async move {
|_, _, tx: Transaction, _, data: (SimpleRequestRpc, [u8; 8])| async move {
// confirm receipt
let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap();
let transfer: TransfersResponse = data
@@ -342,7 +354,7 @@ test!(
(builder.build().unwrap(), wallet_rpc)
},
|_, tx: Transaction, _, data: SimpleRequestRpc| async move {
|_, _, tx: Transaction, _, data: SimpleRequestRpc| async move {
// confirm receipt
let _: EmptyResponse = data.json_rpc_call("refresh", None).await.unwrap();
let transfer: TransfersResponse = data

View File

@@ -19,13 +19,14 @@ use monero_wallet::{
transaction::Transaction,
block::Block,
rpc::{FeeRate, RpcError, Rpc},
address::{Network as MoneroNetwork, SubaddressIndex, AddressSpec},
ViewPair, DecoySelection, Decoys,
scan::{SpendableOutput, Scanner},
address::{Network as MoneroNetwork, SubaddressIndex},
ViewPair, GuaranteedViewPair, WalletOutput, GuaranteedScanner, DecoySelection, Decoys,
send::{
SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine,
},
};
#[cfg(test)]
use monero_wallet::Scanner;
use tokio::time::sleep;
@@ -45,7 +46,7 @@ use crate::{
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Output(SpendableOutput, Vec<u8>);
pub struct Output(WalletOutput);
const EXTERNAL_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(0, 0);
const BRANCH_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(1, 0);
@@ -59,7 +60,7 @@ impl OutputTrait<Monero> for Output {
type Id = [u8; 32];
fn kind(&self) -> OutputType {
match self.0.output.metadata.subaddress {
match self.0.subaddress() {
EXTERNAL_SUBADDRESS => OutputType::External,
BRANCH_SUBADDRESS => OutputType::Branch,
CHANGE_SUBADDRESS => OutputType::Change,
@@ -69,15 +70,15 @@ impl OutputTrait<Monero> for Output {
}
fn id(&self) -> Self::Id {
self.0.output.data.key.compress().to_bytes()
self.0.key().compress().to_bytes()
}
fn tx_id(&self) -> [u8; 32] {
self.0.output.absolute.tx
self.0.transaction()
}
fn key(&self) -> EdwardsPoint {
EdwardsPoint(self.0.output.data.key - (EdwardsPoint::generator().0 * self.0.key_offset()))
EdwardsPoint(self.0.key() - (EdwardsPoint::generator().0 * self.0.key_offset()))
}
fn presumed_origin(&self) -> Option<Address> {
@@ -89,26 +90,22 @@ impl OutputTrait<Monero> for Output {
}
fn data(&self) -> &[u8] {
&self.1
let Some(data) = self.0.arbitrary_data().first() else { return &[] };
// If the data is too large, prune it
// This should cause decoding the instruction to fail, and trigger a refund as appropriate
if data.len() > usize::try_from(MAX_DATA_LEN).unwrap() {
return &[];
}
data
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
self.0.write(writer)?;
writer.write_all(&u16::try_from(self.1.len()).unwrap().to_le_bytes())?;
writer.write_all(&self.1)?;
Ok(())
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let output = SpendableOutput::read(reader)?;
let mut data_len = [0; 2];
reader.read_exact(&mut data_len)?;
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
reader.read_exact(&mut data)?;
Ok(Output(output, data))
Ok(Output(WalletOutput::read(reader)?))
}
}
@@ -258,20 +255,16 @@ impl Monero {
Monero { rpc: res.unwrap() }
}
fn view_pair(spend: EdwardsPoint) -> ViewPair {
ViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0))
fn view_pair(spend: EdwardsPoint) -> GuaranteedViewPair {
GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0)).unwrap()
}
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
Address::new(Self::view_pair(spend).address(
MoneroNetwork::Mainnet,
AddressSpec::Featured { subaddress, payment_id: None, guaranteed: true },
))
.unwrap()
Address::new(Self::view_pair(spend).address(MoneroNetwork::Mainnet, subaddress, None)).unwrap()
}
fn scanner(spend: EdwardsPoint) -> Scanner {
let mut scanner = Scanner::from_view(Self::view_pair(spend), None);
fn scanner(spend: EdwardsPoint) -> GuaranteedScanner {
let mut scanner = GuaranteedScanner::new(Self::view_pair(spend));
debug_assert!(EXTERNAL_SUBADDRESS.is_none());
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
@@ -281,7 +274,7 @@ impl Monero {
async fn median_fee(&self, block: &Block) -> Result<FeeRate, NetworkError> {
let mut fees = vec![];
for tx_hash in &block.txs {
for tx_hash in &block.transactions {
let tx =
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
@@ -358,7 +351,7 @@ impl Monero {
payments.push(Payment {
address: Address::new(
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
.address(MoneroNetwork::Mainnet, AddressSpec::Legacy),
.legacy_address(MoneroNetwork::Mainnet),
)
.unwrap(),
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
@@ -425,13 +418,12 @@ impl Monero {
#[cfg(test)]
fn test_scanner() -> Scanner {
Scanner::from_view(Self::test_view_pair(), Some(std::collections::HashSet::new()))
Scanner::new(Self::test_view_pair())
}
#[cfg(test)]
fn test_address() -> Address {
Address::new(Self::test_view_pair().address(MoneroNetwork::Mainnet, AddressSpec::Legacy))
.unwrap()
Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap()
}
}
@@ -512,34 +504,17 @@ impl Network for Monero {
}
};
let mut txs = outputs
.iter()
.filter_map(|outputs| Some(outputs.not_locked()).filter(|outputs| !outputs.is_empty()))
.collect::<Vec<_>>();
// Miner transactions are required to explicitly state their timelock, so this does exclude
// those (which have an extended timelock we don't want to deal with)
let raw_outputs = outputs.not_additionally_locked();
let mut outputs = Vec::with_capacity(raw_outputs.len());
for output in raw_outputs {
// This should be pointless as we shouldn't be able to scan for any other subaddress
// This just ensures nothing invalid makes it through
for tx_outputs in &txs {
for output in tx_outputs {
// This just helps ensures nothing invalid makes it through
assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARD_SUBADDRESS]
.contains(&output.output.metadata.subaddress));
}
}
.contains(&output.subaddress()));
let mut outputs = Vec::with_capacity(txs.len());
for mut tx_outputs in txs.drain(..) {
for output in tx_outputs.drain(..) {
let mut data = output.arbitrary_data().first().cloned().unwrap_or(vec![]);
// The Output serialization code above uses u16 to represent length
data.truncate(u16::MAX.into());
// Monero data segments should be <= 255 already, and MAX_DATA_LEN is currently 512
// This just allows either Monero to change, or MAX_DATA_LEN to change, without introducing
// complicationso
data.truncate(MAX_DATA_LEN.try_into().unwrap());
outputs.push(Output(output, data));
}
outputs.push(Output(output));
}
outputs
@@ -561,7 +536,7 @@ impl Network for Monero {
block: &Block,
res: &mut HashMap<[u8; 32], (usize, [u8; 32], Transaction)>,
) {
for hash in &block.txs {
for hash in &block.transactions {
let tx = {
let mut tx;
while {
@@ -708,7 +683,7 @@ impl Network for Monero {
eventuality: &Eventuality,
) -> Transaction {
let block = self.rpc.get_block_by_number(block).await.unwrap();
for tx in &block.txs {
for tx in &block.transactions {
let tx = self.rpc.get_transaction(*tx).await.unwrap();
if eventuality.matches(&tx) {
return tx;
@@ -752,12 +727,8 @@ impl Network for Monero {
}
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
let outputs = Self::test_scanner()
.scan(&self.rpc, &new_block)
.await
.unwrap()
.swap_remove(0)
.ignore_timelock();
let outputs =
Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock();
let amount = outputs[0].commitment().amount;
// The dust should always be sufficient for the fee

View File

@@ -51,7 +51,8 @@ impl TryFrom<Vec<u8>> for Address {
// Decode as SCALE
let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?;
// Convert over
Ok(Address(MoneroAddress::new(
Ok(Address(
MoneroAddress::new(
Network::Mainnet,
match addr.kind {
EncodedAddressType::Legacy => AddressType::Legacy,
@@ -68,7 +69,9 @@ impl TryFrom<Vec<u8>> for Address {
},
Ed25519::read_G::<&[u8]>(&mut addr.spend.as_ref()).map_err(|_| ())?.0,
Ed25519::read_G::<&[u8]>(&mut addr.view.as_ref()).map_err(|_| ())?.0,
)))
)
.map_err(|_| ())?,
))
}
}

View File

@@ -1,7 +1,6 @@
use std::{
sync::{OnceLock, Arc, Mutex},
time::{Duration, Instant},
collections::HashSet,
};
use zeroize::Zeroizing;
@@ -88,14 +87,10 @@ async fn mint_and_burn_test() {
// Mine a Monero block
let monero_blocks = {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_wallet::{
rpc::Rpc,
ViewPair,
address::{Network, AddressSpec},
};
use monero_wallet::{rpc::Rpc, ViewPair, address::Network};
let addr = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE))
.address(Network::Mainnet, AddressSpec::Legacy)
.legacy_address(Network::Mainnet)
.to_string();
let rpc = producer_handles.monero(ops).await;
@@ -104,8 +99,8 @@ async fn mint_and_burn_test() {
let block =
rpc.get_block(rpc.generate_blocks(&addr, 1).await.unwrap().0[0]).await.unwrap();
let mut txs = Vec::with_capacity(block.txs.len());
for tx in &block.txs {
let mut txs = Vec::with_capacity(block.transactions.len());
for tx in &block.transactions {
txs.push(rpc.get_transaction(*tx).await.unwrap());
}
res.push((serde_json::json!([hex::encode(block.serialize())]), txs));
@@ -351,25 +346,21 @@ async fn mint_and_burn_test() {
use monero_wallet::{
io::decompress_point,
ringct::RctType,
transaction::Timelock,
rpc::{FeePriority, Rpc},
ViewPair, DecoySelection, Decoys,
address::{Network, AddressType, MoneroAddress},
scan::Scanner,
ViewPair, Scanner, DecoySelection, Decoys,
send::{Change, SignableTransaction},
};
// Grab the first output on the chain
let rpc = handles[0].monero(&ops).await;
let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE));
let mut scanner = Scanner::from_view(view_pair.clone(), Some(HashSet::new()));
let mut scanner = Scanner::new(view_pair.clone());
let output = scanner
.scan(&rpc, &rpc.get_block_by_number(1).await.unwrap())
.await
.unwrap()
.swap_remove(0)
.unlocked(Timelock::Block(rpc.get_height().await.unwrap()))
.unwrap()
.additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0)
.swap_remove(0);
let decoys = Decoys::fingerprintable_canonical_select(
@@ -396,10 +387,11 @@ async fn mint_and_burn_test() {
decompress_point(monero_key_pair.1.to_vec().try_into().unwrap()).unwrap(),
ED25519_BASEPOINT_POINT *
processor::additional_key::<processor::networks::monero::Monero>(0).0,
),
)
.unwrap(),
1_100_000_000_000,
)],
Change::new(&view_pair, false),
Change::new(&view_pair),
vec![Shorthand::transfer(None, serai_addr).encode()],
rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
)
@@ -483,7 +475,8 @@ async fn mint_and_burn_test() {
AddressType::Legacy,
spend,
ED25519_BASEPOINT_TABLE * &view,
);
)
.unwrap();
(spend, view, addr)
};
@@ -586,12 +579,9 @@ async fn mint_and_burn_test() {
// Verify the received Monero TX
{
use monero_wallet::{transaction::Transaction, rpc::Rpc, ViewPair, scan::Scanner};
use monero_wallet::{transaction::Transaction, rpc::Rpc, ViewPair, Scanner};
let rpc = handles[0].monero(&ops).await;
let mut scanner = Scanner::from_view(
ViewPair::new(monero_spend, Zeroizing::new(monero_view)),
Some(HashSet::new()),
);
let mut scanner = Scanner::new(ViewPair::new(monero_spend, Zeroizing::new(monero_view)));
// Check for up to 5 minutes
let mut found = false;
@@ -599,14 +589,12 @@ async fn mint_and_burn_test() {
while i < (5 * 6) {
if let Ok(block) = rpc.get_block_by_number(start_monero_block).await {
start_monero_block += 1;
let outputs = scanner.scan(&rpc, &block).await.unwrap();
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked();
if !outputs.is_empty() {
assert_eq!(outputs.len(), 1);
let outputs = outputs[0].not_locked();
assert_eq!(outputs.len(), 1);
assert_eq!(block.txs.len(), 1);
let tx = rpc.get_transaction(block.txs[0]).await.unwrap();
assert_eq!(block.transactions.len(), 1);
let tx = rpc.get_transaction(block.transactions[0]).await.unwrap();
let tx_fee = match &tx {
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
_ => panic!("fetched TX wasn't a signed V2 TX"),

View File

@@ -405,11 +405,7 @@ impl Coordinator {
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
rpc::Rpc,
ViewPair,
address::{Network, AddressSpec},
};
use monero_wallet::{rpc::Rpc, address::Network, ViewPair};
let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC");
let _: EmptyResponse = rpc
@@ -419,7 +415,7 @@ impl Coordinator {
"wallet_address": ViewPair::new(
ED25519_BASEPOINT_POINT,
Zeroizing::new(Scalar::ONE),
).address(Network::Mainnet, AddressSpec::Legacy).to_string(),
).legacy_address(Network::Mainnet).to_string(),
"amount_of_blocks": 1,
})),
)

View File

@@ -1,5 +1,3 @@
use std::collections::HashSet;
use zeroize::Zeroizing;
use rand_core::{RngCore, OsRng};
@@ -104,7 +102,7 @@ pub enum Wallet {
handle: String,
spend_key: Zeroizing<curve25519_dalek::scalar::Scalar>,
view_pair: monero_wallet::ViewPair,
inputs: Vec<monero_wallet::scan::ReceivedOutput>,
last_tx: (usize, [u8; 32]),
},
}
@@ -190,18 +188,10 @@ impl Wallet {
NetworkId::Monero => {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
rpc::Rpc,
address::{Network, AddressSpec},
ViewPair,
scan::Scanner,
};
use monero_wallet::{rpc::Rpc, address::Network, ViewPair};
let mut bytes = [0; 64];
OsRng.fill_bytes(&mut bytes);
let spend_key = Scalar::from_bytes_mod_order_wide(&bytes);
OsRng.fill_bytes(&mut bytes);
let view_key = Scalar::from_bytes_mod_order_wide(&bytes);
let spend_key = Scalar::random(&mut OsRng);
let view_key = Scalar::random(&mut OsRng);
let view_pair =
ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key));
@@ -214,10 +204,7 @@ impl Wallet {
.json_rpc_call(
"generateblocks",
Some(serde_json::json!({
"wallet_address": view_pair.address(
Network::Mainnet,
AddressSpec::Legacy
).to_string(),
"wallet_address": view_pair.legacy_address(Network::Mainnet).to_string(),
"amount_of_blocks": 200,
})),
)
@@ -225,19 +212,11 @@ impl Wallet {
.unwrap();
let block = rpc.get_block(rpc.get_block_hash(height).await.unwrap()).await.unwrap();
let output = Scanner::from_view(view_pair.clone(), Some(HashSet::new()))
.scan(&rpc, &block)
.await
.unwrap()
.remove(0)
.ignore_timelock()
.remove(0);
Wallet::Monero {
handle,
spend_key: Zeroizing::new(spend_key),
view_pair,
inputs: vec![output.output.clone()],
last_tx: (height, block.miner_transaction.hash()),
}
}
NetworkId::Serai => panic!("creating a wallet for for Serai"),
@@ -434,7 +413,7 @@ impl Wallet {
)
}
Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => {
Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut last_tx } => {
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
@@ -442,8 +421,7 @@ impl Wallet {
ringct::RctType,
rpc::{FeePriority, Rpc},
address::{Network, AddressType, Address},
DecoySelection, Decoys,
scan::{SpendableOutput, Scanner},
Scanner, DecoySelection, Decoys,
send::{Change, SignableTransaction},
};
use processor::{additional_key, networks::Monero};
@@ -452,21 +430,28 @@ impl Wallet {
let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC");
// Prepare inputs
let outputs = std::mem::take(inputs);
let mut these_inputs = vec![];
for output in outputs {
these_inputs.push(
SpendableOutput::from(&rpc, output)
let current_height = rpc.get_height().await.unwrap();
let mut inputs = vec![];
for block in last_tx.0 .. current_height {
let block = rpc.get_block_by_number(block).await.unwrap();
if (block.miner_transaction.hash() == last_tx.1) ||
block.transactions.contains(&last_tx.1)
{
inputs = Scanner::new(view_pair.clone())
.scan(&rpc, &block)
.await
.expect("prior transaction was never published"),
);
.unwrap()
.ignore_additional_timelock();
}
}
assert!(!inputs.is_empty());
let mut decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng,
&rpc,
16,
rpc.get_height().await.unwrap(),
&these_inputs,
&inputs,
)
.await
.unwrap();
@@ -478,7 +463,8 @@ impl Wallet {
AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true },
to_spend_key,
ED25519_BASEPOINT_POINT * to_view_key.0,
);
)
.unwrap();
// Create and sign the TX
const AMOUNT: u64 = 1_000_000_000_000;
@@ -491,9 +477,9 @@ impl Wallet {
let tx = SignableTransaction::new(
RctType::ClsagBulletproofPlus,
outgoing_view_key,
these_inputs.drain(..).zip(decoys.drain(..)).collect(),
inputs.drain(..).zip(decoys.drain(..)).collect(),
vec![(to_addr, AMOUNT)],
Change::new(view_pair, false),
Change::new(view_pair),
data,
rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
)
@@ -501,13 +487,9 @@ impl Wallet {
.sign(&mut OsRng, spend_key)
.unwrap();
// Push the change output
inputs.push(
Scanner::from_view(view_pair.clone(), Some(HashSet::new()))
.scan_transaction(&tx)
.ignore_timelock()
.remove(0),
);
// Update the last TX to track the change output
last_tx.0 = current_height;
last_tx.1 = tx.hash();
(tx.serialize(), Balance { coin: Coin::Monero, amount: Amount(AMOUNT) })
}
@@ -532,9 +514,9 @@ impl Wallet {
)
.unwrap(),
Wallet::Monero { view_pair, .. } => {
use monero_wallet::address::{Network, AddressSpec};
use monero_wallet::address::Network;
ExternalAddress::new(
networks::monero::Address::new(view_pair.address(Network::Mainnet, AddressSpec::Legacy))
networks::monero::Address::new(view_pair.legacy_address(Network::Mainnet))
.unwrap()
.into(),
)