1use core::cmp::Ordering;
2use std_shims::prelude::*;
3use std_shims::io::{self, Read, Write};
4
5use zeroize::Zeroize;
6
7use crate::{
8 io::*,
9 ed25519::*,
10 primitives::{UpperBound, LowerBound, keccak256},
11 ring_signatures::RingSignature,
12 ringct::{bulletproofs::Bulletproof, PrunedRctProofs},
13};
14
15#[derive(Clone, PartialEq, Eq, Debug)]
17pub enum Input {
18 Gen(usize),
20 ToKey {
22 amount: Option<u64>,
24 key_offsets: Vec<u64>,
26 key_image: CompressedPoint,
28 },
29}
30
31impl Input {
32 const NON_GEN_SIZE_LOWER_BOUND: LowerBound<usize> =
35 LowerBound(1 + <u64 as VarInt>::LOWER_BOUND + <usize as VarInt>::LOWER_BOUND + 32);
36
37 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
39 match self {
40 Input::Gen(height) => {
41 w.write_all(&[255])?;
42 VarInt::write(height, w)
43 }
44
45 Input::ToKey { amount, key_offsets, key_image } => {
46 w.write_all(&[2])?;
47 VarInt::write(&amount.unwrap_or(0), w)?;
48 write_vec(VarInt::write, key_offsets, w)?;
49 key_image.write(w)
50 }
51 }
52 }
53
54 pub fn serialize(&self) -> Vec<u8> {
56 let mut res = vec![];
57 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
58 res
59 }
60
61 pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
63 Ok(match read_byte(r)? {
64 255 => Input::Gen(VarInt::read(r)?),
65 2 => {
66 let amount = VarInt::read(r)?;
67 let amount = if amount == 0 { None } else { Some(amount) };
73 Input::ToKey {
74 amount,
75 key_offsets: read_vec(
77 VarInt::read,
78 Some(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0),
79 r,
80 )?,
81 key_image: CompressedPoint::read(r)?,
82 }
83 }
84 _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
85 })
86 }
87}
88
89#[derive(Clone, PartialEq, Eq, Debug)]
91pub struct Output {
92 pub amount: Option<u64>,
94 pub key: CompressedPoint,
96 pub view_tag: Option<u8>,
98}
99
100impl Output {
101 pub const SIZE_LOWER_BOUND: LowerBound<usize> = LowerBound(<u64 as VarInt>::LOWER_BOUND + 1 + 32);
103 pub const SIZE_UPPER_BOUND: UpperBound<usize> =
105 UpperBound(<u64 as VarInt>::UPPER_BOUND + 1 + 32 + 1);
106
107 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
109 VarInt::write(&self.amount.unwrap_or(0), w)?;
110 w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
111 w.write_all(&self.key.to_bytes())?;
112 if let Some(view_tag) = self.view_tag {
113 w.write_all(&[view_tag])?;
114 }
115 Ok(())
116 }
117
118 pub fn serialize(&self) -> Vec<u8> {
120 let mut res = Vec::with_capacity(Self::SIZE_UPPER_BOUND.0);
121 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
122 res
123 }
124
125 pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
127 let amount = VarInt::read(r)?;
128 let amount = if rct {
129 if amount != 0 {
130 Err(io::Error::other("RCT TX output wasn't 0"))?;
131 }
132 None
133 } else {
134 Some(amount)
135 };
136
137 let view_tag = match read_byte(r)? {
138 2 => false,
139 3 => true,
140 _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
141 };
142
143 Ok(Output {
144 amount,
145 key: CompressedPoint::read(r)?,
146 view_tag: if view_tag { Some(read_byte(r)?) } else { None },
147 })
148 }
149}
150
151#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
156pub enum Timelock {
157 None,
159 Block(usize),
161 Time(u64),
163}
164
165impl Timelock {
166 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
168 match self {
169 Timelock::None => VarInt::write(&0u8, w),
170 Timelock::Block(block) => VarInt::write(block, w),
171 Timelock::Time(time) => VarInt::write(time, w),
172 }
173 }
174
175 pub fn serialize(&self) -> Vec<u8> {
177 let mut res = Vec::with_capacity(1);
178 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
179 res
180 }
181
182 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
184 const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000;
185
186 let raw = <u64 as VarInt>::read(r)?;
187 Ok(if raw == 0 {
188 Timelock::None
189 } else if raw <
190 u64::try_from(TIMELOCK_BLOCK_THRESHOLD)
191 .expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64")
192 {
193 Timelock::Block(usize::try_from(raw).expect(
194 "timelock overflowed usize despite being less than a const representable with a usize",
195 ))
196 } else {
197 Timelock::Time(raw)
198 })
199 }
200}
201
202impl PartialOrd for Timelock {
203 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
204 match (self, other) {
205 (Timelock::None, Timelock::None) => Some(Ordering::Equal),
206 (Timelock::None, _) => Some(Ordering::Less),
207 (_, Timelock::None) => Some(Ordering::Greater),
208 (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
209 (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
210 _ => None,
211 }
212 }
213}
214
215#[derive(Clone, PartialEq, Eq, Debug)]
220pub struct TransactionPrefix {
221 pub additional_timelock: Timelock,
226 pub inputs: Vec<Input>,
228 pub outputs: Vec<Output>,
230 pub extra: Vec<u8>,
235}
236
237impl TransactionPrefix {
238 pub const MINER_INPUTS: usize = 1;
240 pub const NON_MINER_INPUTS_UPPER_BOUND: UpperBound<usize> = UpperBound(
244 Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0 / Input::NON_GEN_SIZE_LOWER_BOUND.0,
245 );
246 pub const INPUTS_UPPER_BOUND: UpperBound<usize> = UpperBound(monero_primitives::const_max!(
248 Self::MINER_INPUTS,
249 Self::NON_MINER_INPUTS_UPPER_BOUND.0
250 ));
251
252 pub const NON_MINER_OUTPUTS_UPPER_BOUND: UpperBound<usize> =
254 UpperBound(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0 / Output::SIZE_LOWER_BOUND.0);
255
256 fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
260 self.additional_timelock.write(w)?;
261 write_vec(Input::write, &self.inputs, w)?;
262 write_vec(Output::write, &self.outputs, w)?;
263 VarInt::write(&self.extra.len(), w)?;
264 w.write_all(&self.extra)
265 }
266
267 pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
276 let additional_timelock = Timelock::read(r)?;
277
278 let inputs = read_vec(|r| Input::read(r), Some(Self::INPUTS_UPPER_BOUND.0), r)?;
279 if inputs.is_empty() {
280 Err(io::Error::other("transaction had no inputs"))?;
281 }
282 let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
283
284 let max_outputs = if is_miner_tx { None } else { Some(Self::NON_MINER_OUTPUTS_UPPER_BOUND.0) };
285 let mut prefix = TransactionPrefix {
286 additional_timelock,
287 inputs,
288 outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), max_outputs, r)?,
289 extra: vec![],
290 };
291 let max_extra =
293 if is_miner_tx { None } else { Some(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0) };
294 prefix.extra = read_vec(read_byte, max_extra, r)?;
295 Ok(prefix)
296 }
297
298 fn hash(&self, version: u64) -> [u8; 32] {
299 let mut buf = vec![];
300 VarInt::write(&version, &mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
301 self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
302 keccak256(buf)
303 }
304}
305
306#[expect(private_bounds)]
307mod sealed {
308 use core::fmt::Debug;
309 use crate::ringct::*;
310 use super::*;
311
312 pub(crate) trait PotentiallyPrunedRingSignatures:
313 Clone + PartialEq + Eq + Default + Debug
314 {
315 fn signatures_to_write(&self) -> &[RingSignature];
316 fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self>;
317 }
318
319 impl PotentiallyPrunedRingSignatures for Vec<RingSignature> {
320 fn signatures_to_write(&self) -> &[RingSignature] {
321 self
322 }
323 fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self> {
324 let mut signatures = Vec::with_capacity(inputs.len());
325 for input in inputs {
326 match input {
327 Input::ToKey { key_offsets, .. } => {
328 signatures.push(RingSignature::read(key_offsets.len(), r)?);
329 }
330 Input::Gen { .. } => {
331 Err(io::Error::other("reading signatures for a transaction with non-`ToKey` inputs"))?;
332 }
333 }
334 }
335 Ok(signatures)
336 }
337 }
338
339 impl PotentiallyPrunedRingSignatures for () {
340 fn signatures_to_write(&self) -> &[RingSignature] {
341 &[]
342 }
343 fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result<Self> {
344 Ok(())
345 }
346 }
347
348 pub(crate) trait PotentiallyPrunedRctProofs: Clone + PartialEq + Eq + Debug {
349 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()>;
350 fn potentially_pruned_read(
351 ring_length: usize,
352 inputs: usize,
353 outputs: usize,
354 r: &mut impl Read,
355 ) -> io::Result<Option<Self>>;
356 fn potentially_pruned_rct_type(&self) -> RctType;
357 fn base(&self) -> &RctBase;
358 }
359
360 impl PotentiallyPrunedRctProofs for RctProofs {
361 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()> {
362 self.write(w)
363 }
364 fn potentially_pruned_read(
365 ring_length: usize,
366 inputs: usize,
367 outputs: usize,
368 r: &mut impl Read,
369 ) -> io::Result<Option<Self>> {
370 RctProofs::read(ring_length, inputs, outputs, r)
371 }
372 fn potentially_pruned_rct_type(&self) -> RctType {
373 self.rct_type()
374 }
375 fn base(&self) -> &RctBase {
376 &self.base
377 }
378 }
379
380 impl PotentiallyPrunedRctProofs for PrunedRctProofs {
381 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()> {
382 self.base.write(w, self.rct_type)
383 }
384 fn potentially_pruned_read(
385 _ring_length: usize,
386 inputs: usize,
387 outputs: usize,
388 r: &mut impl Read,
389 ) -> io::Result<Option<Self>> {
390 Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base }))
391 }
392 fn potentially_pruned_rct_type(&self) -> RctType {
393 self.rct_type
394 }
395 fn base(&self) -> &RctBase {
396 &self.base
397 }
398 }
399
400 trait Sealed {}
401
402 pub trait PotentiallyPruned: Sealed {
404 type RingSignatures: PotentiallyPrunedRingSignatures;
406 type RctProofs: PotentiallyPrunedRctProofs;
408 }
409 #[derive(Clone, PartialEq, Eq, Debug)]
411 pub struct NotPruned;
412 impl Sealed for NotPruned {}
413 impl PotentiallyPruned for NotPruned {
414 type RingSignatures = Vec<RingSignature>;
415 type RctProofs = RctProofs;
416 }
417 #[derive(Clone, PartialEq, Eq, Debug)]
419 pub struct Pruned;
420 impl Sealed for Pruned {}
421 impl PotentiallyPruned for Pruned {
422 type RingSignatures = ();
423 type RctProofs = PrunedRctProofs;
424 }
425}
426pub use sealed::*;
427
428#[derive(Clone, PartialEq, Eq, Debug)]
430pub enum Transaction<P: PotentiallyPruned = NotPruned> {
431 V1 {
433 prefix: TransactionPrefix,
435 signatures: P::RingSignatures,
437 },
438 V2 {
440 prefix: TransactionPrefix,
442 proofs: Option<P::RctProofs>,
444 },
445}
446
447enum PrunableHash<'a> {
448 V1(&'a [RingSignature]),
449 V2([u8; 32]),
450}
451
452impl<P: PotentiallyPruned> Transaction<P> {
453 pub fn version(&self) -> u8 {
455 match self {
456 Transaction::V1 { .. } => 1,
457 Transaction::V2 { .. } => 2,
458 }
459 }
460
461 pub fn prefix(&self) -> &TransactionPrefix {
463 match self {
464 Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
465 }
466 }
467
468 pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
470 match self {
471 Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
472 }
473 }
474
475 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
480 VarInt::write(&self.version(), w)?;
481 match self {
482 Transaction::V1 { prefix, signatures } => {
483 prefix.write(w)?;
484 for ring_sig in signatures.signatures_to_write() {
485 ring_sig.write(w)?;
486 }
487 }
488 Transaction::V2 { prefix, proofs } => {
489 prefix.write(w)?;
490 match proofs {
491 None => w.write_all(&[0])?,
492 Some(proofs) => proofs.potentially_pruned_write(w)?,
493 }
494 }
495 }
496 Ok(())
497 }
498
499 pub fn serialize(&self) -> Vec<u8> {
501 let mut res = Vec::with_capacity(2048);
502 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
503 res
504 }
505
506 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
512 let version = VarInt::read(r)?;
513 let prefix = TransactionPrefix::read(r, version)?;
514
515 if version == 1 {
516 let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
517 Default::default()
518 } else {
519 P::RingSignatures::read_signatures(&prefix.inputs, r)?
520 };
521
522 Ok(Transaction::V1 { prefix, signatures })
523 } else if version == 2 {
524 let proofs = P::RctProofs::potentially_pruned_read(
525 prefix.inputs.first().map_or(0, |input| match input {
526 Input::Gen(_) => 0,
527 Input::ToKey { key_offsets, .. } => key_offsets.len(),
528 }),
529 prefix.inputs.len(),
530 prefix.outputs.len(),
531 r,
532 )?;
533
534 Ok(Transaction::V2 { prefix, proofs })
535 } else {
536 Err(io::Error::other("tried to deserialize unknown version"))
537 }
538 }
539
540 #[expect(clippy::needless_pass_by_value)]
542 fn hash_with_prunable_hash_internal(&self, prunable: PrunableHash<'_>) -> [u8; 32] {
543 match self {
544 Transaction::V1 { prefix, .. } => {
545 let mut buf = Vec::with_capacity(512);
546
547 VarInt::write(&self.version(), &mut buf)
549 .expect("write failed but <Vec as io::Write> doesn't fail");
550 prefix.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
551
552 let PrunableHash::V1(signatures) = prunable else {
554 panic!("hashing v1 TX with non-v1 prunable data")
555 };
556 for signature in signatures {
557 signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
558 }
559
560 keccak256(buf)
561 }
562 Transaction::V2 { prefix, proofs } => {
563 let mut hashes = Vec::with_capacity(96);
564
565 hashes.extend(prefix.hash(2));
566
567 if let Some(proofs) = proofs {
568 let mut buf = Vec::with_capacity(512);
569 proofs
570 .base()
571 .write(&mut buf, proofs.potentially_pruned_rct_type())
572 .expect("write failed but <Vec as io::Write> doesn't fail");
573 hashes.extend(keccak256(&buf));
574 } else {
575 hashes.extend(keccak256([0]));
577 }
578 let PrunableHash::V2(prunable_hash) = prunable else {
579 panic!("hashing v2 TX with non-v2 prunable data")
580 };
581 hashes.extend(prunable_hash);
582
583 keccak256(hashes)
584 }
585 }
586 }
587}
588
589impl Transaction<NotPruned> {
590 pub const NON_MINER_SIZE_UPPER_BOUND: UpperBound<usize> = UpperBound(1_000_000);
594
595 pub fn prunable_hash(&self) -> Option<[u8; 32]> {
599 match self {
600 Transaction::V1 { .. } => None,
601 Transaction::V2 { proofs, .. } => Some(if let Some(proofs) = proofs {
602 let mut buf = Vec::with_capacity(1024);
603 proofs
604 .prunable
605 .write(&mut buf, proofs.rct_type())
606 .expect("write failed but <Vec as io::Write> doesn't fail");
607 keccak256(buf)
608 } else {
609 [0; 32]
610 }),
611 }
612 }
613
614 pub fn hash(&self) -> [u8; 32] {
616 match self {
617 Transaction::V1 { signatures, .. } => {
618 self.hash_with_prunable_hash_internal(PrunableHash::V1(signatures))
619 }
620 Transaction::V2 { .. } => self.hash_with_prunable_hash_internal(PrunableHash::V2(
621 self.prunable_hash().expect("V2 transaction didn't have a prunable hash"),
622 )),
623 }
624 }
625
626 pub fn signature_hash(&self) -> Option<[u8; 32]> {
630 Some(match self {
631 Transaction::V1 { prefix, .. } => {
632 if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
633 None?;
634 }
635 self.hash_with_prunable_hash_internal(PrunableHash::V1(&[]))
636 }
637 Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash_internal({
638 let Some(proofs) = proofs else { None? };
639 let mut buf = Vec::with_capacity(1024);
640 proofs
641 .prunable
642 .signature_write(&mut buf)
643 .expect("write failed but <Vec as io::Write> doesn't fail");
644 PrunableHash::V2(keccak256(buf))
645 }),
646 })
647 }
648
649 pub fn pruned_with_prunable(self) -> (Transaction<Pruned>, Vec<u8>) {
651 let mut buf = Vec::with_capacity(512);
652
653 match self {
654 Transaction::V1 { prefix, signatures } => {
655 for signature in signatures {
656 signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
657 }
658
659 (Transaction::V1 { prefix, signatures: () }, buf)
660 }
661 Transaction::V2 { prefix, proofs } => {
662 match &proofs {
663 None => (),
664 Some(proofs) => proofs.prunable.write(&mut buf, proofs.rct_type()).unwrap(),
665 }
666
667 (
668 Transaction::V2 {
669 prefix,
670 proofs: proofs
671 .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
672 },
673 buf,
674 )
675 }
676 }
677 }
678
679 fn is_rct_bulletproof(&self) -> bool {
680 match self {
681 Transaction::V1 { .. } => false,
682 Transaction::V2 { proofs, .. } => {
683 let Some(proofs) = proofs else { return false };
684 proofs.rct_type().bulletproof()
685 }
686 }
687 }
688
689 fn is_rct_bulletproof_plus(&self) -> bool {
690 match self {
691 Transaction::V1 { .. } => false,
692 Transaction::V2 { proofs, .. } => {
693 let Some(proofs) = proofs else { return false };
694 proofs.rct_type().bulletproof_plus()
695 }
696 }
697 }
698
699 pub fn weight(&self) -> usize {
701 let blob_size = self.serialize().len();
702
703 let bp = self.is_rct_bulletproof();
704 let bp_plus = self.is_rct_bulletproof_plus();
705 if !(bp || bp_plus) {
706 blob_size
707 } else {
708 blob_size +
709 Bulletproof::calculate_clawback(
710 bp_plus,
711 match self {
712 Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
713 Transaction::V2 { prefix, .. } => prefix.outputs.len(),
714 },
715 )
716 .0
717 }
718 }
719}
720
721impl Transaction<Pruned> {
722 pub fn hash_with_prunable_hash(&self, prunable_hash: [u8; 32]) -> Option<[u8; 32]> {
728 match self {
729 Transaction::V1 { .. } => None?,
730 Transaction::V2 { .. } => {
731 Some(self.hash_with_prunable_hash_internal(PrunableHash::V2(prunable_hash)))
732 }
733 }
734 }
735}
736
737impl From<Transaction<NotPruned>> for Transaction<Pruned> {
738 fn from(tx: Transaction<NotPruned>) -> Transaction<Pruned> {
739 match tx {
740 Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () },
741 Transaction::V2 { prefix, proofs } => Transaction::V2 {
742 prefix,
743 proofs: proofs
744 .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
745 },
746 }
747 }
748}