Skip to content

feat: introducing BlockComponent#8719

Closed
ksn6 wants to merge 1 commit into
anza-xyz:masterfrom
ksn6:introduce-block-component
Closed

feat: introducing BlockComponent#8719
ksn6 wants to merge 1 commit into
anza-xyz:masterfrom
ksn6:introduce-block-component

Conversation

@ksn6
Copy link
Copy Markdown

@ksn6 ksn6 commented Oct 28, 2025

Problem

We need a unified, extensible data structure to represent the components of a Solana block, covering both normal transaction entries and special markers.

The structure must support a number of Alpenglow-related needs, e.g.:

... all the while remaining flexible for new marker types in the future.

Summary of Changes

We introduce a new enum, BlockComponent, which represents one logical element within a block. This code was originally designed in the alpenglow repository.

We are upstreaming the code identically, solely with the exception of employing wincode for Entry serde.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.3%. Comparing base (674bc91) to head (b31c73f).

Additional details and impacted files
@@            Coverage Diff            @@
##           master    #8719     +/-   ##
=========================================
  Coverage    83.3%    83.3%             
=========================================
  Files         852      853      +1     
  Lines      370790   373049   +2259     
=========================================
+ Hits       308906   311027   +2121     
- Misses      61884    62022    +138     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ksn6 ksn6 force-pushed the introduce-block-component branch from b31c73f to b76277e Compare October 30, 2025 07:49
Copy link
Copy Markdown

@akhi3030 akhi3030 left a comment

Choose a reason for hiding this comment

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

I didn't review the whole PR but came up with a few comments that I think would also apply to the structs that I didn't review yet. I hope they are useful.

DeserializationFailed(String),
}

impl fmt::Display for BlockComponentError {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider deriving Error for this type and then you do not need to implement Display. See consensus_pool.rs for an example.

impl Error for BlockComponentError {}

// Conversion from bincode::Error to BlockComponentError
impl From<bincode::Error> for BlockComponentError {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you derive Error above, you might be able to use the from macro to remove this. But given how you are parsing the message, it might not work.

Comment on lines +155 to +163
if msg.contains("exceeds maximum") {
// Extract numbers if possible, otherwise use defaults
Self::TooManyEntries { count: 0, max: 0 }
} else if msg.contains("Unknown") {
Self::UnknownVariant {
variant_type: "Unknown".to_string(),
id: 0,
}
} else if msg.contains("Unsupported") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this type of parsing feels quite fragile. I.e. I don't think the actual strings are considered to be part of the API of the library. I wonder what kind of impact we will have if this parsing fails.

If the parsing fails and that can cause significant breakage for us, I think we need to add tests to ensure that we can detect changes in the error strings.

If the parsing fails and that will not break anything for us, then I propose we do not implement the parsing.

if entries.is_empty() {
return Err(BlockComponentError::EmptyEntryBatch);
}
Self::validate_entry_batch_length(entries.len())?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

can this function also check that len is not 0 and then the check above can be removed?

///
/// # Errors
/// Returns an error if the entries vector is empty or exceeds the maximum allowed size.
pub fn new_entry_batch(entries: Vec<Entry>) -> Result<Self, BlockComponentError> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BlockComponent is an enum so despite having this constructor here, it is still possible to create an instance of it that will be considered invalid according to this function.

In order to ensure that we cannot construct invalid types, we would have to do something like below:

struct Entries(Vec<Entry>);

pub enum BlockComponent {
    EntryBatch(Entries),
    BlockMarker(VersionedBlockMarker),
}

And move the check from this function into the constructor for Entries. Note that it is important that the member of Entries is not pub. Otherwise one can bypass the validation.


match self {
Self::EntryBatch(entries) => {
Self::validate_entry_batch_length(entries.len())?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you use the suggestion above to introduce a struct Entries(Vec<Entry>), then you can remove this check as it should be guaranteed to be valid.

Comment on lines +455 to +475
pub fn to_bytes(&self) -> Result<Vec<u8>, BlockComponentError> {
let mut buffer = Vec::new();

match self {
Self::EntryBatch(entries) => {
Self::validate_entry_batch_length(entries.len())?;

buffer = wincode::serialize(entries)
.map_err(|e| BlockComponentError::SerializationFailed(e.to_string()))?;
}
Self::BlockMarker(marker) => {
// Write zero entry count
buffer.extend_from_slice(&0u64.to_le_bytes());
// Write marker data
let marker_bytes = marker.to_bytes()?;
buffer.extend_from_slice(&marker_bytes);
}
}

Ok(buffer)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
pub fn to_bytes(&self) -> Result<Vec<u8>, BlockComponentError> {
let mut buffer = Vec::new();
match self {
Self::EntryBatch(entries) => {
Self::validate_entry_batch_length(entries.len())?;
buffer = wincode::serialize(entries)
.map_err(|e| BlockComponentError::SerializationFailed(e.to_string()))?;
}
Self::BlockMarker(marker) => {
// Write zero entry count
buffer.extend_from_slice(&0u64.to_le_bytes());
// Write marker data
let marker_bytes = marker.to_bytes()?;
buffer.extend_from_slice(&marker_bytes);
}
}
Ok(buffer)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, BlockComponentError> {
match self {
Self::EntryBatch(entries) => {
Self::validate_entry_batch_length(entries.len())?;
wincode::serialize(entries)
.map_err(|e| BlockComponentError::SerializationFailed(e.to_string()))
}
Self::BlockMarker(marker) => {
// Write zero entry count
let mut buffer = 0u64.to_le_bytes().to_vec();
// Write marker data
let marker_bytes = marker.to_bytes()?;
buffer.extend_from_slice(&marker_bytes);
Ok(buffer)
}
}
}

This seems like less code and achieves the same.

cursor += bytes_consumed;
}

assert_eq!(cursor, data.len());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this seems like it should return an error instead of panicking.

If we are guaranteed that the whole data will be used, that seems to suggest that data is valid and then the function should not return an error anywhere but panic everywhere.

/// # Errors
/// Returns an error if any component fails to serialize.
pub fn to_bytes_multiple(components: &[Self]) -> Result<Vec<u8>, BlockComponentError> {
let mut result = Vec::new();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we can use serialized_size() below to do Vec::with_capacity() to reduce reallocations.

Comment on lines +572 to +582
impl Serialize for BlockComponent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let bytes = self.to_bytes().map_err(serde::ser::Error::custom)?;
serializer.serialize_bytes(&bytes)
}
}

impl<'de> Deserialize<'de> for BlockComponent {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If we are implementing serde serialize and deserialize, then why do we need to additionally implement to_bytes(), from_bytes(), etc.?

@ksn6
Copy link
Copy Markdown
Author

ksn6 commented Nov 5, 2025

@akhi3030 - appreciate the comments here.

This code is a bit messy atm - @cpubot and I are rewriting the entirety of this via wincode, which will provide performance improvements + substantially simplify the code.

Closing this PR.

@ksn6 ksn6 closed this Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants