Skip to content

Commit d98c933

Browse files
chore: combined fixes (#10995)
2 parents bded9ca + 8ae94cb commit d98c933

16 files changed

Lines changed: 575 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to Zebra are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org).
77

8+
## [Unreleased]
9+
10+
### Security
11+
12+
- Reserve space for the block header and transaction count when selecting block template
13+
transactions, so blocks mined from Zebra's templates can no longer exceed the consensus size
14+
limit ([GHSA-95m2-vx53-v2jw](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-95m2-vx53-v2jw)).
15+
- Avoid quadratic validation work when checking the remaining transparent value of blocks with
16+
many transactions ([GHSA-4g24-549m-hp75](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4g24-549m-hp75)).
17+
- Prevent a peer from stalling chain synchronization by delivering a rejected
18+
block body that shares its header hash with a later valid block
19+
([GHSA-8gxx-hc65-vv82](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8gxx-hc65-vv82)).
20+
- Score misbehavior for peers that directly push consensus-invalid transactions, matching the
21+
treatment of peers that advertise them
22+
([GHSA-g7c4-2w6c-cr3r](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-g7c4-2w6c-cr3r)).
23+
824
## [Zebra 6.0.0](https://github.com/ZcashFoundation/zebra/releases/tag/v6.0.0) - 2026-07-10
925

1026
### Added

zebra-chain/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Security
11+
12+
- Computing `transaction::Transaction::value_balance` no longer clones the entire UTXO map per
13+
call (GHSA-4g24-549m-hp75).
14+
15+
### Added
16+
17+
- `block::Header::{SERIALIZED_SIZE, REGTEST_SERIALIZED_SIZE, serialized_size}`
18+
- `work::equihash::Solution::{SERIALIZED_SIZE, REGTEST_SERIALIZED_SIZE, serialized_size}`
19+
820
## [11.1.0] - 2026-07-10
921

1022
### Added

zebra-chain/src/block/header.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,28 @@ impl Header {
139139
pub fn hash(&self) -> Hash {
140140
Hash::from(self)
141141
}
142+
143+
/// The serialized size of a block header on Mainnet and Testnet (except Regtest), in bytes:
144+
/// the fields before the nonce, the 32-byte nonce, and the length-prefixed solution.
145+
pub const SERIALIZED_SIZE: usize = Solution::INPUT_LENGTH + 32 + Solution::SERIALIZED_SIZE;
146+
147+
/// The serialized size of a block header on Regtest, in bytes:
148+
/// the fields before the nonce, the 32-byte nonce, and the length-prefixed solution.
149+
pub const REGTEST_SERIALIZED_SIZE: usize =
150+
Solution::INPUT_LENGTH + 32 + Solution::REGTEST_SERIALIZED_SIZE;
151+
152+
/// Returns the size of a serialized block header on `network`, in bytes.
153+
///
154+
/// Every header field has a fixed size, except the Equihash solution,
155+
/// whose size is constant per network, so this is also constant per network:
156+
/// [`Self::REGTEST_SERIALIZED_SIZE`] on Regtest, [`Self::SERIALIZED_SIZE`] everywhere else.
157+
pub fn serialized_size(network: &Network) -> usize {
158+
if network.is_regtest() {
159+
Self::REGTEST_SERIALIZED_SIZE
160+
} else {
161+
Self::SERIALIZED_SIZE
162+
}
163+
}
142164
}
143165

144166
/// A header with a count of the number of transactions in its block.

zebra-chain/src/block/tests/vectors.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ fn blockheader_serialization() {
188188
}
189189
}
190190

191+
/// Checks that [`Header::serialized_size`] matches the actual size of serialized headers
192+
/// on every network.
193+
#[test]
194+
fn blockheader_serialized_size() {
195+
let _init_guard = zebra_test::init();
196+
197+
// `BLOCKS` contains Mainnet and Testnet blocks, whose headers have the same size.
198+
for block in zebra_test::vectors::BLOCKS.iter() {
199+
let mut header = block[..Header::serialized_size(&Network::Mainnet)]
200+
.zcash_deserialize_into::<Header>()
201+
.expect("blockheader test vector should deserialize");
202+
203+
let serialized_header = header
204+
.zcash_serialize_to_vec()
205+
.expect("blockheader test vector should serialize");
206+
207+
assert_eq!(
208+
serialized_header.len(),
209+
Header::serialized_size(&Network::Mainnet),
210+
"serialized header size should match Header::serialized_size on Mainnet"
211+
);
212+
213+
// Regtest headers only differ in the size of the Equihash solution.
214+
header.solution = crate::work::equihash::Solution::from_bytes(
215+
&[0; crate::work::equihash::REGTEST_SOLUTION_SIZE],
216+
)
217+
.expect("Regtest solution size should be valid");
218+
219+
let serialized_header = header
220+
.zcash_serialize_to_vec()
221+
.expect("Regtest blockheader should serialize");
222+
223+
assert_eq!(
224+
serialized_header.len(),
225+
Header::serialized_size(&Network::new_regtest(Default::default())),
226+
"serialized header size should match Header::serialized_size on Regtest"
227+
);
228+
}
229+
}
230+
191231
#[test]
192232
fn round_trip_blocks() {
193233
let _init_guard = zebra_test::init();

zebra-chain/src/transaction.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use crate::{
5050
serialization::ZcashSerialize,
5151
sprout,
5252
transparent::{
53-
self, outputs_from_utxos,
53+
self,
5454
CoinbaseSpendRestriction::{self, *},
5555
},
5656
value_balance::{ValueBalance, ValueBalanceError},
@@ -1562,7 +1562,16 @@ impl Transaction {
15621562
&self,
15631563
utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
15641564
) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
1565-
self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone()))
1565+
let outputs = self
1566+
.spent_outpoints()
1567+
.filter_map(|outpoint| {
1568+
utxos
1569+
.get(&outpoint)
1570+
.map(|utxo| (outpoint, utxo.output.clone()))
1571+
})
1572+
.collect();
1573+
1574+
self.value_balance_from_outputs(&outputs)
15661575
}
15671576

15681577
/// Converts [`Transaction`] to [`zcash_primitives::transaction::Transaction`].

zebra-chain/src/work/equihash.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde_big_array::BigArray;
77

88
use crate::{
99
block::Header,
10+
parameters::Network,
1011
serialization::{
1112
zcash_deserialize_bytes_external_count, zcash_serialize_bytes, CompactSizeMessage,
1213
SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
@@ -112,6 +113,27 @@ impl Solution {
112113
}
113114
}
114115

116+
/// The serialized size of a solution on Mainnet and Testnet (except Regtest), in bytes:
117+
/// the 1344-byte solution and its 3-byte CompactSize length prefix (`0xfd` + `u16`).
118+
pub const SERIALIZED_SIZE: usize = 3 + SOLUTION_SIZE;
119+
120+
/// The serialized size of a solution on Regtest, in bytes:
121+
/// the 36-byte solution and its 1-byte CompactSize length prefix.
122+
pub const REGTEST_SERIALIZED_SIZE: usize = 1 + REGTEST_SOLUTION_SIZE;
123+
124+
/// Returns the size of the serialized solution on `network`, in bytes,
125+
/// including its CompactSize length prefix.
126+
///
127+
/// The solution size is constant per network, so this is also constant per network:
128+
/// [`Self::REGTEST_SERIALIZED_SIZE`] on Regtest, [`Self::SERIALIZED_SIZE`] everywhere else.
129+
pub fn serialized_size(network: &Network) -> usize {
130+
if network.is_regtest() {
131+
Self::REGTEST_SERIALIZED_SIZE
132+
} else {
133+
Self::SERIALIZED_SIZE
134+
}
135+
}
136+
115137
/// Returns a [`Solution`] of `[0; SOLUTION_SIZE]` to be used in block proposals.
116138
pub fn for_proposal() -> Self {
117139
// TODO: Accept network as an argument, and if it's Regtest, return the shorter null solution.

zebra-rpc/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Fixed
11+
12+
- Block template transaction selection now reserves space for the block header and the
13+
transaction count, so assembled blocks can no longer exceed the consensus size limit
14+
(GHSA-95m2-vx53-v2jw).
15+
816
## [11.1.0] - 2026-07-10
917

1018
### Changed

zebra-rpc/src/methods/types/get_block_template/zip317.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ use rand::{
1515

1616
use zebra_chain::{
1717
amount::Amount,
18-
block::{Height, MAX_BLOCK_BYTES},
18+
block::{Header, Height, MAX_BLOCK_BYTES},
1919
parameters::Network,
20-
transaction::{self, zip317::BLOCK_UNPAID_ACTION_LIMIT, VerifiedUnminedTx},
20+
serialization::{CompactSizeMessage, ZcashSerialize},
21+
transaction::{
22+
self, zip317::BLOCK_UNPAID_ACTION_LIMIT, VerifiedUnminedTx, MIN_TRANSPARENT_TX_SIZE,
23+
},
2124
};
2225
use zebra_consensus::MAX_BLOCK_SIGOPS;
2326
use zebra_node_services::mempool::TransactionDependencies;
@@ -96,6 +99,12 @@ pub fn select_mempool_transactions(
9699
let mut remaining_block_sigops = MAX_BLOCK_SIGOPS;
97100
let mut remaining_block_unpaid_actions: u32 = BLOCK_UNPAID_ACTION_LIMIT;
98101

102+
// `MAX_BLOCK_BYTES` limits the whole serialized block, so reserve space for the block header
103+
// and the transaction count before budgeting transactions, or the assembled block could
104+
// exceed the consensus size limit (GHSA-95m2-vx53-v2jw).
105+
remaining_block_bytes -= Header::serialized_size(net);
106+
remaining_block_bytes -= max_transaction_count_size();
107+
99108
// Adjust the limits based on the coinbase transaction
100109
remaining_block_bytes -= fake_coinbase_tx.data.as_ref().len();
101110
remaining_block_sigops -= fake_coinbase_tx.sigops;
@@ -138,6 +147,25 @@ pub fn select_mempool_transactions(
138147
selected_txs
139148
}
140149

150+
/// Returns the maximum possible serialized size of a block's transaction count, in bytes.
151+
///
152+
/// The transaction count is a CompactSize whose width grows with the count. A serialized
153+
/// transaction takes at least [`MIN_TRANSPARENT_TX_SIZE`] bytes, so a block can never contain
154+
/// more than `MAX_BLOCK_BYTES / MIN_TRANSPARENT_TX_SIZE` transactions, which bounds the width.
155+
fn max_transaction_count_size() -> usize {
156+
let max_transaction_count: usize = (MAX_BLOCK_BYTES / MIN_TRANSPARENT_TX_SIZE)
157+
.try_into()
158+
.expect("fits in memory");
159+
160+
let max_transaction_count = CompactSizeMessage::try_from(max_transaction_count)
161+
.expect("the maximum transaction count is below the CompactSize message limit");
162+
163+
max_transaction_count
164+
.zcash_serialize_to_vec()
165+
.expect("serialization into a vec can't fail")
166+
.len()
167+
}
168+
141169
/// Returns a fee-weighted index and the total weight of `transactions`.
142170
///
143171
/// Returns `None` if there are no transactions, or if the weights are invalid.

zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
use zcash_keys::address::Address;
66
use zcash_transparent::address::TransparentAddress;
77

8-
use zebra_chain::{block::Height, parameters::Network, transaction, transparent::OutPoint};
8+
use zebra_chain::{
9+
amount::Amount,
10+
block::{Header, Height, MAX_BLOCK_BYTES},
11+
parameters::Network,
12+
transaction,
13+
transparent::OutPoint,
14+
};
915
use zebra_node_services::mempool::TransactionDependencies;
1016

11-
use crate::methods::types::get_block_template::MinerParams;
17+
use crate::methods::types::{get_block_template::MinerParams, transaction::TransactionTemplate};
1218

13-
use super::select_mempool_transactions;
19+
use super::{max_transaction_count_size, select_mempool_transactions};
1420

1521
#[test]
1622
fn excludes_tx_with_unselected_dependencies() {
@@ -105,3 +111,62 @@ fn includes_tx_with_selected_dependencies() {
105111
"should return a dependency depth of 1 for the dependent tx"
106112
);
107113
}
114+
115+
/// Checks that transaction selection reserves space for the block header and the transaction
116+
/// count, which [`MAX_BLOCK_BYTES`] covers: a transaction exactly filling the remaining safe
117+
/// budget is selected, and a transaction one byte larger is not (GHSA-95m2-vx53-v2jw).
118+
#[test]
119+
fn reserves_space_for_block_header_and_transaction_count() {
120+
let network = Network::Mainnet;
121+
let height = Height(1_000_000);
122+
let miner_params =
123+
MinerParams::from(Address::from(TransparentAddress::PublicKeyHash([0x7e; 20])));
124+
125+
let coinbase_tx_size =
126+
TransactionTemplate::new_coinbase(&network, height, &miner_params, Amount::zero())
127+
.expect("valid coinbase transaction template")
128+
.data
129+
.as_ref()
130+
.len();
131+
132+
let safe_budget = usize::try_from(MAX_BLOCK_BYTES).expect("fits in memory")
133+
- Header::serialized_size(&network)
134+
- max_transaction_count_size()
135+
- coinbase_tx_size;
136+
137+
let mut unmined_tx = network
138+
.unmined_transactions_in_blocks(..)
139+
.next()
140+
.expect("should not be empty");
141+
142+
unmined_tx.transaction.size = safe_budget;
143+
144+
assert_eq!(
145+
select_mempool_transactions(
146+
&network,
147+
height,
148+
&miner_params,
149+
vec![unmined_tx.clone()],
150+
TransactionDependencies::default(),
151+
None,
152+
)
153+
.len(),
154+
1,
155+
"should select a transaction exactly filling the safe block budget"
156+
);
157+
158+
unmined_tx.transaction.size = safe_budget + 1;
159+
160+
assert_eq!(
161+
select_mempool_transactions(
162+
&network,
163+
height,
164+
&miner_params,
165+
vec![unmined_tx],
166+
TransactionDependencies::default(),
167+
None,
168+
),
169+
vec![],
170+
"should not select a transaction one byte over the safe block budget"
171+
);
172+
}

zebra-state/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Security
11+
12+
- Checking the remaining transaction value of a block is no longer quadratic in the number of
13+
transactions (GHSA-4g24-549m-hp75).
14+
- The state service now accepts children of a block that was accepted and has the same
15+
block header hash (due to [ZIP-244](https://zips.z.cash/zip-0244)) as a block that
16+
was previously rejected
17+
([GHSA-8gxx-hc65-vv82](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8gxx-hc65-vv82)).
18+
819
## [10.1.0] - 2026-07-10
920

1021
### Added

0 commit comments

Comments
 (0)