Skip to content

Commit

Permalink
Implement SafePreallocate. Resolves ZcashFoundation#1880
Browse files Browse the repository at this point in the history
  • Loading branch information
preston-evans98 committed Mar 18, 2021
1 parent d19585c commit fac8c2e
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 22 deletions.
16 changes: 15 additions & 1 deletion zebra-chain/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ pub use header::BlockTimeError;
pub use header::{CountedHeader, Header};
pub use height::Height;
pub use root_hash::RootHash;
pub use serialize::MAX_BLOCK_BYTES;

use serde::{Deserialize, Serialize};

use crate::{fmt::DisplayToDebug, parameters::Network, transaction::Transaction, transparent};
use crate::{
fmt::DisplayToDebug,
parameters::Network,
serialization::{SafePreallocate, MAX_PROTOCOL_MESSAGE_LEN},
transaction::Transaction,
transparent,
};

/// A Zcash block, containing a header and a list of transactions.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -80,3 +87,10 @@ impl<'a> From<&'a Block> for Hash {
(&block.header).into()
}
}

impl SafePreallocate for Hash {
fn max_allocation() -> u64 {
// A block Hash has takes 32 bytes so we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN / 32) in a single message
(MAX_PROTOCOL_MESSAGE_LEN as u64) / 32
}
}
16 changes: 15 additions & 1 deletion zebra-chain/src/block/header.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use chrono::{DateTime, Duration, Utc};
use thiserror::Error;

use crate::work::{difficulty::CompactDifficulty, equihash::Solution};
use crate::{
serialization::{SafePreallocate, MAX_PROTOCOL_MESSAGE_LEN},
work::{difficulty::CompactDifficulty, equihash::Solution},
};

use super::{merkle, Hash, Height};

Expand Down Expand Up @@ -118,3 +121,14 @@ pub struct CountedHeader {
pub header: Header,
pub transaction_count: usize,
}

// Includes the 32-byte nonce and 3-byte equihash length field.
const BLOCK_HEADER_LENGTH: usize =
crate::work::equihash::Solution::INPUT_LENGTH + 32 + 3 + crate::work::equihash::SOLUTION_SIZE;

impl SafePreallocate for CountedHeader {
fn max_allocation() -> u64 {
// An CountedHeader has BLOCK_HEADER_LENGTH bytes + 1 byte for the transaction count
(MAX_PROTOCOL_MESSAGE_LEN / (BLOCK_HEADER_LENGTH + 1)) as u64
}
}
16 changes: 15 additions & 1 deletion zebra-chain/src/sapling/output.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::io;

use crate::{
block::MAX_BLOCK_BYTES,
primitives::Groth16Proof,
serialization::{serde_helpers, SerializationError, ZcashDeserialize, ZcashSerialize},
serialization::{
serde_helpers, SafePreallocate, SerializationError, ZcashDeserialize, ZcashSerialize,
},
};

use super::{commitment, keys, note};
Expand Down Expand Up @@ -51,3 +54,14 @@ 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;

/// We can never receive more outputs in a single message from an honest peer than would fit in a single block
impl SafePreallocate for Output {
fn max_allocation() -> u64 {
MAX_BLOCK_BYTES / OUTPUT_SIZE
}
}
16 changes: 15 additions & 1 deletion zebra-chain/src/sapling/spend.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::io;

use crate::{
block::MAX_BLOCK_BYTES,
primitives::{
redjubjub::{self, SpendAuth},
Groth16Proof,
},
serialization::{
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
ReadZcashExt, SafePreallocate, SerializationError, WriteZcashExt, ZcashDeserialize,
ZcashSerialize,
},
};

Expand Down Expand Up @@ -56,3 +58,15 @@ impl ZcashDeserialize for Spend {
})
}
}

/// An output contains: a 32 byte cv, a 32 byte anchor, a 32 byte nullifier,
/// a 32 byte rk, a 192 byte zkproof, and a 64 byte spendAuthSig
/// [ps]: https://zips.z.cash/protocol/protocol.pdf#spendencoding
const SPEND_SIZE: u64 = 32 + 32 + 32 + 32 + 192 + 64;

/// We can never receive more spends in a single message from an honest peer than would fit in a single block
impl SafePreallocate for Spend {
fn max_allocation() -> u64 {
MAX_BLOCK_BYTES / SPEND_SIZE
}
}
4 changes: 2 additions & 2 deletions zebra-chain/src/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ pub mod sha256d;
pub use error::SerializationError;
pub use read_zcash::ReadZcashExt;
pub use write_zcash::WriteZcashExt;
pub use zcash_deserialize::{ZcashDeserialize, ZcashDeserializeInto};
pub use zcash_serialize::ZcashSerialize;
pub use zcash_deserialize::{SafePreallocate, ZcashDeserialize, ZcashDeserializeInto};
pub use zcash_serialize::{ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN};

#[cfg(test)]
mod proptests;
5 changes: 4 additions & 1 deletion zebra-chain/src/serialization/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io;
use std::{io, num::TryFromIntError};

use thiserror::Error;

Expand All @@ -13,6 +13,9 @@ pub enum SerializationError {
// XXX refine errors
#[error("parse error: {0}")]
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`
#[error("input couldn't be parsed as a zatoshi `Amount`: {source}")]
Amount {
Expand Down
28 changes: 20 additions & 8 deletions zebra-chain/src/serialization/zcash_deserialize.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io;
use std::{convert::TryInto, io};

use super::{ReadZcashExt, SerializationError};

Expand All @@ -17,15 +17,15 @@ pub trait ZcashDeserialize: Sized {
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError>;
}

impl<T: ZcashDeserialize> ZcashDeserialize for Vec<T> {
impl<T: ZcashDeserialize + SafePreallocate> ZcashDeserialize for Vec<T> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let len = reader.read_compactsize()?;
// We're given len, so we could preallocate. But blindly preallocating
// without a size bound can allow DOS attacks, and there's no way to
// pass a size bound in a ZcashDeserialize impl, so instead we allocate
// as we read from the reader. (The maximum block and transaction sizes
// limit the eventual size of these allocations.)
let mut vec = Vec::new();
if len > T::max_allocation() {
return Err(SerializationError::Parse(
"Vector longer than max_allocation",
));
}
let mut vec = Vec::with_capacity(len.try_into()?);
for _ in 0..len {
vec.push(T::zcash_deserialize(&mut reader)?);
}
Expand All @@ -49,3 +49,15 @@ impl<R: io::Read> ZcashDeserializeInto for R {
T::zcash_deserialize(self)
}
}

/// Blind preallocation of a Vec<T: SafePreallocate> can be done safely. 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: SafePreallocate>
/// 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 to low could cause Zebra to fall out of consensus by rejecting all messages containing a valid block.
pub trait SafePreallocate {
/// Provides a ***loose upper bound*** on the size of the Vec<T: SafePreallocate>
/// which can possibly be received from an honest peer.
fn max_allocation() -> u64;
}
5 changes: 5 additions & 0 deletions zebra-chain/src/serialization/zcash_serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ impl<T: ZcashSerialize> ZcashSerialize for Vec<T> {
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;
32 changes: 30 additions & 2 deletions zebra-chain/src/transaction/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use std::{io, sync::Arc};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};

use crate::{
block::MAX_BLOCK_BYTES,
parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID},
primitives::ZkSnarkProof,
serialization::{
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto,
ZcashSerialize,
ReadZcashExt, SafePreallocate, SerializationError, WriteZcashExt, ZcashDeserialize,
ZcashDeserializeInto, ZcashSerialize,
},
sprout,
};
Expand Down Expand Up @@ -342,3 +343,30 @@ where
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 and a 4 byte locktime
const MIN_TRANSPARENT_TX_SIZE: u64 = MIN_TRANSPARENT_INPUT_SIZE + 4;

/// No valid Zcash message contains more transactions than can fit in a single block
impl SafePreallocate for Arc<Transaction> {
fn max_allocation() -> u64 {
MAX_BLOCK_BYTES / MIN_TRANSPARENT_TX_SIZE
}
}
/// No valid Zcash message contains more transactions inputs than can fit in maximally large transaction
impl SafePreallocate for transparent::Input {
fn max_allocation() -> u64 {
MAX_BLOCK_BYTES / MIN_TRANSPARENT_INPUT_SIZE
}
}
/// No valid Zcash message contains more transactions outputs than can fit in maximally large transaction
impl SafePreallocate for transparent::Output {
fn max_allocation() -> u64 {
MAX_BLOCK_BYTES / MIN_TRANSPARENT_OUTPUT_SIZE
}
}
12 changes: 10 additions & 2 deletions zebra-network/src/meta_addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use chrono::{DateTime, TimeZone, Utc};

use zebra_chain::serialization::{
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
ReadZcashExt, SafePreallocate, SerializationError, WriteZcashExt, ZcashDeserialize,
ZcashSerialize,
};

use crate::protocol::types::PeerServices;
use crate::protocol::{external::MAX_PROTOCOL_MESSAGE_LEN, types::PeerServices};

/// Peer connection state, based on our interactions with the peer.
///
Expand Down Expand Up @@ -200,6 +201,13 @@ impl ZcashDeserialize for MetaAddr {
})
}
}
/// 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 SafePreallocate for MetaAddr {
fn max_allocation() -> u64 {
(MAX_PROTOCOL_MESSAGE_LEN / META_ADDR_SIZE) as u64
}
}

#[cfg(test)]
mod tests {
Expand Down
2 changes: 1 addition & 1 deletion zebra-network/src/protocol/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ mod message;
/// Newtype wrappers for primitive types.
pub mod types;

pub use codec::Codec;
pub use codec::{Codec, MAX_PROTOCOL_MESSAGE_LEN};
pub use inv::InventoryHash;
pub use message::Message;
2 changes: 1 addition & 1 deletion zebra-network/src/protocol/external/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use super::{
const HEADER_LEN: usize = 24usize;

/// 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.
pub struct Codec {
Expand Down
13 changes: 12 additions & 1 deletion zebra-network/src/protocol/external/inv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};

use zebra_chain::{
block,
serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize},
serialization::{
ReadZcashExt, SafePreallocate, SerializationError, ZcashDeserialize, ZcashSerialize,
},
transaction,
};

use super::MAX_PROTOCOL_MESSAGE_LEN;

/// 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
Expand Down Expand Up @@ -81,3 +85,10 @@ impl ZcashDeserialize for InventoryHash {
}
}
}

impl SafePreallocate for InventoryHash {
fn max_allocation() -> u64 {
// An Inventory hash takes 32 bytes, so we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN / 32) in a single message
(MAX_PROTOCOL_MESSAGE_LEN / 32) as u64
}
}

0 comments on commit fac8c2e

Please sign in to comment.