monero_wallet/
view_pair.rs

1use core::ops::Deref as _;
2
3use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
4
5#[cfg(feature = "compile-time-generators")]
6use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
7#[cfg(not(feature = "compile-time-generators"))]
8use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
9
10use crate::{
11  ed25519::{Scalar, Point},
12  address::{Network, AddressType, SubaddressIndex, MoneroAddress},
13};
14
15/// An error while working with a ViewPair.
16#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
17pub enum ViewPairError {
18  /// The spend key was torsioned.
19  ///
20  /// Torsioned spend keys are of questionable spendability. This library avoids that question by
21  /// rejecting such ViewPairs.
22  // CLSAG seems to support it if the challenge does a torsion clear, FCMP++ should ship with a
23  // torsion clear, yet it's not worth it to modify CLSAG sign to generate challenges until the
24  // torsion clears and ensure spendability (nor can we reasonably guarantee that in the future)
25  #[error("torsioned spend key")]
26  TorsionedSpendKey,
27}
28
29/// The pair of keys necessary to scan transactions.
30///
31/// This is composed of the public spend key and the private view key.
32#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
33pub struct ViewPair {
34  pub(crate) spend: Point,
35  pub(crate) view: Zeroizing<Scalar>,
36}
37
38impl ViewPair {
39  /// Create a new ViewPair.
40  ///
41  /// This function runs in time variable to the validity of the arguments.
42  pub fn new(spend: Point, view: Zeroizing<Scalar>) -> Result<Self, ViewPairError> {
43    if !spend.into().is_torsion_free() {
44      Err(ViewPairError::TorsionedSpendKey)?;
45    }
46    Ok(ViewPair { spend, view })
47  }
48
49  /// The public spend key for this ViewPair.
50  pub fn spend(&self) -> Point {
51    self.spend
52  }
53
54  /// The public view key for this ViewPair.
55  pub fn view(&self) -> Point {
56    Point::from(Zeroizing::new((*self.view).into()).deref() * ED25519_BASEPOINT_TABLE)
57  }
58
59  pub(crate) fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar {
60    Scalar::hash(Zeroizing::new(
61      [
62        b"SubAddr\0".as_slice(),
63        Zeroizing::new(<[u8; 32]>::from(*self.view)).as_slice(),
64        &index.account().to_le_bytes(),
65        &index.address().to_le_bytes(),
66      ]
67      .concat(),
68    ))
69  }
70
71  pub(crate) fn subaddress_keys(&self, index: SubaddressIndex) -> (Point, Point) {
72    let scalar = self.subaddress_derivation(index);
73    let spend = self.spend.into() + (&scalar.into() * ED25519_BASEPOINT_TABLE);
74    let view = Zeroizing::new((*self.view).into()).deref() * spend;
75    (Point::from(spend), Point::from(view))
76  }
77
78  /// Derive a legacy address from this ViewPair.
79  ///
80  /// Subaddresses SHOULD be used instead.
81  pub fn legacy_address(&self, network: Network) -> MoneroAddress {
82    MoneroAddress::new(network, AddressType::Legacy, self.spend, self.view())
83  }
84
85  /// Derive a legacy integrated address from this ViewPair.
86  ///
87  /// Subaddresses SHOULD be used instead.
88  pub fn legacy_integrated_address(&self, network: Network, payment_id: [u8; 8]) -> MoneroAddress {
89    MoneroAddress::new(network, AddressType::LegacyIntegrated(payment_id), self.spend, self.view())
90  }
91
92  /// Derive a subaddress from this ViewPair.
93  pub fn subaddress(&self, network: Network, subaddress: SubaddressIndex) -> MoneroAddress {
94    let (spend, view) = self.subaddress_keys(subaddress);
95    MoneroAddress::new(network, AddressType::Subaddress, spend, view)
96  }
97}
98
99/// The pair of keys necessary to scan outputs immune to the burning bug.
100///
101/// This is composed of the public spend key and a non-zero private view key.
102///
103/// 'Guaranteed' outputs, or transactions outputs to the burning bug, are not officially specified
104/// by the Monero project. They should only be used if necessary. No support outside of
105/// monero-wallet is promised.
106#[derive(Clone, PartialEq, Eq, Zeroize)] // Does not impl `ZeroizeOnDrop` since `ViewPair` does
107pub struct GuaranteedViewPair(pub(crate) ViewPair);
108
109impl GuaranteedViewPair {
110  /// Create a new GuaranteedViewPair.
111  pub fn new(spend: Point, view: Zeroizing<Scalar>) -> Result<Self, ViewPairError> {
112    ViewPair::new(spend, view).map(GuaranteedViewPair)
113  }
114
115  /// The public spend key for this GuaranteedViewPair.
116  pub fn spend(&self) -> Point {
117    self.0.spend()
118  }
119
120  /// The public view key for this GuaranteedViewPair.
121  pub fn view(&self) -> Point {
122    self.0.view()
123  }
124
125  /// Returns an address with the provided specification.
126  ///
127  /// The returned address will be a featured address with the guaranteed flag set. These should
128  /// not be presumed to be interoperable with any other software.
129  pub fn address(
130    &self,
131    network: Network,
132    subaddress: Option<SubaddressIndex>,
133    payment_id: Option<[u8; 8]>,
134  ) -> MoneroAddress {
135    let (spend, view) = if let Some(index) = subaddress {
136      self.0.subaddress_keys(index)
137    } else {
138      (self.spend(), self.view())
139    };
140
141    MoneroAddress::new(
142      network,
143      AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed: true },
144      spend,
145      view,
146    )
147  }
148}