Limit transaction size in the mempool (#2917)

* Limit tx size

Zebra now limits the transaction size in the `zcash_deserialize()` method for
`Transaction`.

* Remove unused error variants (#2941)

Co-authored-by: Conrado Gouvea <conrado@zfnd.org>

* Limit tx size

Zebra now limits the transaction size in the `zcash_deserialize()` method for
`Transaction`.

* Test the tx deserialization limit

Co-authored-by: Conrado Gouvea <conrado@zfnd.org>
This commit is contained in:
Marek 2021-10-25 20:25:28 +02:00 committed by GitHub
parent 451448ef99
commit 2920d838ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 38 deletions

View File

@ -522,10 +522,13 @@ impl ZcashSerialize for Transaction {
} }
impl ZcashDeserialize for Transaction { impl ZcashDeserialize for Transaction {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> { fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError> {
// If the limit is reached, we'll get an UnexpectedEof error.
let mut limited_reader = reader.take(MAX_BLOCK_BYTES);
let (version, overwintered) = { let (version, overwintered) = {
const LOW_31_BITS: u32 = (1 << 31) - 1; const LOW_31_BITS: u32 = (1 << 31) - 1;
let header = reader.read_u32::<LittleEndian>()?; let header = limited_reader.read_u32::<LittleEndian>()?;
(header & LOW_31_BITS, header >> 31 != 0) (header & LOW_31_BITS, header >> 31 != 0)
}; };
@ -537,22 +540,22 @@ impl ZcashDeserialize for Transaction {
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
match (version, overwintered) { match (version, overwintered) {
(1, false) => Ok(Transaction::V1 { (1, false) => Ok(Transaction::V1 {
inputs: Vec::zcash_deserialize(&mut reader)?, inputs: Vec::zcash_deserialize(&mut limited_reader)?,
outputs: Vec::zcash_deserialize(&mut reader)?, outputs: Vec::zcash_deserialize(&mut limited_reader)?,
lock_time: LockTime::zcash_deserialize(&mut reader)?, lock_time: LockTime::zcash_deserialize(&mut limited_reader)?,
}), }),
(2, false) => { (2, false) => {
// Version 2 transactions use Sprout-on-BCTV14. // Version 2 transactions use Sprout-on-BCTV14.
type OptV2Jsd = Option<JoinSplitData<Bctv14Proof>>; type OptV2Jsd = Option<JoinSplitData<Bctv14Proof>>;
Ok(Transaction::V2 { Ok(Transaction::V2 {
inputs: Vec::zcash_deserialize(&mut reader)?, inputs: Vec::zcash_deserialize(&mut limited_reader)?,
outputs: Vec::zcash_deserialize(&mut reader)?, outputs: Vec::zcash_deserialize(&mut limited_reader)?,
lock_time: LockTime::zcash_deserialize(&mut reader)?, lock_time: LockTime::zcash_deserialize(&mut limited_reader)?,
joinsplit_data: OptV2Jsd::zcash_deserialize(&mut reader)?, joinsplit_data: OptV2Jsd::zcash_deserialize(&mut limited_reader)?,
}) })
} }
(3, true) => { (3, true) => {
let id = reader.read_u32::<LittleEndian>()?; let id = limited_reader.read_u32::<LittleEndian>()?;
// Consensus rule: // Consensus rule:
// > [Overwinter only, pre-Sapling] The transaction version number MUST be 3, and the version group ID MUST be 0x03C48270. // > [Overwinter only, pre-Sapling] The transaction version number MUST be 3, and the version group ID MUST be 0x03C48270.
// //
@ -565,15 +568,15 @@ impl ZcashDeserialize for Transaction {
// Version 3 transactions use Sprout-on-BCTV14. // Version 3 transactions use Sprout-on-BCTV14.
type OptV3Jsd = Option<JoinSplitData<Bctv14Proof>>; type OptV3Jsd = Option<JoinSplitData<Bctv14Proof>>;
Ok(Transaction::V3 { Ok(Transaction::V3 {
inputs: Vec::zcash_deserialize(&mut reader)?, inputs: Vec::zcash_deserialize(&mut limited_reader)?,
outputs: Vec::zcash_deserialize(&mut reader)?, outputs: Vec::zcash_deserialize(&mut limited_reader)?,
lock_time: LockTime::zcash_deserialize(&mut reader)?, lock_time: LockTime::zcash_deserialize(&mut limited_reader)?,
expiry_height: block::Height(reader.read_u32::<LittleEndian>()?), expiry_height: block::Height(limited_reader.read_u32::<LittleEndian>()?),
joinsplit_data: OptV3Jsd::zcash_deserialize(&mut reader)?, joinsplit_data: OptV3Jsd::zcash_deserialize(&mut limited_reader)?,
}) })
} }
(4, true) => { (4, true) => {
let id = reader.read_u32::<LittleEndian>()?; let id = limited_reader.read_u32::<LittleEndian>()?;
// Consensus rules: // Consensus rules:
// > [Sapling to Canopy inclusive, pre-NU5] The transaction version number MUST be 4, and the version group ID MUST be 0x892F2085. // > [Sapling to Canopy inclusive, pre-NU5] The transaction version number MUST be 4, and the version group ID MUST be 0x892F2085.
// > // >
@ -597,20 +600,20 @@ impl ZcashDeserialize for Transaction {
// instead we have to pull the component parts out manually and // instead we have to pull the component parts out manually and
// then assemble them. // then assemble them.
let inputs = Vec::zcash_deserialize(&mut reader)?; let inputs = Vec::zcash_deserialize(&mut limited_reader)?;
let outputs = Vec::zcash_deserialize(&mut reader)?; let outputs = Vec::zcash_deserialize(&mut limited_reader)?;
let lock_time = LockTime::zcash_deserialize(&mut reader)?; let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?;
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?); let expiry_height = block::Height(limited_reader.read_u32::<LittleEndian>()?);
let value_balance = (&mut reader).zcash_deserialize_into()?; let value_balance = (&mut limited_reader).zcash_deserialize_into()?;
let shielded_spends = Vec::zcash_deserialize(&mut reader)?; let shielded_spends = Vec::zcash_deserialize(&mut limited_reader)?;
let shielded_outputs = let shielded_outputs =
Vec::<sapling::OutputInTransactionV4>::zcash_deserialize(&mut reader)? Vec::<sapling::OutputInTransactionV4>::zcash_deserialize(&mut limited_reader)?
.into_iter() .into_iter()
.map(Output::from_v4) .map(Output::from_v4)
.collect(); .collect();
let joinsplit_data = OptV4Jsd::zcash_deserialize(&mut reader)?; let joinsplit_data = OptV4Jsd::zcash_deserialize(&mut limited_reader)?;
let sapling_transfers = if !shielded_spends.is_empty() { let sapling_transfers = if !shielded_spends.is_empty() {
Some(sapling::TransferData::SpendsAndMaybeOutputs { Some(sapling::TransferData::SpendsAndMaybeOutputs {
@ -636,7 +639,7 @@ impl ZcashDeserialize for Transaction {
Some(transfers) => Some(sapling::ShieldedData { Some(transfers) => Some(sapling::ShieldedData {
value_balance, value_balance,
transfers, transfers,
binding_sig: reader.read_64_bytes()?.into(), binding_sig: limited_reader.read_64_bytes()?.into(),
}), }),
None => None, None => None,
}; };
@ -658,31 +661,30 @@ impl ZcashDeserialize for Transaction {
// > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A. // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
// //
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
let id = reader.read_u32::<LittleEndian>()?; let id = limited_reader.read_u32::<LittleEndian>()?;
if id != TX_V5_VERSION_GROUP_ID { if id != TX_V5_VERSION_GROUP_ID {
return Err(SerializationError::Parse("expected TX_V5_VERSION_GROUP_ID")); return Err(SerializationError::Parse("expected TX_V5_VERSION_GROUP_ID"));
} }
// convert the nConsensusBranchId to a NetworkUpgrade // convert the nConsensusBranchId to a NetworkUpgrade
let network_upgrade = NetworkUpgrade::from_branch_id( let network_upgrade =
reader.read_u32::<LittleEndian>()?, NetworkUpgrade::from_branch_id(limited_reader.read_u32::<LittleEndian>()?)
) .ok_or(SerializationError::Parse(
.ok_or(SerializationError::Parse( "expected a valid network upgrade from the consensus branch id",
"expected a valid network upgrade from the consensus branch id", ))?;
))?;
// transaction validity time and height limits // transaction validity time and height limits
let lock_time = LockTime::zcash_deserialize(&mut reader)?; let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?;
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?); let expiry_height = block::Height(limited_reader.read_u32::<LittleEndian>()?);
// transparent // transparent
let inputs = Vec::zcash_deserialize(&mut reader)?; let inputs = Vec::zcash_deserialize(&mut limited_reader)?;
let outputs = Vec::zcash_deserialize(&mut reader)?; let outputs = Vec::zcash_deserialize(&mut limited_reader)?;
// sapling // sapling
let sapling_shielded_data = (&mut reader).zcash_deserialize_into()?; let sapling_shielded_data = (&mut limited_reader).zcash_deserialize_into()?;
// orchard // orchard
let orchard_shielded_data = (&mut reader).zcash_deserialize_into()?; let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?;
Ok(Transaction::V5 { Ok(Transaction::V5 {
network_upgrade, network_upgrade,

View File

@ -1,5 +1,6 @@
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use chrono::{DateTime, NaiveDateTime, Utc};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -200,6 +201,60 @@ fn zip243_deserialize_and_round_trip() {
assert_eq!(&zebra_test::vectors::ZIP243_3[..], &data3[..]); assert_eq!(&zebra_test::vectors::ZIP243_3[..], &data3[..]);
} }
#[test]
fn deserialize_large_transaction() {
zebra_test::init();
// Create a dummy input and output.
let input =
transparent::Input::zcash_deserialize(&zebra_test::vectors::DUMMY_INPUT1[..]).unwrap();
let output =
transparent::Output::zcash_deserialize(&zebra_test::vectors::DUMMY_OUTPUT1[..]).unwrap();
// Create a lock time.
let lock_time = LockTime::Time(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(61, 0),
Utc,
));
// Serialize the input so that we can determine its serialized size.
let mut input_data = Vec::new();
input
.zcash_serialize(&mut input_data)
.expect("input should serialize");
// Calculate the number of inputs that fit into the transaction size limit.
let tx_inputs_num = MAX_BLOCK_BYTES as usize / input_data.len();
// Set the precalculated amount of inputs and a single output.
let inputs = std::iter::repeat(input)
.take(tx_inputs_num)
.collect::<Vec<_>>();
let outputs = vec![output];
// Create an oversized transaction. Adding the output and lock time causes
// the transaction to overflow the threshold.
let oversized_tx = Transaction::V1 {
inputs,
outputs,
lock_time,
};
// Serialize the transaction.
let mut tx_data = Vec::new();
oversized_tx
.zcash_serialize(&mut tx_data)
.expect("transaction should serialize");
// Check that the transaction is oversized.
assert!(tx_data.len() > MAX_BLOCK_BYTES as usize);
// The deserialization should fail because the transaction is too big.
Transaction::zcash_deserialize(&tx_data[..])
.expect_err("transaction should not deserialize due to its size");
}
// Transaction V5 test vectors // Transaction V5 test vectors
/// An empty transaction v5, with no Orchard, Sapling, or Transparent data /// An empty transaction v5, with no Orchard, Sapling, or Transparent data