From 309033483807cedd507a20116d18d0edce2f606a Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:08:33 -0400 Subject: [PATCH 1/6] feat: introducing BlockComponent --- Cargo.lock | 4 + entry/Cargo.toml | 4 + entry/src/block_component.rs | 699 +++++++++++++++++++++++++++++++++++ entry/src/lib.rs | 1 + 4 files changed, 708 insertions(+) create mode 100644 entry/src/block_component.rs diff --git a/Cargo.lock b/Cargo.lock index 1ead803181a7b8..f0eb2525229a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8504,6 +8504,7 @@ version = "4.0.0-alpha.0" dependencies = [ "agave-logger", "agave-reserved-account-keys", + "agave-votor-messages", "assert_matches", "bincode", "crossbeam-channel", @@ -8515,6 +8516,8 @@ dependencies = [ "rayon", "serde", "solana-address 2.0.0", + "solana-bls-signatures", + "solana-clock", "solana-entry", "solana-hash 3.1.0", "solana-keypair", @@ -8533,6 +8536,7 @@ dependencies = [ "solana-system-transaction", "solana-transaction", "solana-transaction-error", + "thiserror 2.0.17", "wincode", ] diff --git a/entry/Cargo.toml b/entry/Cargo.toml index 488691010db303..3f16f02ef35b34 100644 --- a/entry/Cargo.toml +++ b/entry/Cargo.toml @@ -21,6 +21,7 @@ agave-unstable-api = [] dev-context-only-utils = [] [dependencies] +agave-votor-messages = { workspace = true } bincode = { workspace = true } crossbeam-channel = { workspace = true } dlopen2 = { workspace = true } @@ -29,6 +30,8 @@ num_cpus = { workspace = true } rayon = { workspace = true } serde = { workspace = true } solana-address = { workspace = true } +solana-bls-signatures = { workspace = true } +solana-clock = { workspace = true } solana-hash = { workspace = true } solana-measure = { workspace = true } solana-merkle-tree = { workspace = true } @@ -42,6 +45,7 @@ solana-short-vec = { workspace = true } solana-signature = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } +thiserror = { workspace = true } wincode = { workspace = true } [dev-dependencies] diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs new file mode 100644 index 00000000000000..f788241f1e28f6 --- /dev/null +++ b/entry/src/block_component.rs @@ -0,0 +1,699 @@ +/// Block components using wincode serialization. +/// +/// A `BlockComponent` represents either an entry batch or a special block marker. +/// Most of the time, a block component contains a vector of entries. However, periodically, +/// there are special messages that a block needs to contain. To accommodate these special +/// messages, `BlockComponent` allows for the inclusion of special data via `VersionedBlockMarker`. +/// +/// ## Serialization Layouts +/// +/// All numeric fields use little-endian encoding. +/// +/// ### BlockComponent with EntryBatch +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Entry Count (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ bincode Entry 0 (variable) │ +/// ├─────────────────────────────────────────┤ +/// │ bincode Entry 1 (variable) │ +/// ├─────────────────────────────────────────┤ +/// │ ... │ +/// ├─────────────────────────────────────────┤ +/// │ bincode Entry N-1 (variable) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### BlockComponent with BlockMarker +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Entry Count = 0 (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Marker Version (2 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Marker Data (variable) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### BlockMarkerV1 Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Variant ID (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ Byte Length (2 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Variant Data (variable) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### BlockHeaderV1 Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Parent Slot (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Parent Block ID (32 bytes) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### UpdateParentV1 Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Parent Slot (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Parent Block ID (32 bytes) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### BlockFooterV1 Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Bank Hash (32 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Producer Time Nanos (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ User Agent Length (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ User Agent Bytes (0-255 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Final Cert Present (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ FinalCertificate (if present, variable) │ +/// ├─────────────────────────────────────────┤ +/// │ Skip reward cert Present (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ SkipRewardCert (if present, variable) │ +/// ├─────────────────────────────────────────┤ +/// │ Notar reward cert Present (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ NotarRewardCert (if present, variable) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### FinalCertificate Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Slot (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Block ID (32 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Final Aggregate (VotesAggregate) │ +/// ├─────────────────────────────────────────┤ +/// │ Notar Aggregate Present (1 byte) │ +/// ├─────────────────────────────────────────┤ +/// │ Notar Aggregate (if present) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### VotesAggregate Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ BLS Signature Compressed (96 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Bitmap Length (2 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Bitmap (variable) │ +/// └─────────────────────────────────────────┘ +/// ``` +/// +/// ### GenesisCertificate Layout +/// ```text +/// ┌─────────────────────────────────────────┐ +/// │ Genesis Slot (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Genesis Block ID (32 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ BLS Signature (192 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Bitmap length (max 512) (8 bytes) │ +/// ├─────────────────────────────────────────┤ +/// │ Bitmap (up to 512 bytes) │ +/// └─────────────────────────────────────────┘ +/// ``` +use { + crate::entry::Entry, + agave_votor_messages::consensus_message::{Certificate, CertificateType}, + solana_bls_signatures::{ + Signature as BLSSignature, SignatureCompressed as BLSSignatureCompressed, + }, + solana_clock::Slot, + solana_hash::Hash, + std::mem::MaybeUninit, + wincode::{ + containers::{Pod, Vec as WincodeVec}, + error::write_length_encoding_overflow, + io::{Reader, Writer}, + len::{BincodeLen, SeqLen}, + ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteResult, + }, +}; + +/// 1-byte length prefix (max 255 elements). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct U8Len; + +impl SeqLen for U8Len { + fn read<'de, T>(reader: &mut impl Reader<'de>) -> ReadResult { + u8::get(reader).map(|len| len as usize) + } + + fn write(writer: &mut impl Writer, len: usize) -> WriteResult<()> { + let Ok(len) = len.try_into() else { + return Err(write_length_encoding_overflow("u8::MAX")); + }; + Ok(writer.write(&[len])?) + } + + fn write_bytes_needed(_len: usize) -> WriteResult { + Ok(1) + } +} + +/// 2-byte length prefix (max 65535 elements). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct U16Len; + +impl SeqLen for U16Len { + fn read<'de, T>(reader: &mut impl Reader<'de>) -> ReadResult { + u16::get(reader).map(|len| len as usize) + } + + fn write(writer: &mut impl Writer, len: usize) -> WriteResult<()> { + let Ok(len): Result = len.try_into() else { + return Err(write_length_encoding_overflow("u16::MAX")); + }; + Ok(writer.write(&len.to_le_bytes())?) + } + + fn write_bytes_needed(_len: usize) -> WriteResult { + Ok(2) + } +} + +/// Placeholder for skip reward certificate. +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct SkipRewardCertificate { + pub data: Vec, +} + +/// Placeholder for notar reward certificate. +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct NotarRewardCertificate { + pub data: Vec, +} + +/// Wraps a value with a u16 length prefix for TLV-style serialization. +/// +/// The length prefix represents the serialized byte size of the inner value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LengthPrefixed { + inner: T, +} + +impl LengthPrefixed { + pub fn new(inner: T) -> Self { + Self { inner } + } + + pub fn inner(&self) -> &T { + &self.inner + } + + pub fn into_inner(self) -> T { + self.inner + } +} + +impl> SchemaWrite for LengthPrefixed { + type Src = Self; + + const TYPE_META: TypeMeta = match T::TYPE_META { + TypeMeta::Static { size, zero_copy } => TypeMeta::Static { + size: size + std::mem::size_of::(), + zero_copy, + }, + TypeMeta::Dynamic => TypeMeta::Dynamic, + }; + + fn size_of(src: &Self::Src) -> WriteResult { + let inner_size = T::size_of(&src.inner)?; + Ok(std::mem::size_of::() + inner_size) + } + + fn write(writer: &mut impl Writer, src: &Self::Src) -> WriteResult<()> { + let inner_size = T::size_of(&src.inner)?; + let Ok(len): Result = inner_size.try_into() else { + return Err(write_length_encoding_overflow("u16::MAX")); + }; + u16::write(writer, &len)?; + T::write(writer, &src.inner) + } +} + +impl<'de, T: SchemaRead<'de, Dst = T>> SchemaRead<'de> for LengthPrefixed { + type Dst = Self; + + const TYPE_META: TypeMeta = match T::TYPE_META { + TypeMeta::Static { size, zero_copy } => TypeMeta::Static { + size: size + std::mem::size_of::(), + zero_copy, + }, + TypeMeta::Dynamic => TypeMeta::Dynamic, + }; + + fn read(reader: &mut impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let _len = u16::get(reader)?; + let inner_dst = unsafe { &mut *(&raw mut (*dst.as_mut_ptr()).inner).cast() }; + T::read(reader, inner_dst)?; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BlockComponentError { + #[error("Entry count {count} exceeds max {max}")] + TooManyEntries { count: usize, max: usize }, + #[error("Entry batch cannot be empty")] + EmptyEntryBatch, +} + +/// Block production metadata. User agent is capped at 255 bytes. +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct BlockFooterV1 { + #[wincode(with = "Pod")] + pub bank_hash: Hash, + pub block_producer_time_nanos: u64, + #[wincode(with = "WincodeVec")] + pub block_user_agent: Vec, + pub final_cert: Option, + pub skip_reward_cert: Option, + pub notar_reward_cert: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct BlockHeaderV1 { + pub parent_slot: Slot, + #[wincode(with = "Pod")] + pub parent_block_id: Hash, +} + +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct UpdateParentV1 { + pub new_parent_slot: Slot, + #[wincode(with = "Pod")] + pub new_parent_block_id: Hash, +} + +/// Attests to genesis block finalization with a BLS aggregate signature. +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct GenesisCertificate { + pub slot: Slot, + #[wincode(with = "Pod")] + pub block_id: Hash, + #[wincode(with = "Pod")] + pub bls_signature: BLSSignature, + #[wincode(with = "WincodeVec")] + pub bitmap: Vec, +} + +impl GenesisCertificate { + /// Max bitmap size in bytes (supports up to 4096 validators). + pub const MAX_BITMAP_SIZE: usize = 512; +} + +impl TryFrom for GenesisCertificate { + type Error = String; + + fn try_from(cert: Certificate) -> Result { + let CertificateType::Genesis(slot, block_id) = cert.cert_type else { + return Err("expected genesis certificate".into()); + }; + if cert.bitmap.len() > Self::MAX_BITMAP_SIZE { + return Err(format!( + "bitmap size {} exceeds max {}", + cert.bitmap.len(), + Self::MAX_BITMAP_SIZE + )); + } + Ok(Self { + slot, + block_id, + bls_signature: cert.signature, + bitmap: cert.bitmap, + }) + } +} + +impl From for Certificate { + fn from(cert: GenesisCertificate) -> Self { + Self { + cert_type: CertificateType::Genesis(cert.slot, cert.block_id), + signature: cert.bls_signature, + bitmap: cert.bitmap, + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +pub struct FinalCertificate { + pub slot: Slot, + #[wincode(with = "Pod")] + pub block_id: Hash, + pub final_aggregate: VotesAggregate, + pub notar_aggregate: Option, +} + +impl FinalCertificate { + #[cfg(feature = "dev-context-only-utils")] + pub fn new_for_tests() -> FinalCertificate { + FinalCertificate { + slot: 1234567890, + block_id: Hash::new_from_array([1u8; 32]), + final_aggregate: VotesAggregate { + signature: BLSSignatureCompressed::default(), + bitmap: vec![42; 64], + }, + notar_aggregate: None, + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, SchemaRead, SchemaWrite)] +pub struct VotesAggregate { + #[wincode(with = "Pod")] + signature: BLSSignatureCompressed, + #[wincode(with = "WincodeVec")] + bitmap: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +pub enum VersionedBlockFooter { + #[wincode(tag = 1)] + V1(BlockFooterV1), +} + +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +pub enum VersionedBlockHeader { + #[wincode(tag = 1)] + V1(BlockHeaderV1), +} + +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +pub enum VersionedUpdateParent { + #[wincode(tag = 1)] + V1(UpdateParentV1), +} + +/// TLV-encoded marker variants. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +pub enum BlockMarkerV1 { + BlockFooter(LengthPrefixed), + BlockHeader(LengthPrefixed), + UpdateParent(LengthPrefixed), + GenesisCertificate(LengthPrefixed), +} + +impl BlockMarkerV1 { + pub fn new_block_footer(f: VersionedBlockFooter) -> Self { + Self::BlockFooter(LengthPrefixed::new(f)) + } + + pub fn new_block_header(h: VersionedBlockHeader) -> Self { + Self::BlockHeader(LengthPrefixed::new(h)) + } + + pub fn new_update_parent(u: VersionedUpdateParent) -> Self { + Self::UpdateParent(LengthPrefixed::new(u)) + } + + pub fn new_genesis_certificate(c: GenesisCertificate) -> Self { + Self::GenesisCertificate(LengthPrefixed::new(c)) + } + + pub fn as_block_footer(&self) -> Option<&VersionedBlockFooter> { + match self { + Self::BlockFooter(lp) => Some(lp.inner()), + _ => None, + } + } + + pub fn as_block_header(&self) -> Option<&VersionedBlockHeader> { + match self { + Self::BlockHeader(lp) => Some(lp.inner()), + _ => None, + } + } + + pub fn as_update_parent(&self) -> Option<&VersionedUpdateParent> { + match self { + Self::UpdateParent(lp) => Some(lp.inner()), + _ => None, + } + } + + pub fn as_genesis_certificate(&self) -> Option<&GenesisCertificate> { + match self { + Self::GenesisCertificate(lp) => Some(lp.inner()), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u16")] +pub enum VersionedBlockMarker { + #[wincode(tag = 1)] + V1(BlockMarkerV1), +} + +impl VersionedBlockMarker { + pub const fn new(marker: BlockMarkerV1) -> Self { + Self::V1(marker) + } + + pub fn new_block_footer(f: BlockFooterV1) -> Self { + let f = VersionedBlockFooter::V1(f); + let f = BlockMarkerV1::BlockFooter(LengthPrefixed::new(f)); + VersionedBlockMarker::V1(f) + } + + pub fn new_block_header(h: BlockHeaderV1) -> Self { + let h = VersionedBlockHeader::V1(h); + let h = BlockMarkerV1::BlockHeader(LengthPrefixed::new(h)); + VersionedBlockMarker::V1(h) + } + + pub fn new_update_parent(u: UpdateParentV1) -> Self { + let u = VersionedUpdateParent::V1(u); + let u = BlockMarkerV1::UpdateParent(LengthPrefixed::new(u)); + VersionedBlockMarker::V1(u) + } + + pub fn new_genesis_certificate(g: GenesisCertificate) -> Self { + let g = BlockMarkerV1::GenesisCertificate(LengthPrefixed::new(g)); + VersionedBlockMarker::V1(g) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum BlockComponent { + EntryBatch(Vec), + BlockMarker(VersionedBlockMarker), +} + +impl BlockComponent { + const MAX_ENTRIES: usize = u32::MAX as usize; + const ENTRY_COUNT_SIZE: usize = 8; + + pub fn new_entry_batch(entries: Vec) -> Result { + if entries.is_empty() { + return Err(BlockComponentError::EmptyEntryBatch); + } + + if entries.len() >= Self::MAX_ENTRIES { + return Err(BlockComponentError::TooManyEntries { + count: entries.len(), + max: Self::MAX_ENTRIES, + }); + } + + Ok(Self::EntryBatch(entries)) + } + + pub const fn new_block_marker(marker: VersionedBlockMarker) -> Self { + Self::BlockMarker(marker) + } + + pub const fn as_marker(&self) -> Option<&VersionedBlockMarker> { + match self { + Self::BlockMarker(m) => Some(m), + _ => None, + } + } + + pub fn infer_is_entry_batch(data: &[u8]) -> Option { + data.get(..Self::ENTRY_COUNT_SIZE)? + .try_into() + .ok() + .map(|b| u64::from_le_bytes(b) != 0) + } + + pub fn infer_is_block_marker(data: &[u8]) -> Option { + Self::infer_is_entry_batch(data).map(|is_entry_batch| !is_entry_batch) + } +} + +impl SchemaWrite for BlockComponent { + type Src = Self; + + fn size_of(src: &Self::Src) -> WriteResult { + match src { + Self::EntryBatch(entries) => { + // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes + // the map_err. + let size = bincode::serialized_size(entries).map_err(|_| { + wincode::WriteError::Custom("Couldn't invoke bincode::serialized_size") + })?; + Ok(size as usize) + } + Self::BlockMarker(marker) => { + let marker_size = wincode::serialized_size(marker)? as usize; + Ok(Self::ENTRY_COUNT_SIZE + marker_size) + } + } + } + + fn write(writer: &mut impl Writer, src: &Self::Src) -> WriteResult<()> { + match src { + Self::EntryBatch(entries) => { + // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes + // the map_err. + let bytes = bincode::serialize(entries).map_err(|_| { + wincode::WriteError::Custom("Couldn't invoke bincode::serialize") + })?; + writer.write(&bytes)?; + Ok(()) + } + Self::BlockMarker(marker) => { + writer.write(&0u64.to_le_bytes())?; + ::write(writer, marker) + } + } + } +} + +impl<'de> SchemaRead<'de> for BlockComponent { + type Dst = Self; + + fn read(reader: &mut impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + // Read the entry count (first 8 bytes) to determine variant + let count_bytes = reader.fill_array::<8>()?; + let entry_count = u64::from_le_bytes(*count_bytes); + + if entry_count == 0 { + // This is a BlockMarker - consume the count bytes and read the marker + reader.consume(8)?; + dst.write(Self::BlockMarker(VersionedBlockMarker::get(reader)?)); + } else { + // This is an EntryBatch - read in the rest of the data. We do not anticipate having + // cases where we need to deserialize multiple BlockComponents from a single slice, and + // do not know where the delimiters are ahead of time. + // First, get all remaining bytes to deserialize + let data = reader.fill_buf(usize::MAX)?; + + // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes the + // map_err. + let entries: Vec = bincode::deserialize(data) + .map_err(|_| wincode::ReadError::Custom("Couldn't deserialize entries."))?; + + if entries.len() >= Self::MAX_ENTRIES { + return Err(wincode::ReadError::Custom("Too many entries")); + } + + // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes the + // map_err. + let consumed = bincode::serialized_size(&entries) + .map_err(|_| wincode::ReadError::Custom("Couldn't determine serialized size."))? + as usize; + reader.consume(consumed)?; + + dst.write(Self::EntryBatch(entries)); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use {super::*, std::iter::repeat_n}; + + fn mock_entries(n: usize) -> Vec { + repeat_n(Entry::default(), n).collect() + } + + fn sample_footer() -> BlockFooterV1 { + BlockFooterV1 { + bank_hash: Hash::new_unique(), + block_producer_time_nanos: 1234567890, + block_user_agent: b"test-agent".to_vec(), + final_cert: Some(FinalCertificate::new_for_tests()), + skip_reward_cert: None, + notar_reward_cert: None, + } + } + + #[test] + fn round_trips() { + let header = BlockHeaderV1 { + parent_slot: 12345, + parent_block_id: Hash::new_unique(), + }; + let bytes = wincode::serialize(&header).unwrap(); + assert_eq!( + header, + wincode::deserialize::(&bytes).unwrap() + ); + + let footer = sample_footer(); + let bytes = wincode::serialize(&footer).unwrap(); + assert_eq!( + footer, + wincode::deserialize::(&bytes).unwrap() + ); + + let cert = GenesisCertificate { + slot: 999, + block_id: Hash::new_unique(), + bls_signature: BLSSignature::default(), + bitmap: vec![1, 2, 3], + }; + let bytes = wincode::serialize(&cert).unwrap(); + assert_eq!( + cert, + wincode::deserialize::(&bytes).unwrap() + ); + + let marker = VersionedBlockMarker::new_block_footer(footer.clone()); + let bytes = wincode::serialize(&marker).unwrap(); + assert_eq!( + marker, + wincode::deserialize::(&bytes).unwrap() + ); + + let comp = BlockComponent::new_entry_batch(mock_entries(5)).unwrap(); + let bytes = wincode::serialize(&comp).unwrap(); + let deser: BlockComponent = wincode::deserialize(&bytes).unwrap(); + assert_eq!(comp, deser); + + let comp = BlockComponent::new_block_marker(marker); + let bytes = wincode::serialize(&comp).unwrap(); + let deser: BlockComponent = wincode::deserialize(&bytes).unwrap(); + assert_eq!(comp, deser); + } +} diff --git a/entry/src/lib.rs b/entry/src/lib.rs index af233d12d968f6..16a59fddde2394 100644 --- a/entry/src/lib.rs +++ b/entry/src/lib.rs @@ -8,6 +8,7 @@ ) )] #![allow(clippy::arithmetic_side_effects)] +pub mod block_component; pub mod entry; pub mod poh; mod wincode; From 81677ea19ea1177f36b26fafba88ed80130ce1a7 Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:24:36 -0400 Subject: [PATCH 2/6] ci --- dev-bins/Cargo.lock | 4 ++++ programs/sbf/Cargo.lock | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 0ec7f77221e7ec..b339d99be43c64 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7179,6 +7179,7 @@ dependencies = [ name = "solana-entry" version = "4.0.0-alpha.0" dependencies = [ + "agave-votor-messages", "bincode", "crossbeam-channel", "dlopen2", @@ -7187,6 +7188,8 @@ dependencies = [ "rayon", "serde", "solana-address 2.0.0", + "solana-bls-signatures", + "solana-clock", "solana-hash 3.1.0", "solana-measure", "solana-merkle-tree", @@ -7200,6 +7203,7 @@ dependencies = [ "solana-signature", "solana-transaction", "solana-transaction-error", + "thiserror 2.0.17", "wincode", ] diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 223b3bf89717a4..a0612d71aebb26 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6953,6 +6953,7 @@ dependencies = [ name = "solana-entry" version = "4.0.0-alpha.0" dependencies = [ + "agave-votor-messages", "bincode", "crossbeam-channel", "dlopen2", @@ -6961,6 +6962,8 @@ dependencies = [ "rayon", "serde", "solana-address 2.0.0", + "solana-bls-signatures", + "solana-clock", "solana-hash 3.1.0", "solana-measure", "solana-merkle-tree", @@ -6974,6 +6977,7 @@ dependencies = [ "solana-signature", "solana-transaction", "solana-transaction-error", + "thiserror 2.0.17", "wincode", ] From ded2c08d1e9f06b7403ee5c802ac4c1f8c9b021d Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:30:22 -0400 Subject: [PATCH 3/6] address comments --- entry/src/block_component.rs | 44 ++++++------------------------------ 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index f788241f1e28f6..660b5f916e180c 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -553,16 +553,9 @@ impl SchemaWrite for BlockComponent { fn size_of(src: &Self::Src) -> WriteResult { match src { - Self::EntryBatch(entries) => { - // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes - // the map_err. - let size = bincode::serialized_size(entries).map_err(|_| { - wincode::WriteError::Custom("Couldn't invoke bincode::serialized_size") - })?; - Ok(size as usize) - } + Self::EntryBatch(entries) => >::size_of(entries), Self::BlockMarker(marker) => { - let marker_size = wincode::serialized_size(marker)? as usize; + let marker_size = VersionedBlockMarker::size_of(marker)?; Ok(Self::ENTRY_COUNT_SIZE + marker_size) } } @@ -570,18 +563,10 @@ impl SchemaWrite for BlockComponent { fn write(writer: &mut impl Writer, src: &Self::Src) -> WriteResult<()> { match src { - Self::EntryBatch(entries) => { - // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes - // the map_err. - let bytes = bincode::serialize(entries).map_err(|_| { - wincode::WriteError::Custom("Couldn't invoke bincode::serialize") - })?; - writer.write(&bytes)?; - Ok(()) - } + Self::EntryBatch(entries) => >::write(writer, entries), Self::BlockMarker(marker) => { writer.write(&0u64.to_le_bytes())?; - ::write(writer, marker) + VersionedBlockMarker::write(writer, marker) } } } @@ -597,31 +582,16 @@ impl<'de> SchemaRead<'de> for BlockComponent { if entry_count == 0 { // This is a BlockMarker - consume the count bytes and read the marker - reader.consume(8)?; + // SAFETY: fill_array::<8>() above guarantees at least 8 bytes are available + unsafe { reader.consume_unchecked(8) }; dst.write(Self::BlockMarker(VersionedBlockMarker::get(reader)?)); } else { - // This is an EntryBatch - read in the rest of the data. We do not anticipate having - // cases where we need to deserialize multiple BlockComponents from a single slice, and - // do not know where the delimiters are ahead of time. - // First, get all remaining bytes to deserialize - let data = reader.fill_buf(usize::MAX)?; - - // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes the - // map_err. - let entries: Vec = bincode::deserialize(data) - .map_err(|_| wincode::ReadError::Custom("Couldn't deserialize entries."))?; + let entries: Vec = >::get(reader)?; if entries.len() >= Self::MAX_ENTRIES { return Err(wincode::ReadError::Custom("Too many entries")); } - // TODO(ksn): replace with wincode:: upon upstreaming to Agave. This also removes the - // map_err. - let consumed = bincode::serialized_size(&entries) - .map_err(|_| wincode::ReadError::Custom("Couldn't determine serialized size."))? - as usize; - reader.consume(consumed)?; - dst.write(Self::EntryBatch(entries)); } From 65518bd0bed7aef51d59599c7c2c63c1adec034a Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:19:00 -0400 Subject: [PATCH 4/6] address comments --- entry/src/block_component.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 660b5f916e180c..505c227e03ec7d 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -227,9 +227,9 @@ impl> SchemaWrite for LengthPrefixed { type Src = Self; const TYPE_META: TypeMeta = match T::TYPE_META { - TypeMeta::Static { size, zero_copy } => TypeMeta::Static { + TypeMeta::Static { size, .. } => TypeMeta::Static { size: size + std::mem::size_of::(), - zero_copy, + zero_copy: false, }, TypeMeta::Dynamic => TypeMeta::Dynamic, }; @@ -253,9 +253,9 @@ impl<'de, T: SchemaRead<'de, Dst = T>> SchemaRead<'de> for LengthPrefixed { type Dst = Self; const TYPE_META: TypeMeta = match T::TYPE_META { - TypeMeta::Static { size, zero_copy } => TypeMeta::Static { + TypeMeta::Static { size, .. } => TypeMeta::Static { size: size + std::mem::size_of::(), - zero_copy, + zero_copy: false, }, TypeMeta::Dynamic => TypeMeta::Dynamic, }; From 2c7b0cebb086895df3eeb5152e128dbf070373a5 Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:53:19 -0400 Subject: [PATCH 5/6] address comments --- entry/src/block_component.rs | 59 ++++++------------------------------ 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 505c227e03ec7d..1c7b739e3268f6 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -204,14 +204,20 @@ pub struct NotarRewardCertificate { /// Wraps a value with a u16 length prefix for TLV-style serialization. /// /// The length prefix represents the serialized byte size of the inner value. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LengthPrefixed { +#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +pub struct LengthPrefixed + for<'a> SchemaRead<'a, Dst = T>> { + len: u16, inner: T, } -impl LengthPrefixed { +impl + for<'a> SchemaRead<'a, Dst = T>> LengthPrefixed { pub fn new(inner: T) -> Self { - Self { inner } + let inner_size = T::size_of(&inner).unwrap(); + let len = inner_size + .try_into() + .map_err(|_| write_length_encoding_overflow("u16::MAX")) + .unwrap(); + Self { len, inner } } pub fn inner(&self) -> &T { @@ -223,51 +229,6 @@ impl LengthPrefixed { } } -impl> SchemaWrite for LengthPrefixed { - type Src = Self; - - const TYPE_META: TypeMeta = match T::TYPE_META { - TypeMeta::Static { size, .. } => TypeMeta::Static { - size: size + std::mem::size_of::(), - zero_copy: false, - }, - TypeMeta::Dynamic => TypeMeta::Dynamic, - }; - - fn size_of(src: &Self::Src) -> WriteResult { - let inner_size = T::size_of(&src.inner)?; - Ok(std::mem::size_of::() + inner_size) - } - - fn write(writer: &mut impl Writer, src: &Self::Src) -> WriteResult<()> { - let inner_size = T::size_of(&src.inner)?; - let Ok(len): Result = inner_size.try_into() else { - return Err(write_length_encoding_overflow("u16::MAX")); - }; - u16::write(writer, &len)?; - T::write(writer, &src.inner) - } -} - -impl<'de, T: SchemaRead<'de, Dst = T>> SchemaRead<'de> for LengthPrefixed { - type Dst = Self; - - const TYPE_META: TypeMeta = match T::TYPE_META { - TypeMeta::Static { size, .. } => TypeMeta::Static { - size: size + std::mem::size_of::(), - zero_copy: false, - }, - TypeMeta::Dynamic => TypeMeta::Dynamic, - }; - - fn read(reader: &mut impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { - let _len = u16::get(reader)?; - let inner_dst = unsafe { &mut *(&raw mut (*dst.as_mut_ptr()).inner).cast() }; - T::read(reader, inner_dst)?; - Ok(()) - } -} - #[derive(Debug, thiserror::Error)] pub enum BlockComponentError { #[error("Entry count {count} exceeds max {max}")] From d5ad9a6a2434b8daae32485eac8693bb1e0e5607 Mon Sep 17 00:00:00 2001 From: ksn6 <2163784+ksn6@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:56:49 -0400 Subject: [PATCH 6/6] ci --- entry/src/block_component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 1c7b739e3268f6..6f0da034d92c62 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -143,7 +143,7 @@ use { error::write_length_encoding_overflow, io::{Reader, Writer}, len::{BincodeLen, SeqLen}, - ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteResult, + ReadResult, SchemaRead, SchemaWrite, WriteResult, }, };