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#[expect(clippy::absurd_extreme_comparisons)]
17const _INPUT_GEN_MAY_USE_USIZE: () = {
18 assert!(usize::MAX >= 500_000_000);
21};
22
23#[derive(Clone, PartialEq, Eq, Debug)]
25pub enum Input {
26 Gen(usize),
28 ToKey {
30 amount: Option<u64>,
32 key_offsets: Vec<u64>,
34 key_image: CompressedPoint,
36 },
37}
38
39impl Input {
40 const NON_GEN_SIZE_LOWER_BOUND: LowerBound<usize> =
43 LowerBound(1 + <u64 as VarInt>::LOWER_BOUND + <usize as VarInt>::LOWER_BOUND + 32);
44
45 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
47 match self {
48 Input::Gen(height) => {
49 w.write_all(&[255])?;
50 VarInt::write(height, w)
51 }
52
53 Input::ToKey { amount, key_offsets, key_image } => {
54 w.write_all(&[2])?;
55 VarInt::write(&amount.unwrap_or(0), w)?;
56 write_vec(VarInt::write, key_offsets, w)?;
57 key_image.write(w)
58 }
59 }
60 }
61
62 pub fn serialize(&self) -> Vec<u8> {
64 let mut res = vec![];
65 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
66 res
67 }
68
69 pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
71 Ok(match read_byte(r)? {
72 255 => Input::Gen(VarInt::read(r)?),
73 2 => {
74 let amount = VarInt::read(r)?;
75 let amount = if amount == 0 { None } else { Some(amount) };
81 Input::ToKey {
82 amount,
83 key_offsets: read_vec(
85 VarInt::read,
86 Some(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0),
87 r,
88 )?,
89 key_image: CompressedPoint::read(r)?,
90 }
91 }
92 _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
93 })
94 }
95}
96
97#[derive(Clone, PartialEq, Eq, Debug)]
99pub struct Output {
100 pub amount: Option<u64>,
102 pub key: CompressedPoint,
104 pub view_tag: Option<u8>,
106}
107
108impl Output {
109 pub const SIZE_LOWER_BOUND: LowerBound<usize> = LowerBound(<u64 as VarInt>::LOWER_BOUND + 1 + 32);
111 pub const SIZE_UPPER_BOUND: UpperBound<usize> =
113 UpperBound(<u64 as VarInt>::UPPER_BOUND + 1 + 32 + 1);
114
115 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
117 VarInt::write(&self.amount.unwrap_or(0), w)?;
118 w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
119 w.write_all(&self.key.to_bytes())?;
120 if let Some(view_tag) = self.view_tag {
121 w.write_all(&[view_tag])?;
122 }
123 Ok(())
124 }
125
126 pub fn serialize(&self) -> Vec<u8> {
128 let mut res = Vec::with_capacity(Self::SIZE_UPPER_BOUND.0);
129 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
130 res
131 }
132
133 pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
135 let amount = VarInt::read(r)?;
136 let amount = if rct {
137 if amount != 0 {
138 Err(io::Error::other("RCT TX output wasn't 0"))?;
139 }
140 None
141 } else {
142 Some(amount)
143 };
144
145 let view_tag = match read_byte(r)? {
146 2 => false,
147 3 => true,
148 _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
149 };
150
151 Ok(Output {
152 amount,
153 key: CompressedPoint::read(r)?,
154 view_tag: if view_tag { Some(read_byte(r)?) } else { None },
155 })
156 }
157}
158
159#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
164pub enum Timelock {
165 None,
167 Block(usize),
169 Time(u64),
171}
172
173impl Timelock {
174 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
176 match self {
177 Timelock::None => VarInt::write(&0u8, w),
178 Timelock::Block(block) => VarInt::write(block, w),
179 Timelock::Time(time) => VarInt::write(time, w),
180 }
181 }
182
183 pub fn serialize(&self) -> Vec<u8> {
185 let mut res = Vec::with_capacity(1);
186 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
187 res
188 }
189
190 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
192 const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000;
193
194 let raw = <u64 as VarInt>::read(r)?;
195 Ok(if raw == 0 {
196 Timelock::None
197 } else if raw <
198 u64::try_from(TIMELOCK_BLOCK_THRESHOLD)
199 .expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64")
200 {
201 Timelock::Block(usize::try_from(raw).expect(
202 "timelock overflowed usize despite being less than a const representable with a usize",
203 ))
204 } else {
205 Timelock::Time(raw)
206 })
207 }
208}
209
210impl PartialOrd for Timelock {
211 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
212 match (self, other) {
213 (Timelock::None, Timelock::None) => Some(Ordering::Equal),
214 (Timelock::None, _) => Some(Ordering::Less),
215 (_, Timelock::None) => Some(Ordering::Greater),
216 (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
217 (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
218 _ => None,
219 }
220 }
221}
222
223#[derive(Clone, PartialEq, Eq, Debug)]
228pub struct TransactionPrefix {
229 pub additional_timelock: Timelock,
234 pub inputs: Vec<Input>,
236 pub outputs: Vec<Output>,
238 pub extra: Vec<u8>,
243}
244
245impl TransactionPrefix {
246 pub const MINER_INPUTS: usize = 1;
248 pub const NON_MINER_INPUTS_UPPER_BOUND: UpperBound<usize> = UpperBound(
252 Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0 / Input::NON_GEN_SIZE_LOWER_BOUND.0,
253 );
254 pub const INPUTS_UPPER_BOUND: UpperBound<usize> = UpperBound(monero_primitives::const_max!(
256 Self::MINER_INPUTS,
257 Self::NON_MINER_INPUTS_UPPER_BOUND.0
258 ));
259
260 pub const NON_MINER_OUTPUTS_UPPER_BOUND: UpperBound<usize> =
262 UpperBound(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0 / Output::SIZE_LOWER_BOUND.0);
263
264 fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
268 self.additional_timelock.write(w)?;
269 write_vec(Input::write, &self.inputs, w)?;
270 write_vec(Output::write, &self.outputs, w)?;
271 VarInt::write(&self.extra.len(), w)?;
272 w.write_all(&self.extra)
273 }
274
275 pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
284 let additional_timelock = Timelock::read(r)?;
285
286 let inputs = read_vec(|r| Input::read(r), Some(Self::INPUTS_UPPER_BOUND.0), r)?;
287 if inputs.is_empty() {
288 Err(io::Error::other("transaction had no inputs"))?;
289 }
290 let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
291
292 let max_outputs = if is_miner_tx { None } else { Some(Self::NON_MINER_OUTPUTS_UPPER_BOUND.0) };
293 let mut prefix = TransactionPrefix {
294 additional_timelock,
295 inputs,
296 outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), max_outputs, r)?,
297 extra: vec![],
298 };
299 let max_extra =
301 if is_miner_tx { None } else { Some(Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0) };
302 prefix.extra = read_vec(read_byte, max_extra, r)?;
303 Ok(prefix)
304 }
305
306 fn hash(&self, version: u64) -> [u8; 32] {
307 let mut buf = vec![];
308 VarInt::write(&version, &mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
309 self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
310 keccak256(buf)
311 }
312}
313
314#[expect(private_bounds)]
315mod sealed {
316 use core::fmt::Debug;
317 use crate::ringct::*;
318 use super::*;
319
320 pub(crate) trait PotentiallyPrunedRingSignatures:
321 Clone + PartialEq + Eq + Default + Debug
322 {
323 fn signatures_to_write(&self) -> &[RingSignature];
324 fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self>;
325 }
326
327 impl PotentiallyPrunedRingSignatures for Vec<RingSignature> {
328 fn signatures_to_write(&self) -> &[RingSignature] {
329 self
330 }
331 fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self> {
332 let mut signatures = Vec::with_capacity(inputs.len());
333 for input in inputs {
334 match input {
335 Input::ToKey { key_offsets, .. } => {
336 signatures.push(RingSignature::read(key_offsets.len(), r)?);
337 }
338 Input::Gen { .. } => {
339 Err(io::Error::other("reading signatures for a transaction with non-`ToKey` inputs"))?;
340 }
341 }
342 }
343 Ok(signatures)
344 }
345 }
346
347 impl PotentiallyPrunedRingSignatures for () {
348 fn signatures_to_write(&self) -> &[RingSignature] {
349 &[]
350 }
351 fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result<Self> {
352 Ok(())
353 }
354 }
355
356 pub(crate) trait PotentiallyPrunedRctProofs: Clone + PartialEq + Eq + Debug {
357 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()>;
358 fn potentially_pruned_read(
359 ring_length: usize,
360 inputs: usize,
361 outputs: usize,
362 r: &mut impl Read,
363 ) -> io::Result<Option<Self>>;
364 fn potentially_pruned_rct_type(&self) -> RctType;
365 fn base(&self) -> &RctBase;
366 }
367
368 impl PotentiallyPrunedRctProofs for RctProofs {
369 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()> {
370 self.write(w)
371 }
372 fn potentially_pruned_read(
373 ring_length: usize,
374 inputs: usize,
375 outputs: usize,
376 r: &mut impl Read,
377 ) -> io::Result<Option<Self>> {
378 RctProofs::read(ring_length, inputs, outputs, r)
379 }
380 fn potentially_pruned_rct_type(&self) -> RctType {
381 self.rct_type()
382 }
383 fn base(&self) -> &RctBase {
384 &self.base
385 }
386 }
387
388 impl PotentiallyPrunedRctProofs for PrunedRctProofs {
389 fn potentially_pruned_write(&self, w: &mut impl Write) -> io::Result<()> {
390 self.base.write(w, self.rct_type)
391 }
392 fn potentially_pruned_read(
393 _ring_length: usize,
394 inputs: usize,
395 outputs: usize,
396 r: &mut impl Read,
397 ) -> io::Result<Option<Self>> {
398 Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base }))
399 }
400 fn potentially_pruned_rct_type(&self) -> RctType {
401 self.rct_type
402 }
403 fn base(&self) -> &RctBase {
404 &self.base
405 }
406 }
407
408 trait Sealed {}
409
410 pub trait PotentiallyPruned: Sealed {
412 type RingSignatures: PotentiallyPrunedRingSignatures;
414 type RctProofs: PotentiallyPrunedRctProofs;
416 }
417 #[derive(Clone, PartialEq, Eq, Debug)]
419 pub struct NotPruned;
420 impl Sealed for NotPruned {}
421 impl PotentiallyPruned for NotPruned {
422 type RingSignatures = Vec<RingSignature>;
423 type RctProofs = RctProofs;
424 }
425 #[derive(Clone, PartialEq, Eq, Debug)]
427 pub struct Pruned;
428 impl Sealed for Pruned {}
429 impl PotentiallyPruned for Pruned {
430 type RingSignatures = ();
431 type RctProofs = PrunedRctProofs;
432 }
433}
434pub use sealed::*;
435
436#[derive(Clone, PartialEq, Eq, Debug)]
438pub enum Transaction<P: PotentiallyPruned = NotPruned> {
439 V1 {
441 prefix: TransactionPrefix,
443 signatures: P::RingSignatures,
445 },
446 V2 {
448 prefix: TransactionPrefix,
450 proofs: Option<P::RctProofs>,
452 },
453}
454
455enum PrunableHash<'a> {
456 V1(&'a [RingSignature]),
457 V2([u8; 32]),
458}
459
460impl<P: PotentiallyPruned> Transaction<P> {
461 pub fn version(&self) -> u8 {
463 match self {
464 Transaction::V1 { .. } => 1,
465 Transaction::V2 { .. } => 2,
466 }
467 }
468
469 pub fn prefix(&self) -> &TransactionPrefix {
471 match self {
472 Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
473 }
474 }
475
476 pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
478 match self {
479 Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
480 }
481 }
482
483 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
488 VarInt::write(&self.version(), w)?;
489 match self {
490 Transaction::V1 { prefix, signatures } => {
491 prefix.write(w)?;
492 for ring_sig in signatures.signatures_to_write() {
493 ring_sig.write(w)?;
494 }
495 }
496 Transaction::V2 { prefix, proofs } => {
497 prefix.write(w)?;
498 match proofs {
499 None => w.write_all(&[0])?,
500 Some(proofs) => proofs.potentially_pruned_write(w)?,
501 }
502 }
503 }
504 Ok(())
505 }
506
507 pub fn serialize(&self) -> Vec<u8> {
509 let mut res = Vec::with_capacity(2048);
510 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
511 res
512 }
513
514 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
520 let version = VarInt::read(r)?;
521 let prefix = TransactionPrefix::read(r, version)?;
522
523 if version == 1 {
524 let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
525 Default::default()
526 } else {
527 P::RingSignatures::read_signatures(&prefix.inputs, r)?
528 };
529
530 Ok(Transaction::V1 { prefix, signatures })
531 } else if version == 2 {
532 let proofs = P::RctProofs::potentially_pruned_read(
533 prefix.inputs.first().map_or(0, |input| match input {
534 Input::Gen(_) => 0,
535 Input::ToKey { key_offsets, .. } => key_offsets.len(),
536 }),
537 prefix.inputs.len(),
538 prefix.outputs.len(),
539 r,
540 )?;
541
542 Ok(Transaction::V2 { prefix, proofs })
543 } else {
544 Err(io::Error::other("tried to deserialize unknown version"))
545 }
546 }
547
548 #[expect(clippy::needless_pass_by_value)]
550 fn hash_with_prunable_hash_internal(&self, prunable: PrunableHash<'_>) -> [u8; 32] {
551 match self {
552 Transaction::V1 { prefix, .. } => {
553 let mut buf = Vec::with_capacity(512);
554
555 VarInt::write(&self.version(), &mut buf)
557 .expect("write failed but <Vec as io::Write> doesn't fail");
558 prefix.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
559
560 let PrunableHash::V1(signatures) = prunable else {
562 panic!("hashing v1 TX with non-v1 prunable data")
563 };
564 for signature in signatures {
565 signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
566 }
567
568 keccak256(buf)
569 }
570 Transaction::V2 { prefix, proofs } => {
571 let mut hashes = Vec::with_capacity(96);
572
573 hashes.extend(prefix.hash(2));
574
575 if let Some(proofs) = proofs {
576 let mut buf = Vec::with_capacity(512);
577 proofs
578 .base()
579 .write(&mut buf, proofs.potentially_pruned_rct_type())
580 .expect("write failed but <Vec as io::Write> doesn't fail");
581 hashes.extend(keccak256(&buf));
582 } else {
583 hashes.extend(keccak256([0]));
585 }
586 let PrunableHash::V2(prunable_hash) = prunable else {
587 panic!("hashing v2 TX with non-v2 prunable data")
588 };
589 hashes.extend(prunable_hash);
590
591 keccak256(hashes)
592 }
593 }
594 }
595}
596
597impl Transaction<NotPruned> {
598 pub const NON_MINER_SIZE_UPPER_BOUND: UpperBound<usize> = UpperBound(1_000_000);
602
603 pub fn prunable_hash(&self) -> Option<[u8; 32]> {
607 match self {
608 Transaction::V1 { .. } => None,
609 Transaction::V2 { proofs, .. } => Some(if let Some(proofs) = proofs {
610 let mut buf = Vec::with_capacity(1024);
611 proofs
612 .prunable
613 .write(&mut buf, proofs.rct_type())
614 .expect("write failed but <Vec as io::Write> doesn't fail");
615 keccak256(buf)
616 } else {
617 [0; 32]
618 }),
619 }
620 }
621
622 pub fn hash(&self) -> [u8; 32] {
624 match self {
625 Transaction::V1 { signatures, .. } => {
626 self.hash_with_prunable_hash_internal(PrunableHash::V1(signatures))
627 }
628 Transaction::V2 { .. } => self.hash_with_prunable_hash_internal(PrunableHash::V2(
629 self.prunable_hash().expect("V2 transaction didn't have a prunable hash"),
630 )),
631 }
632 }
633
634 pub fn signature_hash(&self) -> Option<[u8; 32]> {
638 Some(match self {
639 Transaction::V1 { prefix, .. } => {
640 if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
641 None?;
642 }
643 self.hash_with_prunable_hash_internal(PrunableHash::V1(&[]))
644 }
645 Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash_internal({
646 let Some(proofs) = proofs else { None? };
647 let mut buf = Vec::with_capacity(1024);
648 proofs
649 .prunable
650 .signature_write(&mut buf)
651 .expect("write failed but <Vec as io::Write> doesn't fail");
652 PrunableHash::V2(keccak256(buf))
653 }),
654 })
655 }
656
657 pub fn pruned_with_prunable(self) -> (Transaction<Pruned>, Vec<u8>) {
659 let mut buf = Vec::with_capacity(512);
660
661 match self {
662 Transaction::V1 { prefix, signatures } => {
663 for signature in signatures {
664 signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
665 }
666
667 (Transaction::V1 { prefix, signatures: () }, buf)
668 }
669 Transaction::V2 { prefix, proofs } => {
670 match &proofs {
671 None => (),
672 Some(proofs) => proofs.prunable.write(&mut buf, proofs.rct_type()).unwrap(),
673 }
674
675 (
676 Transaction::V2 {
677 prefix,
678 proofs: proofs
679 .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
680 },
681 buf,
682 )
683 }
684 }
685 }
686
687 fn is_rct_bulletproof(&self) -> bool {
688 match self {
689 Transaction::V1 { .. } => false,
690 Transaction::V2 { proofs, .. } => {
691 let Some(proofs) = proofs else { return false };
692 proofs.rct_type().bulletproof()
693 }
694 }
695 }
696
697 fn is_rct_bulletproof_plus(&self) -> bool {
698 match self {
699 Transaction::V1 { .. } => false,
700 Transaction::V2 { proofs, .. } => {
701 let Some(proofs) = proofs else { return false };
702 proofs.rct_type().bulletproof_plus()
703 }
704 }
705 }
706
707 pub fn weight(&self) -> usize {
709 let blob_size = self.serialize().len();
710
711 let bp = self.is_rct_bulletproof();
712 let bp_plus = self.is_rct_bulletproof_plus();
713 if !(bp || bp_plus) {
714 blob_size
715 } else {
716 blob_size +
717 Bulletproof::calculate_clawback(
718 bp_plus,
719 match self {
720 Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
721 Transaction::V2 { prefix, .. } => prefix.outputs.len(),
722 },
723 )
724 .0
725 }
726 }
727}
728
729impl Transaction<Pruned> {
730 pub fn hash_with_prunable_hash(&self, prunable_hash: [u8; 32]) -> Option<[u8; 32]> {
736 match self {
737 Transaction::V1 { .. } => None?,
738 Transaction::V2 { .. } => {
739 Some(self.hash_with_prunable_hash_internal(PrunableHash::V2(prunable_hash)))
740 }
741 }
742 }
743}
744
745impl From<Transaction<NotPruned>> for Transaction<Pruned> {
746 fn from(tx: Transaction<NotPruned>) -> Transaction<Pruned> {
747 match tx {
748 Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () },
749 Transaction::V2 { prefix, proofs } => Transaction::V2 {
750 prefix,
751 proofs: proofs
752 .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
753 },
754 }
755 }
756}