monero_oxide/
transaction.rs

1use core::cmp::Ordering;
2#[allow(unused_imports)]
3use std_shims::prelude::*;
4use std_shims::io::{self, Read, Write};
5
6use zeroize::Zeroize;
7
8use crate::{
9  io::*,
10  primitives::keccak256,
11  ring_signatures::RingSignature,
12  ringct::{bulletproofs::Bulletproof, PrunedRctProofs},
13};
14
15/// The maximum size for a non-miner transaction.
16// https://github.com/monero-project/monero
17//   /blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/src/cryptonote_config.h#L41
18pub const MAX_NON_MINER_TRANSACTION_SIZE: usize = 1_000_000;
19
20const MAX_MINER_TRANSACTION_INPUTS: usize = 1;
21
22const NON_MINER_TRANSACTION_INPUT_SIZE_LOWER_BOUND: usize = 32;
23const NON_MINER_TRANSACTION_INPUTS_UPPER_BOUND: usize =
24  MAX_NON_MINER_TRANSACTION_SIZE / NON_MINER_TRANSACTION_INPUT_SIZE_LOWER_BOUND;
25
26const fn const_max(a: usize, b: usize) -> usize {
27  if a > b {
28    a
29  } else {
30    b
31  }
32}
33
34/// An upper bound for the amount of inputs within a Monero transaction.
35///
36/// This is not guaranteed to be the maximum amount of inputs within a Monero transaction. It is a
37/// value greater than or equal to the maximum amount of inputs allowed within a Monero
38/// transaction.
39pub const INPUTS_UPPER_BOUND: usize =
40  const_max(MAX_MINER_TRANSACTION_INPUTS, NON_MINER_TRANSACTION_INPUTS_UPPER_BOUND);
41
42const NON_MINER_TRANSACTION_OUTPUT_SIZE_LOWER_BOUND: usize = 32;
43const MAX_NON_MINER_TRANSACTION_OUTPUTS: usize =
44  MAX_NON_MINER_TRANSACTION_SIZE / NON_MINER_TRANSACTION_OUTPUT_SIZE_LOWER_BOUND;
45
46/// An input in the Monero protocol.
47#[derive(Clone, PartialEq, Eq, Debug)]
48pub enum Input {
49  /// An input for a miner transaction, which is generating new coins.
50  Gen(usize),
51  /// An input spending an output on-chain.
52  ToKey {
53    /// The pool this input spends an output of.
54    amount: Option<u64>,
55    /// The decoys used by this input's ring, specified as their offset distance from each other.
56    key_offsets: Vec<u64>,
57    /// The key image (linking tag, nullifer) for the spent output.
58    key_image: CompressedPoint,
59  },
60}
61
62impl Input {
63  /// Write the Input.
64  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
65    match self {
66      Input::Gen(height) => {
67        w.write_all(&[255])?;
68        VarInt::write(height, w)
69      }
70
71      Input::ToKey { amount, key_offsets, key_image } => {
72        w.write_all(&[2])?;
73        VarInt::write(&amount.unwrap_or(0), w)?;
74        write_vec(VarInt::write, key_offsets, w)?;
75        key_image.write(w)
76      }
77    }
78  }
79
80  /// Serialize the Input to a `Vec<u8>`.
81  pub fn serialize(&self) -> Vec<u8> {
82    let mut res = vec![];
83    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
84    res
85  }
86
87  /// Read an Input.
88  pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
89    Ok(match read_byte(r)? {
90      255 => Input::Gen(VarInt::read(r)?),
91      2 => {
92        let amount = VarInt::read(r)?;
93        // https://github.com/monero-project/monero/
94        //   blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
95        //   src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
96        // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
97        // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
98        let amount = if amount == 0 { None } else { Some(amount) };
99        Input::ToKey {
100          amount,
101          // Each offset takes at least one byte, and this won't be in a miner transaction
102          key_offsets: read_vec(VarInt::read, Some(MAX_NON_MINER_TRANSACTION_SIZE), r)?,
103          key_image: CompressedPoint::read(r)?,
104        }
105      }
106      _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
107    })
108  }
109}
110
111/// An output in the Monero protocol.
112#[derive(Clone, PartialEq, Eq, Debug)]
113pub struct Output {
114  /// The pool this output should be sorted into.
115  pub amount: Option<u64>,
116  /// The key which can spend this output.
117  pub key: CompressedPoint,
118  /// The view tag for this output, as used to accelerate scanning.
119  pub view_tag: Option<u8>,
120}
121
122impl Output {
123  /// Write the Output.
124  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
125    VarInt::write(&self.amount.unwrap_or(0), w)?;
126    w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
127    w.write_all(&self.key.to_bytes())?;
128    if let Some(view_tag) = self.view_tag {
129      w.write_all(&[view_tag])?;
130    }
131    Ok(())
132  }
133
134  /// Write the Output to a `Vec<u8>`.
135  pub fn serialize(&self) -> Vec<u8> {
136    let mut res = Vec::with_capacity(<u64 as VarInt>::UPPER_BOUND + 1 + 32 + 1);
137    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
138    res
139  }
140
141  /// Read an Output.
142  pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
143    let amount = VarInt::read(r)?;
144    let amount = if rct {
145      if amount != 0 {
146        Err(io::Error::other("RCT TX output wasn't 0"))?;
147      }
148      None
149    } else {
150      Some(amount)
151    };
152
153    let view_tag = match read_byte(r)? {
154      2 => false,
155      3 => true,
156      _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
157    };
158
159    Ok(Output {
160      amount,
161      key: CompressedPoint::read(r)?,
162      view_tag: if view_tag { Some(read_byte(r)?) } else { None },
163    })
164  }
165}
166
167/// An additional timelock for a Monero transaction.
168///
169/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
170/// longer of the two will be the timelock used.
171#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
172pub enum Timelock {
173  /// No additional timelock.
174  None,
175  /// Additionally locked until this block.
176  Block(usize),
177  /// Additionally locked until this many seconds since the epoch.
178  Time(u64),
179}
180
181impl Timelock {
182  /// Write the Timelock.
183  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
184    match self {
185      Timelock::None => VarInt::write(&0u8, w),
186      Timelock::Block(block) => VarInt::write(block, w),
187      Timelock::Time(time) => VarInt::write(time, w),
188    }
189  }
190
191  /// Serialize the Timelock to a `Vec<u8>`.
192  pub fn serialize(&self) -> Vec<u8> {
193    let mut res = Vec::with_capacity(1);
194    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
195    res
196  }
197
198  /// Read a Timelock.
199  pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
200    const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000;
201
202    let raw = <u64 as VarInt>::read(r)?;
203    Ok(if raw == 0 {
204      Timelock::None
205    } else if raw <
206      u64::try_from(TIMELOCK_BLOCK_THRESHOLD)
207        .expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64")
208    {
209      Timelock::Block(usize::try_from(raw).expect(
210        "timelock overflowed usize despite being less than a const representable with a usize",
211      ))
212    } else {
213      Timelock::Time(raw)
214    })
215  }
216}
217
218impl PartialOrd for Timelock {
219  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
220    match (self, other) {
221      (Timelock::None, Timelock::None) => Some(Ordering::Equal),
222      (Timelock::None, _) => Some(Ordering::Less),
223      (_, Timelock::None) => Some(Ordering::Greater),
224      (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
225      (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
226      _ => None,
227    }
228  }
229}
230
231/// The transaction prefix.
232///
233/// This is common to all transaction versions and contains most parts of the transaction needed to
234/// handle it. It excludes any proofs.
235#[derive(Clone, PartialEq, Eq, Debug)]
236pub struct TransactionPrefix {
237  /// The timelock this transaction is additionally constrained by.
238  ///
239  /// All transactions on the blockchain are subject to a 10-block lock. This adds a further
240  /// constraint.
241  pub additional_timelock: Timelock,
242  /// The inputs for this transaction.
243  pub inputs: Vec<Input>,
244  /// The outputs for this transaction.
245  pub outputs: Vec<Output>,
246  /// The additional data included within the transaction.
247  ///
248  /// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
249  /// scan the transaction.
250  pub extra: Vec<u8>,
251}
252
253impl TransactionPrefix {
254  /// Write a TransactionPrefix.
255  ///
256  /// This is distinct from Monero in that it won't write any version.
257  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
258    self.additional_timelock.write(w)?;
259    write_vec(Input::write, &self.inputs, w)?;
260    write_vec(Output::write, &self.outputs, w)?;
261    VarInt::write(&self.extra.len(), w)?;
262    w.write_all(&self.extra)
263  }
264
265  /// Read a TransactionPrefix.
266  ///
267  /// This is distinct from Monero in that it won't read the version. The version must be passed
268  /// in.
269  ///
270  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
271  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
272  /// specific set of consensus rules.
273  pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
274    let additional_timelock = Timelock::read(r)?;
275
276    let inputs = read_vec(|r| Input::read(r), Some(INPUTS_UPPER_BOUND), r)?;
277    if inputs.is_empty() {
278      Err(io::Error::other("transaction had no inputs"))?;
279    }
280    let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
281
282    let max_outputs = if is_miner_tx { None } else { Some(MAX_NON_MINER_TRANSACTION_OUTPUTS) };
283    let mut prefix = TransactionPrefix {
284      additional_timelock,
285      inputs,
286      outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), max_outputs, r)?,
287      extra: vec![],
288    };
289    let max_extra = if is_miner_tx { None } else { Some(MAX_NON_MINER_TRANSACTION_SIZE) };
290    prefix.extra = read_vec(read_byte, max_extra, r)?;
291    Ok(prefix)
292  }
293
294  fn hash(&self, version: u64) -> [u8; 32] {
295    let mut buf = vec![];
296    VarInt::write(&version, &mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
297    self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
298    keccak256(buf)
299  }
300}
301
302#[allow(private_bounds)]
303mod sealed {
304  use core::fmt::Debug;
305  use crate::ringct::*;
306  use super::*;
307
308  pub(crate) trait PotentiallyPrunedRingSignatures:
309    Clone + PartialEq + Eq + Default + Debug
310  {
311    fn signatures_to_write(&self) -> &[RingSignature];
312    fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self>;
313  }
314
315  impl PotentiallyPrunedRingSignatures for Vec<RingSignature> {
316    fn signatures_to_write(&self) -> &[RingSignature] {
317      self
318    }
319    fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result<Self> {
320      let mut signatures = Vec::with_capacity(inputs.len());
321      for input in inputs {
322        match input {
323          Input::ToKey { key_offsets, .. } => {
324            signatures.push(RingSignature::read(key_offsets.len(), r)?)
325          }
326          _ => Err(io::Error::other("reading signatures for a transaction with non-ToKey inputs"))?,
327        }
328      }
329      Ok(signatures)
330    }
331  }
332
333  impl PotentiallyPrunedRingSignatures for () {
334    fn signatures_to_write(&self) -> &[RingSignature] {
335      &[]
336    }
337    fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result<Self> {
338      Ok(())
339    }
340  }
341
342  pub(crate) trait PotentiallyPrunedRctProofs: Clone + PartialEq + Eq + Debug {
343    fn write(&self, w: &mut impl Write) -> io::Result<()>;
344    fn read(
345      ring_length: usize,
346      inputs: usize,
347      outputs: usize,
348      r: &mut impl Read,
349    ) -> io::Result<Option<Self>>;
350    fn rct_type(&self) -> RctType;
351    fn base(&self) -> &RctBase;
352  }
353
354  impl PotentiallyPrunedRctProofs for RctProofs {
355    fn write(&self, w: &mut impl Write) -> io::Result<()> {
356      self.write(w)
357    }
358    fn read(
359      ring_length: usize,
360      inputs: usize,
361      outputs: usize,
362      r: &mut impl Read,
363    ) -> io::Result<Option<Self>> {
364      RctProofs::read(ring_length, inputs, outputs, r)
365    }
366    fn rct_type(&self) -> RctType {
367      self.rct_type()
368    }
369    fn base(&self) -> &RctBase {
370      &self.base
371    }
372  }
373
374  impl PotentiallyPrunedRctProofs for PrunedRctProofs {
375    fn write(&self, w: &mut impl Write) -> io::Result<()> {
376      self.base.write(w, self.rct_type)
377    }
378    fn read(
379      _ring_length: usize,
380      inputs: usize,
381      outputs: usize,
382      r: &mut impl Read,
383    ) -> io::Result<Option<Self>> {
384      Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base }))
385    }
386    fn rct_type(&self) -> RctType {
387      self.rct_type
388    }
389    fn base(&self) -> &RctBase {
390      &self.base
391    }
392  }
393
394  trait Sealed {}
395
396  /// A trait representing either pruned or not pruned proofs.
397  pub trait PotentiallyPruned: Sealed {
398    /// Potentially-pruned ring signatures.
399    type RingSignatures: PotentiallyPrunedRingSignatures;
400    /// Potentially-pruned RingCT proofs.
401    type RctProofs: PotentiallyPrunedRctProofs;
402  }
403  /// A marker for an object which isn't pruned.
404  #[derive(Clone, PartialEq, Eq, Debug)]
405  pub struct NotPruned;
406  impl Sealed for NotPruned {}
407  impl PotentiallyPruned for NotPruned {
408    type RingSignatures = Vec<RingSignature>;
409    type RctProofs = RctProofs;
410  }
411  /// A marker for an object which is pruned.
412  #[derive(Clone, PartialEq, Eq, Debug)]
413  pub struct Pruned;
414  impl Sealed for Pruned {}
415  impl PotentiallyPruned for Pruned {
416    type RingSignatures = ();
417    type RctProofs = PrunedRctProofs;
418  }
419}
420pub use sealed::*;
421
422/// A Monero transaction.
423#[allow(clippy::large_enum_variant)]
424#[derive(Clone, PartialEq, Eq, Debug)]
425pub enum Transaction<P: PotentiallyPruned = NotPruned> {
426  /// A version 1 transaction, used by the original Cryptonote codebase.
427  V1 {
428    /// The transaction's prefix.
429    prefix: TransactionPrefix,
430    /// The transaction's ring signatures.
431    signatures: P::RingSignatures,
432  },
433  /// A version 2 transaction, used by the RingCT protocol.
434  V2 {
435    /// The transaction's prefix.
436    prefix: TransactionPrefix,
437    /// The transaction's proofs.
438    proofs: Option<P::RctProofs>,
439  },
440}
441
442enum PrunableHash<'a> {
443  V1(&'a [RingSignature]),
444  V2([u8; 32]),
445}
446
447#[allow(private_bounds)]
448impl<P: PotentiallyPruned> Transaction<P> {
449  /// Get the version of this transaction.
450  pub fn version(&self) -> u8 {
451    match self {
452      Transaction::V1 { .. } => 1,
453      Transaction::V2 { .. } => 2,
454    }
455  }
456
457  /// Get the TransactionPrefix of this transaction.
458  pub fn prefix(&self) -> &TransactionPrefix {
459    match self {
460      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
461    }
462  }
463
464  /// Get a mutable reference to the TransactionPrefix of this transaction.
465  pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
466    match self {
467      Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
468    }
469  }
470
471  /// Write the Transaction.
472  ///
473  /// Some writable transactions may not be readable if they're malformed, per Monero's consensus
474  /// rules.
475  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
476    VarInt::write(&self.version(), w)?;
477    match self {
478      Transaction::V1 { prefix, signatures } => {
479        prefix.write(w)?;
480        for ring_sig in signatures.signatures_to_write() {
481          ring_sig.write(w)?;
482        }
483      }
484      Transaction::V2 { prefix, proofs } => {
485        prefix.write(w)?;
486        match proofs {
487          None => w.write_all(&[0])?,
488          Some(proofs) => proofs.write(w)?,
489        }
490      }
491    }
492    Ok(())
493  }
494
495  /// Write the Transaction to a `Vec<u8>`.
496  pub fn serialize(&self) -> Vec<u8> {
497    let mut res = Vec::with_capacity(2048);
498    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
499    res
500  }
501
502  /// Read a Transaction.
503  ///
504  /// This MAY error if miscellaneous Monero conseusus rules are broken, as useful when
505  /// deserializing. The result is not guaranteed to follow all Monero consensus rules or any
506  /// specific set of consensus rules.
507  pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
508    let version = VarInt::read(r)?;
509    let prefix = TransactionPrefix::read(r, version)?;
510
511    if version == 1 {
512      let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
513        Default::default()
514      } else {
515        P::RingSignatures::read_signatures(&prefix.inputs, r)?
516      };
517
518      Ok(Transaction::V1 { prefix, signatures })
519    } else if version == 2 {
520      let proofs = P::RctProofs::read(
521        prefix.inputs.first().map_or(0, |input| match input {
522          Input::Gen(_) => 0,
523          Input::ToKey { key_offsets, .. } => key_offsets.len(),
524        }),
525        prefix.inputs.len(),
526        prefix.outputs.len(),
527        r,
528      )?;
529
530      Ok(Transaction::V2 { prefix, proofs })
531    } else {
532      Err(io::Error::other("tried to deserialize unknown version"))
533    }
534  }
535
536  // The hash of the transaction.
537  #[allow(clippy::needless_pass_by_value)]
538  fn hash_with_prunable_hash(&self, prunable: PrunableHash<'_>) -> [u8; 32] {
539    match self {
540      Transaction::V1 { prefix, .. } => {
541        let mut buf = Vec::with_capacity(512);
542
543        // We don't use `self.write` as that may write the signatures (if this isn't pruned)
544        VarInt::write(&self.version(), &mut buf)
545          .expect("write failed but <Vec as io::Write> doesn't fail");
546        prefix.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
547
548        // We explicitly write the signatures ourselves here
549        let PrunableHash::V1(signatures) = prunable else {
550          panic!("hashing v1 TX with non-v1 prunable data")
551        };
552        for signature in signatures {
553          signature.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
554        }
555
556        keccak256(buf)
557      }
558      Transaction::V2 { prefix, proofs } => {
559        let mut hashes = Vec::with_capacity(96);
560
561        hashes.extend(prefix.hash(2));
562
563        if let Some(proofs) = proofs {
564          let mut buf = Vec::with_capacity(512);
565          proofs
566            .base()
567            .write(&mut buf, proofs.rct_type())
568            .expect("write failed but <Vec as io::Write> doesn't fail");
569          hashes.extend(keccak256(&buf));
570        } else {
571          // Serialization of RctBase::Null
572          hashes.extend(keccak256([0]));
573        }
574        let PrunableHash::V2(prunable_hash) = prunable else {
575          panic!("hashing v2 TX with non-v2 prunable data")
576        };
577        hashes.extend(prunable_hash);
578
579        keccak256(hashes)
580      }
581    }
582  }
583}
584
585impl Transaction<NotPruned> {
586  /// The hash of the transaction.
587  pub fn hash(&self) -> [u8; 32] {
588    match self {
589      Transaction::V1 { signatures, .. } => {
590        self.hash_with_prunable_hash(PrunableHash::V1(signatures))
591      }
592      Transaction::V2 { proofs, .. } => {
593        self.hash_with_prunable_hash(PrunableHash::V2(if let Some(proofs) = proofs {
594          let mut buf = Vec::with_capacity(1024);
595          proofs
596            .prunable
597            .write(&mut buf, proofs.rct_type())
598            .expect("write failed but <Vec as io::Write> doesn't fail");
599          keccak256(buf)
600        } else {
601          [0; 32]
602        }))
603      }
604    }
605  }
606
607  /// Calculate the hash of this transaction as needed for signing it.
608  ///
609  /// This returns None if the transaction is without signatures.
610  pub fn signature_hash(&self) -> Option<[u8; 32]> {
611    Some(match self {
612      Transaction::V1 { prefix, .. } => {
613        if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) {
614          None?;
615        }
616        self.hash_with_prunable_hash(PrunableHash::V1(&[]))
617      }
618      Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash({
619        let Some(proofs) = proofs else { None? };
620        let mut buf = Vec::with_capacity(1024);
621        proofs
622          .prunable
623          .signature_write(&mut buf)
624          .expect("write failed but <Vec as io::Write> doesn't fail");
625        PrunableHash::V2(keccak256(buf))
626      }),
627    })
628  }
629
630  fn is_rct_bulletproof(&self) -> bool {
631    match self {
632      Transaction::V1 { .. } => false,
633      Transaction::V2 { proofs, .. } => {
634        let Some(proofs) = proofs else { return false };
635        proofs.rct_type().bulletproof()
636      }
637    }
638  }
639
640  fn is_rct_bulletproof_plus(&self) -> bool {
641    match self {
642      Transaction::V1 { .. } => false,
643      Transaction::V2 { proofs, .. } => {
644        let Some(proofs) = proofs else { return false };
645        proofs.rct_type().bulletproof_plus()
646      }
647    }
648  }
649
650  /// Calculate the transaction's weight.
651  pub fn weight(&self) -> usize {
652    let blob_size = self.serialize().len();
653
654    let bp = self.is_rct_bulletproof();
655    let bp_plus = self.is_rct_bulletproof_plus();
656    if !(bp || bp_plus) {
657      blob_size
658    } else {
659      blob_size +
660        Bulletproof::calculate_clawback(
661          bp_plus,
662          match self {
663            Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
664            Transaction::V2 { prefix, .. } => prefix.outputs.len(),
665          },
666        )
667        .0
668    }
669  }
670}
671
672impl From<Transaction<NotPruned>> for Transaction<Pruned> {
673  fn from(tx: Transaction<NotPruned>) -> Transaction<Pruned> {
674    match tx {
675      Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () },
676      Transaction::V2 { prefix, proofs } => Transaction::V2 {
677        prefix,
678        proofs: proofs
679          .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }),
680      },
681    }
682  }
683}