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/// An input in the Monero protocol.
16#[derive(Clone, PartialEq, Eq, Debug)]
17pub enum Input {
18  /// An input for a miner transaction, which is generating new coins.
19  Gen(usize),
20  /// An input spending an output on-chain.
21  ToKey {
22    /// The pool this input spends an output of.
23    amount: Option<u64>,
24    /// The decoys used by this input's ring, specified as their offset distance from each other.
25    key_offsets: Vec<u64>,
26    /// The key image (linking tag, nullifer) for the spent output.
27    key_image: CompressedPoint,
28  },
29}
30
31impl Input {
32  /// The lower bound for the size of an input which isn't `Input::Gen(_)`.
33  // `<usize as VarInt>::LOWER_BOUND` is used for the lower-bound of a `Vec`'s encoding's length
34  const NON_GEN_SIZE_LOWER_BOUND: LowerBound<usize> =
35    LowerBound(1 + <u64 as VarInt>::LOWER_BOUND + <usize as VarInt>::LOWER_BOUND + 32);
36
37  /// Write the Input.
38  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  /// Serialize the Input to a `Vec<u8>`.
55  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  /// Read an Input.
62  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        // https://github.com/monero-project/monero/
68        //   blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
69        //   src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
70        // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
71        // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
72        let amount = if amount == 0 { None } else { Some(amount) };
73        Input::ToKey {
74          amount,
75          // Each offset takes at least one byte, and this won't be in a miner transaction
76          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/// An output in the Monero protocol.
90#[derive(Clone, PartialEq, Eq, Debug)]
91pub struct Output {
92  /// The pool this output should be sorted into.
93  pub amount: Option<u64>,
94  /// The key which can spend this output.
95  pub key: CompressedPoint,
96  /// The view tag for this output, as used to accelerate scanning.
97  pub view_tag: Option<u8>,
98}
99
100impl Output {
101  /// The lower bound on the size of an output.
102  pub const SIZE_LOWER_BOUND: LowerBound<usize> = LowerBound(<u64 as VarInt>::LOWER_BOUND + 1 + 32);
103  /// The upper bound on the size of an output.
104  pub const SIZE_UPPER_BOUND: UpperBound<usize> =
105    UpperBound(<u64 as VarInt>::UPPER_BOUND + 1 + 32 + 1);
106
107  /// Write the Output.
108  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  /// Write the Output to a `Vec<u8>`.
119  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  /// Read an Output.
126  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/// An additional timelock for a Monero transaction.
152///
153/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
154/// longer of the two will be the timelock used.
155#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
156pub enum Timelock {
157  /// No additional timelock.
158  None,
159  /// Additionally locked until this block.
160  Block(usize),
161  /// Additionally locked until this many seconds since the epoch.
162  Time(u64),
163}
164
165impl Timelock {
166  /// Write the Timelock.
167  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  /// Serialize the Timelock to a `Vec<u8>`.
176  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  /// Read a Timelock.
183  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/// The transaction prefix.
216///
217/// This is common to all transaction versions and contains most parts of the transaction needed to
218/// handle it. It excludes any proofs.
219#[derive(Clone, PartialEq, Eq, Debug)]
220pub struct TransactionPrefix {
221  /// The timelock this transaction is additionally constrained by.
222  ///
223  /// All transactions on the blockchain are subject to a 10-block lock. This adds a further
224  /// constraint.
225  pub additional_timelock: Timelock,
226  /// The inputs for this transaction.
227  pub inputs: Vec<Input>,
228  /// The outputs for this transaction.
229  pub outputs: Vec<Output>,
230  /// The additional data included within the transaction.
231  ///
232  /// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
233  /// scan the transaction.
234  pub extra: Vec<u8>,
235}
236
237impl TransactionPrefix {
238  /// The amount of inputs within a miner transaction.
239  pub const MINER_INPUTS: usize = 1;
240  /// The amount of inputs allowed within a non-miner transaction.
241  // This is defined as the amount of whole (minimally-sized) inputs which would fit in the largest
242  // possible transaction.
243  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  /// The upper bound for the amount of inputs allowed within a transaction.
247  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  /// The upper bound for the amount of outputs allowed within a non-miner transaction.
253  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  /// Write a TransactionPrefix.
257  ///
258  /// This is distinct from Monero in that it won't write any version.
259  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  /// Read a TransactionPrefix.
268  ///
269  /// This is distinct from Monero in that it won't read the version. The version must be passed
270  /// in.
271  ///
272  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
273  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
274  /// specific set of consensus rules.
275  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    // Miner transactions have no limits on their size within the Monero protocol, unfortunately
292    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  /// A trait representing either pruned or not pruned proofs.
403  pub trait PotentiallyPruned: Sealed {
404    /// Potentially-pruned ring signatures.
405    type RingSignatures: PotentiallyPrunedRingSignatures;
406    /// Potentially-pruned RingCT proofs.
407    type RctProofs: PotentiallyPrunedRctProofs;
408  }
409  /// A marker for an object which isn't pruned.
410  #[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  /// A marker for an object which is pruned.
418  #[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/// A Monero transaction.
429#[derive(Clone, PartialEq, Eq, Debug)]
430pub enum Transaction<P: PotentiallyPruned = NotPruned> {
431  /// A version 1 transaction, used by the original Cryptonote codebase.
432  V1 {
433    /// The transaction's prefix.
434    prefix: TransactionPrefix,
435    /// The transaction's ring signatures.
436    signatures: P::RingSignatures,
437  },
438  /// A version 2 transaction, used by the RingCT protocol.
439  V2 {
440    /// The transaction's prefix.
441    prefix: TransactionPrefix,
442    /// The transaction's proofs.
443    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  /// Get the version of this transaction.
454  pub fn version(&self) -> u8 {
455    match self {
456      Transaction::V1 { .. } => 1,
457      Transaction::V2 { .. } => 2,
458    }
459  }
460
461  /// Get the TransactionPrefix of this transaction.
462  pub fn prefix(&self) -> &TransactionPrefix {
463    match self {
464      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
465    }
466  }
467
468  /// Get a mutable reference to the TransactionPrefix of this transaction.
469  pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
470    match self {
471      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
472    }
473  }
474
475  /// Write the Transaction.
476  ///
477  /// Some writable transactions may not be readable if they're malformed, per Monero's consensus
478  /// rules.
479  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  /// Write the Transaction to a `Vec<u8>`.
500  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  /// Read a Transaction.
507  ///
508  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
509  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
510  /// specific set of consensus rules.
511  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  // The hash of the transaction.
541  #[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        // We don't use `self.write` as that may write the signatures (if this isn't pruned)
548        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        // We explicitly write the signatures ourselves here
553        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          // Serialization of RctBase::Null
576          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  /// The maximum size for a non-miner transaction.
591  // https://github.com/monero-project/monero
592  //   /blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/src/cryptonote_config.h#L41
593  pub const NON_MINER_SIZE_UPPER_BOUND: UpperBound<usize> = UpperBound(1_000_000);
594
595  /// The prunable hash of the transaction.
596  ///
597  /// This will return `None` for V1 transactions which do not have a well-defined prunable hash.
598  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  /// The hash of the transaction.
615  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  /// Calculate the hash of this transaction as needed for signing it.
627  ///
628  /// This returns None if the transaction is without signatures.
629  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  /// Splits this transaction into its pruned and serialized prunable part.
650  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  /// Calculate the transaction's weight.
700  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  /// Return the hash of the pruned transaction.
723  ///
724  /// This requires the transaction be version 2 and the hash of the pruned data be provided. If
725  /// the proofs are `RctType::Null`, `prunable_hash` MUST equal `[0; 32]` for the result to be
726  /// correct.
727  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}