1use crate::account::{Keypair, PqSignature, Pubkey};
4use crate::hash::Hash;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Instruction {
11 pub program_id: Pubkey,
13
14 pub accounts: Vec<Pubkey>,
16
17 pub data: Vec<u8>,
19}
20
21pub const DEFAULT_COMPUTE_BUDGET: u64 = 200_000;
25
26pub const MAX_COMPUTE_BUDGET: u64 = 1_400_000;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Message {
33 pub instructions: Vec<Instruction>,
35
36 pub recent_blockhash: Hash,
38
39 #[serde(default)]
45 pub compute_budget: Option<u64>,
46
47 #[serde(default)]
52 pub compute_unit_price: Option<u64>,
53}
54
55impl Message {
56 pub fn new(instructions: Vec<Instruction>, recent_blockhash: Hash) -> Self {
57 Message {
58 instructions,
59 recent_blockhash,
60 compute_budget: None,
61 compute_unit_price: None,
62 }
63 }
64
65 pub fn effective_compute_budget(&self) -> u64 {
67 match self.compute_budget {
68 Some(b) if b > 0 => b.min(MAX_COMPUTE_BUDGET),
69 _ => DEFAULT_COMPUTE_BUDGET,
70 }
71 }
72
73 pub fn effective_compute_unit_price(&self) -> u64 {
75 self.compute_unit_price.unwrap_or(0)
76 }
77
78 pub fn serialize(&self) -> Vec<u8> {
84 bincode::serialize(self).unwrap_or_else(|e| {
85 panic!(
86 "FATAL: Message serialization failed ({}). This indicates data corruption or OOM.",
87 e
88 )
89 })
90 }
91
92 pub fn try_serialize(&self) -> Result<Vec<u8>, String> {
94 bincode::serialize(self).map_err(|e| format!("Message serialization failed: {}", e))
95 }
96
97 pub fn hash(&self) -> Hash {
99 Hash::hash(&self.serialize())
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
108pub enum TransactionType {
109 #[default]
110 Native,
111 Evm,
112}
113
114pub const TX_WIRE_MAGIC: [u8; 2] = [0x4D, 0x54];
119
120pub const TX_WIRE_VERSION: u8 = 1;
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Transaction {
126 pub signatures: Vec<PqSignature>,
129
130 pub message: Message,
132
133 #[serde(default)]
136 pub tx_type: TransactionType,
137}
138
139pub const MAX_INSTRUCTIONS_PER_TX: usize = 64;
141pub const MAX_SIGNATURES_PER_TX: usize = MAX_INSTRUCTIONS_PER_TX;
147pub const MAX_INSTRUCTION_DATA: usize = 204_800; pub const MAX_DEPLOY_INSTRUCTION_DATA: usize = 4_194_304; pub const MAX_TRANSACTION_SERIALIZED_SIZE: u64 = 5 * 1024 * 1024;
156pub const MAX_ACCOUNTS_PER_IX: usize = 64;
158
159impl Transaction {
160 pub fn new(message: Message) -> Self {
161 Transaction {
162 signatures: Vec::new(),
163 message,
164 tx_type: TransactionType::Native,
165 }
166 }
167
168 pub fn new_evm(message: Message) -> Self {
170 Transaction {
171 signatures: Vec::new(),
172 message,
173 tx_type: TransactionType::Evm,
174 }
175 }
176
177 pub fn is_evm(&self) -> bool {
179 self.tx_type == TransactionType::Evm
180 || self.message.recent_blockhash == crate::Hash([0xEE; 32])
181 }
182
183 pub fn signature(&self) -> Hash {
185 self.hash()
186 }
187
188 pub fn message_hash(&self) -> Hash {
196 self.message.hash()
197 }
198
199 pub fn sender(&self) -> Pubkey {
201 self.message.instructions[0].accounts[0]
202 }
203
204 pub fn hash(&self) -> Hash {
213 let data = bincode::serialize(self).expect("Transaction bincode serialization failed");
214 Hash::hash(&data)
215 }
216
217 pub fn required_signers(&self) -> Result<HashSet<Pubkey>, String> {
219 if self.message.instructions.is_empty() {
220 return Err("No instructions".to_string());
221 }
222
223 let mut required_signers = HashSet::new();
224 for ix in &self.message.instructions {
225 let Some(first_account) = ix.accounts.first() else {
226 return Err("Instruction has no accounts".to_string());
227 };
228 required_signers.insert(*first_account);
229 }
230
231 Ok(required_signers)
232 }
233
234 pub fn verify_required_signatures(&self) -> Result<HashSet<Pubkey>, String> {
237 if self.signatures.is_empty() {
238 return Err("No signatures".to_string());
239 }
240
241 let required_signers = self.required_signers()?;
242 if self.signatures.len() < required_signers.len() {
243 return Err(format!(
244 "Insufficient signatures: got {}, need {}",
245 self.signatures.len(),
246 required_signers.len()
247 ));
248 }
249
250 let message_bytes = self.message.serialize();
251 let mut verified_signers = HashSet::with_capacity(required_signers.len());
252
253 for signature in &self.signatures {
254 let signer = signature.signer_address();
255 if !required_signers.contains(&signer) || verified_signers.contains(&signer) {
256 continue;
257 }
258
259 if Keypair::verify(&signer, &message_bytes, signature) {
260 verified_signers.insert(signer);
261 }
262 }
263
264 for signer in &required_signers {
265 if !verified_signers.contains(signer) {
266 return Err(format!(
267 "Missing or invalid signature for account {}",
268 signer
269 ));
270 }
271 }
272
273 Ok(verified_signers)
274 }
275
276 pub fn validate_structure(&self) -> Result<(), String> {
278 if self.signatures.len() > MAX_SIGNATURES_PER_TX {
279 return Err(format!(
280 "Too many signatures: {} (max {})",
281 self.signatures.len(),
282 MAX_SIGNATURES_PER_TX
283 ));
284 }
285 if self.message.instructions.is_empty() {
286 return Err("No instructions".to_string());
287 }
288 if self.message.instructions.len() > MAX_INSTRUCTIONS_PER_TX {
289 return Err(format!(
290 "Too many instructions: {} (max {})",
291 self.message.instructions.len(),
292 MAX_INSTRUCTIONS_PER_TX
293 ));
294 }
295 for (i, ix) in self.message.instructions.iter().enumerate() {
296 let is_system_deploy = ix.program_id == crate::Pubkey([0u8; 32])
300 && !ix.data.is_empty()
301 && ix.data[0] == 17;
302 let is_contract_deploy =
303 ix.program_id == crate::Pubkey([0xFFu8; 32]) && ix.data.starts_with(b"{\"Deploy\"");
304 let data_limit = if is_system_deploy || is_contract_deploy {
305 MAX_DEPLOY_INSTRUCTION_DATA
306 } else {
307 MAX_INSTRUCTION_DATA
308 };
309 if ix.data.len() > data_limit {
310 return Err(format!(
311 "Instruction {} data too large: {} bytes (max {})",
312 i,
313 ix.data.len(),
314 data_limit
315 ));
316 }
317 if ix.accounts.len() > MAX_ACCOUNTS_PER_IX {
318 return Err(format!(
319 "Instruction {} has too many accounts: {} (max {})",
320 i,
321 ix.accounts.len(),
322 MAX_ACCOUNTS_PER_IX
323 ));
324 }
325 }
326 let serialized_size = bincode::serialized_size(self)
327 .map_err(|e| format!("Transaction size serialization failed: {}", e))?;
328 if serialized_size > MAX_TRANSACTION_SERIALIZED_SIZE {
329 return Err(format!(
330 "Transaction serialized size too large: {} bytes (max {})",
331 serialized_size, MAX_TRANSACTION_SERIALIZED_SIZE
332 ));
333 }
334 Ok(())
335 }
336
337 pub fn to_wire(&self) -> Vec<u8> {
344 let payload = bincode::serialize(self).expect("Transaction bincode serialization failed");
345 let mut buf = Vec::with_capacity(4 + payload.len());
346 buf.extend_from_slice(&TX_WIRE_MAGIC);
347 buf.push(TX_WIRE_VERSION);
348 buf.push(self.tx_type as u8);
349 buf.extend_from_slice(&payload);
350 buf
351 }
352
353 pub fn from_wire(data: &[u8], max_wire_bytes: u64) -> Result<Self, String> {
362 if data.len() as u64 > max_wire_bytes {
363 return Err(format!(
364 "Transaction wire payload too large: {} bytes (max {})",
365 data.len(),
366 max_wire_bytes
367 ));
368 }
369
370 if data.len() >= 4 && data[0..2] == TX_WIRE_MAGIC {
372 let version = data[2];
373 if version != TX_WIRE_VERSION {
374 return Err(format!("Unsupported wire version: {}", version));
375 }
376 let type_byte = data[3];
377 let tx_type = match type_byte {
378 0 => TransactionType::Native,
379 1 => TransactionType::Evm,
380 _ => return Err(format!("Unknown transaction type byte: {}", type_byte)),
381 };
382 let payload = &data[4..];
383 let mut tx: Self = bounded_bincode_deser(payload, max_wire_bytes)?;
384 tx.tx_type = tx_type;
386 return Ok(tx);
387 }
388
389 if data.first() == Some(&b'{') {
391 json_deser(data).or_else(|_| bounded_bincode_deser(data, max_wire_bytes))
393 } else {
394 bounded_bincode_deser(data, max_wire_bytes).or_else(|_| json_deser(data))
396 }
397 }
398}
399
400fn bounded_bincode_deser(bytes: &[u8], limit: u64) -> Result<Transaction, String> {
402 use bincode::Options;
403 match std::panic::catch_unwind(|| {
404 bincode::options()
405 .with_limit(limit)
406 .with_fixint_encoding()
407 .allow_trailing_bytes()
408 .deserialize(bytes)
409 }) {
410 Ok(Ok(tx)) => Ok(tx),
411 Ok(Err(e)) => Err(format!("bincode: {}", e)),
412 Err(_) => Err("bincode panicked during deserialization".to_string()),
413 }
414}
415
416fn json_deser(bytes: &[u8]) -> Result<Transaction, String> {
418 serde_json::from_slice(bytes).map_err(|e| format!("JSON: {}", e))
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_transaction_creation() {
427 let program_id = Pubkey([1u8; 32]);
428 let accounts = vec![Pubkey([2u8; 32]), Pubkey([3u8; 32])];
429
430 let instruction = Instruction {
431 program_id,
432 accounts,
433 data: vec![0, 1, 2, 3],
434 };
435
436 let message = Message::new(vec![instruction], Hash::hash(b"recent_block"));
437
438 let tx = Transaction::new(message);
439
440 println!("Transaction signature: {}", tx.signature());
441 assert_eq!(tx.signatures.len(), 0); }
443
444 #[test]
447 fn test_validate_structure_normal_instruction_200kb_limit() {
448 let ix = Instruction {
449 program_id: Pubkey([1u8; 32]),
450 accounts: vec![Pubkey([2u8; 32])],
451 data: vec![0u8; MAX_INSTRUCTION_DATA + 1],
452 };
453 let msg = Message::new(vec![ix], Hash::default());
454 let tx = Transaction::new(msg);
455 assert!(tx.validate_structure().is_err());
456 }
457
458 #[test]
459 fn test_validate_structure_deploy_instruction_allows_large_data() {
460 let mut data = vec![17u8]; data.extend_from_slice(&(100_000u32).to_le_bytes()); data.extend(vec![0u8; 100_000]); let ix = Instruction {
466 program_id: Pubkey([0u8; 32]), accounts: vec![Pubkey([2u8; 32]), Pubkey([3u8; 32])],
468 data,
469 };
470 let msg = Message::new(vec![ix], Hash::default());
471 let tx = Transaction::new(msg);
472 assert!(
473 tx.validate_structure().is_ok(),
474 "Deploy instruction should allow >200KB data"
475 );
476 }
477
478 #[test]
479 fn test_validate_structure_deploy_instruction_4mb_limit() {
480 let mut data = vec![17u8];
482 data.extend(vec![0u8; MAX_DEPLOY_INSTRUCTION_DATA - 1]); let ix = Instruction {
484 program_id: Pubkey([0u8; 32]),
485 accounts: vec![Pubkey([2u8; 32])],
486 data,
487 };
488 let msg = Message::new(vec![ix], Hash::default());
489 let tx = Transaction::new(msg);
490 assert!(tx.validate_structure().is_ok());
491
492 let mut data2 = vec![17u8];
494 data2.extend(vec![0u8; MAX_DEPLOY_INSTRUCTION_DATA + 1]);
495 let ix2 = Instruction {
496 program_id: Pubkey([0u8; 32]),
497 accounts: vec![Pubkey([2u8; 32])],
498 data: data2,
499 };
500 let msg2 = Message::new(vec![ix2], Hash::default());
501 let tx2 = Transaction::new(msg2);
502 assert!(
503 tx2.validate_structure().is_err(),
504 "Deploy instruction over 4MB should be rejected"
505 );
506 }
507
508 #[test]
509 fn test_validate_structure_rejects_too_many_signatures() {
510 let ix = Instruction {
511 program_id: Pubkey([1u8; 32]),
512 accounts: vec![Pubkey([2u8; 32])],
513 data: vec![0],
514 };
515 let msg = Message::new(vec![ix], Hash::default());
516 let mut tx = Transaction::new(msg);
517 tx.signatures = (0..=MAX_SIGNATURES_PER_TX)
518 .map(|idx| crate::account::PqSignature::test_fixture(idx as u8))
519 .collect();
520
521 let result = tx.validate_structure();
522 assert!(result.is_err());
523 assert!(result.unwrap_err().contains("Too many signatures"));
524 }
525
526 #[test]
527 fn test_validate_structure_rejects_serialized_tx_size_over_limit() {
528 let instructions = (0..26)
529 .map(|idx| Instruction {
530 program_id: Pubkey([idx as u8; 32]),
531 accounts: vec![Pubkey([2u8; 32])],
532 data: vec![idx as u8; MAX_INSTRUCTION_DATA],
533 })
534 .collect();
535 let tx = Transaction::new(Message::new(instructions, Hash::default()));
536
537 let serialized_size = bincode::serialized_size(&tx).unwrap();
538 assert!(
539 serialized_size > MAX_TRANSACTION_SERIALIZED_SIZE,
540 "fixture must exceed tx serialized-size cap"
541 );
542
543 let result = tx.validate_structure();
544 assert!(result.is_err());
545 assert!(result.unwrap_err().contains("serialized size too large"));
546 }
547
548 #[test]
549 fn test_from_wire_rejects_oversized_payload_before_deserialization() {
550 let bytes = vec![b'{'; MAX_TRANSACTION_SERIALIZED_SIZE as usize + 1];
551
552 let result = Transaction::from_wire(&bytes, MAX_TRANSACTION_SERIALIZED_SIZE);
553 assert!(result.is_err());
554 assert!(result.unwrap_err().contains("wire payload too large"));
555 }
556
557 #[test]
563 fn test_a3_01_data_field_included_in_signature_hash() {
564 let bh = Hash::default();
565
566 let ix1 = Instruction {
568 program_id: Pubkey([1u8; 32]),
569 accounts: vec![Pubkey([2u8; 32])],
570 data: vec![0x01, 0x02, 0x03],
571 };
572 let ix2 = Instruction {
573 program_id: Pubkey([1u8; 32]),
574 accounts: vec![Pubkey([2u8; 32])],
575 data: vec![0x01, 0x02, 0x04], };
577
578 let msg1 = Message::new(vec![ix1], bh);
579 let msg2 = Message::new(vec![ix2], bh);
580
581 assert_ne!(
583 msg1.serialize(),
584 msg2.serialize(),
585 "A3-01 REGRESSION: Messages with different data must serialize differently"
586 );
587
588 assert_ne!(
590 msg1.hash(),
591 msg2.hash(),
592 "A3-01 REGRESSION: Messages with different data must hash differently"
593 );
594 }
595
596 #[test]
598 fn test_a3_01_program_id_included_in_signature_hash() {
599 let bh = Hash::default();
600
601 let ix1 = Instruction {
602 program_id: Pubkey([1u8; 32]),
603 accounts: vec![Pubkey([2u8; 32])],
604 data: vec![0x01],
605 };
606 let ix2 = Instruction {
607 program_id: Pubkey([99u8; 32]), accounts: vec![Pubkey([2u8; 32])],
609 data: vec![0x01],
610 };
611
612 let msg1 = Message::new(vec![ix1], bh);
613 let msg2 = Message::new(vec![ix2], bh);
614
615 assert_ne!(
616 msg1.hash(),
617 msg2.hash(),
618 "A3-01 REGRESSION: Messages with different program_id must hash differently"
619 );
620 }
621
622 #[test]
624 fn test_a3_01_accounts_included_in_signature_hash() {
625 let bh = Hash::default();
626
627 let ix1 = Instruction {
628 program_id: Pubkey([1u8; 32]),
629 accounts: vec![Pubkey([2u8; 32])],
630 data: vec![0x01],
631 };
632 let ix2 = Instruction {
633 program_id: Pubkey([1u8; 32]),
634 accounts: vec![Pubkey([3u8; 32])], data: vec![0x01],
636 };
637
638 let msg1 = Message::new(vec![ix1], bh);
639 let msg2 = Message::new(vec![ix2], bh);
640
641 assert_ne!(
642 msg1.hash(),
643 msg2.hash(),
644 "A3-01 REGRESSION: Messages with different accounts must hash differently"
645 );
646 }
647
648 #[test]
657 fn test_cross_sdk_message_golden_vector() {
658 let ix = Instruction {
659 program_id: Pubkey([1u8; 32]),
660 accounts: vec![Pubkey([2u8; 32])],
661 data: vec![0x00, 0x01, 0x02, 0x03],
662 };
663 let msg = Message {
664 instructions: vec![ix],
665 recent_blockhash: crate::Hash::new([0xAA; 32]),
666 compute_budget: None,
667 compute_unit_price: None,
668 };
669
670 let bytes = msg.serialize();
671 let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
672
673 let expected = format!(
683 "{}{}{}{}{}{}{}",
684 "0100000000000000", "0101010101010101010101010101010101010101010101010101010101010101", "0100000000000000", "0202020202020202020202020202020202020202020202020202020202020202", "040000000000000000010203", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0000", );
692
693 assert_eq!(
694 hex, expected,
695 "K4-02 GOLDEN VECTOR MISMATCH!\n\
696 This means the Rust bincode serialization changed.\n\
697 JS/Python SDKs MUST also match this exact byte sequence.\n\
698 Got: {}\n\
699 Expected: {}",
700 hex, expected
701 );
702 }
703
704 #[test]
706 fn test_cross_sdk_transaction_golden_vector() {
707 let ix = Instruction {
708 program_id: Pubkey([1u8; 32]),
709 accounts: vec![Pubkey([2u8; 32])],
710 data: vec![0x00, 0x01, 0x02, 0x03],
711 };
712 let msg = Message {
713 instructions: vec![ix],
714 recent_blockhash: crate::Hash::new([0xAA; 32]),
715 compute_budget: None,
716 compute_unit_price: None,
717 };
718 let sig = crate::account::PqSignature::test_fixture(0xBB);
719 let tx = Transaction {
720 signatures: vec![sig],
721 message: msg,
722 tx_type: Default::default(),
723 };
724
725 let bytes = bincode::serialize(&tx).expect("tx serialization");
726 let tx_hash = Hash::hash(&bytes);
727
728 assert_eq!(
729 bytes.len(),
730 5_417,
731 "unexpected serialized PQ transaction length"
732 );
733 assert_eq!(
734 tx_hash,
735 Hash::from_hex("9d0eec7b657276b828c265995ce78b41a3e19b17ab354b11f37254bbc4ee2a91")
736 .unwrap()
737 );
738 }
739}