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;
/// The amount of bits a value within a commitment may use.
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.
#[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.
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<()> {
write_vec(write_varint, &self.offsets, w)?;
w.write_all(&[self.signer_index])?;
write_vec(
write_raw_vec(
|pair, w| {
write_point(&pair[0], 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
/// defined serialization.
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(
read_vec(read_varint, r)?,
offsets,
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"))
}

View File

@@ -6,7 +6,7 @@ use curve25519_dalek::{
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 {
let mut buf_scalars = Vec::with_capacity(pairs.len());

View File

@@ -17,13 +17,13 @@ use curve25519_dalek::edwards::EdwardsPoint;
use monero_io::*;
pub use monero_generators::MAX_COMMITMENTS;
use monero_generators::COMMITMENT_BITS;
use monero_primitives::Commitment;
pub(crate) mod scalar_vector;
pub(crate) mod point_vector;
pub(crate) mod core;
use crate::core::LOG_COMMITMENT_BITS;
pub(crate) mod batch_verifier;
use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier};
@@ -44,6 +44,11 @@ use crate::plus::{
#[cfg(test)]
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(+).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
@@ -265,8 +270,8 @@ impl Bulletproof {
tau_x: read_scalar(r)?,
mu: read_scalar(r)?,
ip: IpProof {
L: read_vec(read_point, r)?,
R: read_vec(read_point, r)?,
L: read_vec(read_point, Some(MAX_LR), r)?,
R: read_vec(read_point, Some(MAX_LR), r)?,
a: read_scalar(r)?,
b: read_scalar(r)?,
},
@@ -284,8 +289,8 @@ impl Bulletproof {
r_answer: read_scalar(r)?,
s_answer: read_scalar(r)?,
delta_answer: read_scalar(r)?,
L: read_vec(read_point, r)?.into_iter().collect(),
R: 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, Some(MAX_LR), r)?.into_iter().collect(),
},
}))
}

View File

@@ -71,7 +71,7 @@ impl Input {
let amount = if amount == 0 { None } else { Some(amount) };
Input::ToKey {
amount,
key_offsets: read_vec(read_varint, r)?,
key_offsets: read_vec(read_varint, None, 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> {
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() {
Err(io::Error::other("transaction had no inputs"))?;
}
@@ -250,10 +250,10 @@ impl TransactionPrefix {
let mut prefix = TransactionPrefix {
additional_timelock,
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![],
};
prefix.extra = read_vec(read_byte, r)?;
prefix.extra = read_vec(read_byte, None, r)?;
Ok(prefix)
}

View File

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

View File

@@ -456,7 +456,7 @@ impl SignableTransaction {
/// 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)?)
String::from_utf8(read_vec(read_byte, None, r)?)
.ok()
.and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
.ok_or_else(|| io::Error::other("invalid address"))
@@ -484,9 +484,9 @@ impl 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)?,
inputs: read_vec(OutputWithDecoys::read, None, r)?,
payments: read_vec(read_payment, None, r)?,
data: read_vec(|r| read_vec(read_byte, None, r), None, r)?,
fee_rate: FeeRate::read(r)?,
};
match res.validate() {