mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 05:59:23 +00:00
Clean the Monero lib for auditing (#577)
* Remove unsafe creation of dalek_ff_group::EdwardsPoint in BP+ * Rename Bulletproofs to Bulletproof, since they are a single Bulletproof Also bifurcates prove with prove_plus, and adds a few documentation items. * Make CLSAG signing private Also adds a bit more documentation and does a bit more tidying. * Remove the distribution cache It's a notable bandwidth/performance improvement, yet it's not ready. We need a dedicated Distribution struct which is managed by the wallet and passed in. While we can do that now, it's not currently worth the effort. * Tidy Borromean/MLSAG a tad * Remove experimental feature from monero-serai * Move amount_decryption into EncryptedAmount::decrypt * Various RingCT doc comments * Begin crate smashing * Further documentation, start shoring up API boundaries of existing crates * Document and clean clsag * Add a dedicated send/recv CLSAG mask struct Abstracts the types used internally. Also moves the tests from monero-serai to monero-clsag. * Smash out monero-bulletproofs Removes usage of dalek-ff-group/multiexp for curve25519-dalek. Makes compiling in the generators an optional feature. Adds a structured batch verifier which should be notably more performant. Documentation and clean up still necessary. * Correct no-std builds for monero-clsag and monero-bulletproofs * Tidy and document monero-bulletproofs I still don't like the impl of the original Bulletproofs... * Error if missing documentation * Smash out MLSAG * Smash out Borromean * Tidy up monero-serai as a meta crate * Smash out RPC, wallet * Document the RPC * Improve docs a bit * Move Protocol to monero-wallet * Incomplete work on using Option to remove panic cases * Finish documenting monero-serai * Remove TODO on reading pseudo_outs for AggregateMlsagBorromean * Only read transactions with one Input::Gen or all Input::ToKey Also adds a helper to fetch a transaction's prefix. * Smash out polyseed * Smash out seed * Get the repo to compile again * Smash out Monero addresses * Document cargo features Credit to @hinto-janai for adding such sections to their work on documenting monero-serai in #568. * Fix deserializing v2 miner transactions * Rewrite monero-wallet's send code I have yet to redo the multisig code and the builder. This should be much cleaner, albeit slower due to redoing work. This compiles with clippy --all-features. I have to finish the multisig/builder for --all-targets to work (and start updating the rest of Serai). * Add SignableTransaction Read/Write * Restore Monero multisig TX code * Correct invalid RPC type def in monero-rpc * Update monero-wallet tests to compile Some are _consistently_ failing due to the inputs we attempt to spend being too young. I'm unsure what's up with that. Most seem to pass _consistently_, implying it's not a random issue yet some configuration/env aspect. * Clean and document monero-address * Sync rest of repo with monero-serai changes * Represent height/block number as a u32 * Diversify ViewPair/Scanner into ViewPair/GuaranteedViewPair and Scanner/GuaranteedScanner Also cleans the Scanner impl. * Remove non-small-order view key bound Guaranteed addresses are in fact guaranteed even with this due to prefixing key images causing zeroing the ECDH to not zero the shared key. * Finish documenting monero-serai * Correct imports for no-std * Remove possible panic in monero-serai on systems < 32 bits This was done by requiring the system's usize can represent a certain number. * Restore the reserialize chain binary * fmt, machete, GH CI * Correct misc TODOs in monero-serai * Have Monero test runner evaluate an Eventuality for all signed TXs * Fix a pair of bugs in the decoy tests Unfortunately, this test is still failing. * Fix remaining bugs in monero-wallet tests * Reject torsioned spend keys to ensure we can spend the outputs we scan * Tidy inlined epee code in the RPC * Correct the accidental swap of stagenet/testnet address bytes * Remove unused dep from processor * Handle Monero fee logic properly in the processor * Document v2 TX/RCT output relation assumed when scanning * Adjust how we mine the initial blocks due to some CI test failures * Fix weight estimation for RctType::ClsagBulletproof TXs * Again increase the amount of blocks we mine prior to running tests * Correct the if check about when to mine blocks on start Finally fixes the lack of decoy candidates failures in CI. * Run Monero on Debian, even for internal testnets Change made due to a segfault incurred when locally testing. https://github.com/monero-project/monero/issues/9141 for the upstream. * Don't attempt running tests on the verify-chain binary Adds a minimum XMR fee to the processor and runs fmt. * Increase minimum Monero fee in processor I'm truly unsure why this is required right now. * Distinguish fee from necessary_fee in monero-wallet If there's no change, the fee is difference of the inputs to the outputs. The prior code wouldn't check that amount is greater than or equal to the necessary fee, and returning the would-be change amount as the fee isn't necessarily helpful. Now the fee is validated in such cases and the necessary fee is returned, enabling operating off of that. * Restore minimum Monero fee from develop
This commit is contained in:
137
coins/monero/wallet/src/send/eventuality.rs
Normal file
137
coins/monero/wallet/src/send/eventuality.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use std_shims::{vec::Vec, io};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
ringct::RctProofs,
|
||||
transaction::{Input, Timelock, Transaction},
|
||||
send::SignableTransaction,
|
||||
};
|
||||
|
||||
/// 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);
|
||||
|
||||
impl From<SignableTransaction> for Eventuality {
|
||||
fn from(tx: SignableTransaction) -> Eventuality {
|
||||
Eventuality(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eventuality {
|
||||
/// Return the `extra` field any transaction following this intent would use.
|
||||
///
|
||||
/// 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 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()
|
||||
}
|
||||
|
||||
/// Return if this TX matches the SignableTransaction this was created from.
|
||||
///
|
||||
/// Matching the SignableTransaction means this transaction created the expected outputs, they're
|
||||
/// scannable, they're not locked, and this transaction claims to use the intended inputs (though
|
||||
/// this is not guaranteed). This 'claim' is evaluated by this transaction using the transaction
|
||||
/// keys derived from the intended inputs. This ensures two SignableTransactions with the same
|
||||
/// intended payments don't match for each other's `Eventuality`s (as they'll have distinct
|
||||
/// inputs intended).
|
||||
#[must_use]
|
||||
pub fn matches(&self, tx: &Transaction) -> bool {
|
||||
// Verify extra
|
||||
if self.0.extra() != tx.prefix().extra {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also ensure no timelock was set
|
||||
if tx.prefix().additional_timelock != Timelock::None {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the amount of inputs aligns
|
||||
if tx.prefix().inputs.len() != self.0.inputs.len() {
|
||||
return false;
|
||||
}
|
||||
// Collect the key images used by this transaction
|
||||
let Ok(key_images) = tx
|
||||
.prefix()
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|input| match input {
|
||||
Input::Gen(_) => Err(()),
|
||||
Input::ToKey { key_image, .. } => Ok(*key_image),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check the outputs
|
||||
if self.0.outputs(&key_images) != tx.prefix().outputs {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the encrypted amounts and commitments
|
||||
let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images);
|
||||
let Transaction::V2 { proofs: Some(RctProofs { ref base, .. }), .. } = tx else {
|
||||
return false;
|
||||
};
|
||||
if base.commitments !=
|
||||
commitments_and_encrypted_amounts
|
||||
.iter()
|
||||
.map(|(commitment, _)| commitment.calculate())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if base.encrypted_amounts !=
|
||||
commitments_and_encrypted_amounts.into_iter().map(|(_, amount)| amount).collect::<Vec<_>>()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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)?))
|
||||
}
|
||||
}
|
||||
583
coins/monero/wallet/src/send/mod.rs
Normal file
583
coins/monero/wallet/src/send/mod.rs
Normal file
@@ -0,0 +1,583 @@
|
||||
use core::{ops::Deref, fmt};
|
||||
use std_shims::{
|
||||
io, vec,
|
||||
vec::Vec,
|
||||
string::{String, ToString},
|
||||
};
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
|
||||
#[cfg(feature = "multisig")]
|
||||
use frost::FrostError;
|
||||
|
||||
use crate::{
|
||||
io::*,
|
||||
generators::{MAX_COMMITMENTS, hash_to_point},
|
||||
primitives::Decoys,
|
||||
ringct::{
|
||||
clsag::{ClsagError, ClsagContext, Clsag},
|
||||
RctType, RctPrunable, RctProofs,
|
||||
},
|
||||
transaction::Transaction,
|
||||
extra::MAX_ARBITRARY_DATA_SIZE,
|
||||
address::{Network, MoneroAddress},
|
||||
rpc::FeeRate,
|
||||
ViewPair, GuaranteedViewPair, WalletOutput,
|
||||
};
|
||||
|
||||
mod tx_keys;
|
||||
mod tx;
|
||||
mod eventuality;
|
||||
pub use eventuality::Eventuality;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod multisig;
|
||||
#[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()
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||||
enum ChangeEnum {
|
||||
None,
|
||||
AddressOnly(MoneroAddress),
|
||||
AddressWithView(MoneroAddress, Zeroizing<Scalar>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChangeEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ChangeEnum::None => f.debug_struct("ChangeEnum::None").finish_non_exhaustive(),
|
||||
ChangeEnum::AddressOnly(addr) => {
|
||||
f.debug_struct("ChangeEnum::AddressOnly").field("addr", &addr).finish()
|
||||
}
|
||||
ChangeEnum::AddressWithView(addr, _) => {
|
||||
f.debug_struct("ChangeEnum::AddressWithView").field("addr", &addr).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specification for a change output.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Change(ChangeEnum);
|
||||
|
||||
impl Change {
|
||||
/// Create a change output specification.
|
||||
///
|
||||
/// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
|
||||
/// its wallet protocol accordingly.
|
||||
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(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a change output specification for a guaranteed view pair.
|
||||
///
|
||||
/// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
|
||||
/// its wallet protocol accordingly.
|
||||
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,
|
||||
// TODO: Support subaddresses
|
||||
None,
|
||||
None,
|
||||
),
|
||||
view.0.view.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a fingerprintable change output specification.
|
||||
///
|
||||
/// You MUST assume this will harm your privacy. Only use this if you know what you're doing.
|
||||
///
|
||||
/// If the change address is Some, this will be unable to optimize the transaction as the
|
||||
/// Monero wallet protocol expects it can (due to presumably having the view key for the change
|
||||
/// output). If a transaction should be optimized, and isn'tm it will be fingerprintable.
|
||||
///
|
||||
/// If the change address is None, there are two fingerprints:
|
||||
///
|
||||
/// 1) The change in the TX is shunted to the fee (making it fingerprintable).
|
||||
///
|
||||
/// 2) If there are two outputs in the TX, Monero would create a payment ID for the non-change
|
||||
/// output so an observer can't tell apart TXs with a payment ID from TXs without a payment
|
||||
/// ID. monero-wallet will simply not create a payment ID in this case, revealing it's a
|
||||
/// monero-wallet TX without change.
|
||||
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
|
||||
if let Some(address) = address {
|
||||
Change(ChangeEnum::AddressOnly(address))
|
||||
} else {
|
||||
Change(ChangeEnum::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||||
enum InternalPayment {
|
||||
Payment(MoneroAddress, u64),
|
||||
Change(MoneroAddress, Option<Zeroizing<Scalar>>),
|
||||
}
|
||||
|
||||
impl InternalPayment {
|
||||
fn address(&self) -> &MoneroAddress {
|
||||
match self {
|
||||
InternalPayment::Payment(addr, _) | InternalPayment::Change(addr, _) => addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for InternalPayment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
InternalPayment::Payment(addr, amount) => f
|
||||
.debug_struct("InternalPayment::Payment")
|
||||
.field("addr", &addr)
|
||||
.field("amount", &amount)
|
||||
.finish(),
|
||||
InternalPayment::Change(addr, _) => {
|
||||
f.debug_struct("InternalPayment::Change").field("addr", &addr).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error while sending Monero.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum SendError {
|
||||
/// The RingCT type to produce proofs for this transaction with weren't supported.
|
||||
#[cfg_attr(feature = "std", error("this library doesn't yet support that RctType"))]
|
||||
UnsupportedRctType,
|
||||
/// The transaction had no inputs specified.
|
||||
#[cfg_attr(feature = "std", error("no inputs"))]
|
||||
NoInputs,
|
||||
/// The decoy quantity was invalid for the specified RingCT type.
|
||||
#[cfg_attr(feature = "std", error("invalid number of decoys"))]
|
||||
InvalidDecoyQuantity,
|
||||
/// The transaction had no outputs specified.
|
||||
#[cfg_attr(feature = "std", error("no outputs"))]
|
||||
NoOutputs,
|
||||
/// The transaction had too many outputs specified.
|
||||
#[cfg_attr(feature = "std", error("too many outputs"))]
|
||||
TooManyOutputs,
|
||||
/// The transaction did not have a change output, and did not have two outputs.
|
||||
///
|
||||
/// Monero requires all transactions have at least two outputs, assuming one payment and one
|
||||
/// change (or at least one dummy and one change). Accordingly, specifying no change and only
|
||||
/// one payment prevents creating a valid transaction
|
||||
#[cfg_attr(feature = "std", error("only one output and no change address"))]
|
||||
NoChange,
|
||||
/// Multiple addresses had payment IDs specified.
|
||||
///
|
||||
/// Only one payment ID is allowed per transaction.
|
||||
#[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))]
|
||||
MultiplePaymentIds,
|
||||
/// Too much arbitrary data was specified.
|
||||
#[cfg_attr(feature = "std", error("too much data"))]
|
||||
TooMuchArbitraryData,
|
||||
/// The created transaction was too large.
|
||||
#[cfg_attr(feature = "std", error("too large of a transaction"))]
|
||||
TooLargeTransaction,
|
||||
/// This transaction could not pay for itself.
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
error(
|
||||
"not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
|
||||
)
|
||||
)]
|
||||
NotEnoughFunds {
|
||||
/// The amount of funds the inputs contributed.
|
||||
inputs: u64,
|
||||
/// The amount of funds the outputs required.
|
||||
outputs: u64,
|
||||
/// The fee necessary to be paid on top.
|
||||
///
|
||||
/// If this is None, it is because the fee was not calculated as the outputs alone caused this
|
||||
/// error.
|
||||
necessary_fee: Option<u64>,
|
||||
},
|
||||
/// This transaction is being signed with the wrong private key.
|
||||
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
||||
WrongPrivateKey,
|
||||
/// This transaction was read from a bytestream which was malicious.
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
error("this SignableTransaction was created by deserializing a malicious serialization")
|
||||
)]
|
||||
MaliciousSerialization,
|
||||
/// There was an error when working with the CLSAGs.
|
||||
#[cfg_attr(feature = "std", error("clsag error ({0})"))]
|
||||
ClsagError(ClsagError),
|
||||
/// There was an error when working with FROST.
|
||||
#[cfg(feature = "multisig")]
|
||||
#[cfg_attr(feature = "std", error("frost error {0}"))]
|
||||
FrostError(FrostError),
|
||||
}
|
||||
|
||||
/// A signable transaction.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct SignableTransaction {
|
||||
rct_type: RctType,
|
||||
outgoing_view_key: Zeroizing<[u8; 32]>,
|
||||
inputs: Vec<(WalletOutput, Decoys)>,
|
||||
payments: Vec<InternalPayment>,
|
||||
data: Vec<Vec<u8>>,
|
||||
fee_rate: FeeRate,
|
||||
}
|
||||
|
||||
struct SignableTransactionWithKeyImages {
|
||||
intent: SignableTransaction,
|
||||
key_images: Vec<EdwardsPoint>,
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
fn validate(&self) -> Result<(), SendError> {
|
||||
match self.rct_type {
|
||||
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
|
||||
_ => Err(SendError::UnsupportedRctType)?,
|
||||
}
|
||||
|
||||
if self.inputs.is_empty() {
|
||||
Err(SendError::NoInputs)?;
|
||||
}
|
||||
for (_, decoys) in &self.inputs {
|
||||
// TODO: Add a function for the ring length
|
||||
if decoys.len() !=
|
||||
match self.rct_type {
|
||||
RctType::ClsagBulletproof => 11,
|
||||
RctType::ClsagBulletproofPlus => 16,
|
||||
_ => panic!("unsupported RctType"),
|
||||
}
|
||||
{
|
||||
Err(SendError::InvalidDecoyQuantity)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check we have at least one non-change output
|
||||
if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
|
||||
Err(SendError::NoOutputs)?;
|
||||
}
|
||||
// If we don't have at least two outputs, as required by Monero, error
|
||||
if self.payments.len() < 2 {
|
||||
Err(SendError::NoChange)?;
|
||||
}
|
||||
// Check we don't have multiple Change outputs due to decoding a malicious serialization
|
||||
{
|
||||
let mut change_count = 0;
|
||||
for payment in &self.payments {
|
||||
change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_, _))));
|
||||
}
|
||||
if change_count > 1 {
|
||||
Err(SendError::MaliciousSerialization)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there's at most one payment ID
|
||||
{
|
||||
let mut payment_ids = 0;
|
||||
for payment in &self.payments {
|
||||
payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
|
||||
}
|
||||
if payment_ids > 1 {
|
||||
Err(SendError::MultiplePaymentIds)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.payments.len() > MAX_COMMITMENTS {
|
||||
Err(SendError::TooManyOutputs)?;
|
||||
}
|
||||
|
||||
// Check the length of each arbitrary data
|
||||
for part in &self.data {
|
||||
if part.len() > MAX_ARBITRARY_DATA_SIZE {
|
||||
Err(SendError::TooMuchArbitraryData)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the length of TX extra
|
||||
// https://github.com/monero-project/monero/pull/8733
|
||||
const MAX_EXTRA_SIZE: usize = 1060;
|
||||
if self.extra().len() > MAX_EXTRA_SIZE {
|
||||
Err(SendError::TooMuchArbitraryData)?;
|
||||
}
|
||||
|
||||
// Make sure we have enough funds
|
||||
let in_amount = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
||||
let payments_amount = self
|
||||
.payments
|
||||
.iter()
|
||||
.filter_map(|payment| match payment {
|
||||
InternalPayment::Payment(_, amount) => Some(amount),
|
||||
InternalPayment::Change(_, _) => None,
|
||||
})
|
||||
.sum::<u64>();
|
||||
let (weight, necessary_fee) = self.weight_and_necessary_fee();
|
||||
if in_amount < (payments_amount + necessary_fee) {
|
||||
Err(SendError::NotEnoughFunds {
|
||||
inputs: in_amount,
|
||||
outputs: payments_amount,
|
||||
necessary_fee: Some(necessary_fee),
|
||||
})?;
|
||||
}
|
||||
|
||||
// The actual limit is half the block size, and for the minimum block size of 300k, that'd be
|
||||
// 150k
|
||||
// wallet2 will only create transactions up to 100k bytes however
|
||||
// TODO: Cite
|
||||
const MAX_TX_SIZE: usize = 100_000;
|
||||
if weight >= MAX_TX_SIZE {
|
||||
Err(SendError::TooLargeTransaction)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new SignableTransaction.
|
||||
///
|
||||
/// `outgoing_view_key` is used to seed the RNGs for this transaction. Anyone with knowledge of
|
||||
/// the outgoing view key will be able to identify a transaction produced with this methodology,
|
||||
/// and the data within it. Accordingly, it must be treated as a private key.
|
||||
///
|
||||
/// `data` represents arbitrary data which will be embedded into the transaction's `extra` field.
|
||||
/// The embedding occurs using an `ExtraField::Nonce` with a custom marker byte (as to not
|
||||
/// conflict with a payment ID).
|
||||
pub fn new(
|
||||
rct_type: RctType,
|
||||
outgoing_view_key: Zeroizing<[u8; 32]>,
|
||||
inputs: Vec<(WalletOutput, Decoys)>,
|
||||
payments: Vec<(MoneroAddress, u64)>,
|
||||
change: Change,
|
||||
data: Vec<Vec<u8>>,
|
||||
fee_rate: FeeRate,
|
||||
) -> Result<SignableTransaction, SendError> {
|
||||
// Re-format the payments and change into a consolidated payments list
|
||||
let mut payments = payments
|
||||
.into_iter()
|
||||
.map(|(addr, amount)| InternalPayment::Payment(addr, amount))
|
||||
.collect::<Vec<_>>();
|
||||
match change.0 {
|
||||
ChangeEnum::None => {}
|
||||
ChangeEnum::AddressOnly(addr) => payments.push(InternalPayment::Change(addr, None)),
|
||||
ChangeEnum::AddressWithView(addr, view) => {
|
||||
payments.push(InternalPayment::Change(addr, Some(view)))
|
||||
}
|
||||
}
|
||||
|
||||
let mut res =
|
||||
SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
|
||||
res.validate()?;
|
||||
|
||||
// Shuffle the payments
|
||||
{
|
||||
let mut rng = res.seeded_rng(b"shuffle_payments");
|
||||
res.payments.shuffle(&mut rng);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// The fee rate this transaction uses.
|
||||
pub fn fee_rate(&self) -> FeeRate {
|
||||
self.fee_rate
|
||||
}
|
||||
|
||||
/// The fee this transaction requires.
|
||||
///
|
||||
/// This is distinct from the fee this transaction will use. If no change output is specified,
|
||||
/// all unspent coins will be shunted to the fee.
|
||||
pub fn necessary_fee(&self) -> u64 {
|
||||
self.weight_and_necessary_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: &(WalletOutput, Decoys), w: &mut W) -> io::Result<()> {
|
||||
input.0.write(w)?;
|
||||
input.1.write(w)
|
||||
}
|
||||
|
||||
fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
|
||||
match payment {
|
||||
InternalPayment::Payment(addr, amount) => {
|
||||
w.write_all(&[0])?;
|
||||
write_vec(write_byte, addr.to_string().as_bytes(), w)?;
|
||||
w.write_all(&amount.to_le_bytes())
|
||||
}
|
||||
InternalPayment::Change(addr, change_view) => {
|
||||
w.write_all(&[1])?;
|
||||
write_vec(write_byte, addr.to_string().as_bytes(), w)?;
|
||||
if let Some(view) = change_view.as_ref() {
|
||||
w.write_all(&[1])?;
|
||||
write_scalar(view, w)
|
||||
} else {
|
||||
w.write_all(&[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_byte(&u8::from(self.rct_type), w)?;
|
||||
w.write_all(self.outgoing_view_key.as_slice())?;
|
||||
write_vec(write_input, &self.inputs, w)?;
|
||||
write_vec(write_payment, &self.payments, w)?;
|
||||
write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
|
||||
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<(WalletOutput, Decoys)> {
|
||||
Ok((WalletOutput::read(r)?, Decoys::read(r)?))
|
||||
}
|
||||
|
||||
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
|
||||
String::from_utf8(read_vec(read_byte, r)?)
|
||||
.ok()
|
||||
.and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
|
||||
.ok_or_else(|| io::Error::other("invalid address"))
|
||||
}
|
||||
|
||||
fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
|
||||
Ok(match read_byte(r)? {
|
||||
0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?),
|
||||
1 => InternalPayment::Change(
|
||||
read_address(r)?,
|
||||
match read_byte(r)? {
|
||||
0 => None,
|
||||
1 => Some(Zeroizing::new(read_scalar(r)?)),
|
||||
_ => Err(io::Error::other("invalid change view"))?,
|
||||
},
|
||||
),
|
||||
_ => Err(io::Error::other("invalid payment"))?,
|
||||
})
|
||||
}
|
||||
|
||||
let res = SignableTransaction {
|
||||
rct_type: RctType::try_from(read_byte(r)?)
|
||||
.map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
|
||||
outgoing_view_key: Zeroizing::new(read_bytes(r)?),
|
||||
inputs: read_vec(read_input, r)?,
|
||||
payments: read_vec(read_payment, r)?,
|
||||
data: read_vec(|r| read_vec(read_byte, r), r)?,
|
||||
fee_rate: FeeRate::read(r)?,
|
||||
};
|
||||
match res.validate() {
|
||||
Ok(()) => {}
|
||||
Err(e) => Err(io::Error::other(e))?,
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn with_key_images(mut self, key_images: Vec<EdwardsPoint>) -> SignableTransactionWithKeyImages {
|
||||
debug_assert_eq!(self.inputs.len(), key_images.len());
|
||||
|
||||
// Sort the inputs by their key images
|
||||
let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::<Vec<_>>();
|
||||
sorted_inputs
|
||||
.sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
|
||||
|
||||
self.inputs = Vec::with_capacity(sorted_inputs.len());
|
||||
let mut key_images = Vec::with_capacity(sorted_inputs.len());
|
||||
for (input, key_image) in sorted_inputs {
|
||||
self.inputs.push(input);
|
||||
key_images.push(key_image);
|
||||
}
|
||||
|
||||
SignableTransactionWithKeyImages { intent: self, key_images }
|
||||
}
|
||||
|
||||
/// Sign this transaction.
|
||||
pub fn sign(
|
||||
self,
|
||||
rng: &mut (impl RngCore + CryptoRng),
|
||||
sender_spend_key: &Zeroizing<Scalar>,
|
||||
) -> Result<Transaction, SendError> {
|
||||
// Calculate the key images
|
||||
let mut key_images = vec![];
|
||||
for (input, _) in &self.inputs {
|
||||
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
|
||||
if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() {
|
||||
Err(SendError::WrongPrivateKey)?;
|
||||
}
|
||||
let key_image = input_key.deref() * hash_to_point(input.key().compress().to_bytes());
|
||||
key_images.push(key_image);
|
||||
}
|
||||
|
||||
// Convert to a SignableTransactionWithKeyImages
|
||||
let tx = self.with_key_images(key_images);
|
||||
|
||||
// Prepare the CLSAG signatures
|
||||
let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
|
||||
for (input, decoys) in &tx.intent.inputs {
|
||||
// Re-derive the input key as this will be in a different order
|
||||
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
|
||||
clsag_signs.push((
|
||||
input_key,
|
||||
ClsagContext::new(decoys.clone(), input.commitment().clone())
|
||||
.map_err(SendError::ClsagError)?,
|
||||
));
|
||||
}
|
||||
|
||||
// Get the output commitments' mask sum
|
||||
let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
|
||||
|
||||
// Get the actual TX, just needing the CLSAGs
|
||||
let mut tx = tx.transaction_without_signatures();
|
||||
|
||||
// Sign the CLSAGs
|
||||
let clsags_and_pseudo_outs =
|
||||
Clsag::sign(rng, clsag_signs, mask_sum, tx.signature_hash().unwrap())
|
||||
.map_err(SendError::ClsagError)?;
|
||||
|
||||
// Fill in the CLSAGs/pseudo-outs
|
||||
let inputs_len = tx.prefix().inputs.len();
|
||||
let Transaction::V2 {
|
||||
proofs:
|
||||
Some(RctProofs {
|
||||
prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
|
||||
..
|
||||
}),
|
||||
..
|
||||
} = tx
|
||||
else {
|
||||
panic!("not signing clsag?")
|
||||
};
|
||||
*clsags = Vec::with_capacity(inputs_len);
|
||||
*pseudo_outs = Vec::with_capacity(inputs_len);
|
||||
for (clsag, pseudo_out) in clsags_and_pseudo_outs {
|
||||
clsags.push(clsag);
|
||||
pseudo_outs.push(pseudo_out);
|
||||
}
|
||||
|
||||
// Return the signed TX
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
304
coins/monero/wallet/src/send/multisig.rs
Normal file
304
coins/monero/wallet/src/send/multisig.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use std_shims::{
|
||||
vec::Vec,
|
||||
io::{self, Read},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use group::ff::Field;
|
||||
use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint};
|
||||
use dalek_ff_group as dfg;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use frost::{
|
||||
curve::Ed25519,
|
||||
Participant, FrostError, ThresholdKeys,
|
||||
dkg::lagrange,
|
||||
sign::{
|
||||
Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
|
||||
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
||||
},
|
||||
};
|
||||
|
||||
use monero_serai::{
|
||||
ringct::{
|
||||
clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig},
|
||||
RctPrunable, RctProofs,
|
||||
},
|
||||
transaction::Transaction,
|
||||
};
|
||||
use crate::send::{SendError, SignableTransaction, key_image_sort};
|
||||
|
||||
/// Initial FROST machine to produce a signed transaction.
|
||||
pub struct TransactionMachine {
|
||||
signable: SignableTransaction,
|
||||
|
||||
i: Participant,
|
||||
|
||||
// The key image generator, and the scalar offset from the spend key
|
||||
key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>,
|
||||
clsags: Vec<(ClsagMultisigMaskSender, AlgorithmMachine<Ed25519, ClsagMultisig>)>,
|
||||
}
|
||||
|
||||
/// Second FROST machine to produce a signed transaction.
|
||||
pub struct TransactionSignMachine {
|
||||
signable: SignableTransaction,
|
||||
|
||||
i: Participant,
|
||||
|
||||
key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>,
|
||||
clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine<Ed25519, ClsagMultisig>)>,
|
||||
|
||||
our_preprocess: Vec<Preprocess<Ed25519, ClsagAddendum>>,
|
||||
}
|
||||
|
||||
/// Final FROST machine to produce a signed transaction.
|
||||
pub struct TransactionSignatureMachine {
|
||||
tx: Transaction,
|
||||
clsags: Vec<AlgorithmSignatureMachine<Ed25519, ClsagMultisig>>,
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
/// Create a FROST signing machine out of this signable transaction.
|
||||
pub fn multisig(self, keys: &ThresholdKeys<Ed25519>) -> Result<TransactionMachine, SendError> {
|
||||
let mut clsags = vec![];
|
||||
|
||||
let mut key_image_generators_and_offsets = vec![];
|
||||
for (i, (input, decoys)) in self.inputs.iter().enumerate() {
|
||||
// Check this is the right set of keys
|
||||
let offset = keys.offset(dfg::Scalar(input.key_offset()));
|
||||
if offset.group_key().0 != input.key() {
|
||||
Err(SendError::WrongPrivateKey)?;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
key_image_generators_and_offsets.push((
|
||||
clsag.key_image_generator(),
|
||||
keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + self.inputs[i].0.key_offset(),
|
||||
));
|
||||
clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset)));
|
||||
}
|
||||
|
||||
Ok(TransactionMachine {
|
||||
signable: self,
|
||||
i: keys.params().i(),
|
||||
key_image_generators_and_offsets,
|
||||
clsags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PreprocessMachine for TransactionMachine {
|
||||
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
|
||||
type Signature = Transaction;
|
||||
type SignMachine = TransactionSignMachine;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
) -> (TransactionSignMachine, Self::Preprocess) {
|
||||
// Iterate over each CLSAG calling preprocess
|
||||
let mut preprocesses = Vec::with_capacity(self.clsags.len());
|
||||
let clsags = self
|
||||
.clsags
|
||||
.drain(..)
|
||||
.map(|(clsag_mask_send, clsag)| {
|
||||
let (clsag, preprocess) = clsag.preprocess(rng);
|
||||
preprocesses.push(preprocess);
|
||||
(clsag_mask_send, clsag)
|
||||
})
|
||||
.collect();
|
||||
let our_preprocess = preprocesses.clone();
|
||||
|
||||
(
|
||||
TransactionSignMachine {
|
||||
signable: self.signable,
|
||||
|
||||
i: self.i,
|
||||
|
||||
key_image_generators_and_offsets: self.key_image_generators_and_offsets,
|
||||
clsags,
|
||||
|
||||
our_preprocess,
|
||||
},
|
||||
preprocesses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
type Params = ();
|
||||
type Keys = ThresholdKeys<Ed25519>;
|
||||
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
|
||||
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
||||
type SignatureMachine = TransactionSignatureMachine;
|
||||
|
||||
fn cache(self) -> CachedPreprocess {
|
||||
unimplemented!(
|
||||
"Monero transactions don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific transaction"
|
||||
);
|
||||
}
|
||||
|
||||
fn from_cache(
|
||||
(): (),
|
||||
_: ThresholdKeys<Ed25519>,
|
||||
_: CachedPreprocess,
|
||||
) -> (Self, Self::Preprocess) {
|
||||
unimplemented!(
|
||||
"Monero transactions don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific transaction"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
||||
self.clsags.iter().map(|clsag| clsag.1.read_preprocess(reader)).collect()
|
||||
}
|
||||
|
||||
fn sign(
|
||||
self,
|
||||
mut commitments: HashMap<Participant, Self::Preprocess>,
|
||||
msg: &[u8],
|
||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
||||
if !msg.is_empty() {
|
||||
panic!("message was passed to the TransactionMachine when it generates its own");
|
||||
}
|
||||
|
||||
// We do not need to be included here, yet this set of signers has yet to be validated
|
||||
// We explicitly remove ourselves to ensure we aren't included twice, if we were redundantly
|
||||
// included
|
||||
commitments.remove(&self.i);
|
||||
|
||||
// Find out who's included
|
||||
let mut included = commitments.keys().copied().collect::<Vec<_>>();
|
||||
// This push won't duplicate due to the above removal
|
||||
included.push(self.i);
|
||||
// unstable sort may reorder elements of equal order
|
||||
// Given our lack of duplicates, we should have no elements of equal order
|
||||
included.sort_unstable();
|
||||
|
||||
// Start calculating the key images, as needed on the TX level
|
||||
let mut key_images = vec![EdwardsPoint::identity(); self.clsags.len()];
|
||||
for (image, (generator, offset)) in
|
||||
key_images.iter_mut().zip(&self.key_image_generators_and_offsets)
|
||||
{
|
||||
*image = generator * offset;
|
||||
}
|
||||
|
||||
// Convert the serialized nonces commitments to a parallelized Vec
|
||||
let mut commitments = (0 .. self.clsags.len())
|
||||
.map(|c| {
|
||||
included
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let preprocess = if *l == self.i {
|
||||
self.our_preprocess[c].clone()
|
||||
} else {
|
||||
commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone()
|
||||
};
|
||||
|
||||
// While here, calculate the key image as needed to call sign
|
||||
// The CLSAG algorithm will independently calculate the key image/verify these shares
|
||||
key_images[c] +=
|
||||
preprocess.addendum.key_image_share().0 * lagrange::<dfg::Scalar>(*l, &included).0;
|
||||
|
||||
Ok((*l, preprocess))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// The above inserted our own preprocess into these maps (which is unnecessary)
|
||||
// Remove it now
|
||||
for map in &mut commitments {
|
||||
map.remove(&self.i);
|
||||
}
|
||||
|
||||
// The actual TX will have sorted its inputs by key image
|
||||
// We apply the same sort now to our CLSAG machines
|
||||
let mut clsags = Vec::with_capacity(self.clsags.len());
|
||||
for ((key_image, clsag), commitments) in key_images.iter().zip(self.clsags).zip(commitments) {
|
||||
clsags.push((key_image, clsag, commitments));
|
||||
}
|
||||
clsags.sort_by(|x, y| key_image_sort(x.0, y.0));
|
||||
let clsags =
|
||||
clsags.into_iter().map(|(_, clsag, commitments)| (clsag, commitments)).collect::<Vec<_>>();
|
||||
|
||||
// Specify the TX's key images
|
||||
let tx = self.signable.with_key_images(key_images);
|
||||
|
||||
// We now need to decide the masks for each CLSAG
|
||||
let clsag_len = clsags.len();
|
||||
let output_masks = tx.intent.sum_output_masks(&tx.key_images);
|
||||
let mut rng = tx.intent.seeded_rng(b"multisig_pseudo_out_masks");
|
||||
let mut sum_pseudo_outs = Scalar::ZERO;
|
||||
let mut to_sign = Vec::with_capacity(clsag_len);
|
||||
for (i, ((clsag_mask_send, clsag), commitments)) in clsags.into_iter().enumerate() {
|
||||
let mut mask = Scalar::random(&mut rng);
|
||||
if i == (clsag_len - 1) {
|
||||
mask = output_masks - sum_pseudo_outs;
|
||||
} else {
|
||||
sum_pseudo_outs += mask;
|
||||
}
|
||||
clsag_mask_send.send(mask);
|
||||
to_sign.push((clsag, commitments));
|
||||
}
|
||||
|
||||
let tx = tx.transaction_without_signatures();
|
||||
let msg = tx.signature_hash().unwrap();
|
||||
|
||||
// Iterate over each CLSAG calling sign
|
||||
let mut shares = Vec::with_capacity(to_sign.len());
|
||||
let clsags = to_sign
|
||||
.drain(..)
|
||||
.map(|(clsag, commitments)| {
|
||||
let (clsag, share) = clsag.sign(commitments, &msg)?;
|
||||
shares.push(share);
|
||||
Ok(clsag)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok((TransactionSignatureMachine { tx, clsags }, shares))
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
||||
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
||||
|
||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
||||
self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
mut self,
|
||||
shares: HashMap<Participant, Self::SignatureShare>,
|
||||
) -> Result<Transaction, FrostError> {
|
||||
let mut tx = self.tx;
|
||||
match tx {
|
||||
Transaction::V2 {
|
||||
proofs:
|
||||
Some(RctProofs {
|
||||
prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
|
||||
..
|
||||
}),
|
||||
..
|
||||
} => {
|
||||
for (c, clsag) in self.clsags.drain(..).enumerate() {
|
||||
let (clsag, pseudo_out) = clsag.complete(
|
||||
shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::<HashMap<_, _>>(),
|
||||
)?;
|
||||
clsags.push(clsag);
|
||||
pseudo_outs.push(pseudo_out);
|
||||
}
|
||||
}
|
||||
_ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"),
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
323
coins/monero/wallet/src/send/tx.rs
Normal file
323
coins/monero/wallet/src/send/tx.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE},
|
||||
Scalar, EdwardsPoint,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
io::{varint_len, write_varint},
|
||||
primitives::Commitment,
|
||||
ringct::{
|
||||
clsag::Clsag, bulletproofs::Bulletproof, EncryptedAmount, RctType, RctBase, RctPrunable,
|
||||
RctProofs,
|
||||
},
|
||||
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
|
||||
extra::{ARBITRARY_DATA_MARKER, PaymentId, ExtraField, Extra},
|
||||
send::{InternalPayment, SignableTransaction, SignableTransactionWithKeyImages},
|
||||
};
|
||||
|
||||
impl SignableTransaction {
|
||||
// Output the inputs for this transaction.
|
||||
pub(crate) fn inputs(&self, key_images: &[EdwardsPoint]) -> Vec<Input> {
|
||||
debug_assert_eq!(self.inputs.len(), key_images.len());
|
||||
|
||||
let mut res = Vec::with_capacity(self.inputs.len());
|
||||
for ((_, decoys), key_image) in self.inputs.iter().zip(key_images) {
|
||||
res.push(Input::ToKey {
|
||||
amount: None,
|
||||
key_offsets: decoys.offsets().to_vec(),
|
||||
key_image: *key_image,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// Output the outputs for this transaction.
|
||||
pub(crate) fn outputs(&self, key_images: &[EdwardsPoint]) -> Vec<Output> {
|
||||
let shared_key_derivations = self.shared_key_derivations(key_images);
|
||||
debug_assert_eq!(self.payments.len(), shared_key_derivations.len());
|
||||
|
||||
let mut res = Vec::with_capacity(self.payments.len());
|
||||
for (payment, shared_key_derivations) in self.payments.iter().zip(&shared_key_derivations) {
|
||||
let key =
|
||||
(&shared_key_derivations.shared_key * ED25519_BASEPOINT_TABLE) + payment.address().spend();
|
||||
res.push(Output {
|
||||
key: key.compress(),
|
||||
amount: None,
|
||||
view_tag: (match self.rct_type {
|
||||
RctType::ClsagBulletproof => false,
|
||||
RctType::ClsagBulletproofPlus => true,
|
||||
_ => panic!("unsupported RctType"),
|
||||
})
|
||||
.then_some(shared_key_derivations.view_tag),
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// Calculate the TX extra for this transaction.
|
||||
pub(crate) fn extra(&self) -> Vec<u8> {
|
||||
let (tx_key, additional_keys) = self.transaction_keys_pub();
|
||||
debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
|
||||
let payment_id_xors = self.payment_id_xors();
|
||||
debug_assert_eq!(self.payments.len(), payment_id_xors.len());
|
||||
|
||||
let amount_of_keys = 1 + additional_keys.len();
|
||||
let mut extra = Extra::new(tx_key, additional_keys);
|
||||
|
||||
if let Some((id, id_xor)) =
|
||||
self.payments.iter().zip(&payment_id_xors).find_map(|(payment, payment_id_xor)| {
|
||||
payment.address().payment_id().map(|id| (id, payment_id_xor))
|
||||
})
|
||||
{
|
||||
let id = (u64::from_le_bytes(id) ^ u64::from_le_bytes(*id_xor)).to_le_bytes();
|
||||
let mut id_vec = Vec::with_capacity(1 + 8);
|
||||
PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
|
||||
extra.push(ExtraField::Nonce(id_vec));
|
||||
} else {
|
||||
// If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment
|
||||
if (self.payments.len() == 2) &&
|
||||
self.payments.iter().any(|payment| matches!(payment, InternalPayment::Change(_, _)))
|
||||
{
|
||||
let (_, payment_id_xor) = self
|
||||
.payments
|
||||
.iter()
|
||||
.zip(&payment_id_xors)
|
||||
.find(|(payment, _)| matches!(payment, InternalPayment::Payment(_, _)))
|
||||
.expect("multiple change outputs?");
|
||||
let mut id_vec = Vec::with_capacity(1 + 8);
|
||||
// The dummy payment ID is [0; 8], which when xor'd with the mask, is just the mask
|
||||
PaymentId::Encrypted(*payment_id_xor).write(&mut id_vec).unwrap();
|
||||
extra.push(ExtraField::Nonce(id_vec));
|
||||
}
|
||||
}
|
||||
|
||||
// Include data if present
|
||||
for part in &self.data {
|
||||
let mut arb = vec![ARBITRARY_DATA_MARKER];
|
||||
arb.extend(part);
|
||||
extra.push(ExtraField::Nonce(arb));
|
||||
}
|
||||
|
||||
let mut serialized = Vec::with_capacity(32 * amount_of_keys);
|
||||
extra.write(&mut serialized).unwrap();
|
||||
serialized
|
||||
}
|
||||
|
||||
pub(crate) fn weight_and_necessary_fee(&self) -> (usize, u64) {
|
||||
/*
|
||||
This transaction is variable length to:
|
||||
- The decoy offsets (fixed)
|
||||
- The TX extra (variable to key images, requiring an interactive protocol)
|
||||
|
||||
Thankfully, the TX extra *length* is fixed. Accordingly, we can calculate the inevitable TX's
|
||||
weight at this time with a shimmed transaction.
|
||||
*/
|
||||
let base_weight = {
|
||||
let mut key_images = Vec::with_capacity(self.inputs.len());
|
||||
let mut clsags = Vec::with_capacity(self.inputs.len());
|
||||
let mut pseudo_outs = Vec::with_capacity(self.inputs.len());
|
||||
for _ in &self.inputs {
|
||||
key_images.push(ED25519_BASEPOINT_POINT);
|
||||
clsags.push(Clsag {
|
||||
D: ED25519_BASEPOINT_POINT,
|
||||
s: vec![
|
||||
Scalar::ZERO;
|
||||
match self.rct_type {
|
||||
RctType::ClsagBulletproof => 11,
|
||||
RctType::ClsagBulletproofPlus => 16,
|
||||
_ => unreachable!("unsupported RCT type"),
|
||||
}
|
||||
],
|
||||
c1: Scalar::ZERO,
|
||||
});
|
||||
pseudo_outs.push(ED25519_BASEPOINT_POINT);
|
||||
}
|
||||
let mut encrypted_amounts = Vec::with_capacity(self.payments.len());
|
||||
let mut bp_commitments = Vec::with_capacity(self.payments.len());
|
||||
let mut commitments = Vec::with_capacity(self.payments.len());
|
||||
for _ in &self.payments {
|
||||
encrypted_amounts.push(EncryptedAmount::Compact { amount: [0; 8] });
|
||||
bp_commitments.push(Commitment::zero());
|
||||
commitments.push(ED25519_BASEPOINT_POINT);
|
||||
}
|
||||
|
||||
let padded_log2 = {
|
||||
let mut log2_find = 0;
|
||||
while (1 << log2_find) < self.payments.len() {
|
||||
log2_find += 1;
|
||||
}
|
||||
log2_find
|
||||
};
|
||||
// This is log2 the padded amount of IPA rows
|
||||
// We have 64 rows per commitment, so we need 64 * c IPA rows
|
||||
// We rewrite this as 2**6 * c
|
||||
// By finding the padded log2 of c, we get 2**6 * 2**p
|
||||
// This declares the log2 to be 6 + p
|
||||
let lr_len = 6 + padded_log2;
|
||||
|
||||
let bulletproof = match self.rct_type {
|
||||
RctType::ClsagBulletproof => {
|
||||
let mut bp = Vec::with_capacity(((9 + (2 * lr_len)) * 32) + 2);
|
||||
let push_point = |bp: &mut Vec<u8>| {
|
||||
bp.push(1);
|
||||
bp.extend([0; 31]);
|
||||
};
|
||||
let push_scalar = |bp: &mut Vec<u8>| bp.extend([0; 32]);
|
||||
for _ in 0 .. 4 {
|
||||
push_point(&mut bp);
|
||||
}
|
||||
for _ in 0 .. 2 {
|
||||
push_scalar(&mut bp);
|
||||
}
|
||||
for _ in 0 .. 2 {
|
||||
write_varint(&lr_len, &mut bp).unwrap();
|
||||
for _ in 0 .. lr_len {
|
||||
push_point(&mut bp);
|
||||
}
|
||||
}
|
||||
for _ in 0 .. 3 {
|
||||
push_scalar(&mut bp);
|
||||
}
|
||||
Bulletproof::read(&mut bp.as_slice()).expect("made an invalid dummy BP")
|
||||
}
|
||||
RctType::ClsagBulletproofPlus => {
|
||||
let mut bp = Vec::with_capacity(((6 + (2 * lr_len)) * 32) + 2);
|
||||
let push_point = |bp: &mut Vec<u8>| {
|
||||
bp.push(1);
|
||||
bp.extend([0; 31]);
|
||||
};
|
||||
let push_scalar = |bp: &mut Vec<u8>| bp.extend([0; 32]);
|
||||
for _ in 0 .. 3 {
|
||||
push_point(&mut bp);
|
||||
}
|
||||
for _ in 0 .. 3 {
|
||||
push_scalar(&mut bp);
|
||||
}
|
||||
for _ in 0 .. 2 {
|
||||
write_varint(&lr_len, &mut bp).unwrap();
|
||||
for _ in 0 .. lr_len {
|
||||
push_point(&mut bp);
|
||||
}
|
||||
}
|
||||
Bulletproof::read_plus(&mut bp.as_slice()).expect("made an invalid dummy BP+")
|
||||
}
|
||||
_ => panic!("unsupported RctType"),
|
||||
};
|
||||
|
||||
// `- 1` to remove the one byte for the 0 fee
|
||||
Transaction::V2 {
|
||||
prefix: TransactionPrefix {
|
||||
additional_timelock: Timelock::None,
|
||||
inputs: self.inputs(&key_images),
|
||||
outputs: self.outputs(&key_images),
|
||||
extra: self.extra(),
|
||||
},
|
||||
proofs: Some(RctProofs {
|
||||
base: RctBase { fee: 0, encrypted_amounts, pseudo_outs: vec![], commitments },
|
||||
prunable: RctPrunable::Clsag { bulletproof, clsags, pseudo_outs },
|
||||
}),
|
||||
}
|
||||
.weight() -
|
||||
1
|
||||
};
|
||||
|
||||
// We now have the base weight, without the fee encoded
|
||||
// The fee itself will impact the weight as its encoding is [1, 9] bytes long
|
||||
let mut possible_weights = Vec::with_capacity(9);
|
||||
for i in 1 ..= 9 {
|
||||
possible_weights.push(base_weight + i);
|
||||
}
|
||||
debug_assert_eq!(possible_weights.len(), 9);
|
||||
|
||||
// We now calculate the fee which would be used for each weight
|
||||
let mut possible_fees = Vec::with_capacity(9);
|
||||
for weight in possible_weights {
|
||||
possible_fees.push(self.fee_rate.calculate_fee_from_weight(weight));
|
||||
}
|
||||
|
||||
// We now look for the fee whose length matches the length used to derive it
|
||||
let mut weight_and_fee = None;
|
||||
for (fee_len, possible_fee) in possible_fees.into_iter().enumerate() {
|
||||
let fee_len = 1 + fee_len;
|
||||
debug_assert!(1 <= fee_len);
|
||||
debug_assert!(fee_len <= 9);
|
||||
|
||||
// We use the first fee whose encoded length is not larger than the length used within this
|
||||
// weight
|
||||
// This should be because the lengths are equal, yet means if somehow none are equal, this
|
||||
// will still terminate successfully
|
||||
if varint_len(possible_fee) <= fee_len {
|
||||
weight_and_fee = Some((base_weight + fee_len, possible_fee));
|
||||
break;
|
||||
}
|
||||
}
|
||||
weight_and_fee.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SignableTransactionWithKeyImages {
|
||||
pub(crate) fn transaction_without_signatures(&self) -> Transaction {
|
||||
let commitments_and_encrypted_amounts =
|
||||
self.intent.commitments_and_encrypted_amounts(&self.key_images);
|
||||
let mut commitments = Vec::with_capacity(self.intent.payments.len());
|
||||
let mut bp_commitments = Vec::with_capacity(self.intent.payments.len());
|
||||
let mut encrypted_amounts = Vec::with_capacity(self.intent.payments.len());
|
||||
for (commitment, encrypted_amount) in commitments_and_encrypted_amounts {
|
||||
commitments.push(commitment.calculate());
|
||||
bp_commitments.push(commitment);
|
||||
encrypted_amounts.push(encrypted_amount);
|
||||
}
|
||||
let bulletproof = {
|
||||
let mut bp_rng = self.intent.seeded_rng(b"bulletproof");
|
||||
(match self.intent.rct_type {
|
||||
RctType::ClsagBulletproof => Bulletproof::prove(&mut bp_rng, &bp_commitments),
|
||||
RctType::ClsagBulletproofPlus => Bulletproof::prove_plus(&mut bp_rng, bp_commitments),
|
||||
_ => panic!("unsupported RctType"),
|
||||
})
|
||||
.expect("couldn't prove BP(+)s for this many payments despite checking in constructor?")
|
||||
};
|
||||
|
||||
Transaction::V2 {
|
||||
prefix: TransactionPrefix {
|
||||
additional_timelock: Timelock::None,
|
||||
inputs: self.intent.inputs(&self.key_images),
|
||||
outputs: self.intent.outputs(&self.key_images),
|
||||
extra: self.intent.extra(),
|
||||
},
|
||||
proofs: Some(RctProofs {
|
||||
base: RctBase {
|
||||
fee: if self
|
||||
.intent
|
||||
.payments
|
||||
.iter()
|
||||
.any(|payment| matches!(payment, InternalPayment::Change(_, _)))
|
||||
{
|
||||
// The necessary fee is the fee
|
||||
self.intent.weight_and_necessary_fee().1
|
||||
} else {
|
||||
// If we don't have a change output, the difference is the fee
|
||||
let inputs =
|
||||
self.intent.inputs.iter().map(|input| input.0.commitment().amount).sum::<u64>();
|
||||
let payments = self
|
||||
.intent
|
||||
.payments
|
||||
.iter()
|
||||
.filter_map(|payment| match payment {
|
||||
InternalPayment::Payment(_, amount) => Some(amount),
|
||||
InternalPayment::Change(_, _) => None,
|
||||
})
|
||||
.sum::<u64>();
|
||||
// Safe since the constructor checks inputs >= (payments + fee)
|
||||
inputs - payments
|
||||
},
|
||||
encrypted_amounts,
|
||||
pseudo_outs: vec![],
|
||||
commitments,
|
||||
},
|
||||
prunable: RctPrunable::Clsag { bulletproof, clsags: vec![], pseudo_outs: vec![] },
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
241
coins/monero/wallet/src/send/tx_keys.rs
Normal file
241
coins/monero/wallet/src/send/tx_keys.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use core::ops::Deref;
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
|
||||
|
||||
use crate::{
|
||||
primitives::{keccak256, Commitment},
|
||||
ringct::EncryptedAmount,
|
||||
SharedKeyDerivations,
|
||||
send::{InternalPayment, SignableTransaction, key_image_sort},
|
||||
};
|
||||
|
||||
impl SignableTransaction {
|
||||
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
|
||||
// Apply the DST
|
||||
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
|
||||
transcript.extend(dst);
|
||||
|
||||
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
|
||||
transcript.extend(self.outgoing_view_key.as_slice());
|
||||
|
||||
// Ensure uniqueness across transactions by binding to a use-once object
|
||||
// The keys for the inputs is binding to their key images, making them use-once
|
||||
let mut input_keys = self.inputs.iter().map(|(input, _)| input.key()).collect::<Vec<_>>();
|
||||
// We sort the inputs mid-way through TX construction, so apply our own sort to ensure a
|
||||
// consistent order
|
||||
// We use the key image sort as it's applicable and well-defined, not because these are key
|
||||
// images
|
||||
input_keys.sort_by(key_image_sort);
|
||||
for key in input_keys {
|
||||
transcript.extend(key.compress().to_bytes());
|
||||
}
|
||||
|
||||
ChaCha20Rng::from_seed(keccak256(&transcript))
|
||||
}
|
||||
|
||||
fn has_payments_to_subaddresses(&self) -> bool {
|
||||
self.payments.iter().any(|payment| match payment {
|
||||
InternalPayment::Payment(addr, _) => addr.is_subaddress(),
|
||||
InternalPayment::Change(addr, view) => {
|
||||
if view.is_some() {
|
||||
// It should not be possible to construct a change specification to a subaddress with a
|
||||
// view key
|
||||
// TODO
|
||||
debug_assert!(!addr.is_subaddress());
|
||||
}
|
||||
addr.is_subaddress()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn should_use_additional_keys(&self) -> bool {
|
||||
let has_payments_to_subaddresses = self.has_payments_to_subaddresses();
|
||||
if !has_payments_to_subaddresses {
|
||||
return false;
|
||||
}
|
||||
|
||||
let has_change_view = self.payments.iter().any(|payment| match payment {
|
||||
InternalPayment::Payment(_, _) => false,
|
||||
InternalPayment::Change(_, view) => view.is_some(),
|
||||
});
|
||||
|
||||
/*
|
||||
If sending to a subaddress, the shared key is not `rG` yet `rB`. Because of this, a
|
||||
per-subaddress shared key is necessary, causing the usage of additional keys.
|
||||
|
||||
The one exception is if we're sending to a subaddress in a 2-output transaction. The second
|
||||
output, the change output, will attempt scanning the singular key `rB` with `v rB`. While we
|
||||
cannot calculate `r vB` with just `r` (as that'd require `vB` when we presumably only have
|
||||
`vG` when sending), since we do in fact have `v` (due to it being our own view key for our
|
||||
change output), we can still calculate the shared secret.
|
||||
*/
|
||||
has_payments_to_subaddresses && !((self.payments.len() == 2) && has_change_view)
|
||||
}
|
||||
|
||||
// Calculate the transaction keys used as randomness.
|
||||
fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
|
||||
let mut rng = self.seeded_rng(b"transaction_keys");
|
||||
|
||||
let tx_key = Zeroizing::new(Scalar::random(&mut rng));
|
||||
|
||||
let mut additional_keys = vec![];
|
||||
if self.should_use_additional_keys() {
|
||||
for _ in 0 .. self.payments.len() {
|
||||
additional_keys.push(Zeroizing::new(Scalar::random(&mut rng)));
|
||||
}
|
||||
}
|
||||
(tx_key, additional_keys)
|
||||
}
|
||||
|
||||
fn ecdhs(&self) -> Vec<Zeroizing<EdwardsPoint>> {
|
||||
let (tx_key, additional_keys) = self.transaction_keys();
|
||||
debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
|
||||
let (tx_key_pub, additional_keys_pub) = self.transaction_keys_pub();
|
||||
debug_assert_eq!(additional_keys_pub.len(), additional_keys.len());
|
||||
|
||||
let mut res = Vec::with_capacity(self.payments.len());
|
||||
for (i, payment) in self.payments.iter().enumerate() {
|
||||
let addr = payment.address();
|
||||
let key_to_use =
|
||||
if addr.is_subaddress() { additional_keys.get(i).unwrap_or(&tx_key) } else { &tx_key };
|
||||
|
||||
let ecdh = match payment {
|
||||
// If we don't have the view key, use the key dedicated for this address (r A)
|
||||
InternalPayment::Payment(_, _) | InternalPayment::Change(_, None) => {
|
||||
Zeroizing::new(key_to_use.deref() * addr.view())
|
||||
}
|
||||
// If we do have the view key, use the commitment to the key (a R)
|
||||
InternalPayment::Change(_, Some(view)) => Zeroizing::new(view.deref() * tx_key_pub),
|
||||
};
|
||||
|
||||
res.push(ecdh);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// Calculate the shared keys and the necessary derivations.
|
||||
pub(crate) fn shared_key_derivations(
|
||||
&self,
|
||||
key_images: &[EdwardsPoint],
|
||||
) -> Vec<Zeroizing<SharedKeyDerivations>> {
|
||||
let ecdhs = self.ecdhs();
|
||||
|
||||
let uniqueness = SharedKeyDerivations::uniqueness(&self.inputs(key_images));
|
||||
|
||||
let mut res = Vec::with_capacity(self.payments.len());
|
||||
for (i, (payment, ecdh)) in self.payments.iter().zip(ecdhs).enumerate() {
|
||||
let addr = payment.address();
|
||||
res.push(SharedKeyDerivations::output_derivations(
|
||||
addr.is_guaranteed().then_some(uniqueness),
|
||||
ecdh,
|
||||
i,
|
||||
));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// Calculate the payment ID XOR masks.
|
||||
pub(crate) fn payment_id_xors(&self) -> Vec<[u8; 8]> {
|
||||
let mut res = Vec::with_capacity(self.payments.len());
|
||||
for ecdh in self.ecdhs() {
|
||||
res.push(SharedKeyDerivations::payment_id_xor(ecdh));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// Calculate the transaction_keys' commitments.
|
||||
//
|
||||
// These depend on the payments. Commitments for payments to subaddresses use the spend key for
|
||||
// the generator.
|
||||
pub(crate) fn transaction_keys_pub(&self) -> (EdwardsPoint, Vec<EdwardsPoint>) {
|
||||
let (tx_key, additional_keys) = self.transaction_keys();
|
||||
debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
|
||||
|
||||
// The single transaction key uses the subaddress's spend key as its generator
|
||||
let has_payments_to_subaddresses = self.has_payments_to_subaddresses();
|
||||
let should_use_additional_keys = self.should_use_additional_keys();
|
||||
if has_payments_to_subaddresses && (!should_use_additional_keys) {
|
||||
debug_assert_eq!(additional_keys.len(), 0);
|
||||
|
||||
let InternalPayment::Payment(addr, _) = self
|
||||
.payments
|
||||
.iter()
|
||||
.find(|payment| matches!(payment, InternalPayment::Payment(_, _)))
|
||||
.expect("payment to subaddress yet no payment")
|
||||
else {
|
||||
panic!("filtered payment wasn't a payment")
|
||||
};
|
||||
|
||||
// TODO: Support subaddresses as change?
|
||||
debug_assert!(addr.is_subaddress());
|
||||
|
||||
return (tx_key.deref() * addr.spend(), vec![]);
|
||||
}
|
||||
|
||||
if should_use_additional_keys {
|
||||
let mut additional_keys_pub = vec![];
|
||||
for (additional_key, payment) in additional_keys.into_iter().zip(&self.payments) {
|
||||
let addr = payment.address();
|
||||
// TODO: Double check this against wallet2
|
||||
if addr.is_subaddress() {
|
||||
additional_keys_pub.push(additional_key.deref() * addr.spend());
|
||||
} else {
|
||||
additional_keys_pub.push(additional_key.deref() * ED25519_BASEPOINT_TABLE)
|
||||
}
|
||||
}
|
||||
return (tx_key.deref() * ED25519_BASEPOINT_TABLE, additional_keys_pub);
|
||||
}
|
||||
|
||||
debug_assert!(!has_payments_to_subaddresses);
|
||||
debug_assert!(!should_use_additional_keys);
|
||||
(tx_key.deref() * ED25519_BASEPOINT_TABLE, vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn commitments_and_encrypted_amounts(
|
||||
&self,
|
||||
key_images: &[EdwardsPoint],
|
||||
) -> Vec<(Commitment, EncryptedAmount)> {
|
||||
let shared_key_derivations = self.shared_key_derivations(key_images);
|
||||
|
||||
let mut res = Vec::with_capacity(self.payments.len());
|
||||
for (payment, shared_key_derivations) in self.payments.iter().zip(shared_key_derivations) {
|
||||
let amount = match payment {
|
||||
InternalPayment::Payment(_, amount) => *amount,
|
||||
InternalPayment::Change(_, _) => {
|
||||
let inputs = self.inputs.iter().map(|input| input.0.commitment().amount).sum::<u64>();
|
||||
let payments = self
|
||||
.payments
|
||||
.iter()
|
||||
.filter_map(|payment| match payment {
|
||||
InternalPayment::Payment(_, amount) => Some(amount),
|
||||
InternalPayment::Change(_, _) => None,
|
||||
})
|
||||
.sum::<u64>();
|
||||
let necessary_fee = self.weight_and_necessary_fee().1;
|
||||
// Safe since the constructor checked this TX has enough funds for itself
|
||||
inputs - (payments + necessary_fee)
|
||||
}
|
||||
};
|
||||
let commitment = Commitment::new(shared_key_derivations.commitment_mask(), amount);
|
||||
let encrypted_amount = EncryptedAmount::Compact {
|
||||
amount: shared_key_derivations.compact_amount_encryption(amount),
|
||||
};
|
||||
res.push((commitment, encrypted_amount));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn sum_output_masks(&self, key_images: &[EdwardsPoint]) -> Scalar {
|
||||
self
|
||||
.commitments_and_encrypted_amounts(key_images)
|
||||
.into_iter()
|
||||
.map(|(commitment, _)| commitment.mask)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user