Respond to 1.1 A2 (also cited as 2 1)

`read_vec` was unbounded. It now accepts an optional bound. In some places, we
are able to define and provide a bound (Bulletproofs(+)' `L` and `R` vectors).
In others, we cannot (the amount of inputs within a transaction, which is not
subject to any rule in the current consensus other than the total transaction
size limit). Usage of `None` in those locations preserves the existing
behavior.
This commit is contained in:
Luke Parker
2025-07-23 08:58:02 -04:00
parent b426bfcfe8
commit 6b8cf6653a
8 changed files with 43 additions and 30 deletions

View File

@@ -51,8 +51,6 @@ pub fn H_pow_2() -> &'static [EdwardsPoint; 64] {
pub const MAX_COMMITMENTS: usize = 16; pub const MAX_COMMITMENTS: usize = 16;
/// The amount of bits a value within a commitment may use. /// The amount of bits a value within a commitment may use.
pub const COMMITMENT_BITS: usize = 64; pub const COMMITMENT_BITS: usize = 64;
/// The logarithm (over 2) of the amount of bits a value within a commitment may use.
pub const LOG_COMMITMENT_BITS: usize = 6; // 2 ** 6 == N
/// Container struct for Bulletproofs(+) generators. /// Container struct for Bulletproofs(+) generators.
#[allow(non_snake_case)] #[allow(non_snake_case)]

View File

@@ -214,6 +214,20 @@ pub fn read_array<R: Read, T: Debug, F: Fn(&mut R) -> io::Result<T>, const N: us
} }
/// Read a length-prefixed variable-length list of elements. /// Read a length-prefixed variable-length list of elements.
pub fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(f: F, r: &mut R) -> io::Result<Vec<T>> { ///
read_raw_vec(f, read_varint(r)?, r) /// An optional bound on the length of the result may be provided. If `None`, the returned `Vec`
/// will be of the length read off the reader, if successfully read. If `Some(_)`, an error will be
/// raised if the length read off the read is greater than the bound.
pub fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
f: F,
length_bound: Option<usize>,
r: &mut R,
) -> io::Result<Vec<T>> {
let declared_length: usize = read_varint(r)?;
if let Some(length_bound) = length_bound {
if declared_length > length_bound {
Err(io::Error::other("vector exceeds bound on length"))?;
}
}
read_raw_vec(f, declared_length, r)
} }

View File

@@ -213,7 +213,7 @@ impl Decoys {
pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> { pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
write_vec(write_varint, &self.offsets, w)?; write_vec(write_varint, &self.offsets, w)?;
w.write_all(&[self.signer_index])?; w.write_all(&[self.signer_index])?;
write_vec( write_raw_vec(
|pair, w| { |pair, w| {
write_point(&pair[0], w)?; write_point(&pair[0], w)?;
write_point(&pair[1], w) write_point(&pair[1], w)
@@ -239,10 +239,12 @@ impl Decoys {
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
/// defined serialization. /// defined serialization.
pub fn read(r: &mut impl io::Read) -> io::Result<Decoys> { pub fn read(r: &mut impl io::Read) -> io::Result<Decoys> {
let offsets = read_vec(read_varint, None, r)?;
let len = offsets.len();
Decoys::new( Decoys::new(
read_vec(read_varint, r)?, offsets,
read_byte(r)?, read_byte(r)?,
read_vec(|r| Ok([read_point(r)?, read_point(r)?]), r)?, read_raw_vec(|r| Ok([read_point(r)?, read_point(r)?]), len, r)?,
) )
.ok_or_else(|| io::Error::other("invalid Decoys")) .ok_or_else(|| io::Error::other("invalid Decoys"))
} }

View File

@@ -6,7 +6,7 @@ use curve25519_dalek::{
edwards::EdwardsPoint, edwards::EdwardsPoint,
}; };
pub(crate) use monero_generators::{MAX_COMMITMENTS, COMMITMENT_BITS, LOG_COMMITMENT_BITS}; pub(crate) use monero_generators::{MAX_COMMITMENTS, COMMITMENT_BITS};
pub(crate) fn multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint { pub(crate) fn multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
let mut buf_scalars = Vec::with_capacity(pairs.len()); let mut buf_scalars = Vec::with_capacity(pairs.len());

View File

@@ -17,13 +17,13 @@ use curve25519_dalek::edwards::EdwardsPoint;
use monero_io::*; use monero_io::*;
pub use monero_generators::MAX_COMMITMENTS; pub use monero_generators::MAX_COMMITMENTS;
use monero_generators::COMMITMENT_BITS;
use monero_primitives::Commitment; use monero_primitives::Commitment;
pub(crate) mod scalar_vector; pub(crate) mod scalar_vector;
pub(crate) mod point_vector; pub(crate) mod point_vector;
pub(crate) mod core; pub(crate) mod core;
use crate::core::LOG_COMMITMENT_BITS;
pub(crate) mod batch_verifier; pub(crate) mod batch_verifier;
use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier}; use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier};
@@ -44,6 +44,11 @@ use crate::plus::{
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
// The logarithm (over 2) of the amount of bits a value within a commitment may use.
const LOG_COMMITMENT_BITS: usize = COMMITMENT_BITS.ilog2() as usize;
// The maximum length of L/R `Vec`s.
const MAX_LR: usize = (MAX_COMMITMENTS.ilog2() as usize) + LOG_COMMITMENT_BITS;
/// An error from proving/verifying Bulletproofs(+). /// An error from proving/verifying Bulletproofs(+).
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(feature = "std", derive(thiserror::Error))]
@@ -265,8 +270,8 @@ impl Bulletproof {
tau_x: read_scalar(r)?, tau_x: read_scalar(r)?,
mu: read_scalar(r)?, mu: read_scalar(r)?,
ip: IpProof { ip: IpProof {
L: read_vec(read_point, r)?, L: read_vec(read_point, Some(MAX_LR), r)?,
R: read_vec(read_point, r)?, R: read_vec(read_point, Some(MAX_LR), r)?,
a: read_scalar(r)?, a: read_scalar(r)?,
b: read_scalar(r)?, b: read_scalar(r)?,
}, },
@@ -284,8 +289,8 @@ impl Bulletproof {
r_answer: read_scalar(r)?, r_answer: read_scalar(r)?,
s_answer: read_scalar(r)?, s_answer: read_scalar(r)?,
delta_answer: read_scalar(r)?, delta_answer: read_scalar(r)?,
L: read_vec(read_point, r)?.into_iter().collect(), L: read_vec(read_point, Some(MAX_LR), r)?.into_iter().collect(),
R: read_vec(read_point, r)?.into_iter().collect(), R: read_vec(read_point, Some(MAX_LR), r)?.into_iter().collect(),
}, },
})) }))
} }

View File

@@ -71,7 +71,7 @@ impl Input {
let amount = if amount == 0 { None } else { Some(amount) }; let amount = if amount == 0 { None } else { Some(amount) };
Input::ToKey { Input::ToKey {
amount, amount,
key_offsets: read_vec(read_varint, r)?, key_offsets: read_vec(read_varint, None, r)?,
key_image: read_torsion_free_point(r)?, key_image: read_torsion_free_point(r)?,
} }
} }
@@ -241,7 +241,7 @@ impl TransactionPrefix {
pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> { pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
let additional_timelock = Timelock::read(r)?; let additional_timelock = Timelock::read(r)?;
let inputs = read_vec(|r| Input::read(r), r)?; let inputs = read_vec(|r| Input::read(r), None, r)?;
if inputs.is_empty() { if inputs.is_empty() {
Err(io::Error::other("transaction had no inputs"))?; Err(io::Error::other("transaction had no inputs"))?;
} }
@@ -250,10 +250,10 @@ impl TransactionPrefix {
let mut prefix = TransactionPrefix { let mut prefix = TransactionPrefix {
additional_timelock, additional_timelock,
inputs, inputs,
outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?, outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), None, r)?,
extra: vec![], extra: vec![],
}; };
prefix.extra = read_vec(read_byte, r)?; prefix.extra = read_vec(read_byte, None, r)?;
Ok(prefix) Ok(prefix)
} }

View File

@@ -181,16 +181,10 @@ impl ExtraField {
size size
}), }),
1 => ExtraField::PublicKey(read_point(r)?), 1 => ExtraField::PublicKey(read_point(r)?),
2 => ExtraField::Nonce({ 2 => ExtraField::Nonce(read_vec(read_byte, Some(MAX_TX_EXTRA_NONCE_SIZE), r)?),
let nonce = read_vec(read_byte, r)?;
if nonce.len() > MAX_TX_EXTRA_NONCE_SIZE {
Err(io::Error::other("too long nonce"))?;
}
nonce
}),
3 => ExtraField::MergeMining(read_varint(r)?, read_bytes(r)?), 3 => ExtraField::MergeMining(read_varint(r)?, read_bytes(r)?),
4 => ExtraField::PublicKeys(read_vec(read_point, r)?), 4 => ExtraField::PublicKeys(read_vec(read_point, None, r)?),
0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, r)?), 0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, None, r)?),
_ => Err(io::Error::other("unknown extra field"))?, _ => Err(io::Error::other("unknown extra field"))?,
}) })
} }

View File

@@ -456,7 +456,7 @@ impl SignableTransaction {
/// defined serialization. /// defined serialization.
pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> { pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> { fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
String::from_utf8(read_vec(read_byte, r)?) String::from_utf8(read_vec(read_byte, None, r)?)
.ok() .ok()
.and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok()) .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
.ok_or_else(|| io::Error::other("invalid address")) .ok_or_else(|| io::Error::other("invalid address"))
@@ -484,9 +484,9 @@ impl SignableTransaction {
rct_type: RctType::try_from(read_byte(r)?) rct_type: RctType::try_from(read_byte(r)?)
.map_err(|()| io::Error::other("unsupported/invalid RctType"))?, .map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
outgoing_view_key: Zeroizing::new(read_bytes(r)?), outgoing_view_key: Zeroizing::new(read_bytes(r)?),
inputs: read_vec(OutputWithDecoys::read, r)?, inputs: read_vec(OutputWithDecoys::read, None, r)?,
payments: read_vec(read_payment, r)?, payments: read_vec(read_payment, None, r)?,
data: read_vec(|r| read_vec(read_byte, r), r)?, data: read_vec(|r| read_vec(read_byte, None, r), None, r)?,
fee_rate: FeeRate::read(r)?, fee_rate: FeeRate::read(r)?,
}; };
match res.validate() { match res.validate() {