Skip to main content

monero_oxide/
transaction.rs

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// Ensure `usize` can be used to represent a block number, a prerequesite to so define `Input::Gen`
16#[expect(clippy::absurd_extreme_comparisons)]
17const _INPUT_GEN_MAY_USE_USIZE: () = {
18  // https://github.com/monero-project/monero
19  //   /blob/d85cfa04d86477dcb4001a21f30953a8ab6e1df2/src/cryptonote_config.h#L40
20  assert!(usize::MAX >= 500_000_000);
21};
22
23/// An input in the Monero protocol.
24#[derive(Clone, PartialEq, Eq, Debug)]
25pub enum Input {
26  /// An input for a miner transaction, which is generating new coins.
27  Gen(usize),
28  /// An input spending an output on-chain.
29  ToKey {
30    /// The pool this input spends an output of.
31    amount: Option<u64>,
32    /// The decoys used by this input's ring, specified as their offset distance from each other.
33    key_offsets: Vec<u64>,
34    /// The key image (linking tag, nullifer) for the spent output.
35    key_image: CompressedPoint,
36  },
37}
38
39impl Input {
40  /// The lower bound for the size of an input which isn't `Input::Gen(_)`.
41  // `<usize as VarInt>::LOWER_BOUND` is used for the lower-bound of a `Vec`'s encoding's length
42  const NON_GEN_SIZE_LOWER_BOUND: LowerBound<usize> =
43    LowerBound(1 + <u64 as VarInt>::LOWER_BOUND + <usize as VarInt>::LOWER_BOUND + 32);
44
45  /// Write the Input.
46  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  /// Serialize the Input to a `Vec<u8>`.
63  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  /// Read an Input.
70  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        // https://github.com/monero-project/monero/
76        //   blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
77        //   src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
78        // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
79        // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
80        let amount = if amount == 0 { None } else { Some(amount) };
81        Input::ToKey {
82          amount,
83          // Each offset takes at least one byte, and this won't be in a miner transaction
84          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/// An output in the Monero protocol.
98#[derive(Clone, PartialEq, Eq, Debug)]
99pub struct Output {
100  /// The pool this output should be sorted into.
101  pub amount: Option<u64>,
102  /// The key which can spend this output.
103  pub key: CompressedPoint,
104  /// The view tag for this output, as used to accelerate scanning.
105  pub view_tag: Option<u8>,
106}
107
108impl Output {
109  /// The lower bound on the size of an output.
110  pub const SIZE_LOWER_BOUND: LowerBound<usize> = LowerBound(<u64 as VarInt>::LOWER_BOUND + 1 + 32);
111  /// The upper bound on the size of an output.
112  pub const SIZE_UPPER_BOUND: UpperBound<usize> =
113    UpperBound(<u64 as VarInt>::UPPER_BOUND + 1 + 32 + 1);
114
115  /// Write the Output.
116  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  /// Write the Output to a `Vec<u8>`.
127  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  /// Read an Output.
134  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/// An additional timelock for a Monero transaction.
160///
161/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
162/// longer of the two will be the timelock used.
163#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
164pub enum Timelock {
165  /// No additional timelock.
166  None,
167  /// Additionally locked until this block.
168  Block(usize),
169  /// Additionally locked until this many seconds since the epoch.
170  Time(u64),
171}
172
173impl Timelock {
174  /// Write the Timelock.
175  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  /// Serialize the Timelock to a `Vec<u8>`.
184  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  /// Read a Timelock.
191  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/// The transaction prefix.
224///
225/// This is common to all transaction versions and contains most parts of the transaction needed to
226/// handle it. It excludes any proofs.
227#[derive(Clone, PartialEq, Eq, Debug)]
228pub struct TransactionPrefix {
229  /// The timelock this transaction is additionally constrained by.
230  ///
231  /// All transactions on the blockchain are subject to a 10-block lock. This adds a further
232  /// constraint.
233  pub additional_timelock: Timelock,
234  /// The inputs for this transaction.
235  pub inputs: Vec<Input>,
236  /// The outputs for this transaction.
237  pub outputs: Vec<Output>,
238  /// The additional data included within the transaction.
239  ///
240  /// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
241  /// scan the transaction.
242  pub extra: Vec<u8>,
243}
244
245impl TransactionPrefix {
246  /// The amount of inputs within a miner transaction.
247  pub const MINER_INPUTS: usize = 1;
248  /// The amount of inputs allowed within a non-miner transaction.
249  // This is defined as the amount of whole (minimally-sized) inputs which would fit in the largest
250  // possible transaction.
251  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  /// The upper bound for the amount of inputs allowed within a transaction.
255  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  /// The upper bound for the amount of outputs allowed within a non-miner transaction.
261  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  /// Write a TransactionPrefix.
265  ///
266  /// This is distinct from Monero in that it won't write any version.
267  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  /// Read a TransactionPrefix.
276  ///
277  /// This is distinct from Monero in that it won't read the version. The version must be passed
278  /// in.
279  ///
280  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
281  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
282  /// specific set of consensus rules.
283  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    // Miner transactions have no limits on their size within the Monero protocol, unfortunately
300    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  /// A trait representing either pruned or not pruned proofs.
411  pub trait PotentiallyPruned: Sealed {
412    /// Potentially-pruned ring signatures.
413    type RingSignatures: PotentiallyPrunedRingSignatures;
414    /// Potentially-pruned RingCT proofs.
415    type RctProofs: PotentiallyPrunedRctProofs;
416  }
417  /// A marker for an object which isn't pruned.
418  #[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  /// A marker for an object which is pruned.
426  #[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/// A Monero transaction.
437#[derive(Clone, PartialEq, Eq, Debug)]
438pub enum Transaction<P: PotentiallyPruned = NotPruned> {
439  /// A version 1 transaction, used by the original Cryptonote codebase.
440  V1 {
441    /// The transaction's prefix.
442    prefix: TransactionPrefix,
443    /// The transaction's ring signatures.
444    signatures: P::RingSignatures,
445  },
446  /// A version 2 transaction, used by the RingCT protocol.
447  V2 {
448    /// The transaction's prefix.
449    prefix: TransactionPrefix,
450    /// The transaction's proofs.
451    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  /// Get the version of this transaction.
462  pub fn version(&self) -> u8 {
463    match self {
464      Transaction::V1 { .. } => 1,
465      Transaction::V2 { .. } => 2,
466    }
467  }
468
469  /// Get the TransactionPrefix of this transaction.
470  pub fn prefix(&self) -> &TransactionPrefix {
471    match self {
472      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
473    }
474  }
475
476  /// Get a mutable reference to the TransactionPrefix of this transaction.
477  pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
478    match self {
479      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
480    }
481  }
482
483  /// Write the Transaction.
484  ///
485  /// Some writable transactions may not be readable if they're malformed, per Monero's consensus
486  /// rules.
487  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  /// Write the Transaction to a `Vec<u8>`.
508  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  /// Read a Transaction.
515  ///
516  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
517  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
518  /// specific set of consensus rules.
519  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  // The hash of the transaction.
549  #[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        // We don't use `self.write` as that may write the signatures (if this isn't pruned)
556        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        // We explicitly write the signatures ourselves here
561        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          // Serialization of RctBase::Null
584          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  /// The maximum size for a non-miner transaction.
599  // https://github.com/monero-project/monero
600  //   /blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/src/cryptonote_config.h#L41
601  pub const NON_MINER_SIZE_UPPER_BOUND: UpperBound<usize> = UpperBound(1_000_000);
602
603  /// The prunable hash of the transaction.
604  ///
605  /// This will return `None` for V1 transactions which do not have a well-defined prunable hash.
606  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  /// The hash of the transaction.
623  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  /// Calculate the hash of this transaction as needed for signing it.
635  ///
636  /// This returns None if the transaction is without signatures.
637  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  /// Splits this transaction into its pruned and serialized prunable part.
658  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  /// Calculate the transaction's weight.
708  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  /// Return the hash of the pruned transaction.
731  ///
732  /// This requires the transaction be version 2 and the hash of the pruned data be provided. If
733  /// the proofs are `RctType::Null`, `prunable_hash` MUST equal `[0; 32]` for the result to be
734  /// correct.
735  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}