mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 21:49:26 +00:00
Add SignableTransaction Read/Write
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
use std_shims::vec::Vec;
|
use std_shims::{io, vec::Vec};
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std_shims::sync::OnceLock;
|
use std_shims::sync::OnceLock;
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ use curve25519_dalek::{
|
|||||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use monero_io::*;
|
||||||
use monero_generators::H;
|
use monero_generators::H;
|
||||||
|
|
||||||
mod unreduced_scalar;
|
mod unreduced_scalar;
|
||||||
@@ -166,4 +167,34 @@ impl Decoys {
|
|||||||
pub fn signer_ring_members(&self) -> [EdwardsPoint; 2] {
|
pub fn signer_ring_members(&self) -> [EdwardsPoint; 2] {
|
||||||
self.ring[usize::from(self.signer_index)]
|
self.ring[usize::from(self.signer_index)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the Decoys.
|
||||||
|
pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
write_vec(write_varint, &self.offsets, w)?;
|
||||||
|
w.write_all(&[self.signer_index])?;
|
||||||
|
write_vec(
|
||||||
|
|pair, w| {
|
||||||
|
write_point(&pair[0], w)?;
|
||||||
|
write_point(&pair[1], w)
|
||||||
|
},
|
||||||
|
&self.ring,
|
||||||
|
w,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/// Serialize the Decoys to a `Vec<u8>`.
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res =
|
||||||
|
Vec::with_capacity((1 + (2 * self.offsets.len())) + 1 + 1 + (self.ring.len() * 64));
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
/// Read a set of Decoys.
|
||||||
|
pub fn read(r: &mut impl io::Read) -> io::Result<Decoys> {
|
||||||
|
Decoys::new(
|
||||||
|
read_vec(read_varint, r)?,
|
||||||
|
read_byte(r)?,
|
||||||
|
read_vec(|r| Ok([read_point(r)?, read_point(r)?]), r)?,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| io::Error::other("invalid Decoys"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,13 +97,6 @@ impl Bulletproof {
|
|||||||
(bp_clawback, LR_len)
|
(bp_clawback, LR_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the weight of this proof.
|
|
||||||
pub fn fee_weight(plus: bool, outputs: usize) -> usize {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let (bp_clawback, LR_len) = Bulletproof::calculate_bp_clawback(plus, outputs);
|
|
||||||
32 * (Bulletproof::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof.
|
/// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof.
|
||||||
pub fn prove<R: RngCore + CryptoRng>(
|
pub fn prove<R: RngCore + CryptoRng>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
|
|||||||
@@ -386,11 +386,6 @@ impl Clsag {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The length a CLSAG will take once serialized.
|
|
||||||
pub fn fee_weight(ring_len: usize) -> usize {
|
|
||||||
(ring_len * 32) + 32 + 32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a CLSAG.
|
/// Write a CLSAG.
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
write_raw_vec(write_scalar, &self.s, w)?;
|
write_raw_vec(write_scalar, &self.s, w)?;
|
||||||
|
|||||||
@@ -52,6 +52,24 @@ impl FeeRate {
|
|||||||
Ok(FeeRate { per_weight, mask })
|
Ok(FeeRate { per_weight, mask })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the FeeRate.
|
||||||
|
pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
|
||||||
|
w.write_all(&self.per_weight.to_le_bytes())?;
|
||||||
|
w.write_all(&self.mask.to_le_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the FeeRate to a `Vec<u8>`.
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(16);
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a FeeRate.
|
||||||
|
pub fn read(r: &mut impl io::Read) -> io::Result<FeeRate> {
|
||||||
|
Ok(FeeRate { per_weight: read_u64(r)?, mask: read_u64(r)? })
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the fee to use from the weight.
|
/// Calculate the fee to use from the weight.
|
||||||
///
|
///
|
||||||
/// This function may panic if any of the `FeeRate`'s fields are zero.
|
/// This function may panic if any of the `FeeRate`'s fields are zero.
|
||||||
|
|||||||
@@ -173,12 +173,6 @@ pub struct RctBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RctBase {
|
impl RctBase {
|
||||||
/// The weight of this RctBase as relevant for fees.
|
|
||||||
pub fn fee_weight(outputs: usize, fee: u64) -> usize {
|
|
||||||
// 1 byte for the RCT signature type
|
|
||||||
1 + (outputs * (8 + 32)) + varint_len(fee)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the RctBase.
|
/// Write the RctBase.
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
w.write_all(&[u8::from(rct_type)])?;
|
w.write_all(&[u8::from(rct_type)])?;
|
||||||
@@ -295,16 +289,6 @@ pub enum RctPrunable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RctPrunable {
|
impl RctPrunable {
|
||||||
/// The weight of this RctPrunable as relevant for fees.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub fn fee_weight(bp_plus: bool, ring_len: usize, inputs: usize, outputs: usize) -> usize {
|
|
||||||
// 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one)
|
|
||||||
1 +
|
|
||||||
Bulletproof::fee_weight(bp_plus, outputs) +
|
|
||||||
// There's both the CLSAG and the pseudo-out
|
|
||||||
(inputs * (Clsag::fee_weight(ring_len) + 32))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the RctPrunable.
|
/// Write the RctPrunable.
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
@@ -446,17 +430,6 @@ impl RctProofs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The weight of this RctProofs, as relevant for fees.
|
|
||||||
pub fn fee_weight(
|
|
||||||
bp_plus: bool,
|
|
||||||
ring_len: usize,
|
|
||||||
inputs: usize,
|
|
||||||
outputs: usize,
|
|
||||||
fee: u64,
|
|
||||||
) -> usize {
|
|
||||||
RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(bp_plus, ring_len, inputs, outputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the RctProofs.
|
/// Write the RctProofs.
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
let rct_type = self.rct_type();
|
let rct_type = self.rct_type();
|
||||||
|
|||||||
@@ -32,13 +32,6 @@ pub enum Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
/// The weight of this Input, as relevant for fees.
|
|
||||||
pub fn fee_weight(offsets_weight: usize) -> usize {
|
|
||||||
// Uses 1 byte for the input type
|
|
||||||
// Uses 1 byte for the VarInt amount due to amount being 0
|
|
||||||
1 + 1 + offsets_weight + 32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Input.
|
/// Write the Input.
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
@@ -98,13 +91,6 @@ pub struct Output {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
/// The weight of this Output, as relevant for fees.
|
|
||||||
pub fn fee_weight(view_tags: bool) -> usize {
|
|
||||||
// Uses 1 byte for the output type
|
|
||||||
// Uses 1 byte for the VarInt amount due to amount being 0
|
|
||||||
1 + 1 + 32 + if view_tags { 1 } else { 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Output.
|
/// Write the Output.
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
write_varint(&self.amount.unwrap_or(0), w)?;
|
write_varint(&self.amount.unwrap_or(0), w)?;
|
||||||
@@ -221,23 +207,6 @@ pub struct TransactionPrefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionPrefix {
|
impl TransactionPrefix {
|
||||||
/// The weight of this TransactionPrefix, as relevant for fees.
|
|
||||||
pub fn fee_weight(
|
|
||||||
decoy_weights: &[usize],
|
|
||||||
outputs: usize,
|
|
||||||
view_tags: bool,
|
|
||||||
extra: usize,
|
|
||||||
) -> usize {
|
|
||||||
// Assumes Timelock::None since this library won't let you create a TX with a timelock
|
|
||||||
// 1 input for every decoy weight
|
|
||||||
1 + varint_len(decoy_weights.len()) +
|
|
||||||
decoy_weights.iter().map(|&offsets_weight| Input::fee_weight(offsets_weight)).sum::<usize>() +
|
|
||||||
varint_len(outputs) +
|
|
||||||
(outputs * Output::fee_weight(view_tags)) +
|
|
||||||
varint_len(extra) +
|
|
||||||
extra
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a TransactionPrefix.
|
/// Write a TransactionPrefix.
|
||||||
///
|
///
|
||||||
/// This is distinct from Monero in that it won't write any version.
|
/// This is distinct from Monero in that it won't write any version.
|
||||||
@@ -323,22 +292,6 @@ impl Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The weight of this Transaction, as relevant for fees.
|
|
||||||
// TODO: Replace ring_len, decoy_weights for &[&[usize]], where the inner buf is the decoy
|
|
||||||
// offsets
|
|
||||||
pub fn fee_weight(
|
|
||||||
view_tags: bool,
|
|
||||||
bp_plus: bool,
|
|
||||||
ring_len: usize,
|
|
||||||
decoy_weights: &[usize],
|
|
||||||
outputs: usize,
|
|
||||||
extra: usize,
|
|
||||||
fee: u64,
|
|
||||||
) -> usize {
|
|
||||||
1 + TransactionPrefix::fee_weight(decoy_weights, outputs, view_tags, extra) +
|
|
||||||
RctProofs::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the Transaction.
|
/// Write the Transaction.
|
||||||
///
|
///
|
||||||
/// Some writable transactions may not be readable if they're malformed, per Monero's consensus
|
/// Some writable transactions may not be readable if they're malformed, per Monero's consensus
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ rand_chacha = { version = "0.3", default-features = false }
|
|||||||
# Used to select decoys
|
# Used to select decoys
|
||||||
rand_distr = { version = "0.4", default-features = false }
|
rand_distr = { version = "0.4", default-features = false }
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "group"] }
|
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "group"] }
|
||||||
|
|
||||||
# Multisig dependencies
|
# Multisig dependencies
|
||||||
transcript = { package = "flexible-transcript", path = "../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
transcript = { package = "flexible-transcript", path = "../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
||||||
|
group = { version = "0.13", default-features = false, optional = true }
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true }
|
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true }
|
||||||
frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true }
|
frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true }
|
||||||
|
|
||||||
@@ -72,5 +72,5 @@ std = [
|
|||||||
"monero-address/std",
|
"monero-address/std",
|
||||||
]
|
]
|
||||||
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"]
|
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"]
|
||||||
multisig = ["transcript", "dalek-ff-group", "frost", "monero-serai/multisig", "std"]
|
multisig = ["transcript", "group", "dalek-ff-group", "frost", "monero-serai/multisig", "std"]
|
||||||
default = ["std", "compile-time-generators"]
|
default = ["std", "compile-time-generators"]
|
||||||
|
|||||||
@@ -208,23 +208,6 @@ impl Extra {
|
|||||||
self.0.push(field);
|
self.0.push(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub fn fee_weight(
|
|
||||||
outputs: usize,
|
|
||||||
additional: bool,
|
|
||||||
payment_id: bool,
|
|
||||||
data: &[Vec<u8>]
|
|
||||||
) -> usize {
|
|
||||||
// PublicKey, key
|
|
||||||
(1 + 32) +
|
|
||||||
// PublicKeys, length, additional keys
|
|
||||||
(if additional { 1 + 1 + (outputs * 32) } else { 0 }) +
|
|
||||||
// PaymentId (Nonce), length, encrypted, ID
|
|
||||||
(if payment_id { 1 + 1 + 1 + 8 } else { 0 }) +
|
|
||||||
// Nonce, length, ARBITRARY_DATA_MARKER, data
|
|
||||||
data.iter().map(|v| 1 + varint_len(1 + v.len()) + 1 + v.len()).sum::<usize>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
field.write(w)?;
|
field.write(w)?;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std_shims::io;
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -94,85 +96,15 @@ impl Eventuality {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
self.protocol.write(w)?;
|
self.0.write(w)
|
||||||
write_raw_vec(write_byte, self.r_seed.as_ref(), w)?;
|
|
||||||
write_vec(write_point, &self.inputs, w)?;
|
|
||||||
|
|
||||||
fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
|
|
||||||
match payment {
|
|
||||||
InternalPayment::Payment(payment, need_dummy_payment_id) => {
|
|
||||||
w.write_all(&[0])?;
|
|
||||||
write_vec(write_byte, payment.0.to_string().as_bytes(), w)?;
|
|
||||||
w.write_all(&payment.1.to_le_bytes())?;
|
|
||||||
if *need_dummy_payment_id {
|
|
||||||
w.write_all(&[1])
|
|
||||||
} else {
|
|
||||||
w.write_all(&[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InternalPayment::Change(change, change_view) => {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
write_vec(write_byte, change.0.to_string().as_bytes(), w)?;
|
|
||||||
w.write_all(&change.1.to_le_bytes())?;
|
|
||||||
if let Some(view) = change_view.as_ref() {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
write_scalar(view, w)
|
|
||||||
} else {
|
|
||||||
w.write_all(&[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write_vec(write_payment, &self.payments, w)?;
|
|
||||||
|
|
||||||
write_vec(write_byte, &self.extra, w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
let mut buf = Vec::with_capacity(128);
|
self.0.serialize()
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Eventuality> {
|
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Eventuality> {
|
||||||
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
|
Ok(Eventuality(SignableTransaction::read(r)?))
|
||||||
String::from_utf8(read_vec(read_byte, r)?)
|
|
||||||
.ok()
|
|
||||||
.and_then(|str| MoneroAddress::from_str_raw(&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)?),
|
|
||||||
match read_byte(r)? {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => Err(io::Error::other("invalid need additional"))?,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
1 => InternalPayment::Change(
|
|
||||||
(read_address(r)?, read_u64(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"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Eventuality {
|
|
||||||
protocol: RctType::read(r)?,
|
|
||||||
r_seed: Zeroizing::new(read_bytes::<_, 32>(r)?),
|
|
||||||
inputs: read_vec(read_point, r)?,
|
|
||||||
payments: read_vec(read_payment, r)?,
|
|
||||||
extra: read_vec(read_byte, r)?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use core::{ops::Deref, fmt};
|
use core::{ops::Deref, fmt};
|
||||||
|
use std_shims::io;
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}
|
|||||||
use frost::FrostError;
|
use frost::FrostError;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
io::*,
|
||||||
generators::{MAX_COMMITMENTS, hash_to_point},
|
generators::{MAX_COMMITMENTS, hash_to_point},
|
||||||
primitives::Decoys,
|
primitives::Decoys,
|
||||||
ringct::{
|
ringct::{
|
||||||
@@ -158,7 +160,12 @@ pub enum SendError {
|
|||||||
#[cfg_attr(feature = "std", error("invalid amount of key images specified"))]
|
#[cfg_attr(feature = "std", error("invalid amount of key images specified"))]
|
||||||
InvalidAmountOfKeyImages,
|
InvalidAmountOfKeyImages,
|
||||||
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
||||||
WrongPrivateKey, // TODO
|
WrongPrivateKey,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "std",
|
||||||
|
error("this SignableTransaction was created by deserializing a malicious serialization")
|
||||||
|
)]
|
||||||
|
MaliciousSerialization,
|
||||||
#[cfg_attr(feature = "std", error("clsag error ({0})"))]
|
#[cfg_attr(feature = "std", error("clsag error ({0})"))]
|
||||||
ClsagError(ClsagError),
|
ClsagError(ClsagError),
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
@@ -176,27 +183,24 @@ pub struct SignableTransaction {
|
|||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SignableTransactionWithKeyImages {
|
||||||
|
intent: SignableTransaction,
|
||||||
|
key_images: Vec<EdwardsPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
impl SignableTransaction {
|
impl SignableTransaction {
|
||||||
pub fn new(
|
fn validate(&self) -> Result<(), SendError> {
|
||||||
rct_type: RctType,
|
match self.rct_type {
|
||||||
sender_view_key: Zeroizing<Scalar>,
|
|
||||||
inputs: Vec<(SpendableOutput, Decoys)>,
|
|
||||||
payments: Vec<(MoneroAddress, u64)>,
|
|
||||||
change: Change,
|
|
||||||
data: Vec<Vec<u8>>,
|
|
||||||
fee_rate: FeeRate,
|
|
||||||
) -> Result<SignableTransaction, SendError> {
|
|
||||||
match rct_type {
|
|
||||||
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
|
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
|
||||||
_ => Err(SendError::UnsupportedRctType)?,
|
_ => Err(SendError::UnsupportedRctType)?,
|
||||||
};
|
}
|
||||||
|
|
||||||
if inputs.is_empty() {
|
if self.inputs.is_empty() {
|
||||||
Err(SendError::NoInputs)?;
|
Err(SendError::NoInputs)?;
|
||||||
}
|
}
|
||||||
for (_, decoys) in &inputs {
|
for (_, decoys) in &self.inputs {
|
||||||
if decoys.len() !=
|
if decoys.len() !=
|
||||||
match rct_type {
|
match self.rct_type {
|
||||||
RctType::ClsagBulletproof => 11,
|
RctType::ClsagBulletproof => 11,
|
||||||
RctType::ClsagBulletproofPlus => 16,
|
RctType::ClsagBulletproofPlus => 16,
|
||||||
_ => panic!("unsupported RctType"),
|
_ => panic!("unsupported RctType"),
|
||||||
@@ -206,74 +210,69 @@ impl SignableTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payments.is_empty() {
|
// Check we have at least one non-change output
|
||||||
|
if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
|
||||||
Err(SendError::NoOutputs)?;
|
Err(SendError::NoOutputs)?;
|
||||||
}
|
}
|
||||||
// If we don't have at least two outputs, as required by Monero, error
|
// If we don't have at least two outputs, as required by Monero, error
|
||||||
if (payments.len() == 1) && matches!(change, Change(ChangeEnum::None)) {
|
if self.payments.len() < 2 {
|
||||||
Err(SendError::NoChange)?;
|
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
|
// Make sure there's at most one payment ID
|
||||||
{
|
{
|
||||||
let mut payment_ids = 0;
|
let mut payment_ids = 0;
|
||||||
let mut count = |addr: MoneroAddress| {
|
for payment in &self.payments {
|
||||||
if addr.payment_id().is_some() {
|
payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
|
||||||
payment_ids += 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for payment in &payments {
|
|
||||||
count(payment.0);
|
|
||||||
}
|
|
||||||
match &change.0 {
|
|
||||||
ChangeEnum::None => (),
|
|
||||||
ChangeEnum::AddressOnly(addr) | ChangeEnum::AddressWithView(addr, _) => count(*addr),
|
|
||||||
}
|
}
|
||||||
if payment_ids > 1 {
|
if payment_ids > 1 {
|
||||||
Err(SendError::MultiplePaymentIds)?;
|
Err(SendError::MultiplePaymentIds)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-format the payments and change into a consolidated payments list
|
if self.payments.len() > MAX_COMMITMENTS {
|
||||||
let payments_amount = payments.iter().map(|(_, amount)| amount).sum::<u64>();
|
|
||||||
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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if payments.len() > MAX_COMMITMENTS {
|
|
||||||
Err(SendError::TooManyOutputs)?;
|
Err(SendError::TooManyOutputs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the length of each arbitrary data
|
// Check the length of each arbitrary data
|
||||||
for part in &data {
|
for part in &self.data {
|
||||||
if part.len() > MAX_ARBITRARY_DATA_SIZE {
|
if part.len() > MAX_ARBITRARY_DATA_SIZE {
|
||||||
Err(SendError::TooMuchData)?;
|
Err(SendError::TooMuchData)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = SignableTransaction { rct_type, sender_view_key, inputs, payments, data, fee_rate };
|
|
||||||
|
|
||||||
// Check the length of TX extra
|
// Check the length of TX extra
|
||||||
// https://github.com/monero-project/monero/pull/8733
|
// https://github.com/monero-project/monero/pull/8733
|
||||||
const MAX_EXTRA_SIZE: usize = 1060;
|
const MAX_EXTRA_SIZE: usize = 1060;
|
||||||
if res.extra().len() > MAX_EXTRA_SIZE {
|
if self.extra().len() > MAX_EXTRA_SIZE {
|
||||||
Err(SendError::TooMuchData)?;
|
Err(SendError::TooMuchData)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we have enough funds
|
// Make sure we have enough funds
|
||||||
let in_amount = res.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
let in_amount = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
||||||
|
let payments_amount = self
|
||||||
|
.payments
|
||||||
|
.iter()
|
||||||
|
.filter_map(|payment| match payment {
|
||||||
|
InternalPayment::Payment(_, amount) => Some(amount),
|
||||||
|
InternalPayment::Change(_, _) => None,
|
||||||
|
})
|
||||||
|
.sum::<u64>();
|
||||||
// Necessary so weight_and_fee doesn't underflow
|
// Necessary so weight_and_fee doesn't underflow
|
||||||
if in_amount < payments_amount {
|
if in_amount < payments_amount {
|
||||||
Err(SendError::NotEnoughFunds { inputs: in_amount, outputs: payments_amount, fee: None })?;
|
Err(SendError::NotEnoughFunds { inputs: in_amount, outputs: payments_amount, fee: None })?;
|
||||||
}
|
}
|
||||||
let (weight, fee) = res.weight_and_fee();
|
let (weight, fee) = self.weight_and_fee();
|
||||||
if in_amount < (payments_amount + fee) {
|
if in_amount < (payments_amount + fee) {
|
||||||
Err(SendError::NotEnoughFunds {
|
Err(SendError::NotEnoughFunds {
|
||||||
inputs: in_amount,
|
inputs: in_amount,
|
||||||
@@ -290,6 +289,116 @@ impl SignableTransaction {
|
|||||||
Err(SendError::TooLargeTransaction)?;
|
Err(SendError::TooLargeTransaction)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
rct_type: RctType,
|
||||||
|
sender_view_key: Zeroizing<Scalar>,
|
||||||
|
inputs: Vec<(SpendableOutput, 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 res = SignableTransaction { rct_type, sender_view_key, inputs, payments, data, fee_rate };
|
||||||
|
res.validate()?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
fn write_input<W: io::Write>(input: &(SpendableOutput, Decoys), w: &mut W) -> io::Result<()> {
|
||||||
|
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)?;
|
||||||
|
write_scalar(&self.sender_view_key, w)?;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::with_capacity(256);
|
||||||
|
self.write(&mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
|
||||||
|
fn read_input(r: &mut impl io::Read) -> io::Result<(SpendableOutput, Decoys)> {
|
||||||
|
Ok((SpendableOutput::read(r)?, Decoys::read(r)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_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_raw(&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"))?,
|
||||||
|
sender_view_key: Zeroizing::new(read_scalar(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)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +469,7 @@ impl SignableTransaction {
|
|||||||
.sum::<Scalar>();
|
.sum::<Scalar>();
|
||||||
|
|
||||||
// Get the actual TX, just needing the CLSAGs
|
// Get the actual TX, just needing the CLSAGs
|
||||||
let mut tx = tx.transaction_without_clsags_and_pseudo_outs();
|
let mut tx = tx.transaction_without_signatures();
|
||||||
|
|
||||||
// Sign the CLSAGs
|
// Sign the CLSAGs
|
||||||
let clsags_and_pseudo_outs =
|
let clsags_and_pseudo_outs =
|
||||||
@@ -391,8 +500,3 @@ impl SignableTransaction {
|
|||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SignableTransactionWithKeyImages {
|
|
||||||
intent: SignableTransaction,
|
|
||||||
key_images: Vec<EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ impl SignableTransaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SignableTransactionWithKeyImages {
|
impl SignableTransactionWithKeyImages {
|
||||||
pub(crate) fn transaction_without_clsags_and_pseudo_outs(&self) -> Transaction {
|
pub(crate) fn transaction_without_signatures(&self) -> Transaction {
|
||||||
let commitments_and_encrypted_amounts =
|
let commitments_and_encrypted_amounts =
|
||||||
self.intent.commitments_and_encrypted_amounts(&self.key_images);
|
self.intent.commitments_and_encrypted_amounts(&self.key_images);
|
||||||
let mut commitments = Vec::with_capacity(self.intent.payments.len());
|
let mut commitments = Vec::with_capacity(self.intent.payments.len());
|
||||||
|
|||||||
Reference in New Issue
Block a user