mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 05:09:22 +00:00
Rename the coins folder to networks (#583)
* Rename the coins folder to networks Ethereum isn't a coin. It's a network. Resolves #357. * More renames of coins -> networks in orchestration * Correct paths in tests/ * cargo fmt
This commit is contained in:
137
networks/monero/wallet/src/send/eventuality.rs
Normal file
137
networks/monero/wallet/src/send/eventuality.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use std_shims::{vec::Vec, io};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
ringct::PrunedRctProofs,
|
||||
transaction::{Input, Timelock, Pruned, 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<Pruned>) -> 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(PrunedRctProofs { 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)?))
|
||||
}
|
||||
}
|
||||
581
networks/monero/wallet/src/send/mod.rs
Normal file
581
networks/monero/wallet/src/send/mod.rs
Normal file
@@ -0,0 +1,581 @@
|
||||
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},
|
||||
ringct::{
|
||||
clsag::{ClsagError, ClsagContext, Clsag},
|
||||
RctType, RctPrunable, RctProofs,
|
||||
},
|
||||
transaction::Transaction,
|
||||
address::{Network, SubaddressIndex, MoneroAddress},
|
||||
extra::MAX_ARBITRARY_DATA_SIZE,
|
||||
rpc::FeeRate,
|
||||
ViewPair, GuaranteedViewPair, OutputWithDecoys,
|
||||
};
|
||||
|
||||
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 {
|
||||
AddressOnly(MoneroAddress),
|
||||
Standard { view_pair: ViewPair, subaddress: Option<SubaddressIndex> },
|
||||
Guaranteed { view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex> },
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChangeEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ChangeEnum::AddressOnly(addr) => {
|
||||
f.debug_struct("ChangeEnum::AddressOnly").field("addr", &addr).finish()
|
||||
}
|
||||
ChangeEnum::Standard { subaddress, .. } => f
|
||||
.debug_struct("ChangeEnum::Standard")
|
||||
.field("subaddress", &subaddress)
|
||||
.finish_non_exhaustive(),
|
||||
ChangeEnum::Guaranteed { subaddress, .. } => f
|
||||
.debug_struct("ChangeEnum::Guaranteed")
|
||||
.field("subaddress", &subaddress)
|
||||
.finish_non_exhaustive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specification for a change output.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Change(Option<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_pair: ViewPair, subaddress: Option<SubaddressIndex>) -> Change {
|
||||
Change(Some(ChangeEnum::Standard { view_pair, subaddress }))
|
||||
}
|
||||
|
||||
/// 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_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex>) -> Change {
|
||||
Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress }))
|
||||
}
|
||||
|
||||
/// 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(Some(ChangeEnum::AddressOnly(address)))
|
||||
} else {
|
||||
Change(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
enum InternalPayment {
|
||||
Payment(MoneroAddress, u64),
|
||||
Change(ChangeEnum),
|
||||
}
|
||||
|
||||
impl InternalPayment {
|
||||
fn address(&self) -> MoneroAddress {
|
||||
match self {
|
||||
InternalPayment::Payment(addr, _) => *addr,
|
||||
InternalPayment::Change(change) => match change {
|
||||
ChangeEnum::AddressOnly(addr) => *addr,
|
||||
// Network::Mainnet as the network won't effect the derivations
|
||||
ChangeEnum::Standard { view_pair, subaddress } => match subaddress {
|
||||
Some(subaddress) => view_pair.subaddress(Network::Mainnet, *subaddress),
|
||||
None => view_pair.legacy_address(Network::Mainnet),
|
||||
},
|
||||
ChangeEnum::Guaranteed { view_pair, subaddress } => {
|
||||
view_pair.address(Network::Mainnet, *subaddress, None)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<OutputWithDecoys>,
|
||||
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 input in &self.inputs {
|
||||
if input.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 limit is half the no-penalty block size
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/wallet/wallet2.cpp#L110766-L11085
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/cryptonote_config.h#L61
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/cryptonote_config.h#L64
|
||||
const MAX_TX_SIZE: usize = (300_000 / 2) - 600;
|
||||
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<OutputWithDecoys>,
|
||||
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<_>>();
|
||||
|
||||
if let Some(change) = change.0 {
|
||||
payments.push(InternalPayment::Change(change));
|
||||
}
|
||||
|
||||
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_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(change) => match change {
|
||||
ChangeEnum::AddressOnly(addr) => {
|
||||
w.write_all(&[1])?;
|
||||
write_vec(write_byte, addr.to_string().as_bytes(), w)
|
||||
}
|
||||
ChangeEnum::Standard { view_pair, subaddress } => {
|
||||
w.write_all(&[2])?;
|
||||
write_point(&view_pair.spend(), w)?;
|
||||
write_scalar(&view_pair.view, w)?;
|
||||
if let Some(subaddress) = subaddress {
|
||||
w.write_all(&subaddress.account().to_le_bytes())?;
|
||||
w.write_all(&subaddress.address().to_le_bytes())
|
||||
} else {
|
||||
w.write_all(&0u32.to_le_bytes())?;
|
||||
w.write_all(&0u32.to_le_bytes())
|
||||
}
|
||||
}
|
||||
ChangeEnum::Guaranteed { view_pair, subaddress } => {
|
||||
w.write_all(&[3])?;
|
||||
write_point(&view_pair.spend(), w)?;
|
||||
write_scalar(&view_pair.0.view, w)?;
|
||||
if let Some(subaddress) = subaddress {
|
||||
w.write_all(&subaddress.account().to_le_bytes())?;
|
||||
w.write_all(&subaddress.address().to_le_bytes())
|
||||
} else {
|
||||
w.write_all(&0u32.to_le_bytes())?;
|
||||
w.write_all(&0u32.to_le_bytes())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
write_byte(&u8::from(self.rct_type), w)?;
|
||||
w.write_all(self.outgoing_view_key.as_slice())?;
|
||||
write_vec(OutputWithDecoys::write, &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_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(ChangeEnum::AddressOnly(read_address(r)?)),
|
||||
2 => InternalPayment::Change(ChangeEnum::Standard {
|
||||
view_pair: ViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
|
||||
.map_err(io::Error::other)?,
|
||||
subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
|
||||
}),
|
||||
3 => InternalPayment::Change(ChangeEnum::Guaranteed {
|
||||
view_pair: GuaranteedViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
|
||||
.map_err(io::Error::other)?,
|
||||
subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
|
||||
}),
|
||||
_ => 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(OutputWithDecoys::read, 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 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(input.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
networks/monero/wallet/src/send/multisig.rs
Normal file
304
networks/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 input in &self.inputs {
|
||||
// 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(input.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 + input.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
networks/monero/wallet/src/send/tx.rs
Normal file
323
networks/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, 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 (input, key_image) in self.inputs.iter().zip(key_images) {
|
||||
res.push(Input::ToKey {
|
||||
amount: None,
|
||||
key_offsets: input.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_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_nonce(id_vec);
|
||||
}
|
||||
}
|
||||
|
||||
// Include data if present
|
||||
for part in &self.data {
|
||||
let mut arb = vec![ARBITRARY_DATA_MARKER];
|
||||
arb.extend(part);
|
||||
extra.push_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.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![] },
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
246
networks/monero/wallet/src/send/tx_keys.rs
Normal file
246
networks/monero/wallet/src/send/tx_keys.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
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, OutputWithDecoys,
|
||||
send::{ChangeEnum, 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(OutputWithDecoys::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(change) => match change {
|
||||
ChangeEnum::AddressOnly(addr) => addr.is_subaddress(),
|
||||
// These aren't considered payments to subaddresses as we don't need to send to them as
|
||||
// subaddresses
|
||||
// We can calculate the shared key using the view key, as if we were receiving, instead
|
||||
ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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(change) => match change {
|
||||
ChangeEnum::AddressOnly(_) => false,
|
||||
ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => true,
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
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(ChangeEnum::AddressOnly { .. }) => {
|
||||
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(ChangeEnum::Standard { view_pair, .. }) => {
|
||||
Zeroizing::new(view_pair.view.deref() * tx_key_pub)
|
||||
}
|
||||
InternalPayment::Change(ChangeEnum::Guaranteed { view_pair, .. }) => {
|
||||
Zeroizing::new(view_pair.0.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")
|
||||
};
|
||||
|
||||
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();
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/device/device_default.cpp#L308-L312
|
||||
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.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