monero_oxide/
transaction.rs

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