monero_wallet/extra.rs
1use core::{ops::BitXor, num::NonZero};
2use std_shims::{
3 vec,
4 vec::Vec,
5 io::{self, Read, BufRead, Write},
6};
7
8use zeroize::Zeroize;
9
10use monero_oxide::{
11 io::*,
12 ed25519::{CompressedPoint, Point},
13};
14
15const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
16
17const PAYMENT_ID_MARKER: u8 = 0;
18const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1;
19// Used as it's the highest value not interpretable as a continued VarInt
20pub(crate) const ARBITRARY_DATA_MARKER: u8 = 127;
21
22/// The max amount of data which will fit within a blob of arbitrary data.
23// 1 byte is used for the marker
24pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1;
25
26/// The maximum length for a transaction's extra under current relay rules.
27// https://github.com/monero-project/monero
28// /blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/src/cryptonote_config.h#L217
29pub const MAX_EXTRA_SIZE_BY_RELAY_RULE: usize = 1060;
30
31/// A Payment ID.
32///
33/// This is a legacy method of identifying why Monero was sent to the receiver.
34#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
35pub enum PaymentId {
36 /// A deprecated form of payment ID which is no longer supported.
37 Unencrypted([u8; 32]),
38 /// An encrypted payment ID.
39 Encrypted([u8; 8]),
40}
41
42impl BitXor<[u8; 8]> for PaymentId {
43 type Output = PaymentId;
44
45 fn bitxor(self, bytes: [u8; 8]) -> PaymentId {
46 match self {
47 // Don't perform the xor since this isn't intended to be encrypted with xor
48 PaymentId::Unencrypted(_) => self,
49 PaymentId::Encrypted(id) => {
50 PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes())
51 }
52 }
53 }
54}
55
56impl PaymentId {
57 /// Write the PaymentId.
58 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
59 match self {
60 PaymentId::Unencrypted(id) => {
61 w.write_all(&[PAYMENT_ID_MARKER])?;
62 w.write_all(id)?;
63 }
64 PaymentId::Encrypted(id) => {
65 w.write_all(&[ENCRYPTED_PAYMENT_ID_MARKER])?;
66 w.write_all(id)?;
67 }
68 }
69 Ok(())
70 }
71
72 /// Serialize the PaymentId to a `Vec<u8>`.
73 pub fn serialize(&self) -> Vec<u8> {
74 let mut res = Vec::with_capacity(1 + 8);
75 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
76 res
77 }
78
79 /// Read a PaymentId.
80 pub fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
81 Ok(match read_byte(r)? {
82 0 => PaymentId::Unencrypted(read_bytes(r)?),
83 1 => PaymentId::Encrypted(read_bytes(r)?),
84 _ => Err(io::Error::other("unknown payment ID type"))?,
85 })
86 }
87}
88
89/// A field within the TX extra.
90#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
91pub enum ExtraField {
92 /// Padding.
93 ///
94 /// This is a block of zeroes within the TX extra.
95 Padding(NonZero<u8>),
96 /// The transaction key.
97 ///
98 /// This is a commitment to the randomness used for deriving outputs.
99 PublicKey(CompressedPoint),
100 /// The nonce field.
101 ///
102 /// This is used for data, such as payment IDs.
103 ///
104 /// When read, this is bounded by a maximum size. As we directly expose the field here (without a
105 /// constructor asserting its validity), this means it's possible to create an
106 /// `ExtraField::Nonce` which can be written but not read. Please be careful accordingly.
107 Nonce(Vec<u8>),
108 /// The field for merge-mining.
109 ///
110 /// This is used within miner transactions who are merge-mining Monero to specify the foreign
111 /// block they mined.
112 MergeMining(u64, [u8; 32]),
113 /// The additional transaction keys.
114 ///
115 /// These are the per-output commitments to the randomness used for deriving outputs.
116 PublicKeys(Vec<CompressedPoint>),
117 /// The 'mysterious' Minergate tag.
118 ///
119 /// This was used by a closed source entity without documentation. Support for parsing it was
120 /// added to reduce extra which couldn't be decoded.
121 MysteriousMinergate(Vec<u8>),
122}
123
124impl ExtraField {
125 /// Write the ExtraField.
126 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
127 match self {
128 ExtraField::Padding(size) => {
129 w.write_all(&[0])?;
130 for _ in 1 .. u8::from(*size) {
131 write_byte(&0u8, w)?;
132 }
133 }
134 ExtraField::PublicKey(key) => {
135 w.write_all(&[1])?;
136 key.write(w)?;
137 }
138 ExtraField::Nonce(data) => {
139 w.write_all(&[2])?;
140 /*
141 This uses `write_vec`, where the `Vec` will be length-prefixed with a VarInt, which
142 differs from Monero's `add_extra_nonce_to_tx_extra`:
143
144 https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
145 /src/cryptonote_basic/cryptonote_format_utils.cpp#L726
146
147 This is because the definition in `tx_extra.h` is followed which does consider this a
148 `VarInt`-length-prefixed container:
149
150 https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
151 /src/cryptonote_basic/tx_extra.h#L112-L115
152
153 The former is considered faulty. See https://github.com/monero-project/monero/pull/10220.
154 */
155 write_vec(write_byte, data, w)?;
156 }
157 ExtraField::MergeMining(depth, merkle_root) => {
158 w.write_all(&[3])?;
159 VarInt::write(&(depth.varint_len() + merkle_root.len()), w)?;
160 VarInt::write(depth, w)?;
161 w.write_all(merkle_root)?;
162 }
163 ExtraField::PublicKeys(keys) => {
164 w.write_all(&[4])?;
165 write_vec(CompressedPoint::write, keys, w)?;
166 }
167 ExtraField::MysteriousMinergate(data) => {
168 w.write_all(&[0xDE])?;
169 write_vec(write_byte, data, w)?;
170 }
171 }
172 Ok(())
173 }
174
175 /// Serialize the ExtraField to a `Vec<u8>`.
176 pub fn serialize(&self) -> Vec<u8> {
177 let mut res = Vec::with_capacity(1 + 8);
178 self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
179 res
180 }
181
182 /// Read an ExtraField.
183 ///
184 /// This may be lossy in that `ExtraField::read(&mut buf.as_slice()).serialize() == buf` is not
185 /// guaranteed to hold true.
186 pub fn read<R: BufRead>(r: &mut R) -> io::Result<ExtraField> {
187 Ok(match read_byte(r)? {
188 0 => ExtraField::Padding({
189 // Read until either non-zero, max padding count, or end of buffer
190 let mut size = 1u8;
191 loop {
192 let buf = r.fill_buf()?;
193 let mut n_consume = 0;
194 for v in buf {
195 if *v != 0u8 {
196 Err(io::Error::other("non-zero value after padding"))?;
197 }
198 n_consume += 1;
199 // https://github.com/monero-project/monero
200 // /blob/02357fe53fbcab3f5102183f0837feed68cf5355/src/cryptonote_basic/tx_extra.h#L43
201 if size == u8::MAX {
202 Err(io::Error::other("padding exceeded max count"))?;
203 }
204 size += 1;
205 }
206 if n_consume == 0 {
207 break;
208 }
209 r.consume(n_consume);
210 }
211 NonZero::new(size).expect("size started at 1 but incremented to 0?")
212 }),
213 1 => ExtraField::PublicKey(CompressedPoint::read(r)?),
214 2 => ExtraField::Nonce(read_vec(read_byte, Some(MAX_TX_EXTRA_NONCE_SIZE), r)?),
215 3 => {
216 let field_len = <usize as VarInt>::read(r)?;
217 let depth = <u64 as VarInt>::read(r)?;
218 let merkle_root = read_bytes(r)?;
219
220 match field_len.checked_sub(depth.varint_len() + merkle_root.len()) {
221 Some(remaining) => {
222 for _ in 0 .. remaining {
223 read_byte(r)?;
224 }
225 }
226 None => Err(io::Error::other("`MergeMining` tag had a length smaller than its fields"))?,
227 }
228 ExtraField::MergeMining(depth, merkle_root)
229 }
230 4 => ExtraField::PublicKeys(read_vec(CompressedPoint::read, None, r)?),
231 0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, None, r)?),
232 _ => Err(io::Error::other("unknown extra field"))?,
233 })
234 }
235}
236
237/// The result of decoding a transaction's extra field.
238///
239/// Note that the Monero protocol defines a transaction's `extra` field as a byte vector. This is a
240/// parsed view of such a byte vector, yet the Monero protocol does not require the `extra` field
241/// be parseable. The parsing is also lossy in that
242/// `Extra::read(&mut buf.as_slice()).serialize() == buf` is not guaranteed to hold true.
243#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
244pub struct Extra(pub(crate) Vec<ExtraField>);
245impl Extra {
246 /// The keys within this extra.
247 ///
248 /// This returns all keys specified with `PublicKey` and the first set of keys specified with
249 /// `PublicKeys`. If any are improperly encoded, identity will be yielded in place, intending to
250 /// cause an ECDH of the identity point, as Monero uses upon improperly-encoded points.
251 // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
252 // /src/wallet/wallet2.cpp#L2290-L2300 (use all transaction keys)
253 // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
254 // /src/wallet/wallet2.cpp#L2337-L2340 (use only the first set of additional keys)
255 // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
256 // /src/wallet/wallet2.cpp#L2368-L2373 (public key was improperly encoded)
257 // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
258 // /src/wallet/wallet2.cpp#L2383-L2387 (additional key was improperly encoded)
259 pub fn keys(&self) -> Option<(Vec<Point>, Option<Vec<Point>>)> {
260 let identity = {
261 use curve25519_dalek::{traits::Identity as _, EdwardsPoint};
262 Point::from(EdwardsPoint::identity())
263 };
264
265 let mut keys = vec![];
266 let mut additional = None;
267 for field in &self.0 {
268 match field.clone() {
269 ExtraField::PublicKey(key) => keys.push(key.decompress().unwrap_or(identity)),
270 ExtraField::PublicKeys(keys) => {
271 additional = additional
272 .or(Some(keys.into_iter().map(|key| key.decompress().unwrap_or(identity)).collect()));
273 }
274 ExtraField::Padding(_) |
275 ExtraField::Nonce(_) |
276 ExtraField::MergeMining(_, _) |
277 ExtraField::MysteriousMinergate(_) => (),
278 }
279 }
280 // Don't return any keys if this was non-standard and didn't include the primary key
281 // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
282 // /src/wallet/wallet2.cpp#L2338-L2346
283 if keys.is_empty() {
284 None
285 } else {
286 Some((keys, additional))
287 }
288 }
289
290 /// The payment ID embedded within this extra.
291 // Monero finds the first nonce field and reads the payment ID from it:
292 // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
293 // src/wallet/wallet2.cpp#L2709-L2752
294 pub fn payment_id(&self) -> Option<PaymentId> {
295 for field in &self.0 {
296 if let ExtraField::Nonce(data) = field {
297 let mut reader = data.as_slice();
298 let res = PaymentId::read(&mut reader).ok();
299 // https://github.com/monero-project/monero/blob/8d4c625713e3419573dfcc7119c8848f47cabbaa
300 // /src/cryptonote_basic/cryptonote_format_utils.cpp#L801
301 //
302 // /src/cryptonote_basic/cryptonote_format_utils.cpp#L811
303 if !reader.is_empty() {
304 None?;
305 }
306 return res;
307 }
308 }
309 None
310 }
311
312 /// The arbitrary data within this extra.
313 ///
314 /// This looks for all instances of `ExtraField::Nonce` with a marker byte of 0b0111_1111. This
315 /// is the largest possible value not interpretable as a VarInt, ensuring it's able to be
316 /// interpreted as a VarInt without issue, and that it's the most unlikely value to be used by
317 /// the Monero wallet protocol itself (which itself has assigned marker bytes incrementally). As
318 /// Monero itself does not support including arbitrary data with its wallet however, this was
319 /// first introduced by `monero-wallet` (under the monero-oxide project) and may be bespoke to
320 /// the ecosystem of monero-oxide and dependents of it.
321 ///
322 /// The data is stored without any padding or encryption applied. Applications MUST consider this
323 /// themselves. As Monero does not reserve any space for arbitrary data, the inclusion of _any_
324 /// arbitrary data will _always_ be a fingerprint even before considering what the data is.
325 /// Applications SHOULD include arbitrary data indistinguishable from random, of a popular length
326 /// (such as padded to the next power of two or the maximum length per chunk) IF arbitrary data
327 /// is included at all.
328 ///
329 /// For applications where indistinguishability from 'regular' Monero transactions is required,
330 /// steganography should be considered. Steganography is somewhat-frowned upon however due to it
331 /// bloating the Monero blockchain however and efficient methods are likely specific to
332 /// individual hard forks. They may also have their own privacy implications, which is why no
333 /// methods of stegnography are supported outright by `monero-wallet`.
334 pub fn arbitrary_data(&self) -> Vec<Vec<u8>> {
335 // Only parse arbitrary data from the amount of extra data accepted under the relay rule
336 let serialized = self.serialize();
337 let bounded_extra =
338 Self::read(&mut &serialized[.. serialized.len().min(MAX_EXTRA_SIZE_BY_RELAY_RULE)])
339 .expect("`Extra::read` only fails if the IO fails and `&[u8]` won't");
340
341 let mut res = vec![];
342 for field in &bounded_extra.0 {
343 if let ExtraField::Nonce(data) = field {
344 if data.first() == Some(&ARBITRARY_DATA_MARKER) {
345 res.push(data[1 ..].to_vec());
346 }
347 }
348 }
349 res
350 }
351
352 pub(crate) fn new(key: CompressedPoint, additional: Vec<CompressedPoint>) -> Extra {
353 let mut res = Extra(Vec::with_capacity(3));
354 // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
355 // /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633
356 // We only support pushing nonces which come after these in the sort order
357 res.0.push(ExtraField::PublicKey(key));
358 if !additional.is_empty() {
359 res.0.push(ExtraField::PublicKeys(additional));
360 }
361 res
362 }
363
364 // TODO: This allows pushing a nonce of size greater than allowed. That's likely fine as it's
365 // internal, yet should be better?
366 pub(crate) fn push_nonce(&mut self, nonce: Vec<u8>) {
367 self.0.push(ExtraField::Nonce(nonce));
368 }
369
370 /// Write the Extra.
371 ///
372 /// This will write the value in a sorted fashion.
373 ///
374 /// This is not of deterministic length nor length-prefixed. It should only be written to a
375 /// buffer which will be delimited.
376 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
377 #[cfg(debug_assertions)]
378 let mut written = 0;
379 // https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
380 // /src/cryptonote_basic/cryptonote_format_utils.cpp#L618-L624
381 const SORT_ORDER: [fn(&ExtraField) -> bool; 6] = [
382 |field: &ExtraField| matches!(field, ExtraField::PublicKey(_)),
383 |field: &ExtraField| matches!(field, ExtraField::PublicKeys(_)),
384 |field: &ExtraField| matches!(field, ExtraField::Nonce(_)),
385 |field: &ExtraField| matches!(field, ExtraField::MergeMining(_, _)),
386 |field: &ExtraField| matches!(field, ExtraField::MysteriousMinergate(_)),
387 |field: &ExtraField| matches!(field, ExtraField::Padding(_)),
388 ];
389 // Ensure the length of the `SORT_ORDER` array corresponds to the amount of variants
390 #[cfg(monero_oxide_rust_nightly)]
391 const _SORT_LEN: [(); 0 - core::mem::variant_count::<ExtraField>().abs_diff(SORT_ORDER.len())] =
392 [(); _];
393 for selection in SORT_ORDER {
394 for field in &self.0 {
395 if selection(field) {
396 field.write(w)?;
397 #[cfg(debug_assertions)]
398 {
399 written += 1;
400 }
401 }
402 }
403 }
404 #[cfg(debug_assertions)]
405 debug_assert_eq!(written, self.0.len());
406 Ok(())
407 }
408
409 /// Serialize the Extra to a `Vec<u8>`.
410 ///
411 /// This will write the value in a sorted fashion.
412 pub fn serialize(&self) -> Vec<u8> {
413 let mut buf = vec![];
414 self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
415 buf
416 }
417
418 /// Read an `Extra`.
419 ///
420 /// This is not of deterministic length nor length-prefixed. It should only be read from a buffer
421 /// already delimited.
422 pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> {
423 let mut res = Extra(vec![]);
424 // Extra reads until EOF
425 // We take a BufRead so we can detect when the buffer is empty
426 // `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
427 // exhausted
428 while !r.fill_buf()?.is_empty() {
429 let Ok(field) = ExtraField::read(r) else { break };
430 res.0.push(field);
431 }
432 Ok(res)
433 }
434}