From b7db3d322b3cfeaba38fab8879e30f9e8affc85a Mon Sep 17 00:00:00 2001 From: Yves-Marie Saout Date: Wed, 7 Oct 2026 16:51:46 +0200 Subject: [PATCH 1/2] Accept version 1 transactions in Hivezilla getBlock requests. Alpenglow testnet rejects a slot when the client only advertises version 0. --- hivezilla/service/src/helius.rs | 11 +-------- hivezilla/service/src/rpc.rs | 40 ++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/hivezilla/service/src/helius.rs b/hivezilla/service/src/helius.rs index d9c09aef..3a382700 100644 --- a/hivezilla/service/src/helius.rs +++ b/hivezilla/service/src/helius.rs @@ -305,16 +305,7 @@ impl HeliusRpcClient { async fn block(&self, slot: u64, stats: &mut RuntimeStats) -> Result> { self.call( "getBlock", - json!([ - slot, - { - "encoding": "base64", - "transactionDetails": "full", - "rewards": true, - "commitment": "finalized", - "maxSupportedTransactionVersion": 0 - } - ]), + json!([slot, crate::rpc::get_block_config("finalized")]), stats, ) .await diff --git a/hivezilla/service/src/rpc.rs b/hivezilla/service/src/rpc.rs index 2cd6daa9..06f1fabb 100644 --- a/hivezilla/service/src/rpc.rs +++ b/hivezilla/service/src/rpc.rs @@ -23,6 +23,22 @@ use crate::{ const RPC_X_TOKEN_HEADER: &str = "x-token"; +/// Highest transaction message version these `getBlock` clients can decode. +/// +/// Version 1 is the SIMD-0385 message. Advertising 0 makes the node reject any +/// slot that contains one, which is what Alpenglow testnet returns today. +pub const MAX_SUPPORTED_TRANSACTION_VERSION: u8 = 1; + +pub(crate) fn get_block_config(commitment: &str) -> Value { + json!({ + "encoding": "base64", + "transactionDetails": "full", + "rewards": true, + "commitment": commitment, + "maxSupportedTransactionVersion": MAX_SUPPORTED_TRANSACTION_VERSION + }) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RpcBackfillConfig { pub rpc_url: String, @@ -498,16 +514,7 @@ async fn fetch_get_block( "jsonrpc": "2.0", "id": slot, "method": "getBlock", - "params": [ - slot, - { - "encoding": "base64", - "transactionDetails": "full", - "rewards": true, - "commitment": commitment, - "maxSupportedTransactionVersion": 0 - } - ] + "params": [slot, get_block_config(commitment)] }); rpc.json_rpc(rpc_url, &request, &format!("getBlock slot {slot}")) @@ -579,3 +586,16 @@ fn write_journal(writer: &mut impl Write, record: RpcBackfillJournalRecord) -> R writer.write_all(b"\n")?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::get_block_config; + + #[test] + fn get_block_config_accepts_version_1_transactions() { + let config = get_block_config("finalized"); + assert_eq!(config["maxSupportedTransactionVersion"], 1); + assert_eq!(config["encoding"], "base64"); + assert_eq!(config["commitment"], "finalized"); + } +} From f495e368145c40bde6cd1bc31afd388729fdce82 Mon Sep 17 00:00:00 2001 From: Yves-Marie Saout Date: Wed, 7 Oct 2026 18:44:12 +0200 Subject: [PATCH 2/2] Decode SIMD-0385 transactions with signatures at the end. Alpenglow testnet places the version byte first and the signatures last, so the old signature-prefix reader rejected every version 1 transaction. --- .../of-car-reader/src/short_vec.rs | 37 ++++-- .../src/versioned_transaction.rs | 111 ++++++++++++++---- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/crates/old-faithful/of-car-reader/src/short_vec.rs b/crates/old-faithful/of-car-reader/src/short_vec.rs index ccf05368..ba906f5b 100644 --- a/crates/old-faithful/of-car-reader/src/short_vec.rs +++ b/crates/old-faithful/of-car-reader/src/short_vec.rs @@ -67,13 +67,36 @@ fn decode_short_u16( fn read_short_u16<'de>(mut reader: impl Reader<'de>) -> ReadResult { decode_short_u16(|| reader.take_byte()) .map(|(value, _)| value) - .map_err(|error| match error { - DecodeError::Input(error) => error.into(), - DecodeError::NonCanonical => { - ReadError::InvalidValue("short u16: non-canonical encoding") - } - DecodeError::Overflow => ReadError::LengthEncodingOverflow("u16::MAX"), - }) + .map_err(decode_error_to_read) +} + +/// Continue a short-u16 whose first byte was already consumed. +/// +/// Transaction decoding peeks that byte to distinguish a version-1 payload +/// (`0x81`) from a legacy or v0 signature count. +pub(crate) fn read_short_u16_with_first<'de>( + first: u8, + mut reader: impl Reader<'de>, +) -> ReadResult { + let mut yielded_first = false; + decode_short_u16(|| { + if !yielded_first { + yielded_first = true; + Ok(first) + } else { + reader.take_byte() + } + }) + .map(|(value, _)| value) + .map_err(decode_error_to_read) +} + +fn decode_error_to_read>(error: DecodeError) -> ReadError { + match error { + DecodeError::Input(error) => error.into(), + DecodeError::NonCanonical => ReadError::InvalidValue("short u16: non-canonical encoding"), + DecodeError::Overflow => ReadError::LengthEncodingOverflow("u16::MAX"), + } } #[inline] diff --git a/crates/old-faithful/of-car-reader/src/versioned_transaction.rs b/crates/old-faithful/of-car-reader/src/versioned_transaction.rs index 7ce6888d..1281e999 100644 --- a/crates/old-faithful/of-car-reader/src/versioned_transaction.rs +++ b/crates/old-faithful/of-car-reader/src/versioned_transaction.rs @@ -208,13 +208,62 @@ fn read_v1_message<'de, C: Config>(mut reader: impl Reader<'de>) -> ReadResult { - #[wincode(with = "containers::Vec<&'a [u8; 64], ShortU16>")] pub signatures: Vec<&'a [u8; 64]>, pub message: VersionedMessage<'a>, } +/// SIMD-0385 puts the version byte first and the signatures last. +const V1_TRANSACTION_PREFIX: u8 = MESSAGE_VERSION_PREFIX | 1; + +unsafe impl<'de, C: Config> SchemaRead<'de, C> for VersionedTransaction<'de> { + type Dst = Self; + + fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let first = >::get(reader.by_ref())?; + if first == V1_TRANSACTION_PREFIX { + let message = read_v1_message::(reader.by_ref())?; + let signatures = read_borrowed_signatures( + reader.by_ref(), + usize::from(message.header.num_required_signatures), + )?; + if reader.take_byte().is_ok() { + return Err(trailing_bytes()); + } + dst.write(Self { + signatures, + message: VersionedMessage::V1(message), + }); + return Ok(()); + } + + let signature_count = crate::short_vec::read_short_u16_with_first(first, reader.by_ref())?; + let signatures = read_borrowed_signatures(reader.by_ref(), signature_count)?; + let message = as SchemaRead<'de, C>>::get(reader)?; + dst.write(Self { + signatures, + message, + }); + Ok(()) + } +} + +fn read_borrowed_signatures<'de>( + mut reader: impl Reader<'de>, + count: usize, +) -> ReadResult> { + let mut signatures = Vec::with_capacity(count); + for _ in 0..count { + let bytes = reader.take_borrowed(64)?; + let signature: &[u8; 64] = bytes + .try_into() + .map_err(|_| invalid_value("borrowed signature is not 64 bytes"))?; + signatures.push(signature); + } + Ok(signatures) +} + /// Reusable allocations for transaction and message decoding. /// /// The decoded value continues to borrow signatures, account keys, and the @@ -766,21 +815,24 @@ pub fn deserialize_versioned_transaction_reusing<'de>( ) -> ReadResult> { let result = (|| { let mut reader = src; - let signature_count = read_short_u16_len(&mut reader)?; - let mut signatures = reuse.take_signatures(); - signatures.clear(); - ensure_fixed_items_fit( - reader, - signature_count, - 64, - "truncated transaction signatures", - )?; - reuse.record_outer_use(signatures.capacity(), signature_count); - signatures.reserve(signature_count); - for _ in 0..signature_count { - signatures.push(take_array_ref::<64>(&mut reader)?); + if reader.first() == Some(&V1_TRANSACTION_PREFIX) { + let message = read_versioned_message_reusing(&mut reader, reuse)?; + let signature_count = match &message { + VersionedMessage::V1(body) => usize::from(body.header.num_required_signatures), + _ => return Err(invalid_tag_encoding(1)), + }; + let signatures = read_signature_array(&mut reader, signature_count, reuse)?; + if !reader.is_empty() { + return Err(trailing_bytes()); + } + return Ok(VersionedTransaction { + signatures, + message, + }); } + let signature_count = read_short_u16_len(&mut reader)?; + let signatures = read_signature_array(&mut reader, signature_count, reuse)?; let message = read_versioned_message_reusing(&mut reader, reuse)?; if !reader.is_empty() { return Err(trailing_bytes()); @@ -1091,6 +1143,27 @@ fn copy_bytes_reusing( Ok(value) } +fn read_signature_array<'de>( + reader: &mut &'de [u8], + signature_count: usize, + reuse: &mut VersionedTransactionReuse, +) -> ReadResult> { + let mut signatures = reuse.take_signatures(); + signatures.clear(); + ensure_fixed_items_fit( + reader, + signature_count, + 64, + "truncated transaction signatures", + )?; + reuse.record_outer_use(signatures.capacity(), signature_count); + signatures.reserve(signature_count); + for _ in 0..signature_count { + signatures.push(take_array_ref::<64>(reader)?); + } + Ok(signatures) +} + fn read_short_u16_len(reader: &mut &[u8]) -> ReadResult { let (len, prefix_len) = crate::short_vec::decode_shortu16_len(reader) .map_err(|()| invalid_value("invalid ShortU16 sequence length"))?; @@ -1177,10 +1250,6 @@ mod tests { fn encoded_v1_transaction() -> Vec { let mut bytes = Vec::new(); - // One signature, encoded with Solana's ShortU16 length prefix. - bytes.push(1); - bytes.extend_from_slice(&[7; 64]); - bytes.push(MESSAGE_VERSION_PREFIX | 1); bytes.extend_from_slice(&[1, 0, 1]); bytes.extend_from_slice(&V1_CONFIG_KNOWN_BITS.to_le_bytes()); @@ -1201,6 +1270,7 @@ mod tests { bytes.extend_from_slice(&3u16.to_le_bytes()); bytes.push(0); bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc]); + bytes.extend_from_slice(&[7; 64]); bytes } @@ -1424,7 +1494,7 @@ mod tests { } fn v1_transaction_bytes() -> Vec { - let mut bytes = transaction_prefix(0x13); + let mut bytes = Vec::new(); bytes.push(MESSAGE_VERSION_PREFIX | 1); bytes.extend_from_slice(&[1, 0, 1]); bytes.extend_from_slice(&V1_CONFIG_KNOWN_BITS.to_le_bytes()); @@ -1444,6 +1514,7 @@ mod tests { bytes.extend_from_slice(&[2, 4]); bytes.extend_from_slice(&[6, 8, 10]); bytes.extend_from_slice(&[12]); + bytes.extend_from_slice(&[0x13; 64]); bytes }