Skip to main content

thru_base/
block_parser.rs

1use crate::tn_public_address::tn_pubkey_to_address_string;
2use crate::tn_signature_encoding::tn_signature_to_string;
3use crate::txn_lib::{self, Transaction, WireTxnHdrV1};
4use blake3;
5use std::{collections::HashSet, mem};
6use tracing::{debug, error, warn};
7// use base64::prelude::*;
8
9use ed25519_dalek::{Signature, Verifier, VerifyingKey};
10
11/// Block format structures (mirror the on-wire block layout defined in C)
12pub type FdPubkey = [u8; 32];
13pub type FdSignature = [u8; 64];
14pub type FdBlake3Hash = [u8; 32];
15
16/// Result structure for block parsing with cryptographic verification
17#[derive(Debug, Clone)]
18pub struct BlockParseResult {
19    pub block_hash: [u8; 32],       // 256-bit Blake3 hash (first 32 bytes)
20    pub block_producer: [u8; 32],   // Block producer's public key
21    pub transactions: Vec<Vec<u8>>, // Existing transaction data
22}
23
24/// Comprehensive error handling for block parsing and cryptographic verification
25#[derive(Debug, thiserror::Error)]
26pub enum BlockParseError {
27    #[error("Invalid block structure: {0}")]
28    InvalidBlockStructure(String),
29    #[error("Blake3 hash computation failed: {0}")]
30    HashComputationFailed(String),
31    #[error("Header signature verification failed: {0}")]
32    HeaderSignatureInvalid(String),
33    #[error("Block signature verification failed: {0}")]
34    BlockSignatureInvalid(String),
35    #[error("Ed25519 key error: {0}")]
36    Ed25519KeyError(String),
37    #[error("Account extraction failed: {0}")]
38    AccountExtractionFailed(String),
39}
40
41#[repr(C)]
42#[derive(Clone, Copy, Debug)]
43pub struct TnBlockHeader {
44    pub block_header_sig: FdSignature,
45    pub body: TnBlockHeaderBody,
46}
47
48#[repr(C)]
49#[derive(Clone, Copy, Debug)]
50pub struct TnBlockHeaderBody {
51    pub block_version: u8,
52    pub padding: [u8; 5],
53    pub chain_id: u16,
54    pub block_producer: FdPubkey,
55    pub bond_amount_lock_up: u64,
56    pub expiry_timestamp: u64,
57    pub start_slot: u64,
58    pub expiry_after: u32,
59    pub max_block_size: u32,
60    pub max_compute_units: u64,
61    pub max_state_units: u32,
62    pub reserved: [u8; 4],
63    pub weight_slot: u64,
64    pub block_time_ns: u64,
65}
66
67#[repr(C)]
68#[derive(Clone, Copy, Debug)]
69pub struct TnBlockFooter {
70    pub body: TnBlockFooterBody,
71    pub block_hash: FdBlake3Hash,
72    pub block_sig: FdSignature,
73}
74
75#[repr(C)]
76#[derive(Clone, Copy, Debug)]
77pub struct TnBlockFooterBody {
78    pub attestor_payment: u64,
79}
80
81/// Block parser for extracting transactions from UDS block data
82pub struct BlockParser;
83
84impl BlockParser {
85    /// Parse block data with cryptographic verification and extract transactions
86    pub fn parse_block(data: &[u8]) -> Result<BlockParseResult, BlockParseError> {
87        if data.is_empty() {
88            return Ok(BlockParseResult {
89                block_hash: [0u8; 32],
90                block_producer: [0u8; 32],
91                transactions: Vec::new(),
92            });
93        }
94
95        debug!(
96            "Parsing block data of {} bytes with cryptographic verification",
97            data.len()
98        );
99
100        // Block format: TnBlockHeader + Transactions + TnBlockFooter
101        let header_size = mem::size_of::<TnBlockHeader>();
102        let footer_size = mem::size_of::<TnBlockFooter>();
103
104        if data.len() < header_size + footer_size {
105            return Err(BlockParseError::InvalidBlockStructure(format!(
106                "Block too small: {} bytes, need at least {}",
107                data.len(),
108                header_size + footer_size
109            )));
110        }
111
112        // Parse TnBlockHeader from the beginning
113        let header = Self::parse_header_verified(&data[..header_size])?;
114        debug!(
115            "Parsed block header: version={}, start_slot={}, producer={}",
116            header.body.block_version,
117            header.body.start_slot,
118            tn_pubkey_to_address_string(&header.body.block_producer)
119        );
120
121        // Verify header signature first (fail fast optimization)
122        Self::verify_header_signature(&header)?;
123        debug!("Block header signature verified successfully");
124
125        // Compute block hash (excluding block signature)
126        let block_hash = Self::compute_block_hash(data)?;
127        debug!("Block hash computed successfully");
128
129        // Parse TnBlockFooter from the end
130        let footer_start = data.len() - footer_size;
131        let footer = Self::parse_footer_verified(&data[footer_start..])?;
132        debug!(
133            "Parsed block footer: attestor_payment={}",
134            footer.body.attestor_payment
135        );
136
137        // Verify block signature against computed hash
138        Self::verify_block_signature(&block_hash, &footer, &header.body.block_producer)?;
139        debug!("Block signature verified successfully");
140
141        // Extract transaction data between header and footer
142        let transactions_data = &data[header_size..footer_start];
143        debug!(
144            "Transaction data section: {} bytes",
145            transactions_data.len()
146        );
147
148        // Parse individual transactions from the middle section
149        let transactions = if transactions_data.is_empty() {
150            debug!("No transaction data in block");
151            Vec::new()
152        } else {
153            Self::parse_transactions(transactions_data)
154                .map_err(|e| BlockParseError::InvalidBlockStructure(e))?
155        };
156
157        debug!("Extracted {} transactions from block", transactions.len());
158
159        Ok(BlockParseResult {
160            block_hash,
161            block_producer: header.body.block_producer,
162            transactions,
163        })
164    }
165
166    /// Compute 256-bit Blake3 hash of block data excluding block signature and block hash
167    /// Matches C implementation: fd_blake3_append(&hasher, block_data, block_size - sizeof(fd_signature_t) - BLOCK_HASH_SIZE)
168    fn compute_block_hash(data: &[u8]) -> Result<[u8; 32], BlockParseError> {
169        let footer_size = mem::size_of::<TnBlockFooter>();
170
171        if data.len() < footer_size {
172            return Err(BlockParseError::HashComputationFailed(
173                "Block too small to contain footer".to_string(),
174            ));
175        }
176
177        // Hash all data except the final 64 bytes (block_sig) and 32 bytes (block_hash)
178        // This matches the C implementation which excludes sizeof(fd_signature_t) + BLOCK_HASH_SIZE
179        let sig_size = mem::size_of::<FdSignature>();
180        let hash_size = mem::size_of::<FdBlake3Hash>();
181        let hash_data_end = data.len() - sig_size - hash_size;
182        let hash_data = &data[..hash_data_end];
183
184        debug!(
185            "Computing Blake3 hash over {} bytes (excluding {} byte signature and {} byte hash)",
186            hash_data.len(),
187            sig_size,
188            hash_size
189        );
190
191        // Use Blake3 256-bit output and truncate to 32 bytes
192        let mut hasher = blake3::Hasher::new();
193        hasher.update(hash_data);
194
195        let hash_output = *hasher.finalize().as_bytes();
196
197        debug!("Blake3 hash computation completed successfully");
198        Ok(hash_output)
199    }
200
201    /// Verify block header signature using ed25519
202    /// Matches C implementation: signs/verifies only the header body, not the signature field
203    fn verify_header_signature(header: &TnBlockHeader) -> Result<(), BlockParseError> {
204        debug!("Starting header signature verification");
205
206        // Check if signature is all zeros (unsigned/test data)
207        if header.block_header_sig.iter().all(|&b| b == 0) {
208            debug!("Header signature is all zeros - treating as unsigned block");
209            return Err(BlockParseError::HeaderSignatureInvalid(
210                "Block header is not signed (all-zero signature)".to_string(),
211            ));
212        }
213
214        // Sign/verify only the header body, excluding the signature field
215        // This matches C implementation: fd_ed25519_verify((uchar const *)&header->body, ...)
216        let body_size = mem::size_of::<TnBlockHeaderBody>();
217
218        // Convert header body to bytes for verification
219        let body_bytes =
220            unsafe { std::slice::from_raw_parts(&header.body as *const _ as *const u8, body_size) };
221
222        debug!(
223            "Verifying header signature over {} bytes of header body data",
224            body_bytes.len()
225        );
226
227        // Convert signature and public key to ed25519-dalek types
228        let signature = match Signature::from_slice(&header.block_header_sig) {
229            Ok(sig) => sig,
230            Err(e) => {
231                error!("Invalid header signature format: {}", e);
232                return Err(BlockParseError::HeaderSignatureInvalid(format!(
233                    "Signature format error: {}",
234                    e
235                )));
236            }
237        };
238
239        let verifying_key = VerifyingKey::from_bytes(&header.body.block_producer).map_err(|e| {
240            error!("Invalid producer public key format: {}", e);
241            BlockParseError::Ed25519KeyError(format!("Invalid producer public key: {}", e))
242        })?;
243
244        // Verify the signature against the header body
245        verifying_key.verify(body_bytes, &signature).map_err(|e| {
246            error!("Header signature verification failed: {}", e);
247            BlockParseError::HeaderSignatureInvalid(format!("Verification failed: {}", e))
248        })?;
249
250        debug!("Header signature verification successful");
251        Ok(())
252    }
253
254    /// Verify block signature against computed hash using producer's public key
255    fn verify_block_signature(
256        block_hash: &[u8; 32],
257        footer: &TnBlockFooter,
258        producer_key: &[u8; 32],
259    ) -> Result<(), BlockParseError> {
260        debug!("Starting block signature verification");
261
262        // Check if signature is all zeros (unsigned/test data)
263        if footer.block_sig.iter().all(|&b| b == 0) {
264            debug!("Block signature is all zeros - treating as unsigned block");
265            return Err(BlockParseError::BlockSignatureInvalid(
266                "Block is not signed (all-zero signature)".to_string(),
267            ));
268        }
269
270        debug!("Verifying block signature against computed hash");
271
272        // Convert signature and public key to ed25519-dalek types
273        let signature = match Signature::from_slice(&footer.block_sig) {
274            Ok(sig) => sig,
275            Err(e) => {
276                error!("Invalid block signature format: {}", e);
277                return Err(BlockParseError::BlockSignatureInvalid(format!(
278                    "Signature format error: {}",
279                    e
280                )));
281            }
282        };
283
284        let verifying_key = VerifyingKey::from_bytes(producer_key).map_err(|e| {
285            error!("Invalid producer public key for block verification: {}", e);
286            BlockParseError::Ed25519KeyError(format!("Invalid producer public key: {}", e))
287        })?;
288
289        // Verify the signature against the block hash
290        verifying_key.verify(block_hash, &signature).map_err(|e| {
291            error!("Block signature verification failed: {}", e);
292            BlockParseError::BlockSignatureInvalid(format!("Verification failed: {}", e))
293        })?;
294
295        debug!("Block signature verification successful");
296        Ok(())
297    }
298
299    /// Parse block header from data with error conversion
300    fn parse_header_verified(data: &[u8]) -> Result<TnBlockHeader, BlockParseError> {
301        Self::parse_header(data).map_err(|e| BlockParseError::InvalidBlockStructure(e))
302    }
303
304    /// Parse block footer from data with error conversion
305    fn parse_footer_verified(data: &[u8]) -> Result<TnBlockFooter, BlockParseError> {
306        Self::parse_footer(data).map_err(|e| BlockParseError::InvalidBlockStructure(e))
307    }
308
309    /// Parse block header from data
310    fn parse_header(data: &[u8]) -> Result<TnBlockHeader, String> {
311        if data.len() < mem::size_of::<TnBlockHeader>() {
312            return Err("Insufficient data for block header".to_string());
313        }
314
315        // We'll do a simple byte copy since we're dealing with repr(C) structs
316        let header = unsafe { std::ptr::read(data.as_ptr() as *const TnBlockHeader) };
317
318        debug!(
319            "Block header: version={}, producer={:?}",
320            header.body.block_version,
321            tn_pubkey_to_address_string(&header.body.block_producer)
322        );
323        Ok(header)
324    }
325
326    /// Parse block footer from data
327    fn parse_footer(data: &[u8]) -> Result<TnBlockFooter, String> {
328        if data.len() < mem::size_of::<TnBlockFooter>() {
329            return Err("Insufficient data for block footer".to_string());
330        }
331
332        let footer = unsafe { std::ptr::read(data.as_ptr() as *const TnBlockFooter) };
333
334        debug!(
335            "Block footer: attestor_payment={}",
336            footer.body.attestor_payment
337        );
338        Ok(footer)
339    }
340
341    /// Parse transactions from the middle section of block data
342    fn parse_transactions(data: &[u8]) -> Result<Vec<Vec<u8>>, String> {
343        let mut transactions = Vec::new();
344        let mut offset = 0;
345
346        // Parse individual transactions using Transaction::from_wire
347        while offset < data.len() {
348            // Check if we have enough data for the minimum transaction header
349            let wire_header_size = mem::size_of::<WireTxnHdrV1>();
350            if offset + wire_header_size > data.len() {
351                debug!(
352                    "Remaining data too small for transaction header: {} bytes",
353                    data.len() - offset
354                );
355                break;
356            }
357
358            let remaining_data = &data[offset..];
359
360            // Try to parse the transaction using Transaction::from_wire
361            // We need to find the actual transaction size by attempting to parse it
362            match Self::try_parse_transaction_at_offset(remaining_data) {
363                Ok((transaction_size, transaction_data)) => {
364                    transactions.push(transaction_data);
365                    debug!(
366                        "Parsed transaction {} of size {} bytes",
367                        transactions.len(),
368                        transaction_size
369                    );
370                    offset += transaction_size;
371                }
372                Err(parse_error) => {
373                    warn!(
374                        "Failed to parse transaction at offset {}: {}",
375                        offset, parse_error
376                    );
377                    return Err(parse_error);
378                }
379            }
380        }
381
382        Ok(transactions)
383    }
384
385    /// Try to parse a transaction at the given offset, returning the transaction size and data
386    fn try_parse_transaction_at_offset(data: &[u8]) -> Result<(usize, Vec<u8>), String> {
387        // We need to determine the transaction size by parsing the header and variable-length data
388        let wire_header_size = mem::size_of::<WireTxnHdrV1>();
389
390        if data.len() < wire_header_size {
391            return Err("Not enough data for transaction header".to_string());
392        }
393
394        // Calculate total transaction size
395        let total_size = txn_lib::tn_txn_size(data).map_err(|e| e.to_string())?;
396
397        if data.len() < total_size {
398            return Err(format!(
399                "Not enough data for complete transaction: need {} bytes, have {}",
400                total_size,
401                data.len()
402            ));
403        }
404        // Extract the transaction data
405        let transaction_data = data[..total_size].to_vec();
406
407        // Verify the transaction can be parsed with Transaction::from_wire
408        if Transaction::from_wire(&transaction_data).is_none() {
409            return Err("Transaction::from_wire failed to parse transaction".to_string());
410        }
411        Ok((total_size, transaction_data))
412    }
413
414    /// Extract transaction signature from transaction data and convert to ts... format
415    /// The signature is at the END of the transaction (last 64 bytes).
416    pub fn extract_transaction_signature(tx_data: &[u8]) -> Result<String, String> {
417        if tx_data.len() < txn_lib::TN_TXN_SIGNATURE_SZ {
418            return Err("Transaction too short to contain a signature".to_string());
419        }
420
421        // The signature is at the end   of the transaction (last 64 bytes)
422        let sig_start = tx_data.len() - txn_lib::TN_TXN_SIGNATURE_SZ;
423        let signature_bytes = &tx_data[sig_start..];
424
425        // Convert to fixed-size array for signature utilities
426        let mut sig_array = [0u8; 64];
427        sig_array.copy_from_slice(signature_bytes);
428
429        // Convert to ts... format using existing utilities
430        let signature = tn_signature_to_string(&sig_array);
431
432        debug!("Extracted signature: {}", signature);
433        Ok(signature)
434    }
435
436    /// Extract all account mentions from block transactions
437    /// Returns a HashSet of base64-encoded account addresses
438    pub fn extract_account_mentions(
439        transactions: &[Vec<u8>],
440    ) -> Result<HashSet<String>, BlockParseError> {
441        let mut accounts = HashSet::new();
442
443        debug!(
444            "Extracting account mentions from {} transactions",
445            transactions.len()
446        );
447
448        for (i, tx_data) in transactions.iter().enumerate() {
449            match Self::extract_transaction_accounts(tx_data) {
450                Ok(tx_accounts) => {
451                    debug!(
452                        "Transaction {} contains {} account references: {:?}",
453                        i,
454                        tx_accounts.len(),
455                        tx_accounts
456                    );
457                    accounts.extend(tx_accounts);
458                }
459                Err(e) => {
460                    warn!("Failed to extract accounts from transaction {}: {}", i, e);
461                    // Continue processing other transactions
462                }
463            }
464        }
465
466        debug!(
467            "Extracted {} unique account addresses from block: {:?}",
468            accounts.len(),
469            accounts
470        );
471        Ok(accounts)
472    }
473
474    /// Extract account addresses from a single transaction
475    /// Returns ta... formatted account addresses found in the transaction
476    fn extract_transaction_accounts(tx_data: &[u8]) -> Result<Vec<String>, BlockParseError> {
477        // Minimum size: 112-byte header + 64-byte trailing signature = 176 bytes
478        let header_size = 112;
479        let signature_size = txn_lib::TN_TXN_SIGNATURE_SZ;
480        let min_txn_size = header_size + signature_size;
481        if tx_data.len() < min_txn_size {
482            return Err(BlockParseError::AccountExtractionFailed(format!(
483                "Transaction too small: {} bytes, need at least {} (header={}, signature={})",
484                tx_data.len(),
485                min_txn_size,
486                header_size,
487                signature_size
488            )));
489        }
490
491        debug!(
492            "Extracting accounts from transaction of {} bytes",
493            tx_data.len()
494        );
495        let mut accounts = Vec::new();
496
497        // Extract fee_payer_pubkey (offset 48, 32 bytes)
498        let fee_payer_offset = 48;
499        if tx_data.len() >= fee_payer_offset + 32 {
500            let fee_payer_pubkey: [u8; 32] = tx_data[fee_payer_offset..fee_payer_offset + 32]
501                .try_into()
502                .map_err(|_| {
503                    BlockParseError::AccountExtractionFailed(
504                        "Failed to convert fee_payer_pubkey to [u8; 32]".to_string(),
505                    )
506                })?;
507            let fee_payer_address = tn_pubkey_to_address_string(&fee_payer_pubkey);
508            debug!(
509                "Extracted fee_payer at offset {}: {:?} -> {}",
510                fee_payer_offset,
511                &fee_payer_pubkey[..8],
512                fee_payer_address
513            );
514            accounts.push(fee_payer_address);
515        }
516
517        // Extract program_pubkey (offset 80, 32 bytes)
518        let program_offset = 80;
519        if tx_data.len() >= program_offset + 32 {
520            let program_pubkey: [u8; 32] = tx_data[program_offset..program_offset + 32]
521                .try_into()
522                .map_err(|_| {
523                    BlockParseError::AccountExtractionFailed(
524                        "Failed to convert program_pubkey to [u8; 32]".to_string(),
525                    )
526                })?;
527            let program_address = tn_pubkey_to_address_string(&program_pubkey);
528            debug!(
529                "Extracted program at offset {}: {:?} -> {}",
530                program_offset,
531                &program_pubkey[..8],
532                program_address
533            );
534            accounts.push(program_address);
535        }
536
537        // Extract additional account addresses from variable section
538        // This comes after the fixed header (112 bytes total for WireTxnHdrV1)
539        let header_size = 112;
540        if tx_data.len() > header_size {
541            // Get account counts from header
542            let readwrite_accounts_cnt = u16::from_le_bytes([tx_data[2], tx_data[3]]);
543            let readonly_accounts_cnt = u16::from_le_bytes([tx_data[4], tx_data[5]]);
544
545            debug!(
546                "Transaction has {} readwrite and {} readonly accounts",
547                readwrite_accounts_cnt, readonly_accounts_cnt
548            );
549
550            // Extract additional account addresses (32 bytes each)
551            let additional_accounts_count =
552                (readwrite_accounts_cnt + readonly_accounts_cnt) as usize;
553            let additional_accounts_size = additional_accounts_count * 32;
554
555            if tx_data.len() >= header_size + additional_accounts_size {
556                for i in 0..additional_accounts_count {
557                    let account_offset = header_size + (i * 32);
558                    let account_pubkey: [u8; 32] = tx_data[account_offset..account_offset + 32]
559                        .try_into()
560                        .map_err(|_| {
561                            BlockParseError::AccountExtractionFailed(format!(
562                                "Failed to convert account_pubkey {} to [u8; 32]",
563                                i
564                            ))
565                        })?;
566                    let account_address = tn_pubkey_to_address_string(&account_pubkey);
567                    accounts.push(account_address);
568                }
569            }
570        }
571
572        Ok(accounts)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_block_header_body_chain_id_layout() {
582        let body = TnBlockHeaderBody {
583            block_version: 1,
584            padding: [0; 5],
585            chain_id: 0x1234,
586            block_producer: [0; 32],
587            bond_amount_lock_up: 0,
588            expiry_timestamp: 0,
589            start_slot: 0,
590            expiry_after: 0,
591            max_block_size: 0,
592            max_compute_units: 0,
593            max_state_units: 0,
594            reserved: [0; 4],
595            weight_slot: 0,
596            block_time_ns: 0,
597        };
598
599        let base = &body as *const _ as usize;
600        let chain_id = &body.chain_id as *const _ as usize;
601        assert_eq!(chain_id - base, 6);
602        assert_eq!(body.chain_id, 0x1234);
603        assert_eq!(std::mem::size_of::<TnBlockHeaderBody>(), 104);
604    }
605
606    #[test]
607    fn test_extract_transaction_signature() {
608        // Create a mock transaction with 64 bytes of signature data at the END
609        let mut transaction_data = vec![0u8; 100];
610        // Set some recognizable pattern in the signature bytes (last 64 bytes)
611        let sig_start = transaction_data.len() - 64;
612        for i in 0..64 {
613            transaction_data[sig_start + i] = (i % 256) as u8;
614        }
615
616        let result = BlockParser::extract_transaction_signature(&transaction_data);
617        assert!(result.is_ok());
618
619        let signature = result.unwrap();
620        assert!(!signature.is_empty());
621        // Verify it's in ts... format (90 characters starting with "ts")
622        assert_eq!(
623            signature.len(),
624            90,
625            "Signature should be 90 characters in ts... format"
626        );
627        assert!(
628            signature.starts_with("ts"),
629            "Signature should start with 'ts'"
630        );
631    }
632
633    #[test]
634    fn test_extract_transaction_signature_too_short() {
635        let transaction_data = vec![0u8; 32]; // Too short
636        let result = BlockParser::extract_transaction_signature(&transaction_data);
637        assert!(result.is_err());
638        assert_eq!(
639            result.unwrap_err(),
640            "Transaction too short to contain a signature"
641        );
642    }
643
644    #[test]
645    fn test_parse_empty_block() {
646        let empty_data = vec![];
647        let result = BlockParser::parse_block(&empty_data);
648        assert!(result.is_ok());
649        let block_result = result.unwrap();
650        assert_eq!(block_result.transactions.len(), 0);
651        assert_eq!(block_result.block_hash, [0u8; 32]);
652        assert_eq!(block_result.block_producer, [0u8; 32]);
653    }
654
655    #[test]
656    fn test_parse_block_too_small() {
657        let small_data = vec![0u8; 50]; // Too small for header + footer
658        let result = BlockParser::parse_block(&small_data);
659        assert!(result.is_err());
660        let error_msg = format!("{}", result.unwrap_err());
661        assert!(error_msg.contains("Block too small"));
662    }
663
664    #[test]
665    fn test_parse_block_header_footer_only() {
666        // Test with a block that has valid structure but invalid signatures
667        let header_size = std::mem::size_of::<TnBlockHeader>();
668        let footer_size = std::mem::size_of::<TnBlockFooter>();
669        let mut block_data = vec![0u8; header_size + footer_size];
670
671        // Set block version in header
672        block_data[0] = 1; // block_version
673
674        let result = BlockParser::parse_block(&block_data);
675
676        // The result depends on whether all-zero signatures are considered valid
677        // Let's test both cases and verify we get a reasonable result
678        match result {
679            Ok(block_result) => {
680                // If it succeeds, verify the structure is correct
681                assert_eq!(block_result.transactions.len(), 0);
682                assert_eq!(block_result.block_producer, [0u8; 32]);
683                assert_eq!(block_result.block_hash.len(), 32);
684            }
685            Err(error) => {
686                // If it fails, it should be due to cryptographic verification
687                let error_msg = format!("{}", error);
688                assert!(
689                    error_msg.contains("signature")
690                        || error_msg.contains("key")
691                        || error_msg.contains("Invalid")
692                );
693            }
694        }
695    }
696
697    #[test]
698    fn test_try_parse_transaction_at_offset() {
699        // Create a minimal valid transaction structure for testing
700        // wire format: 112-byte header (no signature prefix) + 64-byte trailing signature
701        // Transaction layout:
702        //   [0,1)   transaction_version
703        //   [1,2)   flags
704        //   [2,4)   readwrite_accounts_cnt
705        //   [4,6)   readonly_accounts_cnt
706        //   [6,8)   instr_data_sz
707        //   ... other fields ...
708        //   [last 64 bytes] signature at END
709        let header_size = 112;
710        let signature_size = 64;
711        let min_txn_size = header_size + signature_size;
712        let mut tx_data = vec![0u8; min_txn_size];
713
714        // Set transaction version to 1 at offset 0 (no signature prefix in new format)
715        tx_data[0] = 1; // transaction_version
716
717        // Set all account counts and instruction size to 0 (minimal transaction)
718        // readwrite_accounts_cnt at offset 2-3
719        tx_data[2] = 0;
720        tx_data[3] = 0;
721        // readonly_accounts_cnt at offset 4-5
722        tx_data[4] = 0;
723        tx_data[5] = 0;
724        // instr_data_sz at offset 6-7
725        tx_data[6] = 0;
726        tx_data[7] = 0;
727
728        // Test the helper function directly
729        let result = BlockParser::try_parse_transaction_at_offset(&tx_data);
730        // This might fail due to Transaction::from_wire validation, which is expected
731        // The important thing is that the function doesn't panic and handles the data correctly
732        match result {
733            Ok((size, data)) => {
734                assert_eq!(size, min_txn_size);
735                assert_eq!(data.len(), min_txn_size);
736            }
737            Err(_) => {
738                // This is expected for invalid transaction data
739                // The test verifies the function handles invalid data gracefully
740            }
741        }
742    }
743
744    #[test]
745    fn test_parse_transactions_empty_data() {
746        let empty_data = vec![];
747        let result = BlockParser::parse_transactions(&empty_data);
748        assert!(result.is_ok());
749        assert_eq!(result.unwrap().len(), 0);
750    }
751
752    #[test]
753    fn test_parse_transactions_insufficient_data() {
754        let short_data = vec![0u8; 32]; // Too short for transaction header
755        let result = BlockParser::parse_transactions(&short_data);
756        assert!(result.is_ok());
757        assert_eq!(result.unwrap().len(), 0); // Should return empty list, not error
758    }
759}