Add SignableTransaction Read/Write

This commit is contained in:
Luke Parker
2024-06-28 05:25:02 -04:00
parent 70c36ed06c
commit abd48e9206
11 changed files with 216 additions and 234 deletions

View File

@@ -3,7 +3,7 @@
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use std_shims::vec::Vec;
use std_shims::{io, vec::Vec};
#[cfg(feature = "std")]
use std_shims::sync::OnceLock;
@@ -17,6 +17,7 @@ use curve25519_dalek::{
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
};
use monero_io::*;
use monero_generators::H;
mod unreduced_scalar;
@@ -166,4 +167,34 @@ impl Decoys {
pub fn signer_ring_members(&self) -> [EdwardsPoint; 2] {
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"))
}
}

View File

@@ -97,13 +97,6 @@ impl Bulletproof {
(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.
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,

View File

@@ -386,11 +386,6 @@ impl Clsag {
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.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_raw_vec(write_scalar, &self.s, w)?;

View File

@@ -52,6 +52,24 @@ impl FeeRate {
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.
///
/// This function may panic if any of the `FeeRate`'s fields are zero.

View File

@@ -173,12 +173,6 @@ pub struct 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.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
w.write_all(&[u8::from(rct_type)])?;
@@ -295,16 +289,6 @@ pub enum 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.
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
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.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
let rct_type = self.rct_type();

View File

@@ -32,13 +32,6 @@ pub enum 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.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
@@ -98,13 +91,6 @@ pub struct 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.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_varint(&self.amount.unwrap_or(0), w)?;
@@ -221,23 +207,6 @@ pub struct 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.
///
/// 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.
///
/// Some writable transactions may not be readable if they're malformed, per Monero's consensus

View File

@@ -30,11 +30,11 @@ rand_chacha = { version = "0.3", default-features = false }
# Used to select decoys
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"] }
# Multisig dependencies
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 }
frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true }
@@ -72,5 +72,5 @@ std = [
"monero-address/std",
]
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"]

View File

@@ -208,23 +208,6 @@ impl Extra {
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<()> {
for field in &self.0 {
field.write(w)?;

View File

@@ -1,3 +1,5 @@
use std_shims::io;
use zeroize::Zeroize;
use crate::{
@@ -94,85 +96,15 @@ impl Eventuality {
true
}
/*
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
self.protocol.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)
self.0.write(w)
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(128);
self.write(&mut buf).unwrap();
buf
self.0.serialize()
}
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Eventuality> {
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"))
Ok(Eventuality(SignableTransaction::read(r)?))
}
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)?,
})
}
*/
}

View File

@@ -1,4 +1,5 @@
use core::{ops::Deref, fmt};
use std_shims::io;
use zeroize::{Zeroize, Zeroizing};
@@ -10,6 +11,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}
use frost::FrostError;
use crate::{
io::*,
generators::{MAX_COMMITMENTS, hash_to_point},
primitives::Decoys,
ringct::{
@@ -158,7 +160,12 @@ pub enum SendError {
#[cfg_attr(feature = "std", error("invalid amount of key images specified"))]
InvalidAmountOfKeyImages,
#[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})"))]
ClsagError(ClsagError),
#[cfg(feature = "multisig")]
@@ -176,27 +183,24 @@ pub struct SignableTransaction {
fee_rate: FeeRate,
}
struct SignableTransactionWithKeyImages {
intent: SignableTransaction,
key_images: Vec<EdwardsPoint>,
}
impl SignableTransaction {
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> {
match rct_type {
fn validate(&self) -> Result<(), SendError> {
match self.rct_type {
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
_ => Err(SendError::UnsupportedRctType)?,
};
}
if inputs.is_empty() {
if self.inputs.is_empty() {
Err(SendError::NoInputs)?;
}
for (_, decoys) in &inputs {
for (_, decoys) in &self.inputs {
if decoys.len() !=
match rct_type {
match self.rct_type {
RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16,
_ => 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)?;
}
// 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)?;
}
// 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;
let mut count = |addr: MoneroAddress| {
if addr.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),
for payment in &self.payments {
payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
}
if payment_ids > 1 {
Err(SendError::MultiplePaymentIds)?;
}
}
// Re-format the payments and change into a consolidated payments list
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 {
if self.payments.len() > MAX_COMMITMENTS {
Err(SendError::TooManyOutputs)?;
}
// Check the length of each arbitrary data
for part in &data {
for part in &self.data {
if part.len() > MAX_ARBITRARY_DATA_SIZE {
Err(SendError::TooMuchData)?;
}
}
let res = SignableTransaction { rct_type, sender_view_key, inputs, payments, data, fee_rate };
// Check the length of TX extra
// https://github.com/monero-project/monero/pull/8733
const MAX_EXTRA_SIZE: usize = 1060;
if res.extra().len() > MAX_EXTRA_SIZE {
if self.extra().len() > MAX_EXTRA_SIZE {
Err(SendError::TooMuchData)?;
}
// 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
if in_amount < payments_amount {
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) {
Err(SendError::NotEnoughFunds {
inputs: in_amount,
@@ -290,6 +289,116 @@ impl SignableTransaction {
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)
}
@@ -360,7 +469,7 @@ impl SignableTransaction {
.sum::<Scalar>();
// 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
let clsags_and_pseudo_outs =
@@ -391,8 +500,3 @@ impl SignableTransaction {
Ok(tx)
}
}
struct SignableTransactionWithKeyImages {
intent: SignableTransaction,
key_images: Vec<EdwardsPoint>,
}

View File

@@ -216,7 +216,7 @@ impl SignableTransaction {
}
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 =
self.intent.commitments_and_encrypted_amounts(&self.key_images);
let mut commitments = Vec::with_capacity(self.intent.payments.len());