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    tn_signature::{sign_transaction, verify_transaction},
10    tn_state_proof::StateProof,
11    StateProofType,
12};
13use bytemuck::{Pod, Zeroable, bytes_of, from_bytes};
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}
40
41impl std::fmt::Display for RpcError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            RpcError::InvalidTransactionSize { size, max_size } => {
45                write!(
46                    f,
47                    "Transaction size {} exceeds maximum allowed size {}",
48                    size, max_size
49                )
50            }
51            RpcError::TrailingBytes { expected, found } => {
52                write!(
53                    f,
54                    "Transaction has trailing bytes: expected {} bytes, found {} bytes",
55                    expected, found
56                )
57            }
58            RpcError::TooManyAccounts { count, max_count } => {
59                write!(
60                    f,
61                    "Too many accounts: {} exceeds maximum {}",
62                    count, max_count
63                )
64            }
65            RpcError::InvalidTransactionSignature => {
66                write!(f, "Invalid transaction signature")
67            }
68            RpcError::InvalidParams(msg) => {
69                write!(f, "Invalid parameters: {}", msg)
70            }
71            RpcError::InvalidFormat => {
72                write!(f, "Invalid transaction format")
73            }
74            RpcError::InvalidVersion => {
75                write!(f, "Invalid transaction version")
76            }
77            RpcError::InvalidFlags => {
78                write!(f, "Invalid transaction flags")
79            }
80            RpcError::InvalidFeePayerStateProofType => {
81                write!(f, "Invalid fee payer state proof type")
82            }
83        }
84    }
85}
86
87impl RpcError {
88    pub fn invalid_transaction_size(size: usize, max_size: usize) -> Self {
89        Self::InvalidTransactionSize { size, max_size }
90    }
91    pub fn trailing_bytes(expected: usize, found: usize) -> Self {
92        Self::TrailingBytes { expected, found }
93    }
94    pub fn too_many_accounts(count: usize, max_count: usize) -> Self {
95        Self::TooManyAccounts { count, max_count }
96    }
97    pub fn invalid_transaction_signature() -> Self {
98        Self::InvalidTransactionSignature
99    }
100    pub fn invalid_params(msg: &'static str) -> Self {
101        Self::InvalidParams(msg)
102    }
103    pub fn invalid_format() -> Self {
104        Self::InvalidFormat
105    }
106    pub fn invalid_version() -> Self {
107        Self::InvalidVersion
108    }
109    pub fn invalid_flags() -> Self {
110        Self::InvalidFlags
111    }
112    pub fn invalid_fee_payer_state_proof_type() -> Self {
113        Self::InvalidFeePayerStateProofType
114    }
115}
116
117/// On-wire transaction header (matches TnTxnHdrV1 layout)
118#[repr(C)]
119#[derive(Clone, Copy, Debug)]
120pub struct WireTxnHdrV1 {
121    pub fee_payer_signature: [u8; 64],
122    pub transaction_version: u8,
123    pub flags: u8,
124    pub readwrite_accounts_cnt: u16,
125    pub readonly_accounts_cnt: u16,
126    pub instr_data_sz: u16,
127    pub req_compute_units: u32,
128    pub req_state_units: u16,
129    pub req_memory_units: u16,
130    pub fee: u64,
131    pub nonce: u64,
132    pub start_slot: u64,
133    pub expiry_after: u32,
134    pub padding_0: [u8; 4],
135    pub fee_payer_pubkey: [u8; 32],
136    pub program_pubkey: [u8; 32],
137}
138
139impl Default for WireTxnHdrV1 {
140    fn default() -> Self {
141        Self {
142            fee_payer_signature: [0u8; 64],
143            transaction_version: 0,
144            flags: 0,
145            readwrite_accounts_cnt: 0,
146            readonly_accounts_cnt: 0,
147            instr_data_sz: 0,
148            req_compute_units: 0,
149            req_state_units: 0,
150            req_memory_units: 0,
151            fee: 0,
152            nonce: 0,
153            start_slot: 0,
154            expiry_after: 0,
155            padding_0: [0u8; 4],
156            fee_payer_pubkey: [0u8; 32],
157            program_pubkey: [0u8; 32],
158        }
159    }
160}
161
162// Manual Pod implementation to avoid derive issues
163unsafe impl Pod for WireTxnHdrV1 {}
164unsafe impl Zeroable for WireTxnHdrV1 {}
165
166/// Normal Rust struct for transaction construction
167#[derive(Clone, Debug, Default)]
168pub struct Transaction {
169    // Core transaction fields
170    pub fee_payer: TnPubkey, // [u8; 32] - who pays the fee
171    pub program: TnPubkey,   // [u8; 32] - target program
172
173    // Account lists (optional)
174    pub rw_accs: Option<Vec<TnPubkey>>, // read-write accounts
175    pub r_accs: Option<Vec<TnPubkey>>,  // read-only accounts
176
177    // Instruction data (optional)
178    pub instructions: Option<Vec<u8>>, // instruction bytes
179
180    // Transaction parameters
181    pub fee: u64,               // transaction fee
182    pub req_compute_units: u32, // requested compute units
183    pub req_state_units: u16,   // requested state units
184    pub req_memory_units: u16,  // requested memory units
185    pub expiry_after: u32,      // expiry time offset
186    pub start_slot: u64,        // starting slot
187    pub nonce: u64,             // transaction nonce
188    pub flags: u8,              // transaction flags
189
190    // Signature (optional until signed)
191    pub signature: Option<TnSignature>, // [u8; 64] - Ed25519 signature
192
193    // Fee payer state proof (optional)
194    pub fee_payer_state_proof: Option<StateProof>, // State proof for fee payer account
195
196    pub fee_payer_account_meta_raw: Option<Vec<u8>>,
197}
198
199impl Transaction {
200    /// Create a new unsigned transaction
201    pub fn new(fee_payer: TnPubkey, program: TnPubkey, fee: u64, nonce: u64) -> Self {
202        Self {
203            fee_payer,
204            program,
205            rw_accs: None,
206            r_accs: None,
207            instructions: None,
208            fee,
209            req_compute_units: 0,
210            req_state_units: 0,
211            req_memory_units: 0,
212            expiry_after: 0,
213            start_slot: 0,
214            nonce,
215            flags: 0,
216            signature: None,
217            fee_payer_state_proof: None,
218            fee_payer_account_meta_raw: None,
219        }
220    }
221
222    pub fn has_fee_payer_state_proof(&self) -> bool {
223        (self.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
224    }
225    pub fn may_compress_account(&self) -> bool {
226        (self.flags & (1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT)) != 0
227    }
228
229    pub fn get_signature(&self) -> Option<crate::Signature> {
230        if let Some(sig) = &self.signature {
231            return Some(crate::Signature::from_bytes(&sig));
232        }
233        None
234    }
235
236    pub fn with_may_compress_account(mut self) -> Self {
237        self.flags |= 1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT;
238        self
239    }
240
241    /// Builder method: set fee payer state proof
242    pub fn with_fee_payer_state_proof(mut self, state_proof: &StateProof) -> Self {
243        self.fee_payer_state_proof = Some(state_proof.clone());
244        // Set the flag bit to indicate presence of state proof
245        self.flags |= 1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT;
246        self
247    }
248
249    /// Builder method: set fee payer account meta as raw bytes
250    pub fn with_fee_payer_account_meta_raw(mut self, account_meta_raw: Vec<u8>) -> Self {
251        self.fee_payer_account_meta_raw = Some(account_meta_raw);
252        self
253    }
254
255    /// Builder method: remove fee payer state proof
256    pub fn without_fee_payer_state_proof(mut self) -> Self {
257        self.fee_payer_state_proof = None;
258        // Clear the flag bit to indicate absence of state proof
259        self.flags &= !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
260        self
261    }
262
263    /// Builder method: add read-write accounts
264    pub fn with_rw_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
265        self.rw_accs = Some(accounts);
266        self
267    }
268
269    /// Builder method: add read-only accounts
270    pub fn with_r_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
271        self.r_accs = Some(accounts);
272        self
273    }
274
275    /// Builder method: add a single read-write account
276    pub fn add_rw_account(mut self, account: TnPubkey) -> Self {
277        match self.rw_accs {
278            Some(ref mut accounts) => accounts.push(account),
279            None => self.rw_accs = Some(vec![account]),
280        }
281        self
282    }
283
284    /// Builder method: add a single read-only account
285    pub fn add_r_account(mut self, account: TnPubkey) -> Self {
286        match self.r_accs {
287            Some(ref mut accounts) => accounts.push(account),
288            None => self.r_accs = Some(vec![account]),
289        }
290        self
291    }
292
293    /// Builder method: add instruction data
294    pub fn with_instructions(mut self, instructions: Vec<u8>) -> Self {
295        self.instructions = Some(instructions);
296        self
297    }
298
299    /// Builder method: set compute units
300    pub fn with_compute_units(mut self, units: u32) -> Self {
301        self.req_compute_units = units;
302        self
303    }
304
305    /// Builder method: set state units
306    pub fn with_state_units(mut self, units: u16) -> Self {
307        self.req_state_units = units;
308        self
309    }
310
311    /// Builder method: set memory units
312    pub fn with_memory_units(mut self, units: u16) -> Self {
313        self.req_memory_units = units;
314        self
315    }
316
317    /// Builder method: set expiry
318    pub fn with_expiry_after(mut self, expiry: u32) -> Self {
319        self.expiry_after = expiry;
320        self
321    }
322
323    /// Builder method: set nonce
324    pub fn with_nonce(mut self, nonce: u64) -> Self {
325        self.nonce = nonce;
326        self
327    }
328
329    /// Builder method: set start slot
330    pub fn with_start_slot(mut self, slot: u64) -> Self {
331        self.start_slot = slot;
332        self
333    }
334
335    /// Sign the transaction with a 32-byte Ed25519 private key
336    pub fn sign(&mut self, private_key: &[u8; 32]) -> Result<(), Box<dyn std::error::Error>> {
337        let wire_bytes = self.to_wire_for_signing();
338        let sig = sign_transaction(&wire_bytes, &self.fee_payer, private_key)
339            .map_err(|e| Box::<dyn std::error::Error>::from(e))?;
340        self.signature = Some(sig);
341        Ok(())
342    }
343
344    /// Verify the transaction signature
345    pub fn verify(&self) -> bool {
346        if let Some(sig_bytes) = &self.signature {
347            let wire_bytes = self.to_wire_for_signing();
348            return verify_transaction(&wire_bytes, sig_bytes, &self.fee_payer).is_ok();
349        }
350        false
351    }
352
353    /// Create wire format for signing (excluding signature field)
354    fn to_wire_for_signing(&self) -> Vec<u8> {
355        // Zero out all bytes first to ensure deterministic padding
356        let mut wire: WireTxnHdrV1 = unsafe { core::mem::zeroed() };
357        // Don't set fee_payer_signature - it will be excluded from signing
358        wire.transaction_version = 1;
359        wire.flags = self.flags;
360        wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
361        wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
362        wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
363        wire.req_compute_units = self.req_compute_units;
364        wire.req_state_units = self.req_state_units;
365        wire.req_memory_units = self.req_memory_units;
366        wire.expiry_after = self.expiry_after;
367        wire.fee = self.fee;
368        wire.nonce = self.nonce;
369        wire.start_slot = self.start_slot;
370        wire.fee_payer_pubkey = self.fee_payer;
371        wire.program_pubkey = self.program;
372
373        let wire_bytes = bytes_of(&wire);
374        // Skip the first 64 bytes (fee_payer_signature) and include the rest
375        let mut result = wire_bytes[64..].to_vec();
376
377        // Append variable-length data
378        if let Some(ref rw_accs) = self.rw_accs {
379            for acc in rw_accs {
380                result.extend_from_slice(acc);
381            }
382        }
383
384        if let Some(ref r_accs) = self.r_accs {
385            for acc in r_accs {
386                result.extend_from_slice(acc);
387            }
388        }
389
390        if let Some(ref instructions) = self.instructions {
391            result.extend_from_slice(instructions);
392        }
393
394        // Append state proof if present
395        if let Some(ref state_proof) = self.fee_payer_state_proof {
396            result.extend_from_slice(&state_proof.to_wire());
397        }
398
399        // Use raw account meta if available, otherwise use structured account meta
400        if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
401            result.extend_from_slice(fee_payer_account_meta_raw);
402        }
403
404        result
405    }
406
407    /// Serialize to on-wire format (WireTxnHdrV1)
408    pub fn to_wire(&self) -> Vec<u8> {
409        let mut wire = WireTxnHdrV1::default();
410        if let Some(sig) = &self.signature {
411            wire.fee_payer_signature = *sig;
412        }
413        wire.transaction_version = 1;
414        wire.flags = self.flags;
415        wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
416        wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
417        wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
418        wire.req_compute_units = self.req_compute_units;
419        wire.req_state_units = self.req_state_units;
420        wire.req_memory_units = self.req_memory_units;
421        wire.expiry_after = self.expiry_after;
422        wire.fee = self.fee;
423        wire.nonce = self.nonce;
424        wire.start_slot = self.start_slot;
425        wire.fee_payer_pubkey = self.fee_payer;
426        wire.program_pubkey = self.program;
427
428        let mut result = bytes_of(&wire).to_vec();
429
430        // Append variable-length data
431        if let Some(ref rw_accs) = self.rw_accs {
432            for acc in rw_accs {
433                result.extend_from_slice(acc);
434            }
435        }
436
437        if let Some(ref r_accs) = self.r_accs {
438            for acc in r_accs {
439                result.extend_from_slice(acc);
440            }
441        }
442
443        if let Some(ref instructions) = self.instructions {
444            result.extend_from_slice(instructions);
445        }
446
447        // Append state proof if present (after instruction data)
448        if let Some(ref state_proof) = self.fee_payer_state_proof {
449            result.extend_from_slice(&state_proof.to_wire());
450        }
451
452        // Use raw account meta if available, otherwise use structured account meta
453        if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
454            result.extend_from_slice(fee_payer_account_meta_raw);
455        }
456
457        result
458    }
459
460    /// Deserialize from on-wire format (WireTxnHdrV1)
461    pub fn from_wire(bytes: &[u8]) -> Option<Self> {
462        if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() {
463            return None;
464        }
465
466        let wire: &WireTxnHdrV1 = from_bytes(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
467        let mut offset = core::mem::size_of::<WireTxnHdrV1>();
468
469        // Parse read-write accounts
470        let rw_accs = if wire.readwrite_accounts_cnt > 0 {
471            let mut accounts = Vec::new();
472            for _ in 0..wire.readwrite_accounts_cnt {
473                if offset + 32 > bytes.len() {
474                    return None;
475                }
476                let mut acc = [0u8; 32];
477                acc.copy_from_slice(&bytes[offset..offset + 32]);
478                accounts.push(acc);
479                offset += 32;
480            }
481            Some(accounts)
482        } else {
483            None
484        };
485
486        // Parse read-only accounts
487        let r_accs = if wire.readonly_accounts_cnt > 0 {
488            let mut accounts = Vec::new();
489            for _ in 0..wire.readonly_accounts_cnt {
490                if offset + 32 > bytes.len() {
491                    return None;
492                }
493                let mut acc = [0u8; 32];
494                acc.copy_from_slice(&bytes[offset..offset + 32]);
495                accounts.push(acc);
496                offset += 32;
497            }
498            Some(accounts)
499        } else {
500            None
501        };
502
503        // Parse instructions
504        let instructions = if wire.instr_data_sz > 0 {
505            if offset + wire.instr_data_sz as usize > bytes.len() {
506                return None;
507            }
508            let instr = bytes[offset..offset + wire.instr_data_sz as usize].to_vec();
509            offset += wire.instr_data_sz as usize;
510            Some(instr)
511        } else {
512            None
513        };
514
515        let mut fee_payer_account_meta_raw: Option<Vec<u8>> = None;
516        // Parse state proof if present
517        let fee_payer_state_proof = if has_fee_payer_state_proof(wire.flags) {
518            if offset >= bytes.len() {
519                return None;
520            }
521            let state_proof_bytes = &bytes[offset..];
522            if let Some(state_proof) = StateProof::from_wire(state_proof_bytes) {
523                offset += state_proof.footprint();
524                if state_proof.header.proof_type == StateProofType::Existing {
525                    if offset + TN_ACCOUNT_META_FOOTPRINT > bytes.len() {
526                        return None;
527                    }
528                    let account_meta_bytes = &bytes[offset..offset + TN_ACCOUNT_META_FOOTPRINT];
529                    fee_payer_account_meta_raw = Some(account_meta_bytes.to_vec());
530                    offset += TN_ACCOUNT_META_FOOTPRINT;
531                }
532                Some(state_proof)
533            } else {
534                return None;
535            }
536        } else {
537            None
538        };
539
540        // Verify we've consumed all bytes
541        if offset != bytes.len() {
542            log::warn!(
543                "Transaction::from_wire: offset != bytes.len() ({} != {})",
544                offset,
545                bytes.len()
546            );
547            return None;
548        }
549
550        Some(Transaction {
551            fee_payer: wire.fee_payer_pubkey,
552            program: wire.program_pubkey,
553            rw_accs,
554            r_accs,
555            instructions,
556            flags: wire.flags,
557            fee: wire.fee,
558            req_compute_units: wire.req_compute_units,
559            req_state_units: wire.req_state_units,
560            req_memory_units: wire.req_memory_units,
561            expiry_after: wire.expiry_after,
562            start_slot: wire.start_slot,
563            nonce: wire.nonce,
564            signature: Some(wire.fee_payer_signature),
565            fee_payer_state_proof,
566            fee_payer_account_meta_raw,
567        })
568    }
569
570    /// Accessor: read a field from serialized bytes by name
571    pub fn get_field_from_wire(bytes: &[u8], field: &str) -> Option<Vec<u8>> {
572        if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() {
573            return None;
574        }
575        let wire: &WireTxnHdrV1 = from_bytes(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
576        match field {
577            "fee_payer_signature" => Some(wire.fee_payer_signature.to_vec()),
578            "transaction_version" => Some(vec![wire.transaction_version]),
579            "flags" => Some(vec![wire.flags]),
580            "readwrite_accounts_cnt" => Some(wire.readwrite_accounts_cnt.to_le_bytes().to_vec()),
581            "readonly_accounts_cnt" => Some(wire.readonly_accounts_cnt.to_le_bytes().to_vec()),
582            "instr_data_sz" => Some(wire.instr_data_sz.to_le_bytes().to_vec()),
583            "req_compute_units" => Some(wire.req_compute_units.to_le_bytes().to_vec()),
584            "req_state_units" => Some(wire.req_state_units.to_le_bytes().to_vec()),
585            "req_memory_units" => Some(wire.req_memory_units.to_le_bytes().to_vec()),
586            "expiry_after" => Some(wire.expiry_after.to_le_bytes().to_vec()),
587            "fee" => Some(wire.fee.to_le_bytes().to_vec()),
588            "nonce" => Some(wire.nonce.to_le_bytes().to_vec()),
589            "start_slot" => Some(wire.start_slot.to_le_bytes().to_vec()),
590            "fee_payer_pubkey" => Some(wire.fee_payer_pubkey.to_vec()),
591            "program_pubkey" => Some(wire.program_pubkey.to_vec()),
592            _ => None,
593        }
594    }
595}
596
597/// Helper function to check if transaction has fee payer state proof
598fn has_fee_payer_state_proof(flags: u8) -> bool {
599    (flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
600}
601
602/// Helper function to extract state proof type from header
603fn extract_state_proof_type(type_slot: u64) -> u64 {
604    (type_slot >> 62) & 0x3 // Extract top 2 bits
605}
606
607/// Helper function to calculate state proof footprint from header
608fn calculate_state_proof_footprint(state_proof_data: &[u8]) -> Result<usize, RpcError> {
609    if state_proof_data.len() < TN_STATE_PROOF_HDR_SIZE {
610        return Err(RpcError::invalid_format());
611    }
612
613    // Extract type_slot (first 8 bytes)
614    let type_slot = u64::from_le_bytes([
615        state_proof_data[0],
616        state_proof_data[1],
617        state_proof_data[2],
618        state_proof_data[3],
619        state_proof_data[4],
620        state_proof_data[5],
621        state_proof_data[6],
622        state_proof_data[7],
623    ]);
624
625    // Extract path_bitset (next 32 bytes) and count set bits
626    let mut sibling_hash_cnt = 0u32;
627    for i in 0..4 {
628        let start = 8 + i * 8;
629        let word = u64::from_le_bytes([
630            state_proof_data[start],
631            state_proof_data[start + 1],
632            state_proof_data[start + 2],
633            state_proof_data[start + 3],
634            state_proof_data[start + 4],
635            state_proof_data[start + 5],
636            state_proof_data[start + 6],
637            state_proof_data[start + 7],
638        ]);
639        sibling_hash_cnt += word.count_ones();
640    }
641
642    let proof_type = extract_state_proof_type(type_slot);
643    let body_sz = (proof_type + sibling_hash_cnt as u64) * 32; // Each hash is 32 bytes
644
645    Ok(TN_STATE_PROOF_HDR_SIZE + body_sz as usize)
646}
647
648pub fn tn_txn_size(bytes: &[u8]) -> Result<usize, RpcError> {
649    // Basic size checks
650    if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() {
651        return Err(RpcError::invalid_format());
652    }
653
654    // Parse the header
655    // Use read_unaligned to safely read from potentially unaligned memory
656    let hdr: WireTxnHdrV1 =
657        unsafe { std::ptr::read_unaligned(bytes.as_ptr() as *const WireTxnHdrV1) };
658    let hdr = &hdr;
659    let mut offset = core::mem::size_of::<WireTxnHdrV1>();
660
661    // Calculate accounts size
662    let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
663    if offset + accs_sz > bytes.len() {
664        return Err(RpcError::invalid_format());
665    }
666    offset += accs_sz;
667
668    // Calculate instruction data size
669    let instr_sz = hdr.instr_data_sz as usize;
670    if offset + instr_sz > bytes.len() {
671        return Err(RpcError::invalid_format());
672    }
673    offset += instr_sz;
674
675    // Handle fee payer state proof if present
676    if has_fee_payer_state_proof(hdr.flags) {
677        // Check state proof header size
678        if offset + TN_STATE_PROOF_HDR_SIZE > bytes.len() {
679            return Err(RpcError::invalid_format());
680        }
681
682        // Calculate state proof footprint
683        let state_proof_data = &bytes[offset..];
684        let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
685
686        if offset + state_proof_sz > bytes.len() {
687            return Err(RpcError::invalid_format());
688        }
689        offset += state_proof_sz;
690
691        // Extract proof type for additional validation
692        let type_slot = u64::from_le_bytes([
693            state_proof_data[0],
694            state_proof_data[1],
695            state_proof_data[2],
696            state_proof_data[3],
697            state_proof_data[4],
698            state_proof_data[5],
699            state_proof_data[6],
700            state_proof_data[7],
701        ]);
702        let proof_type = extract_state_proof_type(type_slot);
703
704        // If proof type is EXISTING, account for account meta
705        if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
706            if offset + TN_ACCOUNT_META_FOOTPRINT > bytes.len() {
707                return Err(RpcError::invalid_format());
708            }
709            offset += TN_ACCOUNT_META_FOOTPRINT;
710        }
711    }
712
713    // Verify we don't exceed the provided bytes
714    if offset > bytes.len() {
715        return Err(RpcError::invalid_format());
716    }
717
718    Ok(offset)
719}
720
721/// Validate a wire-format transaction for protocol correctness (matching C tn_txn_parse_core).
722pub fn validate_wire_transaction(bytes: &[u8]) -> Result<(), RpcError> {
723    const TN_TXN_MTU: usize = 32_768;
724    const TN_TXN_VERSION_OFFSET: usize = 64;
725    const TN_TXN_FLAGS_OFFSET: usize = 65;
726
727    use bytemuck::from_bytes;
728
729    // 1. Check payload size
730    if bytes.len() > TN_TXN_MTU {
731        return Err(RpcError::invalid_transaction_size(bytes.len(), TN_TXN_MTU));
732    }
733
734    // 2. Check transaction version
735    if bytes.len() <= TN_TXN_VERSION_OFFSET {
736        return Err(RpcError::invalid_format());
737    }
738    let transaction_version = bytes[TN_TXN_VERSION_OFFSET];
739    if transaction_version != 0x01 {
740        return Err(RpcError::invalid_version());
741    }
742
743    // 3. Check flags
744    if bytes.len() <= TN_TXN_FLAGS_OFFSET {
745        return Err(RpcError::invalid_format());
746    }
747    let flags = bytes[TN_TXN_FLAGS_OFFSET];
748    // Clear the fee payer proof bit and check that all other bits are 0
749    let flags_without_proof_bit = flags & !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
750    let flags_cleared = flags_without_proof_bit & !(1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT);
751    if flags_cleared != 0 {
752        return Err(RpcError::invalid_flags());
753    }
754
755    // 4. Check header size and parse header
756    if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() {
757        return Err(RpcError::invalid_format());
758    }
759    let hdr: &WireTxnHdrV1 = from_bytes(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
760    let mut offset = core::mem::size_of::<WireTxnHdrV1>();
761
762    // 5. Parse accounts
763    let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
764    if offset + accs_sz > bytes.len() {
765        return Err(RpcError::invalid_format());
766    }
767    offset += accs_sz;
768
769    // 6. Parse instruction data
770    let instr_sz = hdr.instr_data_sz as usize;
771    if offset + instr_sz > bytes.len() {
772        return Err(RpcError::invalid_format());
773    }
774    offset += instr_sz;
775
776    // 7. Handle fee payer state proof if present
777    if has_fee_payer_state_proof(flags) {
778        // Check state proof header size
779        if offset + TN_STATE_PROOF_HDR_SIZE > bytes.len() {
780            return Err(RpcError::invalid_format());
781        }
782
783        // Calculate state proof footprint
784        let state_proof_data = &bytes[offset..];
785        let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
786
787        if offset + state_proof_sz > bytes.len() {
788            return Err(RpcError::invalid_format());
789        }
790
791        // Extract proof type and validate
792        let type_slot = u64::from_le_bytes([
793            state_proof_data[0],
794            state_proof_data[1],
795            state_proof_data[2],
796            state_proof_data[3],
797            state_proof_data[4],
798            state_proof_data[5],
799            state_proof_data[6],
800            state_proof_data[7],
801        ]);
802        let proof_type = extract_state_proof_type(type_slot);
803
804        // Check that proof type is not UPDATING
805        if proof_type == TN_STATE_PROOF_TYPE_UPDATING {
806            return Err(RpcError::invalid_fee_payer_state_proof_type());
807        }
808
809        offset += state_proof_sz;
810
811        // If proof type is EXISTING, expect account meta
812        if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
813            if offset + TN_ACCOUNT_META_FOOTPRINT > bytes.len() {
814                return Err(RpcError::invalid_format());
815            }
816            offset += TN_ACCOUNT_META_FOOTPRINT;
817        }
818    }
819
820    // 8. Check for exact size match (no trailing bytes)
821    if offset != bytes.len() {
822        // return Err(RpcError::invalid_format());
823        return Err(RpcError::trailing_bytes(offset, bytes.len()));
824    }
825    // 5. Signature check (fee payer signature)
826    let wire_for_signing = &bytes[64..]; // Exclude signature field
827    if verify_transaction(
828        wire_for_signing,
829        &hdr.fee_payer_signature,
830        &hdr.fee_payer_pubkey,
831    )
832    .is_err()
833    {
834        return Err(RpcError::invalid_transaction_signature());
835    }
836
837    Ok(())
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use ed25519_dalek::SigningKey;
844
845    fn make_valid_txn_bytes_with_flags(flags: u8) -> Vec<u8> {
846        let signing_key = SigningKey::from(&[1u8; 32]);
847        let verifying_key = signing_key.verifying_key();
848        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
849        tx.rw_accs = Some(vec![[3u8; 32], [4u8; 32]]);
850        tx.r_accs = Some(vec![[5u8; 32]]);
851        tx.instructions = Some(vec![1, 2, 3, 4]);
852        tx.flags = flags;
853        tx.sign(&signing_key.to_bytes()).unwrap();
854        tx.to_wire()
855    }
856
857    fn make_valid_txn_bytes() -> Vec<u8> {
858        make_valid_txn_bytes_with_flags(0)
859    }
860
861    #[test]
862    fn test_tn_txn_size_basic_transaction() {
863        let bytes = make_valid_txn_bytes();
864        let calculated_size = tn_txn_size(&bytes).unwrap();
865
866        // The calculated size should match the actual bytes length
867        assert_eq!(calculated_size, bytes.len());
868    }
869
870    #[test]
871    fn test_tn_txn_size_with_state_proof() {
872        use crate::tn_state_proof::StateProof;
873
874        let signing_key = SigningKey::from(&[1u8; 32]);
875        let verifying_key = signing_key.verifying_key();
876
877        // Create a CREATION state proof
878        let path_bitset = [0u8; 32]; // No set bits = no sibling hashes
879        let existing_leaf_pubkey = [7u8; 32];
880        let existing_leaf_hash = [8u8; 32];
881        let state_proof = StateProof::creation(
882            100,
883            path_bitset,
884            existing_leaf_pubkey,
885            existing_leaf_hash,
886            vec![],
887        );
888
889        // Create transaction with state proof
890        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
891            .with_rw_accounts(vec![[3u8; 32]])
892            .with_instructions(vec![1, 2, 3])
893            .with_fee_payer_state_proof(&state_proof);
894
895        tx.sign(&signing_key.to_bytes()).unwrap();
896        let bytes = tx.to_wire();
897
898        let calculated_size = tn_txn_size(&bytes).unwrap();
899
900        // The calculated size should match the actual bytes length
901        assert_eq!(calculated_size, bytes.len());
902    }
903
904    #[test]
905    fn test_tn_txn_size_minimal_transaction() {
906        let signing_key = SigningKey::from(&[1u8; 32]);
907        let verifying_key = signing_key.verifying_key();
908
909        // Create minimal transaction (no accounts, no instructions)
910        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
911        tx.sign(&signing_key.to_bytes()).unwrap();
912        let bytes = tx.to_wire();
913
914        let calculated_size = tn_txn_size(&bytes).unwrap();
915
916        // The calculated size should match the actual bytes length
917        assert_eq!(calculated_size, bytes.len());
918
919        // Should be exactly the header size for minimal transaction
920        assert_eq!(calculated_size, core::mem::size_of::<WireTxnHdrV1>());
921    }
922
923    #[test]
924    fn test_tn_txn_size_invalid_format() {
925        // Test with bytes too short for header
926        let short_bytes = vec![0u8; 50];
927        let result = tn_txn_size(&short_bytes);
928        assert!(matches!(result, Err(RpcError::InvalidFormat)));
929
930        // Test with header but missing account data
931        let mut bytes = make_valid_txn_bytes();
932        bytes.truncate(core::mem::size_of::<WireTxnHdrV1>() + 10); // Truncate to cause missing data
933        let result = tn_txn_size(&bytes);
934        assert!(matches!(result, Err(RpcError::InvalidFormat)));
935    }
936
937    #[test]
938    fn test_tn_txn_size_consistency_with_validation() {
939        let bytes = make_valid_txn_bytes();
940
941        // Both functions should succeed for valid transactions
942        assert!(validate_wire_transaction(&bytes).is_ok());
943        assert!(tn_txn_size(&bytes).is_ok());
944
945        // Size should match actual length
946        let calculated_size = tn_txn_size(&bytes).unwrap();
947        assert_eq!(calculated_size, bytes.len());
948    }
949
950    #[test]
951    fn test_valid_transaction() {
952        let bytes = make_valid_txn_bytes();
953        assert!(validate_wire_transaction(&bytes).is_ok());
954    }
955
956    #[test]
957    fn test_oversize_transaction() {
958        let mut bytes = make_valid_txn_bytes();
959        bytes.resize(32_769, 0);
960        let err = validate_wire_transaction(&bytes).unwrap_err();
961        assert!(matches!(
962            err,
963            RpcError::InvalidTransactionSize {
964                size: 32_769,
965                max_size: 32_768
966            }
967        ));
968    }
969
970    #[test]
971    fn test_trailing_bytes() {
972        let mut bytes = make_valid_txn_bytes();
973        bytes.push(0);
974        let err = validate_wire_transaction(&bytes).unwrap_err();
975        assert!(matches!(
976            err,
977            RpcError::TrailingBytes {
978                expected: 276,
979                found: 277
980            }
981        ));
982    }
983
984    #[test]
985    fn test_invalid_transaction_version() {
986        let mut bytes = make_valid_txn_bytes();
987        // Corrupt the transaction version (at offset 64)
988        bytes[64] = 0x02; // Invalid version
989        let err = validate_wire_transaction(&bytes).unwrap_err();
990        assert!(matches!(err, RpcError::InvalidVersion));
991    }
992
993    #[test]
994    fn test_invalid_flags() {
995        // Set invalid flag bits (keeping fee payer proof bit, but adding others)
996        let bytes = make_valid_txn_bytes_with_flags(0x07);
997        let err = validate_wire_transaction(&bytes).unwrap_err();
998        assert!(matches!(err, RpcError::InvalidFlags));
999    }
1000
1001    #[test]
1002    fn test_transaction_too_short() {
1003        let bytes = vec![0u8; 50]; // Too short for header
1004        let err = validate_wire_transaction(&bytes).unwrap_err();
1005        assert!(matches!(err, RpcError::InvalidFormat));
1006    }
1007
1008    #[test]
1009    fn test_transaction_with_state_proof() {
1010        use crate::tn_state_proof::{StateProof, StateProofType};
1011
1012        let signing_key = SigningKey::from(&[1u8; 32]);
1013        let verifying_key = signing_key.verifying_key();
1014
1015        // Create a CREATION state proof (doesn't require account meta)
1016        let path_bitset = [0u8; 32]; // No set bits = no sibling hashes
1017        let existing_leaf_pubkey = [7u8; 32];
1018        let existing_leaf_hash = [8u8; 32];
1019        let state_proof = StateProof::creation(
1020            100,
1021            path_bitset,
1022            existing_leaf_pubkey,
1023            existing_leaf_hash,
1024            vec![],
1025        );
1026
1027        // Create transaction with state proof
1028        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1029            .with_fee_payer_state_proof(&state_proof);
1030
1031        // Verify flag is set
1032        assert!(tx.has_fee_payer_state_proof());
1033        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 1);
1034
1035        tx.sign(&signing_key.to_bytes()).unwrap();
1036        let bytes = tx.to_wire();
1037
1038        // Verify state proof is included in wire format
1039        assert!(bytes.len() > 168); // Header + state proof should be larger
1040        assert!(validate_wire_transaction(&bytes).is_ok());
1041
1042        // Test deserialization
1043        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1044        assert!(decoded_tx.has_fee_payer_state_proof());
1045        assert!(decoded_tx.fee_payer_state_proof.is_some());
1046
1047        let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
1048        assert_eq!(decoded_proof.proof_type(), StateProofType::Creation);
1049        assert_eq!(decoded_proof.slot(), 100);
1050    }
1051
1052    #[test]
1053    fn test_transaction_with_state_proof_serialization_round_trip() {
1054        use crate::tn_state_proof::StateProof;
1055
1056        let signing_key = SigningKey::from(&[1u8; 32]);
1057        let verifying_key = signing_key.verifying_key();
1058
1059        // Create a creation state proof with some sibling hashes
1060        let mut path_bitset = [0u8; 32];
1061        path_bitset[0] = 0b11; // Set first 2 bits for 2 sibling hashes
1062        let existing_leaf_pubkey = [7u8; 32];
1063        let existing_leaf_hash = [8u8; 32];
1064        let sibling_hashes = vec![[9u8; 32], [10u8; 32]];
1065
1066        let state_proof = StateProof::creation(
1067            200,
1068            path_bitset,
1069            existing_leaf_pubkey,
1070            existing_leaf_hash,
1071            sibling_hashes.clone(),
1072        );
1073
1074        // Create transaction with complex state proof
1075        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1076            .with_rw_accounts(vec![[3u8; 32], [4u8; 32]])
1077            .with_r_accounts(vec![[5u8; 32]])
1078            .with_instructions(vec![1, 2, 3, 4])
1079            .with_fee_payer_state_proof(&state_proof);
1080
1081        tx.sign(&signing_key.to_bytes()).unwrap();
1082        let bytes = tx.to_wire();
1083
1084        // Test validation
1085        assert!(validate_wire_transaction(&bytes).is_ok());
1086
1087        // Test round-trip serialization
1088        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1089        assert_eq!(decoded_tx.fee_payer, tx.fee_payer);
1090        assert_eq!(decoded_tx.program, tx.program);
1091        assert_eq!(decoded_tx.rw_accs, tx.rw_accs);
1092        assert_eq!(decoded_tx.r_accs, tx.r_accs);
1093        assert_eq!(decoded_tx.instructions, tx.instructions);
1094        assert_eq!(decoded_tx.flags, tx.flags);
1095        assert!(decoded_tx.has_fee_payer_state_proof());
1096
1097        let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
1098        assert_eq!(decoded_proof.slot(), 200);
1099        assert_eq!(decoded_proof.path_bitset(), &path_bitset);
1100    }
1101
1102    #[test]
1103    fn test_transaction_without_state_proof() {
1104        let signing_key = SigningKey::from(&[1u8; 32]);
1105        let verifying_key = signing_key.verifying_key();
1106
1107        let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
1108
1109        // Verify flag is not set
1110        assert!(!tx.has_fee_payer_state_proof());
1111        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
1112        assert!(tx.fee_payer_state_proof.is_none());
1113
1114        tx.sign(&signing_key.to_bytes()).unwrap();
1115        let bytes = tx.to_wire();
1116
1117        assert!(validate_wire_transaction(&bytes).is_ok());
1118
1119        // Test deserialization
1120        let decoded_tx = Transaction::from_wire(&bytes).unwrap();
1121        assert!(!decoded_tx.has_fee_payer_state_proof());
1122        assert!(decoded_tx.fee_payer_state_proof.is_none());
1123    }
1124
1125    #[test]
1126    fn test_transaction_remove_state_proof() {
1127        use crate::tn_state_proof::StateProof;
1128
1129        let signing_key = SigningKey::from(&[1u8; 32]);
1130        let verifying_key = signing_key.verifying_key();
1131
1132        // Create a CREATION state proof
1133        let path_bitset = [0u8; 32];
1134        let existing_leaf_pubkey = [7u8; 32];
1135        let existing_leaf_hash = [8u8; 32];
1136        let state_proof = StateProof::creation(
1137            100,
1138            path_bitset,
1139            existing_leaf_pubkey,
1140            existing_leaf_hash,
1141            vec![],
1142        );
1143
1144        // Create transaction with state proof, then remove it
1145        let tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
1146            .with_fee_payer_state_proof(&state_proof)
1147            .without_fee_payer_state_proof();
1148
1149        // Verify flag is cleared and state proof is removed
1150        assert!(!tx.has_fee_payer_state_proof());
1151        assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
1152        assert!(tx.fee_payer_state_proof.is_none());
1153    }
1154}