Implement Trusted Vector Preallocation (#1920)
* Implement SafePreallocate. Resolves #1880 * Add proptests for SafePreallocate * Apply suggestions from code review Comments which did not include replacement code will be addressed in a follow-up commit. Co-authored-by: teor <teor@riseup.net> * Rename [Safe-> Trusted]Allocate. Add doc and tests Add tests to show that the largest allowed vec under TrustedPreallocate is small enough to fit in a Zcash block/message (depending on type). Add doc comments to all TrustedPreallocate test cases. Tighten bounds on max_trusted_alloc for some types. Note - this commit does NOT include TrustedPreallocate impls for JoinSplitData, String, and Script. These impls will be added in a follow up commit * Implement SafePreallocate. Resolves #1880 * Add proptests for SafePreallocate * Apply suggestions from code review Comments which did not include replacement code will be addressed in a follow-up commit. Co-authored-by: teor <teor@riseup.net> * Rename [Safe-> Trusted]Allocate. Add doc and tests Add tests to show that the largest allowed vec under TrustedPreallocate is small enough to fit in a Zcash block/message (depending on type). Add doc comments to all TrustedPreallocate test cases. Tighten bounds on max_trusted_alloc for some types. Note - this commit does NOT include TrustedPreallocate impls for JoinSplitData, String, and Script. These impls will be added in a follow up commit * Impl TrustedPreallocate for Joinsplit * Impl ZcashDeserialize for Vec<u8> * Arbitrary, TrustedPreallocate, Serialize, and tests for Spend<SharedAnchor> Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
6bb5220b13
commit
0daaf582e2
|
@ -21,10 +21,17 @@ pub use hash::Hash;
|
||||||
pub use header::BlockTimeError;
|
pub use header::BlockTimeError;
|
||||||
pub use header::{CountedHeader, Header};
|
pub use header::{CountedHeader, Header};
|
||||||
pub use height::Height;
|
pub use height::Height;
|
||||||
|
pub use serialize::MAX_BLOCK_BYTES;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{fmt::DisplayToDebug, parameters::Network, transaction::Transaction, transparent};
|
use crate::{
|
||||||
|
fmt::DisplayToDebug,
|
||||||
|
parameters::Network,
|
||||||
|
serialization::{TrustedPreallocate, MAX_PROTOCOL_MESSAGE_LEN},
|
||||||
|
transaction::Transaction,
|
||||||
|
transparent,
|
||||||
|
};
|
||||||
|
|
||||||
/// A Zcash block, containing a header and a list of transactions.
|
/// A Zcash block, containing a header and a list of transactions.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -80,3 +87,63 @@ impl<'a> From<&'a Block> for Hash {
|
||||||
(&block.header).into()
|
(&block.header).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// A serialized Block hash takes 32 bytes
|
||||||
|
const BLOCK_HASH_SIZE: u64 = 32;
|
||||||
|
/// The maximum number of hashes in a valid Zcash protocol message.
|
||||||
|
impl TrustedPreallocate for Hash {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// Every vector type requires a length field of at least one byte for de/serialization.
|
||||||
|
// Since a block::Hash takes 32 bytes, we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN - 1) / 32 hashes in a single message
|
||||||
|
((MAX_PROTOCOL_MESSAGE_LEN - 1) as u64) / BLOCK_HASH_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{Hash, BLOCK_HASH_SIZE, MAX_PROTOCOL_MESSAGE_LEN};
|
||||||
|
use crate::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||||
|
/// Verify that the serialized size of a block hash used to calculate the allocation limit is correct
|
||||||
|
#[test]
|
||||||
|
fn block_hash_size_is_correct(hash in Hash::arbitrary()) {
|
||||||
|
let serialized = hash.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 == BLOCK_HASH_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(200))]
|
||||||
|
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `Hash`s is too large to send via the Zcash Wire Protocol
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash Wire Protocol message
|
||||||
|
#[test]
|
||||||
|
fn block_hash_max_allocation(hash in Hash::arbitrary_with(())) {
|
||||||
|
let max_allocation: usize = Hash::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(Hash::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == Hash::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to send as a protocol message
|
||||||
|
prop_assert!(smallest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of hashes
|
||||||
|
prop_assert!((largest_allowed_vec.len() as u64) == Hash::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to send as a protocol message
|
||||||
|
prop_assert!(largest_allowed_serialized.len() <= MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
use std::usize;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::work::{difficulty::CompactDifficulty, equihash::Solution};
|
use crate::{
|
||||||
|
serialization::{TrustedPreallocate, MAX_PROTOCOL_MESSAGE_LEN},
|
||||||
|
work::{difficulty::CompactDifficulty, equihash::Solution},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{merkle, Hash, Height};
|
use super::{merkle, Hash, Height};
|
||||||
|
|
||||||
|
@ -118,3 +123,79 @@ pub struct CountedHeader {
|
||||||
pub header: Header,
|
pub header: Header,
|
||||||
pub transaction_count: usize,
|
pub transaction_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The serialized size of a Zcash block header.
|
||||||
|
///
|
||||||
|
/// Includes the equihash input, 32-byte nonce, 3-byte equihash length field, and equihash solution.
|
||||||
|
const BLOCK_HEADER_LENGTH: usize =
|
||||||
|
crate::work::equihash::Solution::INPUT_LENGTH + 32 + 3 + crate::work::equihash::SOLUTION_SIZE;
|
||||||
|
|
||||||
|
/// The minimum size for a serialized CountedHeader.
|
||||||
|
///
|
||||||
|
/// A CountedHeader has BLOCK_HEADER_LENGTH bytes + 1 or more bytes for the transaction count
|
||||||
|
const MIN_COUNTED_HEADER_LEN: usize = BLOCK_HEADER_LENGTH + 1;
|
||||||
|
impl TrustedPreallocate for CountedHeader {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// Every vector type requires a length field of at least one byte for de/serialization.
|
||||||
|
// Therefore, we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN - 1) / MIN_COUNTED_HEADER_LEN counted headers in a single message
|
||||||
|
((MAX_PROTOCOL_MESSAGE_LEN - 1) / MIN_COUNTED_HEADER_LEN) as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{CountedHeader, Header, MAX_PROTOCOL_MESSAGE_LEN, MIN_COUNTED_HEADER_LEN};
|
||||||
|
use crate::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
proptest! {
|
||||||
|
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||||
|
|
||||||
|
/// Confirm that each counted header takes at least COUNTED_HEADER_LEN bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn counted_header_min_length(header in Header::arbitrary_with(()), transaction_count in (0..std::u32::MAX)) {
|
||||||
|
let header = CountedHeader {
|
||||||
|
header,
|
||||||
|
transaction_count: transaction_count.try_into().expect("Must run test on platform with at least 32 bit address space"),
|
||||||
|
};
|
||||||
|
let serialized_header = header.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized_header.len() >= MIN_COUNTED_HEADER_LEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(100))]
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `CountedHeaders`s is too large to send via the Zcash Wire Protocol
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash Wire Protocol message
|
||||||
|
#[test]
|
||||||
|
fn counted_header_max_allocation(header in Header::arbitrary_with(())) {
|
||||||
|
let header = CountedHeader {
|
||||||
|
header,
|
||||||
|
transaction_count: 0,
|
||||||
|
};
|
||||||
|
let max_allocation: usize = CountedHeader::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(CountedHeader::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(header.clone());
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == CountedHeader::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to send as a protocol message
|
||||||
|
prop_assert!(smallest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of CountedHeaders
|
||||||
|
prop_assert!((largest_allowed_vec.len() as u64) == CountedHeader::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to send as a protocol message
|
||||||
|
prop_assert!(largest_allowed_serialized.len() <= MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ use proptest::{arbitrary::any, array, collection::vec, prelude::*};
|
||||||
|
|
||||||
use crate::primitives::Groth16Proof;
|
use crate::primitives::Groth16Proof;
|
||||||
|
|
||||||
use super::{keys, note, tree, NoteCommitment, Output, PerSpendAnchor, Spend, ValueCommitment};
|
use super::{
|
||||||
|
keys, note, tree, NoteCommitment, Output, PerSpendAnchor, SharedAnchor, Spend, ValueCommitment,
|
||||||
|
};
|
||||||
|
|
||||||
impl Arbitrary for Spend<PerSpendAnchor> {
|
impl Arbitrary for Spend<PerSpendAnchor> {
|
||||||
type Parameters = ();
|
type Parameters = ();
|
||||||
|
@ -36,6 +38,34 @@ impl Arbitrary for Spend<PerSpendAnchor> {
|
||||||
type Strategy = BoxedStrategy<Self>;
|
type Strategy = BoxedStrategy<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Arbitrary for Spend<SharedAnchor> {
|
||||||
|
type Parameters = ();
|
||||||
|
|
||||||
|
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||||
|
(
|
||||||
|
any::<note::Nullifier>(),
|
||||||
|
array::uniform32(any::<u8>()),
|
||||||
|
any::<Groth16Proof>(),
|
||||||
|
vec(any::<u8>(), 64),
|
||||||
|
)
|
||||||
|
.prop_map(|(nullifier, rpk_bytes, proof, sig_bytes)| Self {
|
||||||
|
per_spend_anchor: (),
|
||||||
|
cv: ValueCommitment(AffinePoint::identity()),
|
||||||
|
nullifier,
|
||||||
|
rk: redjubjub::VerificationKeyBytes::from(rpk_bytes),
|
||||||
|
zkproof: proof,
|
||||||
|
spend_auth_sig: redjubjub::Signature::from({
|
||||||
|
let mut b = [0u8; 64];
|
||||||
|
b.copy_from_slice(sig_bytes.as_slice());
|
||||||
|
b
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy = BoxedStrategy<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
impl Arbitrary for Output {
|
impl Arbitrary for Output {
|
||||||
type Parameters = ();
|
type Parameters = ();
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
block::MAX_BLOCK_BYTES,
|
||||||
primitives::Groth16Proof,
|
primitives::Groth16Proof,
|
||||||
serialization::{serde_helpers, SerializationError, ZcashDeserialize, ZcashSerialize},
|
serialization::{
|
||||||
|
serde_helpers, SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{commitment, keys, note};
|
use super::{commitment, keys, note};
|
||||||
|
@ -75,3 +78,73 @@ impl ZcashDeserialize for Output {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// An output contains: a 32 byte cv, a 32 byte cmu, a 32 byte ephemeral key
|
||||||
|
/// a 580 byte encCiphertext, an 80 byte outCiphertext, and a 192 byte zkproof
|
||||||
|
/// [ps]: https://zips.z.cash/protocol/protocol.pdf#outputencoding
|
||||||
|
const OUTPUT_SIZE: u64 = 32 + 32 + 32 + 580 + 80 + 192;
|
||||||
|
|
||||||
|
/// The maximum number of outputs in a valid Zcash on-chain transaction.
|
||||||
|
///
|
||||||
|
/// If a transaction contains more outputs than can fit in maximally large block, it might be
|
||||||
|
/// valid on the network and in the mempool, but it can never be mined into a block. So
|
||||||
|
/// rejecting these large edge-case transactions can never break consensus
|
||||||
|
impl TrustedPreallocate for Output {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// Since a serialized Vec<Output> uses at least one byte for its length,
|
||||||
|
// the max allocation can never exceed (MAX_BLOCK_BYTES - 1) / OUTPUT_SIZE
|
||||||
|
(MAX_BLOCK_BYTES - 1) / OUTPUT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{Output, MAX_BLOCK_BYTES, OUTPUT_SIZE};
|
||||||
|
use crate::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||||
|
|
||||||
|
/// Confirm that each output takes exactly OUTPUT_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn output_size_is_small_enough(output in Output::arbitrary_with(())) {
|
||||||
|
let serialized = output.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 == OUTPUT_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(100))]
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `Outputs`s is too large to fit in a Zcash block
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash block
|
||||||
|
#[test]
|
||||||
|
fn output_max_allocation_is_big_enough(output in Output::arbitrary_with(())) {
|
||||||
|
|
||||||
|
let max_allocation: usize = Output::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(Output::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(output.clone());
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == Output::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<Output> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!((smallest_disallowed_serialized.len() as u64) >= MAX_BLOCK_BYTES);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of Outputs
|
||||||
|
prop_assert!((largest_allowed_vec.len() as u64) == Output::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to fit in a Zcash block.
|
||||||
|
prop_assert!((largest_allowed_serialized.len() as u64) < MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
block::MAX_BLOCK_BYTES,
|
||||||
primitives::{
|
primitives::{
|
||||||
redjubjub::{self, SpendAuth},
|
redjubjub::{self, SpendAuth},
|
||||||
Groth16Proof,
|
Groth16Proof,
|
||||||
},
|
},
|
||||||
serialization::{
|
serialization::{
|
||||||
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
|
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
|
||||||
|
ZcashSerialize,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,7 +102,6 @@ impl Spend<PerSpendAnchor> {
|
||||||
impl ZcashSerialize for Spend<PerSpendAnchor> {
|
impl ZcashSerialize for Spend<PerSpendAnchor> {
|
||||||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||||
self.cv.zcash_serialize(&mut writer)?;
|
self.cv.zcash_serialize(&mut writer)?;
|
||||||
// TODO: V4 only
|
|
||||||
writer.write_all(&self.per_spend_anchor.0[..])?;
|
writer.write_all(&self.per_spend_anchor.0[..])?;
|
||||||
writer.write_32_bytes(&self.nullifier.into())?;
|
writer.write_32_bytes(&self.nullifier.into())?;
|
||||||
writer.write_all(&<[u8; 32]>::from(self.rk)[..])?;
|
writer.write_all(&<[u8; 32]>::from(self.rk)[..])?;
|
||||||
|
@ -115,7 +116,6 @@ impl ZcashDeserialize for Spend<PerSpendAnchor> {
|
||||||
use crate::sapling::{commitment::ValueCommitment, note::Nullifier};
|
use crate::sapling::{commitment::ValueCommitment, note::Nullifier};
|
||||||
Ok(Spend {
|
Ok(Spend {
|
||||||
cv: ValueCommitment::zcash_deserialize(&mut reader)?,
|
cv: ValueCommitment::zcash_deserialize(&mut reader)?,
|
||||||
// TODO: V4 only
|
|
||||||
per_spend_anchor: tree::Root(reader.read_32_bytes()?),
|
per_spend_anchor: tree::Root(reader.read_32_bytes()?),
|
||||||
nullifier: Nullifier::from(reader.read_32_bytes()?),
|
nullifier: Nullifier::from(reader.read_32_bytes()?),
|
||||||
rk: reader.read_32_bytes()?.into(),
|
rk: reader.read_32_bytes()?.into(),
|
||||||
|
@ -124,3 +124,172 @@ impl ZcashDeserialize for Spend<PerSpendAnchor> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ZcashSerialize for Spend<SharedAnchor> {
|
||||||
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||||
|
self.cv.zcash_serialize(&mut writer)?;
|
||||||
|
writer.write_32_bytes(&self.nullifier.into())?;
|
||||||
|
writer.write_all(&<[u8; 32]>::from(self.rk)[..])?;
|
||||||
|
// zkproof and spend_auth_sig are serialized separately
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zkproof and spend_auth_sig are deserialized separately, so we can only
|
||||||
|
// deserialize Spend<SharedAnchor> in the context of a transaction
|
||||||
|
|
||||||
|
/// The size of a spend with a shared anchor, including associated fields.
|
||||||
|
///
|
||||||
|
/// A Spend contains: a 32 byte cv, a 32 byte anchor (transaction V4 only),
|
||||||
|
/// a 32 byte nullifier, a 32 byte rk, a 192 byte zkproof (serialized separately
|
||||||
|
/// in V5), and a 64 byte spendAuthSig (serialized separately in V5).
|
||||||
|
///
|
||||||
|
/// [ps]: https://zips.z.cash/protocol/protocol.pdf#spendencoding
|
||||||
|
const SHARED_ANCHOR_SPEND_FULL_SIZE: u64 = SHARED_ANCHOR_SPEND_INITIAL_SIZE + 192 + 64;
|
||||||
|
/// The size of a spend with a shared anchor, without associated fields.
|
||||||
|
///
|
||||||
|
/// This is the size of spends in the initial array, there are another
|
||||||
|
/// 2 arrays of zkproofs and spend_auth_sigs required in the transaction format.
|
||||||
|
const SHARED_ANCHOR_SPEND_INITIAL_SIZE: u64 = 32 + 32 + 32;
|
||||||
|
|
||||||
|
/// The size of a spend with a per-spend anchor.
|
||||||
|
const ANCHOR_PER_SPEND_SIZE: u64 = SHARED_ANCHOR_SPEND_FULL_SIZE + 32;
|
||||||
|
|
||||||
|
/// The maximum number of spends in a valid Zcash on-chain transaction V5.
|
||||||
|
///
|
||||||
|
/// If a transaction contains more spends than can fit in maximally large block, it might be
|
||||||
|
/// valid on the network and in the mempool, but it can never be mined into a block. So
|
||||||
|
/// rejecting these large edge-case transactions can never break consensus.
|
||||||
|
impl TrustedPreallocate for Spend<SharedAnchor> {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// Since a serialized Vec<Spend> uses at least one byte for its length,
|
||||||
|
// and the associated fields are required,
|
||||||
|
// a valid max allocation can never exceed this size
|
||||||
|
(MAX_BLOCK_BYTES - 1) / SHARED_ANCHOR_SPEND_FULL_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum number of spends in a valid Zcash on-chain transaction V4.
|
||||||
|
impl TrustedPreallocate for Spend<PerSpendAnchor> {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
(MAX_BLOCK_BYTES - 1) / ANCHOR_PER_SPEND_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{
|
||||||
|
Spend, ANCHOR_PER_SPEND_SIZE, MAX_BLOCK_BYTES, SHARED_ANCHOR_SPEND_FULL_SIZE,
|
||||||
|
SHARED_ANCHOR_SPEND_INITIAL_SIZE,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
sapling::{AnchorVariant, PerSpendAnchor, SharedAnchor},
|
||||||
|
serialization::{TrustedPreallocate, ZcashSerialize},
|
||||||
|
};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||||
|
|
||||||
|
/// Confirm that each spend takes exactly ANCHOR_PER_SPEND_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn anchor_per_spend_size_is_small_enough(spend in Spend::<PerSpendAnchor>::arbitrary_with(())) {
|
||||||
|
let serialized = spend.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 == ANCHOR_PER_SPEND_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm that each spend takes exactly SHARED_SPEND_SIZE bytes when serialized.
|
||||||
|
#[test]
|
||||||
|
fn shared_anchor_spend_size_is_small_enough(spend in Spend::<SharedAnchor>::arbitrary_with(())) {
|
||||||
|
let mut serialized_len = spend.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len();
|
||||||
|
serialized_len += spend.zkproof.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len();
|
||||||
|
serialized_len += &<[u8; 64]>::from(spend.spend_auth_sig).len();
|
||||||
|
prop_assert!(serialized_len as u64 == SHARED_ANCHOR_SPEND_FULL_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(100))]
|
||||||
|
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `Spend`s is too large to fit in a Zcash block
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash block
|
||||||
|
#[test]
|
||||||
|
fn anchor_per_spend_max_allocation_is_big_enough(spend in Spend::<PerSpendAnchor>::arbitrary_with(())) {
|
||||||
|
let (
|
||||||
|
smallest_disallowed_vec_len,
|
||||||
|
smallest_disallowed_serialized_len,
|
||||||
|
largest_allowed_vec_len,
|
||||||
|
largest_allowed_serialized_len,
|
||||||
|
) = spend_max_allocation_is_big_enough(spend);
|
||||||
|
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Spend::<PerSpendAnchor>::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to send as a protocol message
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<Spend> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!((smallest_disallowed_serialized_len as u64) >= MAX_BLOCK_BYTES);
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of spends
|
||||||
|
prop_assert!((largest_allowed_vec_len as u64) == Spend::<PerSpendAnchor>::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to send as a protocol message
|
||||||
|
prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify trusted preallocation for `Spend<SharedAnchor>`
|
||||||
|
#[test]
|
||||||
|
fn shared_spend_max_allocation_is_big_enough(spend in Spend::<SharedAnchor>::arbitrary_with(())) {
|
||||||
|
let (
|
||||||
|
smallest_disallowed_vec_len,
|
||||||
|
smallest_disallowed_serialized_len,
|
||||||
|
largest_allowed_vec_len,
|
||||||
|
largest_allowed_serialized_len,
|
||||||
|
) = spend_max_allocation_is_big_enough(spend);
|
||||||
|
|
||||||
|
prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Spend::<SharedAnchor>::max_allocation());
|
||||||
|
// Calculate the actual size of all required Spend fields
|
||||||
|
//
|
||||||
|
// TODO: modify the test to serialize the associated zkproof and
|
||||||
|
// spend_auth_sig fields
|
||||||
|
prop_assert!((smallest_disallowed_serialized_len as u64)/SHARED_ANCHOR_SPEND_INITIAL_SIZE*SHARED_ANCHOR_SPEND_FULL_SIZE >= MAX_BLOCK_BYTES);
|
||||||
|
|
||||||
|
prop_assert!((largest_allowed_vec_len as u64) == Spend::<SharedAnchor>::max_allocation());
|
||||||
|
prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the
|
||||||
|
fn spend_max_allocation_is_big_enough<AnchorV>(
|
||||||
|
spend: Spend<AnchorV>,
|
||||||
|
) -> (usize, usize, usize, usize)
|
||||||
|
where
|
||||||
|
AnchorV: AnchorVariant,
|
||||||
|
Spend<AnchorV>: TrustedPreallocate + ZcashSerialize + Clone,
|
||||||
|
{
|
||||||
|
let max_allocation: usize = Spend::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(Spend::max_allocation() + 1) {
|
||||||
|
smallest_disallowed_vec.push(spend.clone());
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
let smallest_disallowed_vec_len = smallest_disallowed_vec.len();
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
(
|
||||||
|
smallest_disallowed_vec_len,
|
||||||
|
smallest_disallowed_serialized.len(),
|
||||||
|
largest_allowed_vec.len(),
|
||||||
|
largest_allowed_serialized.len(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,8 +19,8 @@ pub mod sha256d;
|
||||||
pub use error::SerializationError;
|
pub use error::SerializationError;
|
||||||
pub use read_zcash::ReadZcashExt;
|
pub use read_zcash::ReadZcashExt;
|
||||||
pub use write_zcash::WriteZcashExt;
|
pub use write_zcash::WriteZcashExt;
|
||||||
pub use zcash_deserialize::{ZcashDeserialize, ZcashDeserializeInto};
|
pub use zcash_deserialize::{TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto};
|
||||||
pub use zcash_serialize::ZcashSerialize;
|
pub use zcash_serialize::{ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod proptests;
|
mod proptests;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::io;
|
use std::{io, num::TryFromIntError};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -13,6 +13,9 @@ pub enum SerializationError {
|
||||||
// XXX refine errors
|
// XXX refine errors
|
||||||
#[error("parse error: {0}")]
|
#[error("parse error: {0}")]
|
||||||
Parse(&'static str),
|
Parse(&'static str),
|
||||||
|
/// The length of a vec is too large to convert to a usize (and thus, too large to allocate on this platform)
|
||||||
|
#[error("compactsize too large: {0}")]
|
||||||
|
TryFromIntError(#[from] TryFromIntError),
|
||||||
/// An error caused when validating a zatoshi `Amount`
|
/// An error caused when validating a zatoshi `Amount`
|
||||||
#[error("input couldn't be parsed as a zatoshi `Amount`: {source}")]
|
#[error("input couldn't be parsed as a zatoshi `Amount`: {source}")]
|
||||||
Amount {
|
Amount {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::io;
|
use std::{convert::TryInto, io};
|
||||||
|
|
||||||
use super::{ReadZcashExt, SerializationError};
|
use super::{ReadZcashExt, SerializationError, MAX_PROTOCOL_MESSAGE_LEN};
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
/// Consensus-critical serialization for Zcash.
|
/// Consensus-critical serialization for Zcash.
|
||||||
|
@ -18,15 +18,15 @@ pub trait ZcashDeserialize: Sized {
|
||||||
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError>;
|
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ZcashDeserialize> ZcashDeserialize for Vec<T> {
|
impl<T: ZcashDeserialize + TrustedPreallocate> ZcashDeserialize for Vec<T> {
|
||||||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||||
let len = reader.read_compactsize()?;
|
let len = reader.read_compactsize()?;
|
||||||
// We're given len, so we could preallocate. But blindly preallocating
|
if len > T::max_allocation() {
|
||||||
// without a size bound can allow DOS attacks, and there's no way to
|
return Err(SerializationError::Parse(
|
||||||
// pass a size bound in a ZcashDeserialize impl, so instead we allocate
|
"Vector longer than max_allocation",
|
||||||
// as we read from the reader. (The maximum block and transaction sizes
|
));
|
||||||
// limit the eventual size of these allocations.)
|
}
|
||||||
let mut vec = Vec::new();
|
let mut vec = Vec::with_capacity(len.try_into()?);
|
||||||
for _ in 0..len {
|
for _ in 0..len {
|
||||||
vec.push(T::zcash_deserialize(&mut reader)?);
|
vec.push(T::zcash_deserialize(&mut reader)?);
|
||||||
}
|
}
|
||||||
|
@ -65,3 +65,138 @@ impl<R: io::Read> ZcashDeserializeInto for R {
|
||||||
T::zcash_deserialize(self)
|
T::zcash_deserialize(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blind preallocation of a Vec<T: TrustedPreallocate> is based on a bounded length. This is in contrast
|
||||||
|
/// to blind preallocation of a generic Vec<T>, which is a DOS vector.
|
||||||
|
///
|
||||||
|
/// The max_allocation() function provides a loose upper bound on the size of the Vec<T: TrustedPreallocate>
|
||||||
|
/// which can possibly be received from an honest peer. If this limit is too low, Zebra may reject valid messages.
|
||||||
|
/// In the worst case, setting the lower bound too low could cause Zebra to fall out of consensus by rejecting all messages containing a valid block.
|
||||||
|
pub trait TrustedPreallocate {
|
||||||
|
/// Provides a ***loose upper bound*** on the size of the Vec<T: TrustedPreallocate>
|
||||||
|
/// which can possibly be received from an honest peer.
|
||||||
|
fn max_allocation() -> u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length of the longest valid `Vec<u8>` that can be received over the network
|
||||||
|
///
|
||||||
|
/// It takes 5 bytes to encode a compactsize representing any number netween 2^16 and (2^32 - 1)
|
||||||
|
/// MAX_PROTOCOL_MESSAGE_LEN is ~2^21, so the largest Vec<u8> that can be received from an honest peer is
|
||||||
|
/// (MAX_PROTOCOL_MESSAGE_LEN - 5);
|
||||||
|
const MAX_U8_ALLOCATION: usize = MAX_PROTOCOL_MESSAGE_LEN - 5;
|
||||||
|
|
||||||
|
/// Implement ZcashDeserialize for Vec<u8> directly instead of using the blanket Vec implementation
|
||||||
|
///
|
||||||
|
/// This allows us to optimize the inner loop into a single call to `read_exact()`
|
||||||
|
/// Note thate we don't implement TrustedPreallocate for u8.
|
||||||
|
/// This allows the optimization without relying on specialization.
|
||||||
|
impl ZcashDeserialize for Vec<u8> {
|
||||||
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||||
|
let len = reader.read_compactsize()?.try_into()?;
|
||||||
|
if len > MAX_U8_ALLOCATION {
|
||||||
|
return Err(SerializationError::Parse(
|
||||||
|
"Vector longer than max_allocation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut vec = vec![0u8; len];
|
||||||
|
reader.read_exact(&mut vec)?;
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_u8_deserialize {
|
||||||
|
use super::MAX_U8_ALLOCATION;
|
||||||
|
use crate::serialization::MAX_PROTOCOL_MESSAGE_LEN;
|
||||||
|
use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize};
|
||||||
|
use proptest::{collection::size_range, prelude::*};
|
||||||
|
use std::matches;
|
||||||
|
|
||||||
|
// Allow direct serialization of Vec<u8> for these tests. We don't usuall allow this because some types have
|
||||||
|
// specific rules for about serialization of their inner Vec<u8>. This method could be easily misused if it applied
|
||||||
|
// more generally.
|
||||||
|
impl ZcashSerialize for u8 {
|
||||||
|
fn zcash_serialize<W: std::io::Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
|
||||||
|
writer.write_all(&[*self])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(3))]
|
||||||
|
#[test]
|
||||||
|
/// Confirm that deserialize yields the expected result for any vec smaller than `MAX_U8_ALLOCATION`
|
||||||
|
fn u8_ser_deser_roundtrip(input in any_with::<Vec<u8>>(size_range(MAX_U8_ALLOCATION).lift()) ) {
|
||||||
|
let serialized = input.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
let cursor = std::io::Cursor::new(serialized);
|
||||||
|
let deserialized = <Vec<u8>>::zcash_deserialize(cursor).expect("deserialization from vec must succeed");
|
||||||
|
prop_assert_eq!(deserialized, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Confirm that deserialize allows vectors with length up to and including `MAX_U8_ALLOCATION`
|
||||||
|
fn u8_deser_accepts_max_valid_input() {
|
||||||
|
let serialized = vec![0u8; MAX_U8_ALLOCATION]
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
let cursor = std::io::Cursor::new(serialized);
|
||||||
|
let deserialized = <Vec<u8>>::zcash_deserialize(cursor);
|
||||||
|
assert!(deserialized.is_ok())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
/// Confirm that rejects vectors longer than `MAX_U8_ALLOCATION`
|
||||||
|
fn u8_deser_throws_when_input_too_large() {
|
||||||
|
let serialized = vec![0u8; MAX_U8_ALLOCATION + 1]
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
let cursor = std::io::Cursor::new(serialized);
|
||||||
|
let deserialized = <Vec<u8>>::zcash_deserialize(cursor);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
deserialized,
|
||||||
|
Err(SerializationError::Parse(
|
||||||
|
"Vector longer than max_allocation"
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Confirm that every u8 takes exactly 1 byte when serialized.
|
||||||
|
/// This verifies that our calculated `MAX_U8_ALLOCATION` is indeed an upper bound.
|
||||||
|
fn u8_size_is_correct() {
|
||||||
|
for byte in std::u8::MIN..=std::u8::MAX {
|
||||||
|
let serialized = byte
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serialized.len() == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed `Vec<u8>` is too big to include in a Zcash Wire Protocol message
|
||||||
|
/// 2. The largest allowed `Vec<u8>`is exactly the size of a maximal Zcash Wire Protocol message
|
||||||
|
fn u8_max_allocation_is_correct() {
|
||||||
|
let mut shortest_disallowed_vec = vec![0u8; MAX_U8_ALLOCATION + 1];
|
||||||
|
let shortest_disallowed_serialized = shortest_disallowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Confirm that shortest_disallowed_vec is only one item larger than the limit
|
||||||
|
assert_eq!((shortest_disallowed_vec.len() - 1), MAX_U8_ALLOCATION);
|
||||||
|
// Confirm that shortest_disallowed_vec is too large to be included in a valid zcash message
|
||||||
|
assert!(shortest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
shortest_disallowed_vec.pop();
|
||||||
|
let longest_allowed_vec = shortest_disallowed_vec;
|
||||||
|
let longest_allowed_serialized = longest_allowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("serialization to vec must succed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of items
|
||||||
|
assert_eq!(longest_allowed_vec.len(), MAX_U8_ALLOCATION);
|
||||||
|
// Check that our largest_allowed_vec is the size of a maximal protocol message
|
||||||
|
assert_eq!(longest_allowed_serialized.len(), MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,3 +38,8 @@ impl<T: ZcashSerialize> ZcashSerialize for Vec<T> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The maximum length of a Zcash message, in bytes.
|
||||||
|
///
|
||||||
|
/// This value is used to calculate safe preallocation limits for some types
|
||||||
|
pub const MAX_PROTOCOL_MESSAGE_LEN: usize = 2 * 1024 * 1024;
|
||||||
|
|
|
@ -4,10 +4,11 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NonNegative},
|
||||||
primitives::{x25519, ZkSnarkProof},
|
block::MAX_BLOCK_BYTES,
|
||||||
|
primitives::{x25519, Bctv14Proof, Groth16Proof, ZkSnarkProof},
|
||||||
serialization::{
|
serialization::{
|
||||||
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto,
|
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
|
||||||
ZcashSerialize,
|
ZcashDeserializeInto, ZcashSerialize,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,3 +98,133 @@ impl<P: ZkSnarkProof> ZcashDeserialize for JoinSplit<P> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The size of a joinsplit, excluding the ZkProof
|
||||||
|
///
|
||||||
|
/// Excluding the ZkProof, a Joinsplit consists of an 8 byte vpub_old, an 8 byte vpub_new, a 32 byte anchor,
|
||||||
|
/// two 32 byte nullifiers, two 32 byte committments, a 32 byte epheremral key, a 32 byte random seed
|
||||||
|
/// two 32 byte vmacs, and two 601 byte encrypted ciphertexts.
|
||||||
|
const JOINSPLIT_SIZE_WITHOUT_ZKPROOF: u64 =
|
||||||
|
8 + 8 + 32 + (32 * 2) + (32 * 2) + 32 + 32 + (32 * 2) + (601 * 2);
|
||||||
|
/// The size of a version 2 or 3 joinsplit transaction, which uses a BCTV14 proof.
|
||||||
|
///
|
||||||
|
/// A BTCV14 proof takes 296 bytes, per the Zcash [protocol specification §7.2][ps]
|
||||||
|
///
|
||||||
|
/// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding
|
||||||
|
const BCTV14_JOINSPLIT_SIZE: u64 = JOINSPLIT_SIZE_WITHOUT_ZKPROOF + 296;
|
||||||
|
/// The size of a version 4+ joinsplit transaction, which uses a Groth16 proof
|
||||||
|
///
|
||||||
|
/// A Groth16 proof takes 192 bytes, per the Zcash [protocol specification §7.2][ps]
|
||||||
|
///
|
||||||
|
/// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding
|
||||||
|
const GROTH16_JOINSPLIT_SIZE: u64 = JOINSPLIT_SIZE_WITHOUT_ZKPROOF + 192;
|
||||||
|
|
||||||
|
impl TrustedPreallocate for JoinSplit<Bctv14Proof> {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// The longest Vec<JoinSplit> we receive from an honest peer must fit inside a valid block.
|
||||||
|
// Since encoding the length of the vec takes at least one byte
|
||||||
|
// (MAX_BLOCK_BYTES - 1) / BCTV14_JOINSPLIT_SIZE is a loose upper bound on the max allocation
|
||||||
|
(MAX_BLOCK_BYTES - 1) / BCTV14_JOINSPLIT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustedPreallocate for JoinSplit<Groth16Proof> {
|
||||||
|
// The longest Vec<JoinSplit> we receive from an honest peer must fit inside a valid block.
|
||||||
|
// Since encoding the length of the vec takes at least one byte
|
||||||
|
// (MAX_BLOCK_BYTES - 1) / GROTH16_JOINSPLIT_SIZE is a loose upper bound on the max allocation
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
(MAX_BLOCK_BYTES - 1) / GROTH16_JOINSPLIT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{
|
||||||
|
Bctv14Proof, Groth16Proof, JoinSplit, BCTV14_JOINSPLIT_SIZE, GROTH16_JOINSPLIT_SIZE,
|
||||||
|
MAX_BLOCK_BYTES,
|
||||||
|
};
|
||||||
|
use crate::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
use proptest::{prelude::*, proptest};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(1_000))]
|
||||||
|
#[test]
|
||||||
|
/// Confirm that each JoinSplit<Btcv14Proof> takes exactly BCTV14_JOINSPLIT_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
fn joinsplit_btcv14_size_is_correct(joinsplit in <JoinSplit<Bctv14Proof>>::arbitrary_with(())) {
|
||||||
|
let serialized = joinsplit.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 == BCTV14_JOINSPLIT_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Confirm that each JoinSplit<Btcv14Proof> takes exactly GROTH16_JOINSPLIT_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
fn joinsplit_groth16_size_is_correct(joinsplit in <JoinSplit<Groth16Proof>>::arbitrary_with(())) {
|
||||||
|
let serialized = joinsplit.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 == GROTH16_JOINSPLIT_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(100))]
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `JoinSplit<Bctv14Proof>`s is too large to fit in a Zcash block
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash block
|
||||||
|
#[test]
|
||||||
|
fn joinsplit_btcv14_max_allocation_is_correct(joinsplit in <JoinSplit<Bctv14Proof>>::arbitrary_with(())) {
|
||||||
|
|
||||||
|
let max_allocation: usize = <JoinSplit<Bctv14Proof>>::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(<JoinSplit<Bctv14Proof>>::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(joinsplit.clone());
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == <JoinSplit<Bctv14Proof>>::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<<JoinSplit<Bctv14Proof>>> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!((smallest_disallowed_serialized.len() as u64) >= MAX_BLOCK_BYTES);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of <JoinSplit<Bctv14Proof>>
|
||||||
|
prop_assert!((largest_allowed_vec.len() as u64) == <JoinSplit<Bctv14Proof>>::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to fit in a Zcash block.
|
||||||
|
prop_assert!((largest_allowed_serialized.len() as u64) < MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that...
|
||||||
|
/// 1. The smallest disallowed vector of `JoinSplit<Groth16Proof>`s is too large to fit in a Zcash block
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash block
|
||||||
|
#[test]
|
||||||
|
fn joinsplit_groth16_max_allocation_is_correct(joinsplit in <JoinSplit<Groth16Proof>>::arbitrary_with(())) {
|
||||||
|
|
||||||
|
let max_allocation: usize = <JoinSplit<Groth16Proof>>::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(<JoinSplit<Groth16Proof>>::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(joinsplit.clone());
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == <JoinSplit<Groth16Proof>>::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<<JoinSplit<Groth16Proof>>> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!((smallest_disallowed_serialized.len() as u64) >= MAX_BLOCK_BYTES);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of <JoinSplit<Groth16Proof>>
|
||||||
|
prop_assert!((largest_allowed_vec.len() as u64) == <JoinSplit<Groth16Proof>>::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to fit in a Zcash block.
|
||||||
|
prop_assert!((largest_allowed_serialized.len() as u64) < MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,11 +6,12 @@ use std::{io, sync::Arc};
|
||||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
block::MAX_BLOCK_BYTES,
|
||||||
parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID},
|
parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID},
|
||||||
primitives::ZkSnarkProof,
|
primitives::ZkSnarkProof,
|
||||||
serialization::{
|
serialization::{
|
||||||
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto,
|
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
|
||||||
ZcashSerialize,
|
ZcashDeserializeInto, ZcashSerialize,
|
||||||
},
|
},
|
||||||
sprout,
|
sprout,
|
||||||
};
|
};
|
||||||
|
@ -353,3 +354,138 @@ where
|
||||||
T::zcash_serialize(self, writer)
|
T::zcash_serialize(self, writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A Tx Input must have an Outpoint (32 byte hash + 4 byte index), a 4 byte sequence number,
|
||||||
|
/// and a signature script, which always takes a min of 1 byte (for a length 0 script)
|
||||||
|
const MIN_TRANSPARENT_INPUT_SIZE: u64 = 32 + 4 + 4 + 1;
|
||||||
|
/// A Transparent output has an 8 byte value and script which takes a min of 1 byte
|
||||||
|
const MIN_TRANSPARENT_OUTPUT_SIZE: u64 = 8 + 1;
|
||||||
|
// All txs must have at least one input, a 4 byte locktime, and at least one output
|
||||||
|
const MIN_TRANSPARENT_TX_SIZE: u64 = MIN_TRANSPARENT_INPUT_SIZE + 4 + MIN_TRANSPARENT_OUTPUT_SIZE;
|
||||||
|
|
||||||
|
/// No valid Zcash message contains more transactions than can fit in a single block
|
||||||
|
///
|
||||||
|
/// `tx` messages contain a single transaction, and `block` messages are limited to the maximum
|
||||||
|
/// block size.
|
||||||
|
impl TrustedPreallocate for Arc<Transaction> {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// A transparent transaction is the smallest transaction variant
|
||||||
|
MAX_BLOCK_BYTES / MIN_TRANSPARENT_TX_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// The maximum number of inputs in a valid Zcash on-chain transaction.
|
||||||
|
///
|
||||||
|
/// If a transaction contains more inputs than can fit in maximally large block, it might be
|
||||||
|
/// valid on the network and in the mempool, but it can never be mined into a block. So
|
||||||
|
/// rejecting these large edge-case transactions can never break consensus.
|
||||||
|
impl TrustedPreallocate for transparent::Input {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
MAX_BLOCK_BYTES / MIN_TRANSPARENT_INPUT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// The maximum number of outputs in a valid Zcash on-chain transaction.
|
||||||
|
///
|
||||||
|
/// If a transaction contains more outputs than can fit in maximally large block, it might be
|
||||||
|
/// valid on the network and in the mempool, but it can never be mined into a block. So
|
||||||
|
/// rejecting these large edge-case transactions can never break consensus.
|
||||||
|
impl TrustedPreallocate for transparent::Output {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
MAX_BLOCK_BYTES / MIN_TRANSPARENT_OUTPUT_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use super::{
|
||||||
|
transparent::Input, transparent::Output, Transaction, MAX_BLOCK_BYTES,
|
||||||
|
MIN_TRANSPARENT_INPUT_SIZE, MIN_TRANSPARENT_OUTPUT_SIZE, MIN_TRANSPARENT_TX_SIZE,
|
||||||
|
};
|
||||||
|
use crate::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use std::{convert::TryInto, sync::Arc};
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(300))]
|
||||||
|
|
||||||
|
/// Confirm that each spend takes at least MIN_TRANSPARENT_TX_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn tx_size_is_small_enough(tx in Transaction::arbitrary()) {
|
||||||
|
let serialized = tx.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 >= MIN_TRANSPARENT_TX_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm that each spend takes at least MIN_TRANSPARENT_TX_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn transparent_input_size_is_small_enough(input in Input::arbitrary()) {
|
||||||
|
let serialized = input.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 >= MIN_TRANSPARENT_INPUT_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm that each spend takes at least MIN_TRANSPARENT_TX_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
#[test]
|
||||||
|
fn transparent_output_size_is_small_enough(output in Output::arbitrary()) {
|
||||||
|
let serialized = output.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
prop_assert!(serialized.len() as u64 >= MIN_TRANSPARENT_OUTPUT_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
// This test is pretty slow, so only run a few
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(7))]
|
||||||
|
#[test]
|
||||||
|
/// Verify the smallest disallowed vector of `Transaction`s is too large to fit in a Zcash block
|
||||||
|
fn tx_max_allocation_is_big_enough(tx in Transaction::arbitrary()) {
|
||||||
|
|
||||||
|
let max_allocation: usize = <Arc<Transaction>>::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(<Arc<Transaction>>::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(Arc::new(tx.clone()));
|
||||||
|
}
|
||||||
|
let serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == <Arc<Transaction>>::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
prop_assert!(serialized.len() as u64 > MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Verify the smallest disallowed vector of `Input`s is too large to fit in a Zcash block
|
||||||
|
fn input_max_allocation_is_big_enough(input in Input::arbitrary()) {
|
||||||
|
|
||||||
|
let max_allocation: usize = Input::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(Input::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(input.clone());
|
||||||
|
}
|
||||||
|
let serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == Input::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<Input> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!(serialized.len() as u64 >= MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
/// Verify the smallest disallowed vector of `Output`s is too large to fit in a Zcash block
|
||||||
|
fn output_max_allocation_is_big_enough(output in Output::arbitrary()) {
|
||||||
|
|
||||||
|
let max_allocation: usize = Output::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(Output::max_allocation()+1) {
|
||||||
|
smallest_disallowed_vec.push(output.clone());
|
||||||
|
}
|
||||||
|
let serialized = smallest_disallowed_vec.zcash_serialize_to_vec().expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
prop_assert!(((smallest_disallowed_vec.len() - 1) as u64) == Output::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to be included in a valid block
|
||||||
|
// Note that a serialized block always includes at least one byte for the number of transactions,
|
||||||
|
// so any serialized Vec<Output> at least MAX_BLOCK_BYTES long is too large to fit in a block.
|
||||||
|
prop_assert!(serialized.len() as u64 >= MAX_BLOCK_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,10 +10,11 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
|
||||||
use zebra_chain::serialization::{
|
use zebra_chain::serialization::{
|
||||||
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
|
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
|
||||||
|
ZcashSerialize,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::protocol::types::PeerServices;
|
use crate::protocol::{external::MAX_PROTOCOL_MESSAGE_LEN, types::PeerServices};
|
||||||
|
|
||||||
use PeerAddrState::*;
|
use PeerAddrState::*;
|
||||||
|
|
||||||
|
@ -263,6 +264,76 @@ impl ZcashDeserialize for MetaAddr {
|
||||||
Ok(MetaAddr::new_gossiped(&addr, &services, &last_seen))
|
Ok(MetaAddr::new_gossiped(&addr, &services, &last_seen))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// A serialized meta addr has a 4 byte time, 8 byte services, 16 byte IP addr, and 2 byte port
|
||||||
|
const META_ADDR_SIZE: usize = 4 + 8 + 16 + 2;
|
||||||
|
impl TrustedPreallocate for MetaAddr {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// Since a maximal serialized Vec<MetAddr> uses at least three bytes for its length (2MB messages / 30B MetaAddr implies the maximal length is much greater than 253)
|
||||||
|
// the max allocation can never exceed (MAX_PROTOCOL_MESSAGE_LEN - 3) / META_ADDR_SIZE
|
||||||
|
((MAX_PROTOCOL_MESSAGE_LEN - 3) / META_ADDR_SIZE) as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use super::{MetaAddr, MAX_PROTOCOL_MESSAGE_LEN, META_ADDR_SIZE};
|
||||||
|
use super::{PeerAddrState, PeerServices};
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use zebra_chain::serialization::{TrustedPreallocate, ZcashSerialize};
|
||||||
|
#[test]
|
||||||
|
/// Confirm that each MetaAddr takes exactly META_ADDR_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
fn meta_addr_size_is_correct() {
|
||||||
|
let addr = MetaAddr {
|
||||||
|
addr: ([192, 168, 0, 0], 8333).into(),
|
||||||
|
services: PeerServices::default(),
|
||||||
|
last_seen: Utc.timestamp(1_573_680_222, 0),
|
||||||
|
last_connection_state: PeerAddrState::Responded,
|
||||||
|
};
|
||||||
|
let serialized = addr
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serialized.len() == META_ADDR_SIZE)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
/// Verifies that...
|
||||||
|
/// 1. The smallest disallowed vector of `MetaAddrs`s is too large to fit in a legal Zcash message
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash message
|
||||||
|
fn meta_addr_max_allocation_is_correct() {
|
||||||
|
let addr = MetaAddr {
|
||||||
|
addr: ([192, 168, 0, 0], 8333).into(),
|
||||||
|
services: PeerServices::default(),
|
||||||
|
last_seen: Utc.timestamp(1_573_680_222, 0),
|
||||||
|
last_connection_state: PeerAddrState::Responded,
|
||||||
|
};
|
||||||
|
let max_allocation: usize = MetaAddr::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(MetaAddr::max_allocation() + 1) {
|
||||||
|
smallest_disallowed_vec.push(addr);
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
assert!(((smallest_disallowed_vec.len() - 1) as u64) == MetaAddr::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to send in a valid Zcash message
|
||||||
|
assert!(smallest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of MetaAddrs
|
||||||
|
assert!((largest_allowed_vec.len() as u64) == MetaAddr::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to fit in a Zcash message.
|
||||||
|
assert!(largest_allowed_serialized.len() <= MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -7,6 +7,6 @@ mod message;
|
||||||
/// Newtype wrappers for primitive types.
|
/// Newtype wrappers for primitive types.
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use codec::Codec;
|
pub use codec::{Codec, MAX_PROTOCOL_MESSAGE_LEN};
|
||||||
pub use inv::InventoryHash;
|
pub use inv::InventoryHash;
|
||||||
pub use message::Message;
|
pub use message::Message;
|
||||||
|
|
|
@ -32,7 +32,7 @@ use super::{
|
||||||
const HEADER_LEN: usize = 24usize;
|
const HEADER_LEN: usize = 24usize;
|
||||||
|
|
||||||
/// Maximum size of a protocol message body.
|
/// Maximum size of a protocol message body.
|
||||||
const MAX_PROTOCOL_MESSAGE_LEN: usize = 2 * 1024 * 1024;
|
pub use zebra_chain::serialization::MAX_PROTOCOL_MESSAGE_LEN;
|
||||||
|
|
||||||
/// A codec which produces Bitcoin messages from byte streams and vice versa.
|
/// A codec which produces Bitcoin messages from byte streams and vice versa.
|
||||||
pub struct Codec {
|
pub struct Codec {
|
||||||
|
|
|
@ -10,10 +10,14 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block,
|
block,
|
||||||
serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize},
|
serialization::{
|
||||||
|
ReadZcashExt, SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize,
|
||||||
|
},
|
||||||
transaction,
|
transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::MAX_PROTOCOL_MESSAGE_LEN;
|
||||||
|
|
||||||
/// An inventory hash which refers to some advertised or requested data.
|
/// An inventory hash which refers to some advertised or requested data.
|
||||||
///
|
///
|
||||||
/// Bitcoin calls this an "inventory vector" but it is just a typed hash, not a
|
/// Bitcoin calls this an "inventory vector" but it is just a typed hash, not a
|
||||||
|
@ -81,3 +85,85 @@ impl ZcashDeserialize for InventoryHash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INV_HASH_SIZE: usize = 36;
|
||||||
|
impl TrustedPreallocate for InventoryHash {
|
||||||
|
fn max_allocation() -> u64 {
|
||||||
|
// An Inventory hash takes 36 bytes, and we reserve at least one byte for the Vector length
|
||||||
|
// so we can never receive more than ((MAX_PROTOCOL_MESSAGE_LEN - 1) / 36) in a single message
|
||||||
|
((MAX_PROTOCOL_MESSAGE_LEN - 1) / INV_HASH_SIZE) as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_trusted_preallocate {
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use super::{InventoryHash, INV_HASH_SIZE, MAX_PROTOCOL_MESSAGE_LEN};
|
||||||
|
use zebra_chain::{
|
||||||
|
block,
|
||||||
|
serialization::{TrustedPreallocate, ZcashSerialize},
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
#[test]
|
||||||
|
/// Confirm that each InventoryHash takes exactly INV_HASH_SIZE bytes when serialized.
|
||||||
|
/// This verifies that our calculated `TrustedPreallocate::max_allocation()` is indeed an upper bound.
|
||||||
|
fn inv_hash_size_is_correct() {
|
||||||
|
let block_hash = block::Hash([1u8; 32]);
|
||||||
|
let tx_hash = transaction::Hash([1u8; 32]);
|
||||||
|
let inv_block = InventoryHash::Block(block_hash);
|
||||||
|
let serialized_inv_block = inv_block
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serialized_inv_block.len() == INV_HASH_SIZE);
|
||||||
|
|
||||||
|
let inv_filtered_block = InventoryHash::FilteredBlock(block_hash);
|
||||||
|
let serialized_inv_filtered = inv_filtered_block
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serialized_inv_filtered.len() == INV_HASH_SIZE);
|
||||||
|
|
||||||
|
let inv_tx = InventoryHash::Tx(tx_hash);
|
||||||
|
let serialized_inv_tx = inv_tx
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serialized_inv_tx.len() == INV_HASH_SIZE);
|
||||||
|
|
||||||
|
let inv_err = InventoryHash::Error;
|
||||||
|
let serializd_inv_err = inv_err
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
assert!(serializd_inv_err.len() == INV_HASH_SIZE)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
/// Verifies that...
|
||||||
|
/// 1. The smallest disallowed vector of `InventoryHash`s is too large to fit in a legal Zcash message
|
||||||
|
/// 2. The largest allowed vector is small enough to fit in a legal Zcash message
|
||||||
|
fn meta_addr_max_allocation_is_correct() {
|
||||||
|
let inv = InventoryHash::Error;
|
||||||
|
let max_allocation: usize = InventoryHash::max_allocation().try_into().unwrap();
|
||||||
|
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
|
||||||
|
for _ in 0..(InventoryHash::max_allocation() + 1) {
|
||||||
|
smallest_disallowed_vec.push(inv);
|
||||||
|
}
|
||||||
|
let smallest_disallowed_serialized = smallest_disallowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
// Check that our smallest_disallowed_vec is only one item larger than the limit
|
||||||
|
assert!(((smallest_disallowed_vec.len() - 1) as u64) == InventoryHash::max_allocation());
|
||||||
|
// Check that our smallest_disallowed_vec is too big to fit in a Zcash message.
|
||||||
|
assert!(smallest_disallowed_serialized.len() > MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
|
||||||
|
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
|
||||||
|
smallest_disallowed_vec.pop();
|
||||||
|
let largest_allowed_vec = smallest_disallowed_vec;
|
||||||
|
let largest_allowed_serialized = largest_allowed_vec
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("Serialization to vec must succeed");
|
||||||
|
|
||||||
|
// Check that our largest_allowed_vec contains the maximum number of InventoryHashes
|
||||||
|
assert!((largest_allowed_vec.len() as u64) == InventoryHash::max_allocation());
|
||||||
|
// Check that our largest_allowed_vec is small enough to fit in a Zcash message.
|
||||||
|
assert!(largest_allowed_serialized.len() <= MAX_PROTOCOL_MESSAGE_LEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue