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