Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ use super::{trie_cache, trie_recorder, MemoryOptimizedValidationParams};
use alloc::vec::Vec;
use codec::{Decode, Encode};
use cumulus_primitives_core::{
relay_chain::{BlockNumber as RNumber, Hash as RHash, UMPSignal, UMP_SEPARATOR},
relay_chain::{
BlockNumber as RNumber, Hash as RHash, UMPSignal, MAX_HEAD_DATA_SIZE, UMP_SEPARATOR,
},
ClaimQueueOffset, CoreSelector, ParachainBlockData, PersistedValidationData,
};
use frame_support::{
Expand Down Expand Up @@ -160,6 +162,13 @@ where
array_bytes::bytes2hex("0x", p.as_ref()),
array_bytes::bytes2hex("0x", b.header().parent_hash().as_ref()),
);
let encoded_header_size = b.header().encoded_size();
assert!(
encoded_header_size <= MAX_HEAD_DATA_SIZE as usize,
"Header size {} exceeds MAX_HEAD_DATA_SIZE {}",
encoded_header_size,
MAX_HEAD_DATA_SIZE
);
b.header().hash()
});

Expand Down
35 changes: 35 additions & 0 deletions cumulus/pallets/parachain-system/src/validate_block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,38 @@ fn rejects_multiple_blocks_per_pov_when_applying_runtime_upgrade() {
.contains("only one block per PoV is allowed"));
}
}

#[test]
fn validate_block_rejects_huge_header_single_block() {
sp_tracing::try_init_simple();

if env::var("RUN_TEST").is_ok() {
let (client, parent_head) = create_test_client();

let digest_data_exceeding_max_head_data_size = vec![0u8; 1_048_576 + 1024];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use MAX_HEAD_DATA_SIZE + 1 here instead of this hardcoded value?

let pre_digests =
vec![DigestItem::PreRuntime(*b"TEST", digest_data_exceeding_max_head_data_size)];

let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
Vec::new(),
parent_head.clone(),
Default::default(),
pre_digests,
);

call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args(["validate_block_rejects_huge_header_single_block", "--", "--nocapture"])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());

assert!(
dbg!(String::from_utf8(output.stderr).unwrap()).contains("exceeds MAX_HEAD_DATA_SIZE")
);
}
}
2 changes: 1 addition & 1 deletion cumulus/test/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ parameter_types! {
pub const BlockHashCount: BlockNumber = 250;
pub const Version: RuntimeVersion = VERSION;
pub RuntimeBlockLength: BlockLength =
BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO);
BlockLength::max_with_normal_ratio(12 * 1024 * 1024, NORMAL_DISPATCH_RATIO);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can test the assert in validate_block. Otherwise the digests there are too big :D

pub RuntimeBlockWeights: BlockWeights = BlockWeights::builder()
.base_block(BlockExecutionWeight::get())
.for_class(DispatchClass::all(), |weights| {
Expand Down
9 changes: 6 additions & 3 deletions substrate/client/network/sync/src/block_request_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ use std::{
/// Maximum blocks per response.
pub(crate) const MAX_BLOCKS_IN_RESPONSE: usize = 128;

const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;

mod rep {
Expand Down Expand Up @@ -442,11 +441,15 @@ where
};

let new_total_size = total_size +
block_data.header.len() +
block_data.body.iter().map(|ex| ex.len()).sum::<usize>() +
block_data.indexed_body.iter().map(|ex| ex.len()).sum::<usize>();
block_data.indexed_body.iter().map(|ex| ex.len()).sum::<usize>() +
block_data.justification.len() +
block_data.justifications.len();
Comment on lines +444 to +448
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you have to go through these manually instead of something like block_data.encoded_size()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Just kept it as it was before.


// Send at least one block, but make sure to not exceed the limit.
if !blocks.is_empty() && new_total_size > MAX_BODY_BYTES {
// Reserve 20 KiB for protocol overhead.
if !blocks.is_empty() && new_total_size > (MAX_RESPONSE_SIZE as usize - 20 * 1024) {
break
}

Expand Down
34 changes: 32 additions & 2 deletions substrate/client/network/test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ use sp_runtime::{
codec::{Decode, Encode},
generic::BlockId,
traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero},
Justification, Justifications,
Digest, Justification, Justifications,
};
use substrate_test_runtime_client::Sr25519Keyring;
pub use substrate_test_runtime_client::{
Expand Down Expand Up @@ -359,27 +359,57 @@ where
/// Add blocks to the peer -- edit the block before adding. The chain will
/// start at the given block iD.
pub fn generate_blocks_at<F>(
&mut self,
at: BlockId<Block>,
count: usize,
origin: BlockOrigin,
edit_block: F,
headers_only: bool,
inform_sync_about_new_best_block: bool,
announce_block: bool,
fork_choice: ForkChoiceStrategy,
) -> Vec<H256>
where
F: FnMut(BlockBuilder<Block, PeersFullClient>) -> Block,
{
self.generate_blocks_at_with_inherent_digests(
at,
count,
origin,
edit_block,
|_| Digest::default(),
headers_only,
inform_sync_about_new_best_block,
announce_block,
fork_choice,
)
}

pub fn generate_blocks_at_with_inherent_digests<F, G>(
&mut self,
at: BlockId<Block>,
count: usize,
origin: BlockOrigin,
mut edit_block: F,
mut inherent_digests: G,
headers_only: bool,
inform_sync_about_new_best_block: bool,
announce_block: bool,
fork_choice: ForkChoiceStrategy,
) -> Vec<H256>
where
F: FnMut(BlockBuilder<Block, PeersFullClient>) -> Block,
G: FnMut(usize) -> Digest,
{
let mut hashes = Vec::with_capacity(count);
let full_client = self.client.as_client();
let mut at = full_client.block_hash_from_id(&at).unwrap().unwrap();
for _ in 0..count {
for i in 0..count {
let builder = BlockBuilderBuilder::new(&*full_client)
.on_parent_block(at)
.fetch_parent_block_number(&*full_client)
.unwrap()
.with_inherent_digests(inherent_digests(i))
.build()
.unwrap();
let block = edit_block(builder);
Expand Down
45 changes: 45 additions & 0 deletions substrate/client/network/test/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1331,3 +1331,48 @@ async fn syncs_huge_blocks() {
assert_eq!(net.peer(0).client.info().best_number, 33);
assert_eq!(net.peer(1).client.info().best_number, 33);
}

/// Test syncing 512 blocks with ~1.2 MiB headers (empty bodies) to test large header handling.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see 900 KiB digests, where are the other 300 KiB coming from in the header?

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn syncs_blocks_with_large_headers() {
use sc_consensus::ForkChoiceStrategy;
use sp_runtime::{
generic::{BlockId, DigestItem},
Digest,
};

sp_tracing::try_init_simple();
let mut net = TestNet::new(2);

{
let peer = net.peer(0);
let best_hash = peer.client.info().best_hash;
peer.generate_blocks_at_with_inherent_digests(
BlockId::Hash(best_hash),
512,
BlockOrigin::Own,
|builder| builder.build().unwrap().block,
|i| {
let large_data = vec![i as u8; 900 * 1024];
Digest { logs: vec![DigestItem::PreRuntime(*b"test", large_data)] }
},
false,
true,
true,
ForkChoiceStrategy::LongestChain,
);
assert_eq!(peer.client.info().best_number, 512);
}

net.run_until_sync().await;

assert_eq!(net.peer(1).client.info().best_number, 512);
assert!(net.peers()[0].blockchain_canon_equals(&net.peers()[1]));

net.add_full_peer();

net.run_until_sync().await;

assert_eq!(net.peer(2).client.info().best_number, 512);
assert!(net.peers()[0].blockchain_canon_equals(&net.peers()[2]));
}
28 changes: 28 additions & 0 deletions substrate/frame/system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,9 @@ pub mod pallet {
pub type BlockWeight<T: Config> = StorageValue<_, ConsumedWeight, ValueQuery>;

/// Total length (in bytes) for all extrinsics put together, for the current block.
///
/// In contrast to its name it also includes the header overhead and digest size to accurately
/// track block size.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to change the name? Too much of a breaking change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I dont think anyone is reading this anyway, besides frame_system or?

Copy link
Member Author

@bkchr bkchr Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can change if you like.

#[pallet::storage]
#[pallet::whitelist_storage]
pub type AllExtrinsicsLen<T: Config> = StorageValue<_, u32>;
Expand Down Expand Up @@ -1929,6 +1932,28 @@ impl<T: Config> Pallet<T> {

// Remove previous block data from storage
BlockWeight::<T>::kill();

// Account for digest size and empty header overhead in block length.
// This ensures block limits consider the full block size, not just extrinsics.
let digest_size = digest.encoded_size();
let empty_header = <<T as Config>::Block as traits::Block>::Header::new(
*number,
Default::default(),
Default::default(),
*parent_hash,
Default::default(),
);
let empty_header_size = empty_header.encoded_size();
Comment on lines +1939 to +1946
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we cannot hard-code an upper bound and have an integrity test for this encoded size since it is generic over the runtime types? hm...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah not super perfect, but also not that much better solvable.

let overhead = digest_size.saturating_add(empty_header_size) as u32;
AllExtrinsicsLen::<T>::put(overhead);

// Ensure inherent digests don't exceed 20% of the max block size.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this 20% arbitrary or we had this rule already?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arbitrary. Should be more than enough space for inherent digests. Everything we are doing is not bigger than some bytes, so getting ~2MiB for 10MiB sounds more than enough.

let max_block_len = *T::BlockLength::get().max.get(DispatchClass::Mandatory);
let max_digest_len = max_block_len / 5;
assert!(
digest_size <= max_digest_len as usize,
"Inherent digest size ({digest_size}) exceeds 20% of max block length ({max_digest_len})"
);
}

/// Initialize [`INTRABLOCK_ENTROPY`](well_known_keys::INTRABLOCK_ENTROPY).
Expand Down Expand Up @@ -2041,6 +2066,9 @@ impl<T: Config> Pallet<T> {

/// Deposits a log and ensures it matches the block's log data.
pub fn deposit_log(item: generic::DigestItem) {
AllExtrinsicsLen::<T>::mutate(|len| {
*len = Some(len.unwrap_or(0).saturating_add(item.encoded_size() as u32));
});
<Digest<T>>::append(item);
}

Expand Down
88 changes: 88 additions & 0 deletions substrate/frame/system/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use frame_support::{
use mock::{RuntimeOrigin, *};
use sp_core::{hexdisplay::HexDisplay, H256};
use sp_runtime::{
generic::{Digest, DigestItem},
traits::{BlakeTwo256, Header},
DispatchError, DispatchErrorWithPostInfo,
};
Expand Down Expand Up @@ -977,3 +978,90 @@ fn initialize_block_number_must_be_sequential() {
System::initialize(&3, &[0u8; 32].into(), &Default::default());
});
}
#[test]
fn preinherent_digest_is_preserved() {
new_test_ext().execute_with(|| {
let data = vec![42u8; 100];
let digest = Digest { logs: vec![DigestItem::PreRuntime(*b"test", data.clone())] };

System::initialize(&1, &[0u8; 32].into(), &digest);

let stored_digest = <crate::Digest<Test>>::get();
assert_eq!(stored_digest.logs.len(), 1);

if let Some(DigestItem::PreRuntime(id, stored_data)) = stored_digest.logs.first() {
assert_eq!(id, b"test");
assert_eq!(stored_data, &data);
} else {
panic!("Expected PreRuntime digest item");
}

let header = System::finalize();
assert_eq!(header.digest().logs.len(), 1);

if let Some(DigestItem::PreRuntime(id, header_data)) = header.digest().logs.first() {
assert_eq!(id, b"test");
assert_eq!(header_data, &data);
} else {
panic!("Expected PreRuntime digest item in finalized header");
}
});
}

#[test]
fn all_extrinsics_len_includes_digest_and_header_overhead() {
new_test_ext().execute_with(|| {
let data = vec![42u8; 100];
let digest = Digest { logs: vec![DigestItem::PreRuntime(*b"test", data.clone())] };

System::initialize(&1, &[0u8; 32].into(), &digest);

let all_extrinsics_len = System::all_extrinsics_len();

let digest_size = digest.encoded_size();
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
let empty_header = <<Test as Config>::Block as BlockT>::Header::new(
1,
Default::default(),
Default::default(),
[0u8; 32].into(),
Default::default(),
);
let empty_header_size = empty_header.encoded_size();
let expected_overhead = digest_size + empty_header_size;

assert_eq!(all_extrinsics_len as usize, expected_overhead);
assert!(all_extrinsics_len > 100);
});
}

#[test]
fn deposit_log_updates_all_extrinsics_len() {
new_test_ext().execute_with(|| {
System::initialize(&1, &[0u8; 32].into(), &Default::default());

let initial_len = System::all_extrinsics_len();

let log_data = vec![42u8; 1000];
let log_item = DigestItem::Other(log_data.clone());
let log_size = log_item.encoded_size();

System::deposit_log(log_item);

let new_len = System::all_extrinsics_len();
assert_eq!(new_len, initial_len + log_size as u32);
});
}

#[test]
#[should_panic(expected = "Inherent digest size")]
fn inherent_digest_exceeding_20_percent_of_block_length_panics() {
new_test_ext().execute_with(|| {
// Mock has max block length of 1024, so 20% is 204 bytes.
// Create a digest larger than that.
let large_data = vec![42u8; 250];
let digest = Digest { logs: vec![DigestItem::PreRuntime(*b"test", large_data)] };

System::initialize(&1, &[0u8; 32].into(), &digest);
});
}
Loading