Get the repo to compile again

This commit is contained in:
Luke Parker
2024-06-23 10:08:51 -04:00
parent 11dba9173f
commit 0b20004ba1
40 changed files with 1452 additions and 777 deletions

28
Cargo.lock generated
View File

@@ -4867,6 +4867,8 @@ name = "monero-seed"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek",
"hex",
"monero-primitives",
"rand_core", "rand_core",
"std-shims", "std-shims",
"thiserror", "thiserror",
@@ -4931,6 +4933,21 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "monero-wallet-util"
version = "0.1.0"
dependencies = [
"curve25519-dalek",
"hex",
"monero-seed",
"monero-wallet",
"polyseed",
"rand_core",
"std-shims",
"thiserror",
"zeroize",
]
[[package]] [[package]]
name = "multiaddr" name = "multiaddr"
version = "0.18.1" version = "0.18.1"
@@ -5818,6 +5835,7 @@ dependencies = [
name = "polyseed" name = "polyseed"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hex",
"pbkdf2 0.12.2", "pbkdf2 0.12.2",
"rand_core", "rand_core",
"sha3", "sha3",
@@ -8184,15 +8202,7 @@ dependencies = [
"dleq", "dleq",
"flexible-transcript", "flexible-transcript",
"minimal-ed448", "minimal-ed448",
"monero-bulletproofs", "monero-wallet-util",
"monero-clsag",
"monero-generators",
"monero-io",
"monero-mlsag",
"monero-primitives",
"monero-rpc",
"monero-serai",
"monero-wallet",
"multiexp", "multiexp",
"schnorr-signatures", "schnorr-signatures",
] ]

View File

@@ -56,6 +56,7 @@ members = [
"coins/monero/wallet", "coins/monero/wallet",
"coins/monero/wallet/seed", "coins/monero/wallet/seed",
"coins/monero/wallet/polyseed", "coins/monero/wallet/polyseed",
"coins/monero/wallet/util",
"message-queue", "message-queue",

View File

@@ -38,7 +38,6 @@ std = [
"std-shims/std", "std-shims/std",
"zeroize/std", "zeroize/std",
"rand_core/std", "rand_core/std",
"monero-io/std", "monero-io/std",

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022-2023 Luke Parker Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -348,7 +348,7 @@ pub trait Rpc: Sync + Clone + Debug {
// https://github.com/monero-project/monero/issues/8311 // https://github.com/monero-project/monero/issues/8311
if res.as_hex.is_empty() { if res.as_hex.is_empty() {
match tx.prefix.inputs.first() { match tx.prefix().inputs.first() {
Some(Input::Gen { .. }) => (), Some(Input::Gen { .. }) => (),
_ => Err(RpcError::PrunedTransaction)?, _ => Err(RpcError::PrunedTransaction)?,
} }
@@ -431,7 +431,7 @@ pub trait Rpc: Sync + Clone + Debug {
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?; .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
// Make sure this is actually the block for this number // Make sure this is actually the block for this number
match block.miner_tx.prefix.inputs.first() { match block.miner_tx.prefix().inputs.first() {
Some(Input::Gen(actual)) => { Some(Input::Gen(actual)) => {
if usize::try_from(*actual).unwrap() == number { if usize::try_from(*actual).unwrap() == number {
Ok(block) Ok(block)
@@ -733,7 +733,7 @@ pub trait Rpc: Sync + Clone + Debug {
}; };
Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| { Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| {
if fingerprintable_canonical { if fingerprintable_canonical {
Timelock::Block(height) >= txs[i].prefix.timelock Timelock::Block(height) >= txs[i].prefix().timelock
} else { } else {
out.unlocked out.unlocked
} }

View File

@@ -115,7 +115,7 @@ impl TryFrom<u8> for RctType {
impl RctType { impl RctType {
/// True if this RctType uses compact encrypted amounts, false otherwise. /// True if this RctType uses compact encrypted amounts, false otherwise.
fn compact_encrypted_amounts(&self) -> bool { pub fn compact_encrypted_amounts(&self) -> bool {
match self { match self {
RctType::AggregateMlsagBorromean | RctType::MlsagBorromean | RctType::MlsagBulletproofs => { RctType::AggregateMlsagBorromean | RctType::MlsagBorromean | RctType::MlsagBulletproofs => {
false false

View File

@@ -301,6 +301,14 @@ pub enum Transaction {
} }
impl Transaction { impl Transaction {
/// Get the version of this transaction.
pub fn version(&self) -> u8 {
match self {
Transaction::V1 { .. } => 1,
Transaction::V2 { .. } => 2,
}
}
/// Get the TransactionPrefix of this transaction. /// Get the TransactionPrefix of this transaction.
pub fn prefix(&self) -> &TransactionPrefix { pub fn prefix(&self) -> &TransactionPrefix {
match self { match self {
@@ -308,6 +316,13 @@ impl Transaction {
} }
} }
/// Get a mutable reference to the TransactionPrefix of this transaction.
pub fn prefix_mut(&mut self) -> &mut TransactionPrefix {
match self {
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix,
}
}
/// The weight of this Transaction, as relevant for fees. /// The weight of this Transaction, as relevant for fees.
// TODO: Replace ring_len, decoy_weights for &[&[usize]], where the inner buf is the decoy // TODO: Replace ring_len, decoy_weights for &[&[usize]], where the inner buf is the decoy
// offsets // offsets
@@ -329,16 +344,15 @@ impl Transaction {
/// Some writable transactions may not be readable if they're malformed, per Monero's consensus /// Some writable transactions may not be readable if they're malformed, per Monero's consensus
/// rules. /// rules.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
write_varint(&self.version(), w)?;
match self { match self {
Transaction::V1 { prefix, signatures } => { Transaction::V1 { prefix, signatures } => {
write_varint(&1u8, w)?;
prefix.write(w)?; prefix.write(w)?;
for ring_sig in signatures { for ring_sig in signatures {
ring_sig.write(w)?; ring_sig.write(w)?;
} }
} }
Transaction::V2 { prefix, proofs } => { Transaction::V2 { prefix, proofs } => {
write_varint(&2u8, w)?;
prefix.write(w)?; prefix.write(w)?;
match proofs { match proofs {
None => w.write_all(&[0])?, None => w.write_all(&[0])?,

View File

@@ -43,3 +43,6 @@ It also won't act as a wallet, just as a wallet functionality library. wallet2
has several *non-transaction-level* policies, such as always attempting to use has several *non-transaction-level* policies, such as always attempting to use
two inputs to create transactions. These are considered out of scope to two inputs to create transactions. These are considered out of scope to
monero-serai. monero-serai.
Finally, this library only supports producing transactions with CLSAG
signatures. That means this library cannot spend non-RingCT outputs.

View File

@@ -27,6 +27,9 @@ rand_core = { version = "0.6", default-features = false }
sha3 = { version = "0.10", default-features = false } sha3 = { version = "0.10", default-features = false }
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false } pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
[features] [features]
std = [ std = [
"std-shims/std", "std-shims/std",

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -15,6 +15,9 @@ use rand_core::{RngCore, CryptoRng};
use sha3::Sha3_256; use sha3::Sha3_256;
use pbkdf2::pbkdf2_hmac; use pbkdf2::pbkdf2_hmac;
#[cfg(test)]
mod tests;
// Features // Features
const FEATURE_BITS: u8 = 5; const FEATURE_BITS: u8 = 5;
#[allow(dead_code)] #[allow(dead_code)]
@@ -63,10 +66,10 @@ const LAST_BYTE_SECRET_BITS_MASK: u8 = ((1 << (BITS_PER_BYTE - CLEAR_BITS)) - 1)
const SECRET_BITS_PER_WORD: usize = 10; const SECRET_BITS_PER_WORD: usize = 10;
// Amount of words in a seed // The amount of words in a seed.
const POLYSEED_LENGTH: usize = 16; const POLYSEED_LENGTH: usize = 16;
// Amount of characters each word must have if trimmed // Amount of characters each word must have if trimmed
const PREFIX_LEN: usize = 4; pub(crate) const PREFIX_LEN: usize = 4;
const POLY_NUM_CHECK_DIGITS: usize = 1; const POLY_NUM_CHECK_DIGITS: usize = 1;
const DATA_WORDS: usize = POLYSEED_LENGTH - POLY_NUM_CHECK_DIGITS; const DATA_WORDS: usize = POLYSEED_LENGTH - POLY_NUM_CHECK_DIGITS;
@@ -105,18 +108,18 @@ const COIN: u16 = 0;
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum PolyseedError { pub enum PolyseedError {
/// Unsupported feature bits were set. /// The seed was invalid.
#[cfg_attr(feature = "std", error("unsupported features"))] #[cfg_attr(feature = "std", error("invalid seed"))]
UnsupportedFeatures, InvalidSeed,
/// The entropy was invalid. /// The entropy was invalid.
#[cfg_attr(feature = "std", error("invalid entropy"))] #[cfg_attr(feature = "std", error("invalid entropy"))]
InvalidEntropy, InvalidEntropy,
#[cfg_attr(feature = "std", error("invalid seed"))]
/// The seed was invalid.
InvalidSeed,
/// The checksum did not match the data. /// The checksum did not match the data.
#[cfg_attr(feature = "std", error("invalid checksum"))] #[cfg_attr(feature = "std", error("invalid checksum"))]
InvalidChecksum, InvalidChecksum,
/// Unsupported feature bits were set.
#[cfg_attr(feature = "std", error("unsupported features"))]
UnsupportedFeatures,
} }
/// Language options for Polyseed. /// Language options for Polyseed.
@@ -277,7 +280,7 @@ impl Polyseed {
/// Create a new `Polyseed` with specific internals. /// Create a new `Polyseed` with specific internals.
/// ///
/// `birthday` is defined in seconds since the epoch. /// `birthday` is defined in seconds since the epoch.
fn from( pub fn from(
language: Language, language: Language,
features: u8, features: u8,
birthday: u64, birthday: u64,

View File

@@ -0,0 +1,218 @@
use zeroize::Zeroizing;
use rand_core::OsRng;
use crate::*;
#[test]
fn test_polyseed() {
struct Vector {
language: Language,
seed: String,
entropy: String,
birthday: u64,
has_prefix: bool,
has_accent: bool,
}
let vectors = [
Vector {
language: Language::English,
seed: "raven tail swear infant grief assist regular lamp \
duck valid someone little harsh puppy airport language"
.into(),
entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(),
birthday: 1638446400,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Spanish,
seed: "eje fin parte célebre tabú pestaña lienzo puma \
prisión hora regalo lengua existir lápiz lote sonoro"
.into(),
entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(),
birthday: 3118651200,
has_prefix: true,
has_accent: true,
},
Vector {
language: Language::French,
seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \
prouesse réserve ampleur ajuster muter caméra enchère"
.into(),
entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(),
birthday: 1679314966,
has_prefix: true,
has_accent: true,
},
Vector {
language: Language::Italian,
seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \
olandese normale tristezza episodio voragine forbito achille"
.into(),
entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(),
birthday: 1679316358,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Portuguese,
seed: "caverna custear azedo adeus senador apertada sedoso omitir \
sujeito aurora videira molho cartaz gesso dentista tapar"
.into(),
entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(),
birthday: 1679316657,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Czech,
seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \
vesta kabel herna stodola uvolnit ustrnout email"
.into(),
entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(),
birthday: 1679316898,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Korean,
seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \
지우개 보관 절망 말기 시각 귀신"
.into(),
entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(),
birthday: 1679317073,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::Japanese,
seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \
といれ おさない おさえる むかう ぬぐう なふだ せまる"
.into(),
entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(),
birthday: 1679318722,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::ChineseTraditional,
seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(),
entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(),
birthday: 1679426433,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::ChineseSimplified,
seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(),
entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(),
birthday: 1679426817,
has_prefix: false,
has_accent: false,
},
// The following seed requires the language specification in order to calculate
// a single valid checksum
Vector {
language: Language::Spanish,
seed: "impo sort usua cabi venu nobl oliv clim \
cont barr marc auto prod vaca torn fati"
.into(),
entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(),
birthday: 1701511650,
has_prefix: true,
has_accent: true,
},
];
for vector in vectors {
let add_whitespace = |mut seed: String| {
seed.push(' ');
seed
};
let seed_without_accents = |seed: &str| {
seed
.split_whitespace()
.map(|w| w.chars().filter(char::is_ascii).collect::<String>())
.collect::<Vec<_>>()
.join(" ")
};
let trim_seed = |seed: &str| {
let seed_to_trim =
if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() };
seed_to_trim
.split_whitespace()
.map(|w| {
let mut ascii = 0;
let mut to_take = w.len();
for (i, char) in w.chars().enumerate() {
if char.is_ascii() {
ascii += 1;
}
if ascii == PREFIX_LEN {
// +1 to include this character, which put us at the prefix length
to_take = i + 1;
break;
}
}
w.chars().take(to_take).collect::<String>()
})
.collect::<Vec<_>>()
.join(" ")
};
// String -> Seed
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed = Polyseed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
let trim = trim_seed(&vector.seed);
let add_whitespace = add_whitespace(vector.seed.clone());
let seed_without_accents = seed_without_accents(&vector.seed);
// Make sure a version with added whitespace still works
let whitespaced_seed =
Polyseed::from_string(vector.language, Zeroizing::new(add_whitespace)).unwrap();
assert_eq!(seed, whitespaced_seed);
// Check trimmed versions works
if vector.has_prefix {
let trimmed_seed = Polyseed::from_string(vector.language, Zeroizing::new(trim)).unwrap();
assert_eq!(seed, trimmed_seed);
}
// Check versions without accents work
if vector.has_accent {
let seed_without_accents =
Polyseed::from_string(vector.language, Zeroizing::new(seed_without_accents)).unwrap();
assert_eq!(seed, seed_without_accents);
}
let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap());
assert_eq!(*seed.entropy(), entropy);
assert!(seed.birthday().abs_diff(vector.birthday) < TIME_STEP);
// Entropy -> Seed
let from_entropy = Polyseed::from(vector.language, 0, seed.birthday(), entropy).unwrap();
assert_eq!(seed.to_string(), from_entropy.to_string());
// Check against ourselves
{
let seed = Polyseed::new(&mut OsRng, vector.language);
println!("{}. seed: {}", line!(), *seed.to_string());
assert_eq!(seed, Polyseed::from_string(vector.language, seed.to_string()).unwrap());
assert_eq!(
seed,
Polyseed::from(vector.language, 0, seed.birthday(), seed.entropy().clone(),).unwrap()
);
}
}
}
#[test]
fn test_invalid_polyseed() {
// This seed includes unsupported features bits and should error on decode
let seed = "include domain claim resemble urban hire lunch bird \
crucial fire best wife ring warm ignore model"
.into();
let res = Polyseed::from_string(Language::English, Zeroizing::new(seed));
assert_eq!(res, Err(PolyseedError::UnsupportedFeatures));
}

View File

@@ -25,6 +25,10 @@ rand_core = { version = "0.6", default-features = false }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
monero-primitives = { path = "../../primitives", default-features = false, features = ["std"] }
[features] [features]
std = [ std = [
"std-shims/std", "std-shims/std",

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +1,4 @@
use core::ops::Deref; use core::{ops::Deref, fmt};
use std_shims::{ use std_shims::{
sync::OnceLock, sync::OnceLock,
vec::Vec, vec::Vec,
@@ -11,24 +11,29 @@ use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::scalar::Scalar; use curve25519_dalek::scalar::Scalar;
const CLASSIC_SEED_LENGTH: usize = 24; #[cfg(test)]
const CLASSIC_SEED_LENGTH_WITH_CHECKSUM: usize = 25; mod tests;
// The amount of words in a seed without a checksum.
const SEED_LENGTH: usize = 24;
// The amount of words in a seed with a checksum.
const SEED_LENGTH_WITH_CHECKSUM: usize = 25;
/// An error when working with a seed. /// An error when working with a seed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum SeedError { pub enum SeedError {
/// The deprecated English language option was used with a checksum.
///
/// The deprecated English language option did not include a checksum.
#[cfg_attr(feature = "std", error("deprecated English language option included a checksum"))]
DeprecatedEnglishWithChecksum,
#[cfg_attr(feature = "std", error("invalid seed"))] #[cfg_attr(feature = "std", error("invalid seed"))]
/// The seed was invalid. /// The seed was invalid.
InvalidSeed, InvalidSeed,
/// The checksum did not match the data. /// The checksum did not match the data.
#[cfg_attr(feature = "std", error("invalid checksum"))] #[cfg_attr(feature = "std", error("invalid checksum"))]
InvalidChecksum, InvalidChecksum,
/// The deprecated English language option was used with a checksum.
///
/// The deprecated English language option did not include a checksum.
#[cfg_attr(feature = "std", error("deprecated English language option included a checksum"))]
DeprecatedEnglishWithChecksum,
} }
/// Language options. /// Language options.
@@ -211,11 +216,11 @@ fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> Seed {
fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> { fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
// get seed words // get seed words
let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>(); let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>();
if (words.len() != CLASSIC_SEED_LENGTH) && (words.len() != CLASSIC_SEED_LENGTH_WITH_CHECKSUM) { if (words.len() != SEED_LENGTH) && (words.len() != SEED_LENGTH_WITH_CHECKSUM) {
panic!("invalid seed passed to seed_to_bytes"); panic!("invalid seed passed to seed_to_bytes");
} }
let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM; let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
if has_checksum && lang == Language::DeprecatedEnglish { if has_checksum && lang == Language::DeprecatedEnglish {
Err(SeedError::DeprecatedEnglishWithChecksum)?; Err(SeedError::DeprecatedEnglishWithChecksum)?;
} }
@@ -223,7 +228,7 @@ fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, See
// Validate words are in the language word list // Validate words are in the language word list
let lang_word_list: &WordList = &LANGUAGES()[&lang]; let lang_word_list: &WordList = &LANGUAGES()[&lang];
let matched_indices = (|| { let matched_indices = (|| {
let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM; let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
let mut matched_indices = Zeroizing::new(vec![]); let mut matched_indices = Zeroizing::new(vec![]);
// Iterate through all the words and see if they're all present // Iterate through all the words and see if they're all present
@@ -295,6 +300,13 @@ fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, See
/// A Monero seed. /// A Monero seed.
#[derive(Clone, PartialEq, Eq, Zeroize)] #[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct Seed(Language, Zeroizing<String>); pub struct Seed(Language, Zeroizing<String>);
impl fmt::Debug for Seed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Seed").finish_non_exhaustive()
}
}
impl Seed { impl Seed {
/// Create a new seed. /// Create a new seed.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed { pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed {

View File

@@ -0,0 +1,234 @@
use zeroize::Zeroizing;
use rand_core::OsRng;
use curve25519_dalek::scalar::Scalar;
use monero_primitives::keccak256;
use crate::*;
#[test]
fn test_original_seed() {
struct Vector {
language: Language,
seed: String,
spend: String,
view: String,
}
let vectors = [
Vector {
language: Language::Chinese,
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
},
Vector {
language: Language::English,
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
abnormal memoir nylon mostly building shrugged online ember northern \
ruby woes dauntless boil family illness inroads northern"
.into(),
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
},
Vector {
language: Language::Dutch,
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
.into(),
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
},
Vector {
language: Language::French,
seed: "poids vaseux tarte bazar poivre effet entier nuance \
sensuel ennui pacte osselet poudre battre alibi mouton \
stade paquet pliage gibier type question position projet pliage"
.into(),
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
},
Vector {
language: Language::Spanish,
seed: "minero ocupar mirar evadir octubre cal logro miope \
opaco disco ancla litio clase cuello nasal clase \
fiar avance deseo mente grumo negro cordón croqueta clase"
.into(),
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
},
Vector {
language: Language::German,
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
.into(),
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
},
Vector {
language: Language::Italian,
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
forzare meritare litigare lezione segreto evasione votare buio \
licenza cliente dorso natale crescere vento tutelare vetta evasione"
.into(),
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
},
Vector {
language: Language::Portuguese,
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
.into(),
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
},
Vector {
language: Language::Japanese,
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
.into(),
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
},
Vector {
language: Language::Russian,
seed: "шатер икра нация ехать получать инерция доза реальный \
рыжий таможня лопата душа веселый клетка атлас лекция \
обгонять паек наивный лыжный дурак стать ежик задача паек"
.into(),
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
},
Vector {
language: Language::Esperanto,
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
.into(),
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
},
Vector {
language: Language::Lojban,
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
blabi darno dembi janli blabi fenki bukpu burcu blabi"
.into(),
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
},
Vector {
language: Language::DeprecatedEnglish,
seed: "glorious especially puff son moment add youth nowhere \
throw glide grip wrong rhythm consume very swear \
bitter heavy eventually begin reason flirt type unable"
.into(),
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
},
// The following seeds require the language specification in order to calculate
// a single valid checksum
Vector {
language: Language::Spanish,
seed: "pluma laico atraer pintor peor cerca balde buscar \
lancha batir nulo reloj resto gemelo nevera poder columna gol \
oveja latir amplio bolero feliz fuerza nevera"
.into(),
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
},
Vector {
language: Language::Spanish,
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
.into(),
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
},
Vector {
language: Language::English,
seed: "plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus plus"
.into(),
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
},
Vector {
language: Language::Spanish,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
},
Vector {
language: Language::English,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
},
];
for vector in vectors {
fn trim_by_lang(word: &str, lang: Language) -> String {
if lang != Language::DeprecatedEnglish {
word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect()
} else {
word.to_string()
}
}
let trim_seed = |seed: &str| {
seed
.split_whitespace()
.map(|word| trim_by_lang(word, vector.language))
.collect::<Vec<_>>()
.join(" ")
};
// Test against Monero
{
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed = Seed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
let trim = trim_seed(&vector.seed);
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
// For originalal seeds, Monero directly uses the entropy as a spend key
assert_eq!(
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
);
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
// Monero then derives the view key as H(spend)
assert_eq!(
Scalar::from_bytes_mod_order(keccak256(spend)),
Scalar::from_canonical_bytes(view).unwrap()
);
assert_eq!(Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(), seed);
}
// Test against ourselves
{
let seed = Seed::new(&mut OsRng, vector.language);
println!("{}. seed: {}", line!(), *seed.to_string());
let trim = trim_seed(&seed.to_string());
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
assert_eq!(seed, Seed::from_entropy(vector.language, seed.entropy()).unwrap());
assert_eq!(seed, Seed::from_string(vector.language, seed.to_string()).unwrap());
}
}
}

View File

@@ -5,7 +5,7 @@
use core::ops::Deref; use core::ops::Deref;
use std_shims::{ use std_shims::{
io, io as stdio,
collections::{HashSet, HashMap}, collections::{HashSet, HashMap},
}; };
@@ -23,16 +23,14 @@ use monero_serai::{
ringct::{RctType, EncryptedAmount}, ringct::{RctType, EncryptedAmount},
transaction::Input, transaction::Input,
}; };
pub use monero_serai as monero;
pub use monero_serai::*;
pub use monero_rpc as rpc; pub use monero_rpc as rpc;
pub mod extra; pub mod extra;
pub(crate) use extra::{PaymentId, ExtraField, Extra}; pub(crate) use extra::{PaymentId, ExtraField, Extra};
/// Seed creation and parsing functionality.
pub mod seed;
/// Address encoding and decoding functionality. /// Address encoding and decoding functionality.
pub mod address; pub mod address;
use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress}; use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress};
@@ -114,8 +112,8 @@ impl Protocol {
// TODO: Make this an Option when we support pre-RCT protocols // TODO: Make this an Option when we support pre-RCT protocols
pub fn optimal_rct_type(&self) -> RctType { pub fn optimal_rct_type(&self) -> RctType {
match self { match self {
Protocol::v14 => RctType::Clsag, Protocol::v14 => RctType::ClsagBulletproof,
Protocol::v16 => RctType::BulletproofsPlus, Protocol::v16 => RctType::ClsagBulletproofPlus,
Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type, Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type,
} }
} }
@@ -139,7 +137,7 @@ impl Protocol {
} }
} }
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: stdio::Write>(&self, w: &mut W) -> stdio::Result<()> {
match self { match self {
Protocol::v14 => w.write_all(&[0, 14]), Protocol::v14 => w.write_all(&[0, 14]),
Protocol::v16 => w.write_all(&[0, 16]), Protocol::v16 => w.write_all(&[0, 16]),
@@ -148,20 +146,20 @@ impl Protocol {
w.write_all(&[1, 0])?; w.write_all(&[1, 0])?;
w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?; w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?;
w.write_all(&[u8::from(*bp_plus)])?; w.write_all(&[u8::from(*bp_plus)])?;
w.write_all(&[optimal_rct_type.to_byte()])?; w.write_all(&[u8::from(*optimal_rct_type)])?;
w.write_all(&[u8::from(*view_tags)])?; w.write_all(&[u8::from(*view_tags)])?;
w.write_all(&[u8::from(*v16_fee)]) w.write_all(&[u8::from(*v16_fee)])
} }
} }
} }
pub fn read<R: io::Read>(r: &mut R) -> io::Result<Protocol> { pub fn read<R: stdio::Read>(r: &mut R) -> stdio::Result<Protocol> {
Ok(match read_byte(r)? { Ok(match read_byte(r)? {
// Monero protocol // Monero protocol
0 => match read_byte(r)? { 0 => match read_byte(r)? {
14 => Protocol::v14, 14 => Protocol::v14,
16 => Protocol::v16, 16 => Protocol::v16,
_ => Err(io::Error::other("unrecognized monero protocol"))?, _ => Err(stdio::Error::other("unrecognized monero protocol"))?,
}, },
// Custom // Custom
1 => match read_byte(r)? { 1 => match read_byte(r)? {
@@ -170,24 +168,24 @@ impl Protocol {
bp_plus: match read_byte(r)? { bp_plus: match read_byte(r)? {
0 => false, 0 => false,
1 => true, 1 => true,
_ => Err(io::Error::other("invalid bool serialization"))?, _ => Err(stdio::Error::other("invalid bool serialization"))?,
}, },
optimal_rct_type: RctType::from_byte(read_byte(r)?) optimal_rct_type: RctType::try_from(read_byte(r)?)
.ok_or_else(|| io::Error::other("invalid RctType serialization"))?, .map_err(|()| stdio::Error::other("invalid RctType serialization"))?,
view_tags: match read_byte(r)? { view_tags: match read_byte(r)? {
0 => false, 0 => false,
1 => true, 1 => true,
_ => Err(io::Error::other("invalid bool serialization"))?, _ => Err(stdio::Error::other("invalid bool serialization"))?,
}, },
v16_fee: match read_byte(r)? { v16_fee: match read_byte(r)? {
0 => false, 0 => false,
1 => true, 1 => true,
_ => Err(io::Error::other("invalid bool serialization"))?, _ => Err(stdio::Error::other("invalid bool serialization"))?,
}, },
}, },
_ => Err(io::Error::other("unrecognized custom protocol serialization"))?, _ => Err(stdio::Error::other("unrecognized custom protocol serialization"))?,
}, },
_ => Err(io::Error::other("unrecognized protocol serialization"))?, _ => Err(stdio::Error::other("unrecognized protocol serialization"))?,
}) })
} }
} }

View File

@@ -334,22 +334,22 @@ impl Scanner {
/// Scan a transaction to discover the received outputs. /// Scan a transaction to discover the received outputs.
pub fn scan_transaction(&mut self, tx: &Transaction) -> Timelocked<ReceivedOutput> { pub fn scan_transaction(&mut self, tx: &Transaction) -> Timelocked<ReceivedOutput> {
// Only scan RCT TXs since we can only spend RCT outputs // Only scan RCT TXs since we can only spend RCT outputs
if tx.prefix.version != 2 { if tx.version() != 2 {
return Timelocked(tx.prefix.timelock, vec![]); return Timelocked(tx.prefix().timelock, vec![]);
} }
let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) else { let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) else {
return Timelocked(tx.prefix.timelock, vec![]); return Timelocked(tx.prefix().timelock, vec![]);
}; };
let Some((tx_keys, additional)) = extra.keys() else { let Some((tx_keys, additional)) = extra.keys() else {
return Timelocked(tx.prefix.timelock, vec![]); return Timelocked(tx.prefix().timelock, vec![]);
}; };
let payment_id = extra.payment_id(); let payment_id = extra.payment_id();
let mut res = vec![]; let mut res = vec![];
for (o, output) in tx.prefix.outputs.iter().enumerate() { for (o, output) in tx.prefix().outputs.iter().enumerate() {
// https://github.com/serai-dex/serai/issues/106 // https://github.com/serai-dex/serai/issues/106
if let Some(burning_bug) = self.burning_bug.as_ref() { if let Some(burning_bug) = self.burning_bug.as_ref() {
if burning_bug.contains(&output.key) { if burning_bug.contains(&output.key) {
@@ -380,7 +380,7 @@ impl Scanner {
} }
}; };
let (view_tag, shared_key, payment_id_xor) = shared_key( let (view_tag, shared_key, payment_id_xor) = shared_key(
if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix.inputs)) } else { None }, if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix().inputs)) } else { None },
self.pair.view.deref() * key, self.pair.view.deref() * key,
o, o,
); );
@@ -419,7 +419,11 @@ impl Scanner {
commitment.amount = amount; commitment.amount = amount;
// Regular transaction // Regular transaction
} else { } else {
commitment = match tx.proofs.base.encrypted_amounts.get(o) { let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
return Timelocked(tx.prefix().timelock, vec![]);
};
commitment = match proofs.base.encrypted_amounts.get(o) {
Some(amount) => amount.decrypt(shared_key), Some(amount) => amount.decrypt(shared_key),
// This should never happen, yet it may be possible with miner transactions? // This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case // Using get just decreases the possibility of a panic and lets us move on in that case
@@ -428,7 +432,7 @@ impl Scanner {
// If this is a malicious commitment, move to the next output // If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable // Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != tx.proofs.base.commitments.get(o) { if Some(&commitment.calculate()) != proofs.base.commitments.get(o) {
break; break;
} }
} }
@@ -452,7 +456,7 @@ impl Scanner {
} }
} }
Timelocked(tx.prefix.timelock, res) Timelocked(tx.prefix().timelock, res)
} }
/// Scan a block to obtain its spendable outputs. Its the presence in a block giving these /// Scan a block to obtain its spendable outputs. Its the presence in a block giving these
@@ -493,13 +497,13 @@ impl Scanner {
res.push(timelock); res.push(timelock);
} }
index += u64::try_from( index += u64::try_from(
tx.prefix tx.prefix()
.outputs .outputs
.iter() .iter()
// Filter to v2 miner TX outputs/RCT outputs since we're tracking the RCT output index // Filter to v2 miner TX outputs/RCT outputs since we're tracking the RCT output index
.filter(|output| { .filter(|output| {
let is_v2_miner_tx = let is_v2_miner_tx =
(tx.prefix.version == 2) && matches!(tx.prefix.inputs.first(), Some(Input::Gen(..))); (tx.version() == 2) && matches!(tx.prefix().inputs.first(), Some(Input::Gen(..)));
is_v2_miner_tx || output.amount.is_none() is_v2_miner_tx || output.amount.is_none()
}) })
.count(), .count(),

View File

@@ -1,136 +0,0 @@
use core::fmt;
use std_shims::string::String;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use rand_core::{RngCore, CryptoRng};
pub(crate) use monero_seed as classic;
pub(crate) use polyseed;
use classic::{CLASSIC_SEED_LENGTH, CLASSIC_SEED_LENGTH_WITH_CHECKSUM, ClassicSeed};
use polyseed::{POLYSEED_LENGTH, Polyseed};
/// Error when decoding a seed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum SeedError {
#[cfg_attr(feature = "std", error("invalid number of words in seed"))]
InvalidSeedLength,
#[cfg_attr(feature = "std", error("unknown language"))]
UnknownLanguage,
#[cfg_attr(feature = "std", error("invalid checksum"))]
InvalidChecksum,
#[cfg_attr(feature = "std", error("english old seeds don't support checksums"))]
EnglishOldWithChecksum,
#[cfg_attr(feature = "std", error("provided entropy is not valid"))]
InvalidEntropy,
#[cfg_attr(feature = "std", error("invalid seed"))]
InvalidSeed,
#[cfg_attr(feature = "std", error("provided features are not supported"))]
UnsupportedFeatures,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SeedType {
Classic(classic::Language),
Polyseed(polyseed::Language),
}
/// A Monero seed.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub enum Seed {
Classic(ClassicSeed),
Polyseed(Polyseed),
}
impl fmt::Debug for Seed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Seed::Classic(_) => f.debug_struct("Seed::Classic").finish_non_exhaustive(),
Seed::Polyseed(_) => f.debug_struct("Seed::Polyseed").finish_non_exhaustive(),
}
}
}
impl Seed {
/// Creates a new `Seed`.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, seed_type: SeedType) -> Seed {
match seed_type {
SeedType::Classic(lang) => Seed::Classic(ClassicSeed::new(rng, lang)),
SeedType::Polyseed(lang) => Seed::Polyseed(Polyseed::new(rng, lang)),
}
}
/// Parse a seed from a `String`.
pub fn from_string(seed_type: SeedType, words: Zeroizing<String>) -> Result<Seed, SeedError> {
let word_count = words.split_whitespace().count();
match seed_type {
SeedType::Classic(lang) => {
if word_count != CLASSIC_SEED_LENGTH && word_count != CLASSIC_SEED_LENGTH_WITH_CHECKSUM {
Err(SeedError::InvalidSeedLength)?
} else {
ClassicSeed::from_string(lang, words).map(Seed::Classic)
}
}
SeedType::Polyseed(lang) => {
if word_count != POLYSEED_LENGTH {
Err(SeedError::InvalidSeedLength)?
} else {
Polyseed::from_string(lang, words).map(Seed::Polyseed)
}
}
}
}
/// Creates a `Seed` from an entropy and an optional birthday (denoted in seconds since the
/// epoch).
///
/// For `SeedType::Classic`, the birthday is ignored.
///
/// For `SeedType::Polyseed`, the last 13 bytes of `entropy` must be `0`.
// TODO: Return Result, not Option
pub fn from_entropy(
seed_type: SeedType,
entropy: Zeroizing<[u8; 32]>,
birthday: Option<u64>,
) -> Option<Seed> {
match seed_type {
SeedType::Classic(lang) => ClassicSeed::from_entropy(lang, entropy).map(Seed::Classic),
SeedType::Polyseed(lang) => {
Polyseed::from(lang, 0, birthday.unwrap_or(0), entropy).map(Seed::Polyseed).ok()
}
}
}
/// Returns seed as `String`.
pub fn to_string(&self) -> Zeroizing<String> {
match self {
Seed::Classic(seed) => seed.to_string(),
Seed::Polyseed(seed) => seed.to_string(),
}
}
/// Returns the entropy for this seed.
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
match self {
Seed::Classic(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.entropy().clone(),
}
}
/// Returns the key derived from this seed.
pub fn key(&self) -> Zeroizing<[u8; 32]> {
match self {
// Classic does not differentiate between its entropy and its key
Seed::Classic(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.key(),
}
}
/// Returns the birthday of this seed.
pub fn birthday(&self) -> u64 {
match self {
Seed::Classic(_) => 0,
Seed::Polyseed(seed) => seed.birthday(),
}
}
}

View File

@@ -25,9 +25,9 @@ use monero_rpc::RpcError;
pub use monero_rpc::{FeePriority, FeeRate}; pub use monero_rpc::{FeePriority, FeeRate};
use monero_serai::{ use monero_serai::{
io::*, io::*,
generators::hash_to_point,
primitives::{Commitment, keccak256}, primitives::{Commitment, keccak256},
ringct::{ ringct::{
hash_to_point,
clsag::{ClsagError, ClsagContext, Clsag}, clsag::{ClsagError, ClsagContext, Clsag},
bulletproofs::{MAX_COMMITMENTS, Bulletproof}, bulletproofs::{MAX_COMMITMENTS, Bulletproof},
RctBase, RctPrunable, RctProofs, RctBase, RctPrunable, RctProofs,
@@ -181,7 +181,7 @@ fn prepare_inputs(
.map_err(TransactionError::ClsagError)?, .map_err(TransactionError::ClsagError)?,
)); ));
tx.prefix.inputs.push(Input::ToKey { tx.prefix_mut().inputs.push(Input::ToKey {
amount: None, amount: None,
key_offsets: decoys.offsets().to_vec(), key_offsets: decoys.offsets().to_vec(),
key_image: image, key_image: image,
@@ -191,7 +191,7 @@ fn prepare_inputs(
// We now need to sort the inputs by their key image // We now need to sort the inputs by their key image
// We take the transaction's inputs, temporarily // We take the transaction's inputs, temporarily
let mut tx_inputs = Vec::with_capacity(inputs.len()); let mut tx_inputs = Vec::with_capacity(inputs.len());
core::mem::swap(&mut tx_inputs, &mut tx.prefix.inputs); core::mem::swap(&mut tx_inputs, &mut tx.prefix_mut().inputs);
// Then we join them with their signable contexts // Then we join them with their signable contexts
let mut joint = tx_inputs.into_iter().zip(signable).collect::<Vec<_>>(); let mut joint = tx_inputs.into_iter().zip(signable).collect::<Vec<_>>();
@@ -204,11 +204,11 @@ fn prepare_inputs(
} }
}); });
// We now re-create the consumed signable (tx.prefix.inputs already having an empty vector) and // We now re-create the consumed signable (tx.prefix().inputs already having an empty vector) and
// split the joint iterator back into two Vecs // split the joint iterator back into two Vecs
let mut signable = Vec::with_capacity(inputs.len()); let mut signable = Vec::with_capacity(inputs.len());
for (input, signable_i) in joint { for (input, signable_i) in joint {
tx.prefix.inputs.push(input); tx.prefix_mut().inputs.push(input);
signable.push(signable_i); signable.push(signable_i);
} }
@@ -800,24 +800,22 @@ impl SignableTransaction {
} }
( (
Transaction { Transaction::V2 {
prefix: TransactionPrefix { prefix: TransactionPrefix {
version: 2,
timelock: Timelock::None, timelock: Timelock::None,
inputs: vec![], inputs: vec![],
outputs: tx_outputs, outputs: tx_outputs,
extra, extra,
}, },
signatures: vec![], proofs: Some(RctProofs {
proofs: RctProofs {
base: RctBase { base: RctBase {
fee, fee,
encrypted_amounts, encrypted_amounts,
pseudo_outs: vec![], pseudo_outs: vec![],
commitments: commitments.iter().map(Commitment::calculate).collect(), commitments: commitments.iter().map(Commitment::calculate).collect(),
}, },
prunable: RctPrunable::Clsag { bulletproofs: bp, clsags: vec![], pseudo_outs: vec![] }, prunable: RctPrunable::Clsag { bulletproof: bp, clsags: vec![], pseudo_outs: vec![] },
}, }),
}, },
sum, sum,
) )
@@ -853,21 +851,28 @@ impl SignableTransaction {
let signable = prepare_inputs(&self.inputs, spend, &mut tx)?; let signable = prepare_inputs(&self.inputs, spend, &mut tx)?;
let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash()) let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash().unwrap())
.map_err(|_| TransactionError::WrongPrivateKey)?; .map_err(|_| TransactionError::WrongPrivateKey)?;
match tx.proofs.prunable { let fee = match tx {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"), Transaction::V2 {
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { proofs:
Some(RctProofs {
ref base,
prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
}),
..
} => {
clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>()); clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>());
pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::<Vec<_>>()); pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::<Vec<_>>());
base.fee
} }
_ => unreachable!("attempted to sign a TX which wasn't CLSAG"), _ => unreachable!("attempted to sign a TX which wasn't CLSAG"),
} };
if self.has_change { if self.has_change {
debug_assert_eq!( debug_assert_eq!(
self.fee_rate.calculate_fee_from_weight(tx.weight()), self.fee_rate.calculate_fee_from_weight(tx.weight()),
tx.proofs.base.fee, fee,
"transaction used unexpected fee", "transaction used unexpected fee",
); );
} }
@@ -892,7 +897,7 @@ impl Eventuality {
#[must_use] #[must_use]
pub fn matches(&self, tx: &Transaction) -> bool { pub fn matches(&self, tx: &Transaction) -> bool {
if self.payments.len() != tx.prefix.outputs.len() { if self.payments.len() != tx.prefix().outputs.len() {
return false; return false;
} }
@@ -900,12 +905,12 @@ impl Eventuality {
// Even if all the outputs were correct, a malicious extra could still cause a recipient to // Even if all the outputs were correct, a malicious extra could still cause a recipient to
// fail to receive their funds. // fail to receive their funds.
// This is the cheapest check available to perform as it does not require TX-specific ECC ops. // This is the cheapest check available to perform as it does not require TX-specific ECC ops.
if self.extra != tx.prefix.extra { if self.extra != tx.prefix().extra {
return false; return false;
} }
// Also ensure no timelock was set. // Also ensure no timelock was set.
if tx.prefix.timelock != Timelock::None { if tx.prefix().timelock != Timelock::None {
return false; return false;
} }
@@ -914,10 +919,14 @@ impl Eventuality {
&self.r_seed, &self.r_seed,
&self.inputs, &self.inputs,
&mut self.payments.clone(), &mut self.payments.clone(),
uniqueness(&tx.prefix.inputs), uniqueness(&tx.prefix().inputs),
); );
let rct_type = tx.proofs.rct_type(); let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
return false;
};
let rct_type = proofs.rct_type();
if rct_type != self.protocol.optimal_rct_type() { if rct_type != self.protocol.optimal_rct_type() {
return false; return false;
} }
@@ -928,16 +937,16 @@ impl Eventuality {
"created an Eventuality for a very old RctType we don't support proving for" "created an Eventuality for a very old RctType we don't support proving for"
); );
for (o, (expected, actual)) in outputs.iter().zip(tx.prefix.outputs.iter()).enumerate() { for (o, (expected, actual)) in outputs.iter().zip(tx.prefix().outputs.iter()).enumerate() {
// Verify the output, commitment, and encrypted amount. // Verify the output, commitment, and encrypted amount.
if (&Output { if (&Output {
amount: None, amount: None,
key: expected.dest.compress(), key: expected.dest.compress(),
view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()), view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()),
} != actual) || } != actual) ||
(Some(&expected.commitment.calculate()) != tx.proofs.base.commitments.get(o)) || (Some(&expected.commitment.calculate()) != proofs.base.commitments.get(o)) ||
(Some(&EncryptedAmount::Compact { amount: expected.amount }) != (Some(&EncryptedAmount::Compact { amount: expected.amount }) !=
tx.proofs.base.encrypted_amounts.get(o)) proofs.base.encrypted_amounts.get(o))
{ {
return false; return false;
} }

View File

@@ -27,7 +27,7 @@ use frost::{
use monero_serai::{ use monero_serai::{
ringct::{ ringct::{
clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig}, clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig},
RctPrunable, RctPrunable, RctProofs,
}, },
transaction::{Input, Transaction}, transaction::{Input, Transaction},
}; };
@@ -350,7 +350,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
} }
value.2.send(mask); value.2.send(mask);
tx.prefix.inputs.push(Input::ToKey { tx.prefix_mut().inputs.push(Input::ToKey {
amount: None, amount: None,
key_offsets: value.1.offsets().to_vec(), key_offsets: value.1.offsets().to_vec(),
key_image: value.0, key_image: value.0,
@@ -360,7 +360,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
commitments.push(value.4); commitments.push(value.4);
} }
let msg = tx.signature_hash(); let msg = tx.signature_hash().unwrap();
// Iterate over each CLSAG calling sign // Iterate over each CLSAG calling sign
let mut shares = Vec::with_capacity(self.clsags.len()); let mut shares = Vec::with_capacity(self.clsags.len());
@@ -390,9 +390,15 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine {
shares: HashMap<Participant, Self::SignatureShare>, shares: HashMap<Participant, Self::SignatureShare>,
) -> Result<Transaction, FrostError> { ) -> Result<Transaction, FrostError> {
let mut tx = self.tx; let mut tx = self.tx;
match tx.proofs.prunable { match tx {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"), Transaction::V2 {
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { proofs:
Some(RctProofs {
prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
..
}),
..
} => {
for (c, clsag) in self.clsags.drain(..).enumerate() { for (c, clsag) in self.clsags.drain(..).enumerate() {
let (clsag, pseudo_out) = clsag.complete( let (clsag, pseudo_out) = clsag.complete(
shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::<HashMap<_, _>>(), shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::<HashMap<_, _>>(),
@@ -401,11 +407,7 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine {
pseudo_outs.push(pseudo_out); pseudo_outs.push(pseudo_out);
} }
} }
RctPrunable::AggregateMlsagBorromean { .. } | _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"),
RctPrunable::MlsagBorromean { .. } |
RctPrunable::MlsagBulletproofs { .. } => {
unreachable!("attempted to sign a multisig TX which wasn't CLSAG")
}
} }
Ok(tx) Ok(tx)
} }

View File

@@ -0,0 +1,516 @@
use core::ops::Deref;
use std_shims::{
vec::Vec,
string::ToString,
io::{self, Read, Write},
};
use zeroize::{Zeroize, ZeroizeOnDrop};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
use monero_rpc::{RpcError, Rpc};
use monero_serai::{
io::*,
primitives::Commitment,
transaction::{Input, Timelock, Transaction},
block::Block,
};
use crate::{
PaymentId, Extra, address::SubaddressIndex, Scanner, EncryptedAmountExt, uniqueness, shared_key,
};
/// An absolute output ID, defined as its transaction hash and output index.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct AbsoluteId {
pub tx: [u8; 32],
pub o: u8,
}
impl core::fmt::Debug for AbsoluteId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt.debug_struct("AbsoluteId").field("tx", &hex::encode(self.tx)).field("o", &self.o).finish()
}
}
impl AbsoluteId {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.tx)?;
w.write_all(&[self.o])
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(32 + 1);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
Ok(AbsoluteId { tx: read_bytes(r)?, o: read_byte(r)? })
}
}
/// The data contained with an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct OutputData {
pub key: EdwardsPoint,
/// Absolute difference between the spend key and the key in this output
pub key_offset: Scalar,
pub commitment: Commitment,
}
impl core::fmt::Debug for OutputData {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("OutputData")
.field("key", &hex::encode(self.key.compress().0))
.field("key_offset", &hex::encode(self.key_offset.to_bytes()))
.field("commitment", &self.commitment)
.finish()
}
}
impl OutputData {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.key.compress().to_bytes())?;
w.write_all(&self.key_offset.to_bytes())?;
w.write_all(&self.commitment.mask.to_bytes())?;
w.write_all(&self.commitment.amount.to_le_bytes())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(32 + 32 + 32 + 8);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
Ok(OutputData {
key: read_point(r)?,
key_offset: read_scalar(r)?,
commitment: Commitment::new(read_scalar(r)?, read_u64(r)?),
})
}
}
/// The metadata for an output.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Metadata {
/// The subaddress this output was sent to.
pub subaddress: Option<SubaddressIndex>,
/// The payment ID included with this output.
/// There are 2 circumstances in which the reference wallet2 ignores the payment ID
/// but the payment ID will be returned here anyway:
///
/// 1) If the payment ID is tied to an output received by a subaddress account
/// that spent Monero in the transaction (the received output is considered
/// "change" and is not considered a "payment" in this case). If there are multiple
/// spending subaddress accounts in a transaction, the highest index spent key image
/// is used to determine the spending subaddress account.
///
/// 2) If the payment ID is the unencrypted variant and the block's hf version is
/// v12 or higher (https://github.com/serai-dex/serai/issues/512)
pub payment_id: Option<PaymentId>,
/// Arbitrary data encoded in TX extra.
pub arbitrary_data: Vec<Vec<u8>>,
}
impl core::fmt::Debug for Metadata {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("Metadata")
.field("subaddress", &self.subaddress)
.field("payment_id", &self.payment_id)
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
.finish()
}
}
impl Metadata {
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
if let Some(subaddress) = self.subaddress {
w.write_all(&[1])?;
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())?;
} else {
w.write_all(&[0])?;
}
if let Some(payment_id) = self.payment_id {
w.write_all(&[1])?;
payment_id.write(w)?;
} else {
w.write_all(&[0])?;
}
w.write_all(&u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes())?;
for part in &self.arbitrary_data {
w.write_all(&[u8::try_from(part.len()).unwrap()])?;
w.write_all(part)?;
}
Ok(())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(1 + 8 + 1);
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
let subaddress = if read_byte(r)? == 1 {
Some(
SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
.ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
)
} else {
None
};
Ok(Metadata {
subaddress,
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
arbitrary_data: {
let mut data = vec![];
for _ in 0 .. read_u32(r)? {
let len = read_byte(r)?;
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
}
data
},
})
}
}
/// A received output, defined as its absolute ID, data, and metadara.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ReceivedOutput {
pub absolute: AbsoluteId,
pub data: OutputData,
pub metadata: Metadata,
}
impl ReceivedOutput {
pub fn key(&self) -> EdwardsPoint {
self.data.key
}
pub fn key_offset(&self) -> Scalar {
self.data.key_offset
}
pub fn commitment(&self) -> Commitment {
self.data.commitment.clone()
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
&self.metadata.arbitrary_data
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.absolute.write(w)?;
self.data.write(w)?;
self.metadata.write(w)
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
Ok(ReceivedOutput {
absolute: AbsoluteId::read(r)?,
data: OutputData::read(r)?,
metadata: Metadata::read(r)?,
})
}
}
/// A spendable output, defined as a received output and its index on the Monero blockchain.
/// This index is dependent on the Monero blockchain and will only be known once the output is
/// included within a block. This may change if there's a reorganization.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct SpendableOutput {
pub output: ReceivedOutput,
pub global_index: u64,
}
impl SpendableOutput {
/// Update the spendable output's global index. This is intended to be called if a
/// re-organization occurred.
pub async fn refresh_global_index(&mut self, rpc: &impl Rpc) -> Result<(), RpcError> {
self.global_index = *rpc
.get_o_indexes(self.output.absolute.tx)
.await?
.get(usize::from(self.output.absolute.o))
.ok_or(RpcError::InvalidNode(
"node returned output indexes didn't include an index for this output".to_string(),
))?;
Ok(())
}
pub async fn from(rpc: &impl Rpc, output: ReceivedOutput) -> Result<SpendableOutput, RpcError> {
let mut output = SpendableOutput { output, global_index: 0 };
output.refresh_global_index(rpc).await?;
Ok(output)
}
pub fn key(&self) -> EdwardsPoint {
self.output.key()
}
pub fn key_offset(&self) -> Scalar {
self.output.key_offset()
}
pub fn commitment(&self) -> Commitment {
self.output.commitment()
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
self.output.arbitrary_data()
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.output.write(w)?;
w.write_all(&self.global_index.to_le_bytes())
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = vec![];
self.write(&mut serialized).unwrap();
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<SpendableOutput> {
Ok(SpendableOutput { output: ReceivedOutput::read(r)?, global_index: read_u64(r)? })
}
}
/// A collection of timelocked outputs, either received or spendable.
#[derive(Zeroize)]
pub struct Timelocked<O: Clone + Zeroize>(Timelock, Vec<O>);
impl<O: Clone + Zeroize> Drop for Timelocked<O> {
fn drop(&mut self) {
self.zeroize();
}
}
impl<O: Clone + Zeroize> ZeroizeOnDrop for Timelocked<O> {}
impl<O: Clone + Zeroize> Timelocked<O> {
pub fn timelock(&self) -> Timelock {
self.0
}
/// Return the outputs if they're not timelocked, or an empty vector if they are.
#[must_use]
pub fn not_locked(&self) -> Vec<O> {
if self.0 == Timelock::None {
return self.1.clone();
}
vec![]
}
/// Returns None if the Timelocks aren't comparable. Returns Some(vec![]) if none are unlocked.
#[must_use]
pub fn unlocked(&self, timelock: Timelock) -> Option<Vec<O>> {
// If the Timelocks are comparable, return the outputs if they're now unlocked
if self.0 <= timelock {
Some(self.1.clone())
} else {
None
}
}
#[must_use]
pub fn ignore_timelock(&self) -> Vec<O> {
self.1.clone()
}
}
impl Scanner {
/// Scan a transaction to discover the received outputs.
pub fn scan_transaction(&mut self, tx: &Transaction) -> Timelocked<ReceivedOutput> {
// Only scan RCT TXs since we can only spend RCT outputs
if tx.version() != 2 {
return Timelocked(tx.prefix().timelock, vec![]);
}
let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) else {
return Timelocked(tx.prefix().timelock, vec![]);
};
let Some((tx_keys, additional)) = extra.keys() else {
return Timelocked(tx.prefix().timelock, vec![]);
};
let payment_id = extra.payment_id();
let mut res = vec![];
for (o, output) in tx.prefix().outputs.iter().enumerate() {
// https://github.com/serai-dex/serai/issues/106
if let Some(burning_bug) = self.burning_bug.as_ref() {
if burning_bug.contains(&output.key) {
continue;
}
}
let output_key = decompress_point(output.key.to_bytes());
if output_key.is_none() {
continue;
}
let output_key = output_key.unwrap();
let additional = additional.as_ref().map(|additional| additional.get(o));
for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) {
let key = match key {
Some(Some(key)) => key,
Some(None) => {
// This is non-standard. There were additional keys, yet not one for this output
// https://github.com/monero-project/monero/
// blob/04a1e2875d6e35e27bb21497988a6c822d319c28/
// src/cryptonote_basic/cryptonote_format_utils.cpp#L1062
continue;
}
None => {
break;
}
};
let (view_tag, shared_key, payment_id_xor) = shared_key(
if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix().inputs)) } else { None },
self.pair.view.deref() * key,
o,
);
let payment_id = payment_id.map(|id| id ^ payment_id_xor);
if let Some(actual_view_tag) = output.view_tag {
if actual_view_tag != view_tag {
continue;
}
}
// P - shared == spend
let subaddress =
self.subaddresses.get(&(output_key - (&shared_key * ED25519_BASEPOINT_TABLE)).compress());
if subaddress.is_none() {
continue;
}
let subaddress = *subaddress.unwrap();
// If it has torsion, it'll subtract the non-torsioned shared key to a torsioned key
// We will not have a torsioned key in our HashMap of keys, so we wouldn't identify it as
// ours
// If we did though, it'd enable bypassing the included burning bug protection
assert!(output_key.is_torsion_free());
let mut key_offset = shared_key;
if let Some(subaddress) = subaddress {
key_offset += self.pair.subaddress_derivation(subaddress);
}
// Since we've found an output to us, get its amount
let mut commitment = Commitment::zero();
// Miner transaction
if let Some(amount) = output.amount {
commitment.amount = amount;
// Regular transaction
} else {
let proofs = match &tx {
Transaction::V2 { proofs: Some(proofs), .. } => &proofs,
_ => return Timelocked(tx.prefix().timelock, vec![]),
};
commitment = match proofs.base.encrypted_amounts.get(o) {
Some(amount) => amount.decrypt(shared_key),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => break,
};
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != proofs.base.commitments.get(o) {
break;
}
}
if commitment.amount != 0 {
res.push(ReceivedOutput {
absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() },
data: OutputData { key: output_key, key_offset, commitment },
metadata: Metadata { subaddress, payment_id, arbitrary_data: extra.data() },
});
if let Some(burning_bug) = self.burning_bug.as_mut() {
burning_bug.insert(output.key);
}
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
}
Timelocked(tx.prefix().timelock, res)
}
/// Scan a block to obtain its spendable outputs. Its the presence in a block giving these
/// transactions their global index, and this must be batched as asking for the index of specific
/// transactions is a dead giveaway for which transactions you successfully scanned. This
/// function obtains the output indexes for the miner transaction, incrementing from there
/// instead.
pub async fn scan(
&mut self,
rpc: &impl Rpc,
block: &Block,
) -> Result<Vec<Timelocked<SpendableOutput>>, RpcError> {
let mut index = rpc.get_o_indexes(block.miner_tx.hash()).await?[0];
let mut txs = vec![block.miner_tx.clone()];
txs.extend(rpc.get_transactions(&block.txs).await?);
let map = |mut timelock: Timelocked<ReceivedOutput>, index| {
if timelock.1.is_empty() {
None
} else {
Some(Timelocked(
timelock.0,
timelock
.1
.drain(..)
.map(|output| SpendableOutput {
global_index: index + u64::from(output.absolute.o),
output,
})
.collect(),
))
}
};
let mut res = vec![];
for tx in txs {
if let Some(timelock) = map(self.scan_transaction(&tx), index) {
res.push(timelock);
}
index += u64::try_from(
tx.prefix()
.outputs
.iter()
// Filter to v2 miner TX outputs/RCT outputs since we're tracking the RCT output index
.filter(|output| {
let is_v2_miner_tx =
(tx.version() == 2) && matches!(tx.prefix().inputs.first(), Some(Input::Gen(..)));
is_v2_miner_tx || output.amount.is_none()
})
.count(),
)
.unwrap()
}
Ok(res)
}
}

View File

@@ -1,3 +1,2 @@
mod address; mod address;
mod seed;
mod extra; mod extra;

View File

@@ -1,485 +0,0 @@
use zeroize::Zeroizing;
use rand_core::OsRng;
use curve25519_dalek::scalar::Scalar;
use monero_serai::primitives::keccak256;
use crate::seed::{Seed, SeedType, SeedError, classic, polyseed};
#[test]
fn test_classic_seed() {
struct Vector {
language: classic::Language,
seed: String,
spend: String,
view: String,
}
let vectors = [
Vector {
language: classic::Language::Chinese,
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
},
Vector {
language: classic::Language::English,
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
abnormal memoir nylon mostly building shrugged online ember northern \
ruby woes dauntless boil family illness inroads northern"
.into(),
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
},
Vector {
language: classic::Language::Dutch,
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
.into(),
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
},
Vector {
language: classic::Language::French,
seed: "poids vaseux tarte bazar poivre effet entier nuance \
sensuel ennui pacte osselet poudre battre alibi mouton \
stade paquet pliage gibier type question position projet pliage"
.into(),
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
},
Vector {
language: classic::Language::Spanish,
seed: "minero ocupar mirar evadir octubre cal logro miope \
opaco disco ancla litio clase cuello nasal clase \
fiar avance deseo mente grumo negro cordón croqueta clase"
.into(),
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
},
Vector {
language: classic::Language::German,
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
.into(),
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
},
Vector {
language: classic::Language::Italian,
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
forzare meritare litigare lezione segreto evasione votare buio \
licenza cliente dorso natale crescere vento tutelare vetta evasione"
.into(),
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
},
Vector {
language: classic::Language::Portuguese,
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
.into(),
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
},
Vector {
language: classic::Language::Japanese,
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
.into(),
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
},
Vector {
language: classic::Language::Russian,
seed: "шатер икра нация ехать получать инерция доза реальный \
рыжий таможня лопата душа веселый клетка атлас лекция \
обгонять паек наивный лыжный дурак стать ежик задача паек"
.into(),
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
},
Vector {
language: classic::Language::Esperanto,
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
.into(),
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
},
Vector {
language: classic::Language::Lojban,
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
blabi darno dembi janli blabi fenki bukpu burcu blabi"
.into(),
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
},
Vector {
language: classic::Language::EnglishOld,
seed: "glorious especially puff son moment add youth nowhere \
throw glide grip wrong rhythm consume very swear \
bitter heavy eventually begin reason flirt type unable"
.into(),
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
},
// The following seeds require the language specification in order to calculate
// a single valid checksum
Vector {
language: classic::Language::Spanish,
seed: "pluma laico atraer pintor peor cerca balde buscar \
lancha batir nulo reloj resto gemelo nevera poder columna gol \
oveja latir amplio bolero feliz fuerza nevera"
.into(),
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
},
Vector {
language: classic::Language::Spanish,
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
.into(),
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
},
Vector {
language: classic::Language::English,
seed: "plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus plus"
.into(),
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
},
Vector {
language: classic::Language::Spanish,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
},
Vector {
language: classic::Language::English,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
},
];
for vector in vectors {
fn trim_by_lang(word: &str, lang: Language) -> String {
if lang != Language::DeprecatedEnglish {
word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect()
} else {
word.to_string()
}
}
let trim_seed = |seed: &str| {
seed
.split_whitespace()
.map(|word| trim_by_lang(word, vector.language))
.collect::<Vec<_>>()
.join(" ")
};
// Test against Monero
{
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed =
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(vector.seed.clone()))
.unwrap();
let trim = trim_seed(&vector.seed);
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap()
);
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
// For classical seeds, Monero directly uses the entropy as a spend key
assert_eq!(
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
);
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
// Monero then derives the view key as H(spend)
assert_eq!(
Scalar::from_bytes_mod_order(keccak256(spend)),
Scalar::from_canonical_bytes(view).unwrap()
);
assert_eq!(
Seed::from_entropy(SeedType::Classic(vector.language), Zeroizing::new(spend), None)
.unwrap(),
seed
);
}
// Test against ourselves
{
let seed = Seed::new(&mut OsRng, SeedType::Classic(vector.language));
println!("{}. seed: {}", line!(), *seed.to_string());
let trim = trim_seed(&seed.to_string());
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap()
);
assert_eq!(
seed,
Seed::from_entropy(SeedType::Classic(vector.language), seed.entropy(), None).unwrap()
);
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), seed.to_string()).unwrap()
);
}
}
}
#[test]
fn test_polyseed() {
struct Vector {
language: polyseed::Language,
seed: String,
entropy: String,
birthday: u64,
has_prefix: bool,
has_accent: bool,
}
let vectors = [
Vector {
language: polyseed::Language::English,
seed: "raven tail swear infant grief assist regular lamp \
duck valid someone little harsh puppy airport language"
.into(),
entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(),
birthday: 1638446400,
has_prefix: true,
has_accent: false,
},
Vector {
language: polyseed::Language::Spanish,
seed: "eje fin parte célebre tabú pestaña lienzo puma \
prisión hora regalo lengua existir lápiz lote sonoro"
.into(),
entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(),
birthday: 3118651200,
has_prefix: true,
has_accent: true,
},
Vector {
language: polyseed::Language::French,
seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \
prouesse réserve ampleur ajuster muter caméra enchère"
.into(),
entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(),
birthday: 1679314966,
has_prefix: true,
has_accent: true,
},
Vector {
language: polyseed::Language::Italian,
seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \
olandese normale tristezza episodio voragine forbito achille"
.into(),
entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(),
birthday: 1679316358,
has_prefix: true,
has_accent: false,
},
Vector {
language: polyseed::Language::Portuguese,
seed: "caverna custear azedo adeus senador apertada sedoso omitir \
sujeito aurora videira molho cartaz gesso dentista tapar"
.into(),
entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(),
birthday: 1679316657,
has_prefix: true,
has_accent: false,
},
Vector {
language: polyseed::Language::Czech,
seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \
vesta kabel herna stodola uvolnit ustrnout email"
.into(),
entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(),
birthday: 1679316898,
has_prefix: true,
has_accent: false,
},
Vector {
language: polyseed::Language::Korean,
seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \
지우개 보관 절망 말기 시각 귀신"
.into(),
entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(),
birthday: 1679317073,
has_prefix: false,
has_accent: false,
},
Vector {
language: polyseed::Language::Japanese,
seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \
といれ おさない おさえる むかう ぬぐう なふだ せまる"
.into(),
entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(),
birthday: 1679318722,
has_prefix: false,
has_accent: false,
},
Vector {
language: polyseed::Language::ChineseTraditional,
seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(),
entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(),
birthday: 1679426433,
has_prefix: false,
has_accent: false,
},
Vector {
language: polyseed::Language::ChineseSimplified,
seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(),
entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(),
birthday: 1679426817,
has_prefix: false,
has_accent: false,
},
// The following seed requires the language specification in order to calculate
// a single valid checksum
Vector {
language: polyseed::Language::Spanish,
seed: "impo sort usua cabi venu nobl oliv clim \
cont barr marc auto prod vaca torn fati"
.into(),
entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(),
birthday: 1701511650,
has_prefix: true,
has_accent: true,
},
];
for vector in vectors {
let add_whitespace = |mut seed: String| {
seed.push(' ');
seed
};
let seed_without_accents = |seed: &str| {
seed
.split_whitespace()
.map(|w| w.chars().filter(char::is_ascii).collect::<String>())
.collect::<Vec<_>>()
.join(" ")
};
let trim_seed = |seed: &str| {
let seed_to_trim =
if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() };
seed_to_trim
.split_whitespace()
.map(|w| {
let mut ascii = 0;
let mut to_take = w.len();
for (i, char) in w.chars().enumerate() {
if char.is_ascii() {
ascii += 1;
}
if ascii == polyseed::PREFIX_LEN {
// +1 to include this character, which put us at the prefix length
to_take = i + 1;
break;
}
}
w.chars().take(to_take).collect::<String>()
})
.collect::<Vec<_>>()
.join(" ")
};
// String -> Seed
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(vector.seed.clone()))
.unwrap();
let trim = trim_seed(&vector.seed);
let add_whitespace = add_whitespace(vector.seed.clone());
let seed_without_accents = seed_without_accents(&vector.seed);
// Make sure a version with added whitespace still works
let whitespaced_seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(add_whitespace))
.unwrap();
assert_eq!(seed, whitespaced_seed);
// Check trimmed versions works
if vector.has_prefix {
let trimmed_seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(trim)).unwrap();
assert_eq!(seed, trimmed_seed);
}
// Check versions without accents work
if vector.has_accent {
let seed_without_accents = Seed::from_string(
SeedType::Polyseed(vector.language),
Zeroizing::new(seed_without_accents),
)
.unwrap();
assert_eq!(seed, seed_without_accents);
}
let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap());
assert_eq!(seed.entropy(), entropy);
assert!(seed.birthday().abs_diff(vector.birthday) < polyseed::TIME_STEP);
// Entropy -> Seed
let from_entropy =
Seed::from_entropy(SeedType::Polyseed(vector.language), entropy, Some(seed.birthday()))
.unwrap();
assert_eq!(seed.to_string(), from_entropy.to_string());
// Check against ourselves
{
let seed = Seed::new(&mut OsRng, SeedType::Polyseed(vector.language));
println!("{}. seed: {}", line!(), *seed.to_string());
assert_eq!(
seed,
Seed::from_string(SeedType::Polyseed(vector.language), seed.to_string()).unwrap()
);
assert_eq!(
seed,
Seed::from_entropy(
SeedType::Polyseed(vector.language),
seed.entropy(),
Some(seed.birthday())
)
.unwrap()
);
}
}
}
#[test]
fn test_invalid_polyseed() {
// This seed includes unsupported features bits and should error on decode
let seed = "include domain claim resemble urban hire lunch bird \
crucial fire best wife ring warm ignore model"
.into();
let res =
Seed::from_string(SeedType::Polyseed(polyseed::Language::English), Zeroizing::new(seed));
assert_eq!(res, Err(SeedError::UnsupportedFeatures));
}

View File

@@ -1,6 +1,7 @@
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
monero::{transaction::Transaction, DEFAULT_LOCK_WINDOW}, DEFAULT_LOCK_WINDOW,
transaction::Transaction,
rpc::{OutputResponse, Rpc}, rpc::{OutputResponse, Rpc},
SpendableOutput, SpendableOutput,
}; };

View File

@@ -61,16 +61,19 @@ test!(
}, },
|_, mut tx: Transaction, _, eventuality: Eventuality| async move { |_, mut tx: Transaction, _, eventuality: Eventuality| async move {
// 4 explicitly outputs added and one change output // 4 explicitly outputs added and one change output
assert_eq!(tx.prefix.outputs.len(), 5); assert_eq!(tx.prefix().outputs.len(), 5);
// The eventuality's available extra should be the actual TX's // The eventuality's available extra should be the actual TX's
assert_eq!(tx.prefix.extra, eventuality.extra()); assert_eq!(tx.prefix().extra, eventuality.extra());
// The TX should match // The TX should match
assert!(eventuality.matches(&tx)); assert!(eventuality.matches(&tx));
// Mutate the TX // Mutate the TX
tx.proofs.base.commitments[0] += ED25519_BASEPOINT_POINT; let Transaction::V2 { proofs: Some(ref mut proofs), .. } = tx else {
panic!("TX wasn't RingCT")
};
proofs.base.commitments[0] += ED25519_BASEPOINT_POINT;
// Verify it no longer matches // Verify it no longer matches
assert!(!eventuality.matches(&tx)); assert!(!eventuality.matches(&tx));
}, },

View File

@@ -10,7 +10,7 @@ use tokio::sync::Mutex;
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
monero::transaction::Transaction, transaction::Transaction,
rpc::Rpc, rpc::Rpc,
ViewPair, Scanner, ViewPair, Scanner,
address::{Network, AddressType, AddressSpec, AddressMeta, MoneroAddress}, address::{Network, AddressType, AddressSpec, AddressMeta, MoneroAddress},
@@ -80,7 +80,8 @@ pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> Spe
/// Make sure the weight and fee match the expected calculation. /// Make sure the weight and fee match the expected calculation.
pub fn check_weight_and_fee(tx: &Transaction, fee_rate: FeeRate) { pub fn check_weight_and_fee(tx: &Transaction, fee_rate: FeeRate) {
let fee = tx.proofs.base.fee; let Transaction::V2 { proofs: Some(ref proofs), .. } = tx else { panic!("TX wasn't RingCT") };
let fee = proofs.base.fee;
let weight = tx.weight(); let weight = tx.weight();
let expected_weight = fee_rate.calculate_weight_from_fee(fee); let expected_weight = fee_rate.calculate_weight_from_fee(fee);

View File

@@ -2,7 +2,7 @@ use rand_core::OsRng;
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
monero::transaction::Transaction, Protocol, rpc::Rpc, extra::Extra, address::SubaddressIndex, transaction::Transaction, Protocol, rpc::Rpc, extra::Extra, address::SubaddressIndex,
ReceivedOutput, SpendableOutput, DecoySelection, Decoys, SignableTransactionBuilder, ReceivedOutput, SpendableOutput, DecoySelection, Decoys, SignableTransactionBuilder,
}; };
@@ -136,7 +136,7 @@ test!(
assert_eq!(sub_outputs[0].commitment().amount, 1); assert_eq!(sub_outputs[0].commitment().amount, 1);
// Make sure only one R was included in TX extra // Make sure only one R was included in TX extra
assert!(Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) assert!(Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref())
.unwrap() .unwrap()
.keys() .keys()
.unwrap() .unwrap()
@@ -306,7 +306,8 @@ test!(
assert_eq!(outputs[1].commitment().amount, 50000); assert_eq!(outputs[1].commitment().amount, 50000);
// The remainder should get shunted to fee, which is fingerprintable // The remainder should get shunted to fee, which is fingerprintable
assert_eq!(tx.proofs.base.fee, 1000000000000 - 10000 - 50000); let Transaction::V2 { proofs: Some(ref proofs), .. } = tx else { panic!("TX wasn't RingCT") };
assert_eq!(proofs.base.fee, 1000000000000 - 10000 - 50000);
}, },
), ),
); );

View File

@@ -7,7 +7,7 @@ use serde_json::json;
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
monero::transaction::Transaction, transaction::Transaction,
rpc::Rpc, rpc::Rpc,
address::{Network, AddressSpec, SubaddressIndex, MoneroAddress}, address::{Network, AddressSpec, SubaddressIndex, MoneroAddress},
extra::{MAX_TX_EXTRA_NONCE_SIZE, Extra, PaymentId}, extra::{MAX_TX_EXTRA_NONCE_SIZE, Extra, PaymentId},
@@ -226,7 +226,7 @@ test!(
assert_eq!(transfer.transfer.payment_id, hex::encode([0u8; 8])); assert_eq!(transfer.transfer.payment_id, hex::encode([0u8; 8]));
// Make sure only one R was included in TX extra // Make sure only one R was included in TX extra
assert!(Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) assert!(Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref())
.unwrap() .unwrap()
.keys() .keys()
.unwrap() .unwrap()
@@ -285,7 +285,7 @@ test!(
// Make sure 3 additional pub keys are included in TX extra // Make sure 3 additional pub keys are included in TX extra
let keys = let keys =
Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()).unwrap().keys().unwrap().1.unwrap(); Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()).unwrap().keys().unwrap().1.unwrap();
assert_eq!(keys.len(), 3); assert_eq!(keys.len(), 3);
}, },

View File

@@ -0,0 +1,51 @@
[package]
name = "monero-wallet-util"
version = "0.1.0"
description = "Additional utility functions for monero-wallet"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet/util"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
monero-wallet = { path = "..", default-features = false }
monero-seed = { path = "../seed", default-features = false }
polyseed = { path = "../polyseed", default-features = false }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
[features]
std = [
"std-shims/std",
"thiserror",
"zeroize/std",
"rand_core/std",
"monero-wallet/std",
"monero-seed/std",
"polyseed/std",
]
compile-time-generators = ["monero-wallet/compile-time-generators"]
multisig = ["monero-wallet/multisig", "std"]
default = ["std", "compile-time-generators"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -6,6 +6,9 @@ This library is isolated as it adds a notable amount of dependencies to the
tree, and to be a subject to a distinct versioning policy. This library may tree, and to be a subject to a distinct versioning policy. This library may
more frequently undergo breaking API changes. more frequently undergo breaking API changes.
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Features ### Features
- Support for Monero's seed algorithm - Support for Monero's seed algorithm

View File

@@ -1,2 +1,9 @@
pub use monero_seed as seed; #![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use monero_polyseed as seed; #![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
pub use monero_wallet::*;
/// Seed creation and parsing functionality.
pub mod seed;

View File

@@ -0,0 +1,150 @@
use core::fmt;
use std_shims::string::String;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use rand_core::{RngCore, CryptoRng};
pub use monero_seed as original;
pub use polyseed;
use original::{SeedError as OriginalSeedError, Seed as OriginalSeed};
use polyseed::{PolyseedError, Polyseed};
/// An error from working with seeds.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum SeedError {
/// The seed was invalid.
#[cfg_attr(feature = "std", error("invalid seed"))]
InvalidSeed,
/// The entropy was invalid.
#[cfg_attr(feature = "std", error("invalid entropy"))]
InvalidEntropy,
/// The checksum did not match the data.
#[cfg_attr(feature = "std", error("invalid checksum"))]
InvalidChecksum,
/// Unsupported features were enabled.
#[cfg_attr(feature = "std", error("unsupported features"))]
UnsupportedFeatures,
}
impl From<OriginalSeedError> for SeedError {
fn from(error: OriginalSeedError) -> SeedError {
match error {
OriginalSeedError::DeprecatedEnglishWithChecksum | OriginalSeedError::InvalidChecksum => {
SeedError::InvalidChecksum
}
OriginalSeedError::InvalidSeed => SeedError::InvalidSeed,
}
}
}
impl From<PolyseedError> for SeedError {
fn from(error: PolyseedError) -> SeedError {
match error {
PolyseedError::UnsupportedFeatures => SeedError::UnsupportedFeatures,
PolyseedError::InvalidEntropy => SeedError::InvalidEntropy,
PolyseedError::InvalidSeed => SeedError::InvalidSeed,
PolyseedError::InvalidChecksum => SeedError::InvalidChecksum,
}
}
}
/// The type of the seed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SeedType {
/// The seed format originally used by Monero,
Original(monero_seed::Language),
/// Polyseed.
Polyseed(polyseed::Language),
}
/// A seed, internally either the original format or a Polyseed.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub enum Seed {
/// The originally formatted seed.
Original(OriginalSeed),
/// A Polyseed.
Polyseed(Polyseed),
}
impl fmt::Debug for Seed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Seed::Original(_) => f.debug_struct("Seed::Original").finish_non_exhaustive(),
Seed::Polyseed(_) => f.debug_struct("Seed::Polyseed").finish_non_exhaustive(),
}
}
}
impl Seed {
/// Create a new seed.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, seed_type: SeedType) -> Seed {
match seed_type {
SeedType::Original(lang) => Seed::Original(OriginalSeed::new(rng, lang)),
SeedType::Polyseed(lang) => Seed::Polyseed(Polyseed::new(rng, lang)),
}
}
/// Parse a seed from a string.
pub fn from_string(seed_type: SeedType, words: Zeroizing<String>) -> Result<Seed, SeedError> {
match seed_type {
SeedType::Original(lang) => Ok(OriginalSeed::from_string(lang, words).map(Seed::Original)?),
SeedType::Polyseed(lang) => Ok(Polyseed::from_string(lang, words).map(Seed::Polyseed)?),
}
}
/// Create a seed from entropy.
///
/// A birthday may be optionally provided, denoted in seconds since the epoch. For
/// SeedType::Original, it will be ignored. For SeedType::Polyseed, it'll be embedded into the
/// seed.
///
/// For SeedType::Polyseed, the last 13 bytes of `entropy` must be 0.
// TODO: Return Result, not Option
pub fn from_entropy(
seed_type: SeedType,
entropy: Zeroizing<[u8; 32]>,
birthday: Option<u64>,
) -> Option<Seed> {
match seed_type {
SeedType::Original(lang) => OriginalSeed::from_entropy(lang, entropy).map(Seed::Original),
SeedType::Polyseed(lang) => {
Polyseed::from(lang, 0, birthday.unwrap_or(0), entropy).ok().map(Seed::Polyseed)
}
}
}
/// Converts the seed to a string.
pub fn to_string(&self) -> Zeroizing<String> {
match self {
Seed::Original(seed) => seed.to_string(),
Seed::Polyseed(seed) => seed.to_string(),
}
}
/// Get the entropy for this seed.
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
match self {
Seed::Original(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.entropy().clone(),
}
}
/// Get the key derived from this seed.
pub fn key(&self) -> Zeroizing<[u8; 32]> {
match self {
// Original does not differentiate between its entropy and its key
Seed::Original(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.key(),
}
}
/// Get the birthday of this seed, denoted in seconds since the epoch.
pub fn birthday(&self) -> u64 {
match self {
Seed::Original(_) => 0,
Seed::Polyseed(seed) => seed.birthday(),
}
}
}

View File

@@ -15,7 +15,8 @@ use frost::{curve::Ed25519, ThresholdKeys};
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
monero::{ringct::RctType, transaction::Transaction, block::Block}, transaction::Transaction,
block::Block,
Protocol, Protocol,
rpc::{RpcError, Rpc}, rpc::{RpcError, Rpc},
ViewPair, Scanner, ViewPair, Scanner,
@@ -118,7 +119,10 @@ impl TransactionTrait<Monero> for Transaction {
#[cfg(test)] #[cfg(test)]
async fn fee(&self, _: &Monero) -> u64 { async fn fee(&self, _: &Monero) -> u64 {
self.rct_signatures.base.fee match self {
Transaction::V1 { .. } => panic!("v1 TX in test-only function"),
Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee,
}
} }
} }
@@ -282,14 +286,11 @@ impl Monero {
let tx = let tx =
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?; self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate // Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
if tx.rct_signatures.rct_type() != RctType::Null { let fee = match &tx {
continue; Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
} _ => continue,
// This isn't entirely accurate as Bulletproof TXs will have a higher weight than their };
// serialization length fees.push(fee / u64::try_from(tx.weight()).unwrap());
// It's likely 'good enough'
// TODO2: Improve
fees.push(tx.rct_signatures.base.fee / u64::try_from(tx.serialize().len()).unwrap());
} }
fees.sort(); fees.sort();
let fee = fees.get(fees.len() / 2).copied().unwrap_or(0); let fee = fees.get(fees.len() / 2).copied().unwrap_or(0);
@@ -317,7 +318,7 @@ impl Monero {
// Get the protocol for the specified block number // Get the protocol for the specified block number
#[cfg(not(test))] #[cfg(not(test))]
let protocol = Protocol::try_from(block_for_fee.header.major_version) let protocol = Protocol::try_from(block_for_fee.header.hardfork_version)
.map_err(|()| NetworkError::ConnectionError)?; .map_err(|()| NetworkError::ConnectionError)?;
// If this is a test, we won't be using a mainnet node and need a distinct protocol // If this is a test, we won't be using a mainnet node and need a distinct protocol
// determination // determination
@@ -576,10 +577,10 @@ impl Network for Monero {
tx.unwrap() tx.unwrap()
}; };
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix.extra) { if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) {
if eventuality.matches(&tx) { if eventuality.matches(&tx) {
res.insert( res.insert(
eventualities.map.remove(&tx.prefix.extra).unwrap().0, eventualities.map.remove(&tx.prefix().extra).unwrap().0,
(usize::try_from(block.number().unwrap()).unwrap(), tx.id(), tx), (usize::try_from(block.number().unwrap()).unwrap(), tx.id(), tx),
); );
} }

View File

@@ -349,7 +349,8 @@ async fn mint_and_burn_test() {
{ {
use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar};
use monero_wallet::{ use monero_wallet::{
monero::{io::decompress_point, transaction::Timelock}, io::decompress_point,
transaction::Timelock,
Protocol, Protocol,
rpc::Rpc, rpc::Rpc,
ViewPair, Scanner, DecoySelection, Decoys, Change, FeePriority, SignableTransaction, ViewPair, Scanner, DecoySelection, Decoys, Change, FeePriority, SignableTransaction,
@@ -582,7 +583,7 @@ async fn mint_and_burn_test() {
// Verify the received Monero TX // Verify the received Monero TX
{ {
use monero_wallet::{rpc::Rpc, ViewPair, Scanner}; use monero_wallet::{transaction::Transaction, rpc::Rpc, ViewPair, Scanner};
let rpc = handles[0].monero(&ops).await; let rpc = handles[0].monero(&ops).await;
let mut scanner = Scanner::from_view( let mut scanner = Scanner::from_view(
ViewPair::new(monero_spend, Zeroizing::new(monero_view)), ViewPair::new(monero_spend, Zeroizing::new(monero_view)),
@@ -603,7 +604,10 @@ async fn mint_and_burn_test() {
assert_eq!(block.txs.len(), 1); assert_eq!(block.txs.len(), 1);
let tx = rpc.get_transaction(block.txs[0]).await.unwrap(); let tx = rpc.get_transaction(block.txs[0]).await.unwrap();
let tx_fee = tx.rct_signatures.base.fee; let tx_fee = match &tx {
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
_ => panic!("fetched TX wasn't a signed V2 TX"),
};
assert_eq!(outputs[0].commitment().amount, 1_000_000_000_000 - tx_fee); assert_eq!(outputs[0].commitment().amount, 1_000_000_000_000 - tx_fee);
found = true; found = true;

View File

@@ -35,14 +35,4 @@ dkg = { path = "../../crypto/dkg", default-features = false }
bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] } bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] }
monero-io = { path = "../../coins/monero/io", default-features = false } monero-wallet-util = { path = "../../coins/monero/wallet/util", default-features = false, features = ["compile-time-generators"] }
monero-generators = { path = "../../coins/monero/generators", default-features = false }
monero-primitives = { path = "../../coins/monero/primitives", default-features = false }
monero-mlsag = { path = "../../coins/monero/ringct/mlsag", default-features = false }
monero-clsag = { path = "../../coins/monero/ringct/clsag", default-features = false }
monero-bulletproofs = { path = "../../coins/monero/ringct/bulletproofs", default-features = false }
monero-serai = { path = "../../coins/monero", default-features = false }
monero-rpc = { path = "../../coins/monero/rpc", default-features = false }
monero-seed = { path = "../../coins/monero/wallet/seed", default-features = false }
monero-polyseed = { path = "../../coins/monero/wallet/polyseed", default-features = false }
monero-wallet = { path = "../../coins/monero/wallet", default-features = false }

View File

@@ -20,12 +20,4 @@ pub use frost_schnorrkel;
pub use bitcoin_serai; pub use bitcoin_serai;
pub use monero_io; pub use monero_wallet_util;
pub use monero_generators;
pub use monero_primitives;
pub use monero_mlsag;
pub use monero_clsag;
pub use monero_bulletproofs;
pub use monero_serai;
pub use monero_rpc;
pub use monero_wallet;

View File

@@ -579,7 +579,7 @@ impl Coordinator {
} }
NetworkId::Monero => { NetworkId::Monero => {
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{rpc::Rpc, monero::transaction::Transaction}; use monero_wallet::{transaction::Transaction, rpc::Rpc};
let rpc = SimpleRequestRpc::new(rpc_url) let rpc = SimpleRequestRpc::new(rpc_url)
.await .await

View File

@@ -437,8 +437,8 @@ impl Wallet {
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use monero_simple_request_rpc::SimpleRequestRpc; use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
io::decompress_point,
rpc::Rpc, rpc::Rpc,
monero::io::decompress_point,
Protocol, Protocol,
address::{Network, AddressType, AddressMeta, Address}, address::{Network, AddressType, AddressMeta, Address},
SpendableOutput, DecoySelection, Decoys, Change, FeePriority, Scanner, SpendableOutput, DecoySelection, Decoys, Change, FeePriority, Scanner,