Skip to main content

thru_base/
txn_lib.rs

1//! Transaction library: normal Rust struct, signing, serialization, accessors
2//!
3
4pub type TnPubkey = [u8; 32];
5pub type TnHash = [u8; 32];
6pub type TnSignature = [u8; 64];
7
8use crate::{
9    StateProofType,
10    tn_signature::{sign_transaction, verify_transaction},
11    tn_state_proof::StateProof,
12};
13use bytemuck::{Pod, Zeroable, bytes_of, pod_read_unaligned};
14
15pub const TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT: u8 = 0; // Bit position (matching C #define TN_TXN_FLAG_HAS_FEE_PAYER_PROOF (0U))
16pub const TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT: u8 = 1; // Bit position (matching C #define TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT (1U))
17
18// State proof type constants (matching C implementation)
19pub const TN_STATE_PROOF_TYPE_EXISTING: u64 = 0x0;
20pub const TN_STATE_PROOF_TYPE_UPDATING: u64 = 0x1;
21pub const TN_STATE_PROOF_TYPE_CREATION: u64 = 0x2;
22
23// State proof header size constants
24pub const TN_STATE_PROOF_HDR_SIZE: usize = 40; // 8 bytes type_slot + 32 bytes path_bitset
25pub const TN_ACCOUNT_META_FOOTPRINT: usize = 64; // Size of tn_account_meta_t (matching C sizeof)
26
27// TEMPORARY: Minimal local RpcError for test pass (remove when shared error type is available)
28#[derive(Debug, PartialEq)]
29pub enum RpcError {
30    InvalidTransactionSize { size: usize, max_size: usize },
31    TrailingBytes { expected: usize, found: usize },
32    TooManyAccounts { count: usize, max_count: usize },
33    InvalidTransactionSignature,
34    InvalidParams(&'static str),
35    InvalidFormat,
36    InvalidVersion,
37    InvalidFlags,
38    InvalidFeePayerStateProofType,
39    InvalidChainId,
40    DuplicateAccount,
41    UnsortedReadwriteAccounts,
42    UnsortedReadonlyAccounts,
43}
44
45impl std::fmt::Display for RpcError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            RpcError::InvalidTransactionSize { size, max_size } => {
49                write!(
50                    f,
51                    "Transaction size {} exceeds maximum allowed size {}",
52                    size, max_size
53                )
54            }
55            RpcError::TrailingBytes { expected, found } => {
56                write!(
57                    f,
58                    "Transaction has trailing bytes: expected {} bytes, found {} bytes",
59                    expected, found
60                )
61            }
62            RpcError::TooManyAccounts { count, max_count } => {
63                write!(
64                    f,
65                    "Too many accounts: {} exceeds maximum {}",
66                    count, max_count
67                )
68            }
69            RpcError::InvalidTransactionSignature => {
70                write!(f, "Invalid transaction signature")
71            }
72            RpcError::InvalidParams(msg) => {
73                write!(f, "Invalid parameters: {}", msg)
74            }
75            RpcError::InvalidFormat => {
76                write!(f, "Invalid transaction format")
77            }
78            RpcError::InvalidVersion => {
79                write!(f, "Invalid transaction version")
80            }
81            RpcError::InvalidFlags => {
82                write!(f, "Invalid transaction flags")
83            }
84            RpcError::InvalidFeePayerStateProofType => {
85                write!(f, "Invalid fee payer state proof type")
86            }
87            RpcError::InvalidChainId => {
88                write!(f, "Invalid chain ID: chain_id cannot be zero")
89            }
90            RpcError::DuplicateAccount => {
91                write!(f, "Duplicate account in transaction")
92            }
93            RpcError::UnsortedReadwriteAccounts => {
94                write!(f, "Read-write accounts are not strictly ascending")
95            }
96            RpcError::UnsortedReadonlyAccounts => {
97                write!(f, "Read-only accounts are not strictly ascending")
98            }
99        }
100    }
101}
102
103impl std::error::Error for RpcError {}
104
105impl RpcError {
106    pub fn invalid_transaction_size(size: usize, max_size: usize) -> Self {
107        Self::InvalidTransactionSize { size, max_size }
108    }
109    pub fn trailing_bytes(expected: usize, found: usize) -> Self {
110        Self::TrailingBytes { expected, found }
111    }
112    pub fn too_many_accounts(count: usize, max_count: usize) -> Self {
113        Self::TooManyAccounts { count, max_count }
114    }
115    pub fn invalid_transaction_signature() -> Self {
116        Self::InvalidTransactionSignature
117    }
118    pub fn invalid_params(msg: &'static str) -> Self {
119        Self::InvalidParams(msg)
120    }
121    pub fn invalid_format() -> Self {
122        Self::InvalidFormat
123    }
124    pub fn invalid_version() -> Self {
125        Self::InvalidVersion
126    }
127    pub fn invalid_flags() -> Self {
128        Self::InvalidFlags
129    }
130    pub fn invalid_fee_payer_state_proof_type() -> Self {
131        Self::InvalidFeePayerStateProofType
132    }
133    pub fn invalid_chain_id() -> Self {
134        Self::InvalidChainId
135    }
136    pub fn duplicate_account() -> Self {
137        Self::DuplicateAccount
138    }
139    pub fn unsorted_readwrite_accounts() -> Self {
140        Self::UnsortedReadwriteAccounts
141    }
142    pub fn unsorted_readonly_accounts() -> Self {
143        Self::UnsortedReadonlyAccounts
144    }
145}
146
147/// On-wire transaction header (matches TnTxnHdrV1 layout)
148///
149/// Transaction wire format:
150///   [header (112 bytes)]
151///   [input_pubkeys (variable)]
152///   [instr_data (variable)]
153///   [state_proof (optional)]
154///   [account_meta (optional)]
155///   [fee_payer_signature (64 bytes)]
156#[repr(C)]
157#[derive(Clone, Copy, Debug)]
158pub struct WireTxnHdrV1 {
159    pub transaction_version: u8,
160    pub flags: u8,
161    pub readwrite_accounts_cnt: u16,
162    pub readonly_accounts_cnt: u16,
163    pub instr_data_sz: u16,
164    pub req_compute_units: u32,
165    pub req_state_units: u16,
166    pub req_memory_units: u16,
167    pub fee: u64,
168    pub nonce: u64,
169    pub start_slot: u64,
170    pub expiry_after: u32,
171    pub chain_id: u16,
172    pub padding_0: [u8; 2],
173    pub fee_payer_pubkey: [u8; 32],
174    pub program_pubkey: [u8; 32],
175}
176
177/// Size of the signature in bytes (always at end of transaction)
178pub const TN_TXN_SIGNATURE_SZ: usize = 64;
179pub const TN_TXN_MAX_ACCOUNTS: usize = 1024;
180
181impl Default for WireTxnHdrV1 {
182    fn default() -> Self {
183        Self {
184            transaction_version: 0,
185            flags: 0,
186            readwrite_accounts_cnt: 0,
187            readonly_accounts_cnt: 0,
188            instr_data_sz: 0,
189            req_compute_units: 0,
190            req_state_units: 0,
191            req_memory_units: 0,
192            fee: 0,
193            nonce: 0,
194            start_slot: 0,
195            expiry_after: 0,
196            chain_id: 0,
197            padding_0: [0u8; 2],
198            fee_payer_pubkey: [0u8; 32],
199            program_pubkey: [0u8; 32],
200        }
201    }
202}
203
204// Manual Pod implementation to avoid derive issues
205unsafe impl Pod for WireTxnHdrV1 {}
206unsafe impl Zeroable for WireTxnHdrV1 {}
207
208/// Normal Rust struct for transaction construction
209#[derive(Clone, Debug, Default)]
210pub struct Transaction {
211    // Core transaction fields
212    pub fee_payer: TnPubkey, // [u8; 32] - who pays the fee
213    pub program: TnPubkey,   // [u8; 32] - target program
214
215    // Account lists (optional)
216    pub rw_accs: Option<Vec<TnPubkey>>, // read-write accounts
217    pub r_accs: Option<Vec<TnPubkey>>,  // read-only accounts
218
219    // Instruction data (optional)
220    pub instructions: Option<Vec<u8>>, // instruction bytes
221
222    // Transaction parameters
223    pub fee: u64,               // transaction fee
224    pub req_compute_units: u32, // requested compute units
225    pub req_state_units: u16,   // requested state units
226    pub req_memory_units: u16,  // requested memory units
227    pub expiry_after: u32,      // expiry time offset
228    pub start_slot: u64,        // starting slot
229    pub nonce: u64,             // transaction nonce
230    pub flags: u8,              // transaction flags
231    pub chain_id: u16,          // chain identifier (must be non-zero)
232
233    // Signature (optional until signed)
234    pub signature: Option<TnSignature>, // [u8; 64] - Ed25519 signature
235
236    // Fee payer state proof (optional)
237    pub fee_payer_state_proof: Option<StateProof>, // State proof for fee payer account
238
239    pub fee_payer_account_meta_raw: Option<Vec<u8>>,
240}
241
242impl Transaction {
243    /// Create a new unsigned transaction
244    pub fn new(fee_payer: TnPubkey, program: TnPubkey, fee: u64, nonce: u64) -> Self {
245        Self {
246            fee_payer,
247            program,
248            rw_accs: None,
249            r_accs: None,
250            instructions: None,
251            fee,
252            req_compute_units: 0,
253            req_state_units: 0,
254            req_memory_units: 0,
255            expiry_after: 0,
256            start_slot: 0,
257            nonce,
258            flags: 0,
259            chain_id: 1,
260            signature: None,
261            fee_payer_state_proof: None,
262            fee_payer_account_meta_raw: None,
263        }
264    }
265
266    /// Create a minimal transaction with just a program ID and instruction data.
267    /// Used for fake event transactions that don't need accounts or fees.
268    pub fn new_raw_instruction(
269        fee_payer: &TnPubkey,
270        program: &TnPubkey,
271        instruction_data: &[u8],
272    ) -> Result<Self, Box<dyn std::error::Error>> {
273        Ok(Self {
274            fee_payer: *fee_payer,
275            program: *program,
276            rw_accs: None,
277            r_accs: None,
278            instructions: Some(instruction_data.to_vec()),
279            fee: 0,
280            req_compute_units: 0,
281            req_state_units: 0,
282            req_memory_units: 0,
283            expiry_after: 0,
284            start_slot: 0,
285            nonce: 0,
286            flags: 0,
287            chain_id: 1,
288            signature: None,
289            fee_payer_state_proof: None,
290            fee_payer_account_meta_raw: None,
291        })
292    }
293
294    pub fn has_fee_payer_state_proof(&self) -> bool {
295        (self.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
296    }
297    pub fn may_compress_account(&self) -> bool {
298        (self.flags & (1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT)) != 0
299    }
300
301    pub fn get_signature(&self) -> Option<crate::Signature> {
302        if let Some(sig) = &self.signature {
303            return Some(crate::Signature::from_bytes(&sig));
304        }
305        None
306    }
307
308    pub fn with_may_compress_account(mut self) -> Self {
309        self.flags |= 1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT;
310        self
311    }
312
313    /// Builder method: set fee payer state proof
314    pub fn with_fee_payer_state_proof(mut self, state_proof: &StateProof) -> Self {
315        self.fee_payer_state_proof = Some(state_proof.clone());
316        // Set the flag bit to indicate presence of state proof
317        self.flags |= 1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT;
318        self
319    }
320
321    /// Builder method: set fee payer account meta as raw bytes
322    pub fn with_fee_payer_account_meta_raw(mut self, account_meta_raw: Vec<u8>) -> Self {
323        self.fee_payer_account_meta_raw = Some(account_meta_raw);
324        self
325    }
326
327    /// Builder method: remove fee payer state proof
328    pub fn without_fee_payer_state_proof(mut self) -> Self {
329        self.fee_payer_state_proof = None;
330        // Clear the flag bit to indicate absence of state proof
331        self.flags &= !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
332        self
333    }
334
335    /// Builder method: add read-write accounts
336    pub fn with_rw_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
337        self.rw_accs = Some(accounts);
338        self
339    }
340
341    /// Builder method: add read-only accounts
342    pub fn with_r_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
343        self.r_accs = Some(accounts);
344        self
345    }
346
347    /// Builder method: add a single read-write account
348    pub fn add_rw_account(mut self, account: TnPubkey) -> Self {
349        match self.rw_accs {
350            Some(ref mut accounts) => accounts.push(account),
351            None => self.rw_accs = Some(vec![account]),
352        }
353        self
354    }
355
356    /// Builder method: add a single read-only account
357    pub fn add_r_account(mut self, account: TnPubkey) -> Self {
358        match self.r_accs {
359            Some(ref mut accounts) => accounts.push(account),
360            None => self.r_accs = Some(vec![account]),
361        }
362        self
363    }
364
365    /// Builder method: add instruction data
366    pub fn with_instructions(mut self, instructions: Vec<u8>) -> Self {
367        self.instructions = Some(instructions);
368        self
369    }
370
371    /// Builder method: set compute units
372    pub fn with_compute_units(mut self, units: u32) -> Self {
373        self.req_compute_units = units;
374        self
375    }
376
377    /// Builder method: set state units
378    pub fn with_state_units(mut self, units: u16) -> Self {
379        self.req_state_units = units;
380        self
381    }
382
383    /// Builder method: set memory units
384    pub fn with_memory_units(mut self, units: u16) -> Self {
385        self.req_memory_units = units;
386        self
387    }
388
389    /// Builder method: set expiry
390    pub fn with_expiry_after(mut self, expiry: u32) -> Self {
391        self.expiry_after = expiry;
392        self
393    }
394
395    /// Builder method: set nonce
396    pub fn with_nonce(mut self, nonce: u64) -> Self {
397        self.nonce = nonce;
398        self
399    }
400
401    /// Builder method: set start slot
402    pub fn with_start_slot(mut self, slot: u64) -> Self {
403        self.start_slot = slot;
404        self
405    }
406
407    /// Builder method: set chain ID
408    pub fn with_chain_id(mut self, chain_id: u16) -> Self {
409        self.chain_id = chain_id;
410        self
411    }
412
413    /// Sign the transaction with a 32-byte Ed25519 private key.
414    /// Validates the account layout first, so a transaction with duplicate,
415    /// unsorted, or too-many accounts is rejected before signing.
416    pub fn sign(&mut self, private_key: &[u8; 32]) -> Result<(), Box<dyn std::error::Error>> {
417        self.validate()
418            .map_err(|e| Box::<dyn std::error::Error>::from(e))?;
419        self.sign_unchecked(private_key)
420    }
421
422    /// Sign WITHOUT validating the account layout. Intended for constructing
423    /// intentionally-invalid transactions (e.g. negative tests that submit a
424    /// malformed txn and expect the server to reject it). Production callers
425    /// should use `sign`, which validates first.
426    pub fn sign_unchecked(
427        &mut self,
428        private_key: &[u8; 32],
429    ) -> Result<(), Box<dyn std::error::Error>> {
430        let wire_bytes = self.to_wire_for_signing();
431        let sig = sign_transaction(&wire_bytes, &self.fee_payer, private_key)
432            .map_err(|e| Box::<dyn std::error::Error>::from(e))?;
433        self.signature = Some(sig);
434        Ok(())
435    }
436
437    /// Validate account count and duplicate-account rules before signing or serialization.
438    pub fn validate(&self) -> Result<(), RpcError> {
439        let rw_accs = self.rw_accs.as_deref().unwrap_or(&[]);
440        let r_accs = self.r_accs.as_deref().unwrap_or(&[]);
441        validate_account_layout(rw_accs, r_accs, &self.fee_payer, &self.program)
442    }
443
444    /// Verify the transaction signature
445    pub fn verify(&self) -> bool {
446        if let Some(sig_bytes) = &self.signature {
447            let wire_bytes = self.to_wire_for_signing();
448            return verify_transaction(&wire_bytes, sig_bytes, &self.fee_payer).is_ok();
449        }
450        false
451    }
452
453    /// Create wire format for signing (excluding trailing signature)
454    /// The message is: header + accounts + instr_data + state_proof + account_meta
455    fn to_wire_for_signing(&self) -> Vec<u8> {
456        // Zero out all bytes first to ensure deterministic padding
457        let mut wire: WireTxnHdrV1 = unsafe { core::mem::zeroed() };
458        wire.transaction_version = 1;
459        wire.flags = self.flags;
460        wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
461        wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
462        wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
463        wire.req_compute_units = self.req_compute_units;
464        wire.req_state_units = self.req_state_units;
465        wire.req_memory_units = self.req_memory_units;
466        wire.expiry_after = self.expiry_after;
467        wire.chain_id = self.chain_id;
468        wire.fee = self.fee;
469        wire.nonce = self.nonce;
470        wire.start_slot = self.start_slot;
471        wire.fee_payer_pubkey = self.fee_payer;
472        wire.program_pubkey = self.program;
473
474        let mut result = bytes_of(&wire).to_vec();
475
476        // Append variable-length data
477        if let Some(ref rw_accs) = self.rw_accs {
478            for acc in rw_accs {
479                result.extend_from_slice(acc);
480            }
481        }
482
483        if let Some(ref r_accs) = self.r_accs {
484            for acc in r_accs {
485                result.extend_from_slice(acc);
486            }
487        }
488
489        if let Some(ref instructions) = self.instructions {
490            result.extend_from_slice(instructions);
491        }
492
493        // Append state proof if present
494        if let Some(ref state_proof) = self.fee_payer_state_proof {
495            result.extend_from_slice(&state_proof.to_wire());
496        }
497
498        // Use raw account meta if available, otherwise use structured account meta
499        if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
500            result.extend_from_slice(fee_payer_account_meta_raw);
501        }
502
503        result
504    }
505
506    /// Serialize to on-wire format (WireTxnHdrV1).
507    /// Wire format: header + accounts + instr_data + state_proof + account_meta + signature
508    ///
509    /// This is INFALLIBLE and does NOT validate account layout: it must remain
510    /// usable for re-serializing transactions that were ingested from the
511    /// network/block store (which may predate, or otherwise violate, the
512    /// duplicate-account rules — e.g. a stored poison transaction served via
513    /// getTransactionRaw). Validation is enforced at the construction boundary
514    /// (`sign`) and is available explicitly via `try_to_wire`.
515    pub fn to_wire(&self) -> Vec<u8> {
516        self.serialize_wire()
517    }
518
519    /// Fallible serialization: validate account layout, then serialize.
520    /// Use this for newly-built transactions that must satisfy the
521    /// duplicate/sort/count rules before going on the wire.
522    pub fn try_to_wire(&self) -> Result<Vec<u8>, RpcError> {
523        self.validate()?;
524        Ok(self.serialize_wire())
525    }
526
527    /// Internal: pure serialization with no validation.
528    fn serialize_wire(&self) -> Vec<u8> {
529        let mut wire = WireTxnHdrV1::default();
530        wire.transaction_version = 1;
531        wire.flags = self.flags;
532        wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
533        wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
534        wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
535        wire.req_compute_units = self.req_compute_units;
536        wire.req_state_units = self.req_state_units;
537        wire.req_memory_units = self.req_memory_units;
538        wire.expiry_after = self.expiry_after;
539        wire.chain_id = self.chain_id;
540        wire.fee = self.fee;
541        wire.nonce = self.nonce;
542        wire.start_slot = self.start_slot;
543        wire.fee_payer_pubkey = self.fee_payer;
544        wire.program_pubkey = self.program;
545
546        let mut result = bytes_of(&wire).to_vec();
547
548        // Append variable-length data
549        if let Some(ref rw_accs) = self.rw_accs {
550            for acc in rw_accs {
551                result.extend_from_slice(acc);
552            }
553        }
554
555        if let Some(ref r_accs) = self.r_accs {
556            for acc in r_accs {
557                result.extend_from_slice(acc);
558            }
559        }
560
561        if let Some(ref instructions) = self.instructions {
562            result.extend_from_slice(instructions);
563        }
564
565        // Append state proof if present (after instruction data)
566        if let Some(ref state_proof) = self.fee_payer_state_proof {
567            result.extend_from_slice(&state_proof.to_wire());
568        }
569
570        // Use raw account meta if available, otherwise use structured account meta
571        if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
572            result.extend_from_slice(fee_payer_account_meta_raw);
573        }
574
575        // Append signature at the END (last 64 bytes)
576        if let Some(sig) = &self.signature {
577            result.extend_from_slice(sig);
578        } else {
579            // Zero signature if not set
580            result.extend_from_slice(&[0u8; TN_TXN_SIGNATURE_SZ]);
581        }
582
583        result
584    }
585
586    /// Deserialize from on-wire format (WireTxnHdrV1)
587    /// Wire format: header + accounts + instr_data + state_proof + account_meta + signature (at end)
588    pub fn from_wire(bytes: &[u8]) -> Option<Self> {
589        // Minimum size: header + signature at end
590        if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
591            return None;
592        }
593
594        let wire: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
595        let mut offset = core::mem::size_of::<WireTxnHdrV1>();
596
597        let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
598        let mut signature = [0u8; TN_TXN_SIGNATURE_SZ];
599        signature.copy_from_slice(&bytes[sig_start..]);
600
601        // Parse read-write accounts
602        let rw_accs = if wire.readwrite_accounts_cnt > 0 {
603            let mut accounts = Vec::new();
604            for _ in 0..wire.readwrite_accounts_cnt {
605                if offset + 32 > sig_start {
606                    return None;
607                }
608                let mut acc = [0u8; 32];
609                acc.copy_from_slice(&bytes[offset..offset + 32]);
610                accounts.push(acc);
611                offset += 32;
612            }
613            Some(accounts)
614        } else {
615            None
616        };
617
618        // Parse read-only accounts
619        let r_accs = if wire.readonly_accounts_cnt > 0 {
620            let mut accounts = Vec::new();
621            for _ in 0..wire.readonly_accounts_cnt {
622                if offset + 32 > sig_start {
623                    return None;
624                }
625                let mut acc = [0u8; 32];
626                acc.copy_from_slice(&bytes[offset..offset + 32]);
627                accounts.push(acc);
628                offset += 32;
629            }
630            Some(accounts)
631        } else {
632            None
633        };
634
635        // Parse instructions
636        let instructions = if wire.instr_data_sz > 0 {
637            if offset + wire.instr_data_sz as usize > sig_start {
638                return None;
639            }
640            let instr = bytes[offset..offset + wire.instr_data_sz as usize].to_vec();
641            offset += wire.instr_data_sz as usize;
642            Some(instr)
643        } else {
644            None
645        };
646
647        let mut fee_payer_account_meta_raw: Option<Vec<u8>> = None;
648        // Parse state proof if present
649        let fee_payer_state_proof = if has_fee_payer_state_proof(wire.flags) {
650            if offset >= sig_start {
651                return None;
652            }
653            let state_proof_bytes = &bytes[offset..sig_start];
654            if let Some(state_proof) = StateProof::from_wire(state_proof_bytes) {
655                offset += state_proof.footprint();
656                if state_proof.header.proof_type == StateProofType::Existing {
657                    if offset + TN_ACCOUNT_META_FOOTPRINT > sig_start {
658                        return None;
659                    }
660                    let account_meta_bytes = &bytes[offset..offset + TN_ACCOUNT_META_FOOTPRINT];
661                    fee_payer_account_meta_raw = Some(account_meta_bytes.to_vec());
662                    offset += TN_ACCOUNT_META_FOOTPRINT;
663                }
664                Some(state_proof)
665            } else {
666                return None;
667            }
668        } else {
669            None
670        };
671
672        // Verify we've consumed all bytes before signature
673        if offset != sig_start {
674            log::warn!(
675                "Transaction::from_wire: offset != sig_start ({} != {})",
676                offset,
677                sig_start
678            );
679            return None;
680        }
681
682        Some(Transaction {
683            fee_payer: wire.fee_payer_pubkey,
684            program: wire.program_pubkey,
685            rw_accs,
686            r_accs,
687            instructions,
688            flags: wire.flags,
689            chain_id: wire.chain_id,
690            fee: wire.fee,
691            req_compute_units: wire.req_compute_units,
692            req_state_units: wire.req_state_units,
693            req_memory_units: wire.req_memory_units,
694            expiry_after: wire.expiry_after,
695            start_slot: wire.start_slot,
696            nonce: wire.nonce,
697            signature: Some(signature),
698            fee_payer_state_proof,
699            fee_payer_account_meta_raw,
700        })
701    }
702
703    /// Accessor: read a field from serialized bytes by name
704    pub fn get_field_from_wire(bytes: &[u8], field: &str) -> Option<Vec<u8>> {
705        if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
706            return None;
707        }
708        let wire: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
709        match field {
710            "fee_payer_signature" => {
711                let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
712                Some(bytes[sig_start..].to_vec())
713            }
714            "transaction_version" => Some(vec![wire.transaction_version]),
715            "flags" => Some(vec![wire.flags]),
716            "readwrite_accounts_cnt" => Some(wire.readwrite_accounts_cnt.to_le_bytes().to_vec()),
717            "readonly_accounts_cnt" => Some(wire.readonly_accounts_cnt.to_le_bytes().to_vec()),
718            "instr_data_sz" => Some(wire.instr_data_sz.to_le_bytes().to_vec()),
719            "req_compute_units" => Some(wire.req_compute_units.to_le_bytes().to_vec()),
720            "req_state_units" => Some(wire.req_state_units.to_le_bytes().to_vec()),
721            "req_memory_units" => Some(wire.req_memory_units.to_le_bytes().to_vec()),
722            "expiry_after" => Some(wire.expiry_after.to_le_bytes().to_vec()),
723            "chain_id" => Some(wire.chain_id.to_le_bytes().to_vec()),
724            "fee" => Some(wire.fee.to_le_bytes().to_vec()),
725            "nonce" => Some(wire.nonce.to_le_bytes().to_vec()),
726            "start_slot" => Some(wire.start_slot.to_le_bytes().to_vec()),
727            "fee_payer_pubkey" => Some(wire.fee_payer_pubkey.to_vec()),
728            "program_pubkey" => Some(wire.program_pubkey.to_vec()),
729            _ => None,
730        }
731    }
732}
733
734/// Helper function to check if transaction has fee payer state proof
735fn has_fee_payer_state_proof(flags: u8) -> bool {
736    (flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
737}
738
739/// Helper function to extract state proof type from header
740fn extract_state_proof_type(type_slot: u64) -> u64 {
741    (type_slot >> 62) & 0x3 // Extract top 2 bits
742}
743
744/// Helper function to calculate state proof footprint from header
745fn calculate_state_proof_footprint(state_proof_data: &[u8]) -> Result<usize, RpcError> {
746    if state_proof_data.len() < TN_STATE_PROOF_HDR_SIZE {
747        return Err(RpcError::invalid_format());
748    }
749
750    // Extract type_slot (first 8 bytes)
751    let type_slot = u64::from_le_bytes([
752        state_proof_data[0],
753        state_proof_data[1],
754        state_proof_data[2],
755        state_proof_data[3],
756        state_proof_data[4],
757        state_proof_data[5],
758        state_proof_data[6],
759        state_proof_data[7],
760    ]);
761
762    // Extract path_bitset (next 32 bytes) and count set bits
763    let mut sibling_hash_cnt = 0u32;
764    for i in 0..4 {
765        let start = 8 + i * 8;
766        let word = u64::from_le_bytes([
767            state_proof_data[start],
768            state_proof_data[start + 1],
769            state_proof_data[start + 2],
770            state_proof_data[start + 3],
771            state_proof_data[start + 4],
772            state_proof_data[start + 5],
773            state_proof_data[start + 6],
774            state_proof_data[start + 7],
775        ]);
776        sibling_hash_cnt += word.count_ones();
777    }
778
779    let proof_type = extract_state_proof_type(type_slot);
780    let body_sz = (proof_type + sibling_hash_cnt as u64) * 32; // Each hash is 32 bytes
781
782    Ok(TN_STATE_PROOF_HDR_SIZE + body_sz as usize)
783}
784
785pub fn tn_txn_size(bytes: &[u8]) -> Result<usize, RpcError> {
786    // Basic size checks
787    if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
788        return Err(RpcError::invalid_format());
789    }
790
791    // Parse the header
792    // Use read_unaligned to safely read from potentially unaligned memory
793    let hdr: WireTxnHdrV1 =
794        unsafe { std::ptr::read_unaligned(bytes.as_ptr() as *const WireTxnHdrV1) };
795    let hdr = &hdr;
796    let mut offset = core::mem::size_of::<WireTxnHdrV1>();
797
798    // Calculate accounts size
799    let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
800    if offset + accs_sz > bytes.len() {
801        return Err(RpcError::invalid_format());
802    }
803    offset += accs_sz;
804
805    // Calculate instruction data size
806    let instr_sz = hdr.instr_data_sz as usize;
807    if offset + instr_sz > bytes.len() {
808        return Err(RpcError::invalid_format());
809    }
810    offset += instr_sz;
811
812    // Handle fee payer state proof if present
813    if has_fee_payer_state_proof(hdr.flags) {
814        // Check state proof header size
815        if offset + TN_STATE_PROOF_HDR_SIZE > bytes.len() {
816            return Err(RpcError::invalid_format());
817        }
818
819        // Calculate state proof footprint
820        let state_proof_data = &bytes[offset..];
821        let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
822
823        if offset + state_proof_sz > bytes.len() {
824            return Err(RpcError::invalid_format());
825        }
826        offset += state_proof_sz;
827
828        // Extract proof type for additional validation
829        let type_slot = u64::from_le_bytes([
830            state_proof_data[0],
831            state_proof_data[1],
832            state_proof_data[2],
833            state_proof_data[3],
834            state_proof_data[4],
835            state_proof_data[5],
836            state_proof_data[6],
837            state_proof_data[7],
838        ]);
839        let proof_type = extract_state_proof_type(type_slot);
840
841        // If proof type is EXISTING, account for account meta
842        if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
843            if offset + TN_ACCOUNT_META_FOOTPRINT > bytes.len() {
844                return Err(RpcError::invalid_format());
845            }
846            offset += TN_ACCOUNT_META_FOOTPRINT;
847        }
848    }
849
850    // Add trailing signature size
851    offset += TN_TXN_SIGNATURE_SZ;
852
853    // Verify we don't exceed the provided bytes
854    if offset > bytes.len() {
855        return Err(RpcError::invalid_format());
856    }
857
858    Ok(offset)
859}
860
861/// Validate a transaction's account layout (count, duplicates, sort order).
862///
863/// Mirrors the C source of truth tn_validate_txn_accounts
864/// (src/thru/runtime/tn_txn_account_validate.c): a combined merge-walk detects
865/// sort-order violations, within-array duplicates, and cross-array (rw∩ro)
866/// duplicates in a single pass, with the same error precedence (unsorted before
867/// a fee_payer/program duplicate). The fee_payer/program-in-array check runs only
868/// after sort order is confirmed.
869pub fn validate_account_layout(
870    rw_accs: &[TnPubkey],
871    r_accs: &[TnPubkey],
872    fee_payer: &TnPubkey,
873    program: &TnPubkey,
874) -> Result<(), RpcError> {
875    let total_accounts = 2usize
876        .checked_add(rw_accs.len())
877        .and_then(|v| v.checked_add(r_accs.len()))
878        .ok_or_else(|| RpcError::too_many_accounts(usize::MAX, TN_TXN_MAX_ACCOUNTS))?;
879    if total_accounts > TN_TXN_MAX_ACCOUNTS {
880        return Err(RpcError::too_many_accounts(total_accounts, TN_TXN_MAX_ACCOUNTS));
881    }
882    if fee_payer == program {
883        return Err(RpcError::duplicate_account());
884    }
885
886    let (mut i, mut j) = (0usize, 0usize);
887    let mut prev_rw: Option<&[u8; 32]> = None;
888    let mut prev_ro: Option<&[u8; 32]> = None;
889    while i < rw_accs.len() || j < r_accs.len() {
890        let use_rw = j >= r_accs.len() || (i < rw_accs.len() && rw_accs[i] <= r_accs[j]);
891        if use_rw {
892            let key = &rw_accs[i];
893            if j < r_accs.len() && *key == r_accs[j] {
894                return Err(RpcError::duplicate_account());
895            }
896            if let Some(prev) = prev_rw {
897                if prev == key {
898                    return Err(RpcError::duplicate_account());
899                }
900                if prev > key {
901                    return Err(RpcError::unsorted_readwrite_accounts());
902                }
903            }
904            prev_rw = Some(key);
905            i += 1;
906        } else {
907            let key = &r_accs[j];
908            if let Some(prev) = prev_ro {
909                if prev == key {
910                    return Err(RpcError::duplicate_account());
911                }
912                if prev > key {
913                    return Err(RpcError::unsorted_readonly_accounts());
914                }
915            }
916            prev_ro = Some(key);
917            j += 1;
918        }
919    }
920
921    for account in rw_accs.iter().chain(r_accs.iter()) {
922        if account == fee_payer || account == program {
923            return Err(RpcError::duplicate_account());
924        }
925    }
926
927    Ok(())
928}
929
930/// Validate a wire-format transaction for protocol correctness (matching C tn_txn_parse_core).
931pub fn validate_wire_transaction(bytes: &[u8]) -> Result<(), RpcError> {
932    const TN_TXN_MTU: usize = 32_768;
933    // Version and flags are now at offset 0 and 1
934    const TN_TXN_VERSION_OFFSET: usize = 0;
935    const TN_TXN_FLAGS_OFFSET: usize = 1;
936
937    use bytemuck::pod_read_unaligned;
938
939    // 1. Check payload size
940    if bytes.len() > TN_TXN_MTU {
941        return Err(RpcError::invalid_transaction_size(bytes.len(), TN_TXN_MTU));
942    }
943
944    // 2. Check minimum size
945    if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
946        return Err(RpcError::invalid_format());
947    }
948
949    // 3. Check transaction version
950    let transaction_version = bytes[TN_TXN_VERSION_OFFSET];
951    if transaction_version != 0x01 {
952        return Err(RpcError::invalid_version());
953    }
954
955    // 4. Check flags
956    let flags = bytes[TN_TXN_FLAGS_OFFSET];
957    // Clear the fee payer proof bit and check that all other bits are 0
958    let flags_without_proof_bit = flags & !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
959    let flags_cleared = flags_without_proof_bit & !(1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT);
960    if flags_cleared != 0 {
961        return Err(RpcError::invalid_flags());
962    }
963
964    // 5. Parse header
965    let hdr: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
966    let mut offset = core::mem::size_of::<WireTxnHdrV1>();
967
968    let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
969
970    // 6. Validate chain_id is non-zero (matches C tn_txn_parse.c:37)
971    if hdr.chain_id == 0 {
972        return Err(RpcError::invalid_chain_id());
973    }
974
975    // 6. Parse accounts
976    let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
977    if offset + accs_sz > sig_start {
978        return Err(RpcError::invalid_format());
979    }
980    offset += accs_sz;
981
982    // 7. Parse instruction data
983    let instr_sz = hdr.instr_data_sz as usize;
984    if offset + instr_sz > sig_start {
985        return Err(RpcError::invalid_format());
986    }
987    offset += instr_sz;
988
989    // 8. Handle fee payer state proof if present
990    if has_fee_payer_state_proof(flags) {
991        // Check state proof header size
992        if offset + TN_STATE_PROOF_HDR_SIZE > sig_start {
993            return Err(RpcError::invalid_format());
994        }
995
996        let state_proof_data = &bytes[offset..sig_start];
997
998        // Extract and validate the proof type before using the footprint. Only
999        // EXISTING and CREATION are valid; UPDATING and out-of-range types (e.g.
1000        // type 3) are rejected — matching C tn_txn_parse.c, where an invalid type
1001        // yields a zero footprint and is rejected before further parsing.
1002        let type_slot = u64::from_le_bytes([
1003            state_proof_data[0],
1004            state_proof_data[1],
1005            state_proof_data[2],
1006            state_proof_data[3],
1007            state_proof_data[4],
1008            state_proof_data[5],
1009            state_proof_data[6],
1010            state_proof_data[7],
1011        ]);
1012        let proof_type = extract_state_proof_type(type_slot);
1013        if proof_type != TN_STATE_PROOF_TYPE_EXISTING && proof_type != TN_STATE_PROOF_TYPE_CREATION
1014        {
1015            return Err(RpcError::invalid_fee_payer_state_proof_type());
1016        }
1017
1018        // Calculate state proof footprint
1019        let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
1020
1021        if offset + state_proof_sz > sig_start {
1022            return Err(RpcError::invalid_format());
1023        }
1024
1025        offset += state_proof_sz;
1026
1027        // If proof type is EXISTING, expect account meta
1028        if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
1029            if offset + TN_ACCOUNT_META_FOOTPRINT > sig_start {
1030                return Err(RpcError::invalid_format());
1031            }
1032            offset += TN_ACCOUNT_META_FOOTPRINT;
1033        }
1034    }
1035
1036    // 9. Check for exact size match (offset should be at signature start)
1037    if offset != sig_start {
1038        return Err(RpcError::trailing_bytes(
1039            offset + TN_TXN_SIGNATURE_SZ,
1040            bytes.len(),
1041        ));
1042    }
1043
1044    // 9b. Account-layout validation (count, duplicates, sort order) runs after
1045    // the whole wire structure is confirmed and before signature verification,
1046    // matching the C/Go order. Keep in sync with
1047    // src/thru/runtime/tn_txn_account_validate.c.
1048    {
1049        let accs_start = core::mem::size_of::<WireTxnHdrV1>();
1050        let rw_cnt = hdr.readwrite_accounts_cnt as usize;
1051        let ro_cnt = hdr.readonly_accounts_cnt as usize;
1052        let mut rw_accs: Vec<TnPubkey> = Vec::with_capacity(rw_cnt);
1053        for k in 0..rw_cnt {
1054            let s = accs_start + k * 32;
1055            let mut a = [0u8; 32];
1056            a.copy_from_slice(&bytes[s..s + 32]);
1057            rw_accs.push(a);
1058        }
1059        let ro_start = accs_start + rw_cnt * 32;
1060        let mut ro_accs: Vec<TnPubkey> = Vec::with_capacity(ro_cnt);
1061        for k in 0..ro_cnt {
1062            let s = ro_start + k * 32;
1063            let mut a = [0u8; 32];
1064            a.copy_from_slice(&bytes[s..s + 32]);
1065            ro_accs.push(a);
1066        }
1067        validate_account_layout(
1068            &rw_accs,
1069            &ro_accs,
1070            &hdr.fee_payer_pubkey,
1071            &hdr.program_pubkey,
1072        )?;
1073    }
1074
1075    // 10. Signature check
1076    let wire_for_signing = &bytes[..sig_start];
1077    let signature = &bytes[sig_start..];
1078    if verify_transaction(
1079        wire_for_signing,
1080        signature.try_into().expect("signature should be 64 bytes"),
1081        &hdr.fee_payer_pubkey,
1082    )
1083    .is_err()
1084    {
1085        return Err(RpcError::invalid_transaction_signature());
1086    }
1087
1088    Ok(())
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094    use ed25519_dalek::SigningKey;
1095
1096    fn make_valid_txn_bytes_with_flags(flags: u8) -> Vec<u8> {
1097        let signing_key = SigningKey::from(&[1u8; 32]);
1098        let verifying_key = signing_key.verifying_key();
1099        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
1100        tx.rw_accs = Some(vec![[3u8; 32], [4u8; 32]]);
1101        tx.r_accs = Some(vec![[5u8; 32]]);
1102        tx.instructions = Some(vec![1, 2, 3, 4]);
1103        tx.flags = flags;
1104        tx.sign(&signing_key.to_bytes()).unwrap();
1105        tx.to_wire()
1106    }
1107
1108    fn make_valid_txn_bytes() -> Vec<u8> {
1109        make_valid_txn_bytes_with_flags(0)
1110    }
1111
1112    #[test]
1113    fn test_tn_txn_size_basic_transaction() {
1114        let bytes = make_valid_txn_bytes();
1115        let calculated_size = tn_txn_size(&bytes).unwrap();
1116
1117        // The calculated size should match the actual bytes length
1118        assert_eq!(calculated_size, bytes.len());
1119    }
1120
1121    #[test]
1122    fn test_tn_txn_size_with_state_proof() {
1123        use crate::tn_state_proof::StateProof;
1124
1125        let signing_key = SigningKey::from(&[1u8; 32]);
1126        let verifying_key = signing_key.verifying_key();
1127
1128        // Create a CREATION state proof
1129        let path_bitset = [0u8; 32]; // No set bits = no sibling hashes
1130        let existing_leaf_pubkey = [7u8; 32];
1131        let existing_leaf_hash = [8u8; 32];
1132        let state_proof = StateProof::creation(
1133            100,
1134            path_bitset,
1135            existing_leaf_pubkey,
1136            existing_leaf_hash,
1137            vec![],
1138        );
1139
1140        // Create transaction with state proof
1141        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1142            .with_rw_accounts(vec![[3u8; 32]])
1143            .with_instructions(vec![1, 2, 3])
1144            .with_fee_payer_state_proof(&state_proof);
1145
1146        tx.sign(&signing_key.to_bytes()).unwrap();
1147        let bytes = tx.to_wire();
1148
1149        let calculated_size = tn_txn_size(&bytes).unwrap();
1150
1151        // The calculated size should match the actual bytes length
1152        assert_eq!(calculated_size, bytes.len());
1153    }
1154
1155    #[test]
1156    fn test_tn_txn_size_minimal_transaction() {
1157        let signing_key = SigningKey::from(&[1u8; 32]);
1158        let verifying_key = signing_key.verifying_key();
1159
1160        // Create minimal transaction (no accounts, no instructions)
1161        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
1162        tx.sign(&signing_key.to_bytes()).unwrap();
1163        let bytes = tx.to_wire();
1164
1165        let calculated_size = tn_txn_size(&bytes).unwrap();
1166
1167        // The calculated size should match the actual bytes length
1168        assert_eq!(calculated_size, bytes.len());
1169
1170        // Should be header size + trailing signature for minimal transaction
1171        let expected_min_size = core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ;
1172        assert_eq!(calculated_size, expected_min_size);
1173    }
1174
1175    #[test]
1176    fn test_tn_txn_size_invalid_format() {
1177        // Test with bytes too short for header
1178        let short_bytes = vec![0u8; 50];
1179        let result = tn_txn_size(&short_bytes);
1180        assert!(matches!(result, Err(RpcError::InvalidFormat)));
1181
1182        // Test with header but missing account data
1183        let mut bytes = make_valid_txn_bytes();
1184        bytes.truncate(core::mem::size_of::<WireTxnHdrV1>() + 10); // Truncate to cause missing data
1185        let result = tn_txn_size(&bytes);
1186        assert!(matches!(result, Err(RpcError::InvalidFormat)));
1187    }
1188
1189    #[test]
1190    fn test_tn_txn_size_consistency_with_validation() {
1191        let bytes = make_valid_txn_bytes();
1192
1193        // Both functions should succeed for valid transactions
1194        assert!(validate_wire_transaction(&bytes).is_ok());
1195        assert!(tn_txn_size(&bytes).is_ok());
1196
1197        // Size should match actual length
1198        let calculated_size = tn_txn_size(&bytes).unwrap();
1199        assert_eq!(calculated_size, bytes.len());
1200    }
1201
1202    #[test]
1203    fn test_valid_transaction() {
1204        let bytes = make_valid_txn_bytes();
1205        assert!(validate_wire_transaction(&bytes).is_ok());
1206    }
1207
1208    #[test]
1209    fn test_transaction_duplicate_accounts_rejected() {
1210        let signing_key = SigningKey::from(&[1u8; 32]);
1211        let fee = signing_key.verifying_key().to_bytes();
1212        let program = [2u8; 32];
1213        let a = [3u8; 32];
1214        let b = [4u8; 32];
1215
1216        let cases = [
1217            Transaction::new(fee, fee, 100, 42),
1218            Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![fee]),
1219            Transaction::new(fee, program, 100, 42).with_r_accounts(vec![fee]),
1220            Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![program]),
1221            Transaction::new(fee, program, 100, 42).with_r_accounts(vec![program]),
1222            Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![a, a]),
1223            Transaction::new(fee, program, 100, 42).with_r_accounts(vec![a, a]),
1224            Transaction::new(fee, program, 100, 42)
1225                .with_rw_accounts(vec![a])
1226                .with_r_accounts(vec![a, b]),
1227        ];
1228
1229        for mut tx in cases {
1230            assert!(matches!(tx.validate(), Err(RpcError::DuplicateAccount)));
1231            assert!(matches!(tx.try_to_wire(), Err(RpcError::DuplicateAccount)));
1232            assert!(tx.sign(&signing_key.to_bytes()).is_err());
1233        }
1234    }
1235
1236    #[test]
1237    fn test_oversize_transaction() {
1238        let mut bytes = make_valid_txn_bytes();
1239        bytes.resize(32_769, 0);
1240        let err = validate_wire_transaction(&bytes).unwrap_err();
1241        assert!(matches!(
1242            err,
1243            RpcError::InvalidTransactionSize {
1244                size: 32_769,
1245                max_size: 32_768
1246            }
1247        ));
1248    }
1249
1250    #[test]
1251    fn test_trailing_bytes() {
1252        let mut bytes = make_valid_txn_bytes();
1253        let orig_len = bytes.len();
1254        // Insert a byte before the signature (trailing bytes between body and signature)
1255        bytes.insert(orig_len - TN_TXN_SIGNATURE_SZ, 0);
1256        let err = validate_wire_transaction(&bytes).unwrap_err();
1257        // Expected size changes due to new wire format (112-byte header instead of 176)
1258        assert!(matches!(err, RpcError::TrailingBytes { .. }));
1259    }
1260
1261    #[test]
1262    fn test_invalid_transaction_version() {
1263        let mut bytes = make_valid_txn_bytes();
1264        // Corrupt the transaction version
1265        bytes[0] = 0x00; // Invalid version
1266        let err = validate_wire_transaction(&bytes).unwrap_err();
1267        assert!(matches!(err, RpcError::InvalidVersion));
1268    }
1269
1270    #[test]
1271    fn test_invalid_flags() {
1272        // Set invalid flag bits (keeping fee payer proof bit, but adding others)
1273        let bytes = make_valid_txn_bytes_with_flags(0x07);
1274        let err = validate_wire_transaction(&bytes).unwrap_err();
1275        assert!(matches!(err, RpcError::InvalidFlags));
1276    }
1277
1278    #[test]
1279    fn test_invalid_chain_id_zero() {
1280        let mut bytes = make_valid_txn_bytes();
1281
1282        /* Modify chain_id field to 0 (chain_id is at offset 108-109 in WireTxnHdrV1) */
1283        let hdr: &mut WireTxnHdrV1 =
1284            bytemuck::from_bytes_mut(&mut bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
1285        hdr.chain_id = 0;
1286
1287        let err = validate_wire_transaction(&bytes).unwrap_err();
1288        assert!(matches!(err, RpcError::InvalidChainId));
1289    }
1290
1291    #[test]
1292    fn test_transaction_too_short() {
1293        let bytes = vec![0u8; 50]; // Too short for header
1294        let err = validate_wire_transaction(&bytes).unwrap_err();
1295        assert!(matches!(err, RpcError::InvalidFormat));
1296    }
1297
1298    #[test]
1299    fn test_transaction_with_state_proof() {
1300        use crate::tn_state_proof::{StateProof, StateProofType};
1301
1302        let signing_key = SigningKey::from(&[1u8; 32]);
1303        let verifying_key = signing_key.verifying_key();
1304
1305        // Create a CREATION state proof (doesn't require account meta)
1306        let path_bitset = [0u8; 32]; // No set bits = no sibling hashes
1307        let existing_leaf_pubkey = [7u8; 32];
1308        let existing_leaf_hash = [8u8; 32];
1309        let state_proof = StateProof::creation(
1310            100,
1311            path_bitset,
1312            existing_leaf_pubkey,
1313            existing_leaf_hash,
1314            vec![],
1315        );
1316
1317        // Create transaction with state proof
1318        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1319            .with_fee_payer_state_proof(&state_proof);
1320
1321        // Verify flag is set
1322        assert!(tx.has_fee_payer_state_proof());
1323        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 1);
1324
1325        tx.sign(&signing_key.to_bytes()).unwrap();
1326        let bytes = tx.to_wire();
1327
1328        // Verify state proof is included in wire format
1329        assert!(bytes.len() > core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ);
1330        assert!(validate_wire_transaction(&bytes).is_ok());
1331
1332        // Test deserialization
1333        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1334        assert!(decoded_tx.has_fee_payer_state_proof());
1335        assert!(decoded_tx.fee_payer_state_proof.is_some());
1336
1337        let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
1338        assert_eq!(decoded_proof.proof_type(), StateProofType::Creation);
1339        assert_eq!(decoded_proof.slot(), 100);
1340    }
1341
1342    #[test]
1343    fn test_transaction_with_state_proof_serialization_round_trip() {
1344        use crate::tn_state_proof::StateProof;
1345
1346        let signing_key = SigningKey::from(&[1u8; 32]);
1347        let verifying_key = signing_key.verifying_key();
1348
1349        // Create a creation state proof with some sibling hashes
1350        let mut path_bitset = [0u8; 32];
1351        path_bitset[0] = 0b11; // Set first 2 bits for 2 sibling hashes
1352        let existing_leaf_pubkey = [7u8; 32];
1353        let existing_leaf_hash = [8u8; 32];
1354        let sibling_hashes = vec![[9u8; 32], [10u8; 32]];
1355
1356        let state_proof = StateProof::creation(
1357            200,
1358            path_bitset,
1359            existing_leaf_pubkey,
1360            existing_leaf_hash,
1361            sibling_hashes.clone(),
1362        );
1363
1364        // Create transaction with complex state proof
1365        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1366            .with_rw_accounts(vec![[3u8; 32], [4u8; 32]])
1367            .with_r_accounts(vec![[5u8; 32]])
1368            .with_instructions(vec![1, 2, 3, 4])
1369            .with_fee_payer_state_proof(&state_proof);
1370
1371        tx.sign(&signing_key.to_bytes()).unwrap();
1372        let bytes = tx.to_wire();
1373
1374        // Test validation
1375        assert!(validate_wire_transaction(&bytes).is_ok());
1376
1377        // Test round-trip serialization
1378        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1379        assert_eq!(decoded_tx.fee_payer, tx.fee_payer);
1380        assert_eq!(decoded_tx.program, tx.program);
1381        assert_eq!(decoded_tx.rw_accs, tx.rw_accs);
1382        assert_eq!(decoded_tx.r_accs, tx.r_accs);
1383        assert_eq!(decoded_tx.instructions, tx.instructions);
1384        assert_eq!(decoded_tx.flags, tx.flags);
1385        assert!(decoded_tx.has_fee_payer_state_proof());
1386
1387        let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
1388        assert_eq!(decoded_proof.slot(), 200);
1389        assert_eq!(decoded_proof.path_bitset(), &path_bitset);
1390    }
1391
1392    #[test]
1393    fn test_transaction_without_state_proof() {
1394        let signing_key = SigningKey::from(&[1u8; 32]);
1395        let verifying_key = signing_key.verifying_key();
1396
1397        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
1398
1399        // Verify flag is not set
1400        assert!(!tx.has_fee_payer_state_proof());
1401        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
1402        assert!(tx.fee_payer_state_proof.is_none());
1403
1404        tx.sign(&signing_key.to_bytes()).unwrap();
1405        let bytes = tx.to_wire();
1406
1407        assert!(validate_wire_transaction(&bytes).is_ok());
1408
1409        // Test deserialization
1410        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1411        assert!(!decoded_tx.has_fee_payer_state_proof());
1412        assert!(decoded_tx.fee_payer_state_proof.is_none());
1413    }
1414
1415    #[test]
1416    fn test_transaction_remove_state_proof() {
1417        use crate::tn_state_proof::StateProof;
1418
1419        let signing_key = SigningKey::from(&[1u8; 32]);
1420        let verifying_key = signing_key.verifying_key();
1421
1422        // Create a CREATION state proof
1423        let path_bitset = [0u8; 32];
1424        let existing_leaf_pubkey = [7u8; 32];
1425        let existing_leaf_hash = [8u8; 32];
1426        let state_proof = StateProof::creation(
1427            100,
1428            path_bitset,
1429            existing_leaf_pubkey,
1430            existing_leaf_hash,
1431            vec![],
1432        );
1433
1434        // Create transaction with state proof, then remove it
1435        let tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1436            .with_fee_payer_state_proof(&state_proof)
1437            .without_fee_payer_state_proof();
1438
1439        // Verify flag is cleared and state proof is removed
1440        assert!(!tx.has_fee_payer_state_proof());
1441        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
1442        assert!(tx.fee_payer_state_proof.is_none());
1443    }
1444}