diff --git a/Cargo.lock b/Cargo.lock index 632e25921..59bc10446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.61.0", +] + [[package]] name = "console-api" version = "0.6.0" @@ -941,6 +954,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -1033,6 +1058,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -2182,6 +2213,7 @@ dependencies = [ "bech32", "clap", "clap_complete", + "dialoguer", "itertools 0.11.0", "neptune-cash", "proptest", @@ -3330,6 +3362,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" diff --git a/neptune-core-cli/Cargo.toml b/neptune-core-cli/Cargo.toml index 4102fece4..8bf7266eb 100644 --- a/neptune-core-cli/Cargo.toml +++ b/neptune-core-cli/Cargo.toml @@ -36,6 +36,7 @@ tarpc = { version = "^0.34", features = [ ] } tokio = { version = "1.41", features = ["full", "tracing"] } bech32 = ">=0.9, <0.10" +dialoguer = "0.12.0" [dev-dependencies] arbitrary = "1.4.2" diff --git a/neptune-core-cli/src/main.rs b/neptune-core-cli/src/main.rs index 25193fe86..386e04419 100644 --- a/neptune-core-cli/src/main.rs +++ b/neptune-core-cli/src/main.rs @@ -19,6 +19,8 @@ use clap::CommandFactory; use clap::Parser; use clap_complete::generate; use clap_complete::Shell; +use dialoguer::theme::ColorfulTheme; +use dialoguer::Select; use itertools::Itertools; use neptune_cash::api::export::TransactionKernelId; use neptune_cash::api::tx_initiation::builder::tx_output_list_builder::OutputFormat; @@ -188,7 +190,7 @@ enum Command { max_num_blocks: Option, }, - /// Show smallest block interval in the specified range. + /// Show the smallest block interval in the specified range. MinBlockInterval { last_block: BlockSelector, max_num_blocks: Option, @@ -200,7 +202,7 @@ enum Command { max_num_blocks: Option, }, - /// Show largest difficulty in the specified range. + /// Show the largest difficulty in the specified range. MaxBlockDifficulty { last_block: BlockSelector, max_num_blocks: Option, @@ -297,7 +299,7 @@ enum Command { /// block proposals, and new transactions from being received. Freeze, - /// If state updates have been paused, resumes them. Otherwise does nothing. + /// If state updates have been paused, resumes them. Otherwise, does nothing. Unfreeze, /// pause mining @@ -549,12 +551,12 @@ async fn main() -> Result<()> { // prompt user for all shares let mut shares = vec![]; - let capture_integers = Regex::new(r"^(\d+)\/(\d+)$").unwrap(); + let capture_integers = Regex::new(r"^(\d+)\/(\d+)$")?; while shares.len() != *t { println!("Enter share index (\"i/n\"): "); let mut buffer = "".to_string(); - std::io::stdin() + io::stdin() .read_line(&mut buffer) .expect("Cannot accept user input."); let buffer = buffer.trim(); @@ -722,7 +724,7 @@ async fn main() -> Result<()> { } // all other operations need a connection to the server - let server_socket = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), args.port); + let server_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port); let Ok(transport) = tarpc::serde_transport::tcp::connect(server_socket, Json::default).await else { eprintln!("This command requires a connection to `neptune-core`, but that connection could not be established. Is `neptune-core` running?"); @@ -1332,8 +1334,8 @@ async fn main() -> Result<()> { // // Otherwise, we call cookie_hint() RPC to obtain data-dir. // But the API might be disabled, which we detect and fallback to the default data-dir. -async fn get_cookie_hint(client: &RPCClient, args: &Config) -> anyhow::Result { - async fn fallback(client: &RPCClient, args: &Config) -> anyhow::Result { +async fn get_cookie_hint(client: &RPCClient, args: &Config) -> Result { + async fn fallback(client: &RPCClient, args: &Config) -> Result { let network = client.network(context::current()).await??; let data_directory = DataDirectory::get(args.data_dir.clone(), network)?; Ok(auth::CookieHint { @@ -1416,7 +1418,7 @@ fn process_utxo_notifications( network: Network, private_notifications: Vec, receiver_tag: Option, -) -> anyhow::Result<()> { +) -> Result<()> { let data_dir = root_data_dir.utxo_transfer_directory_path(); if !private_notifications.is_empty() { @@ -1428,10 +1430,7 @@ fn process_utxo_notifications( // TODO: It would be better if this timestamp was read from the created // transaction. - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(); // write out one UtxoTransferEntry in a json file, per output let mut wrote_file_cnt = 0usize; @@ -1461,7 +1460,7 @@ fn process_utxo_notifications( let file_path = file_dir.join(&file_name); println!("creating file: {}", file_path.display()); let file = std::fs::File::create_new(&file_path)?; - let mut writer = std::io::BufWriter::new(file); + let mut writer = io::BufWriter::new(file); serde_json::to_writer_pretty(&mut writer, &entry)?; writer.flush()?; @@ -1491,13 +1490,20 @@ or use equivalent claim functionality of your chosen wallet software. } fn enter_seed_phrase_dialog() -> Result { + let mnemonic_length_list = [18, 24]; + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose your mnemonic length") + .default(0) + .items(mnemonic_length_list) + .interact()?; + let mnemonic_length = mnemonic_length_list[selection]; let mut phrase = vec![]; let mut i = 1; loop { print!("{i}. "); - io::stdout().flush()?; + stdout().flush()?; let mut buffer = "".to_string(); - std::io::stdin() + io::stdin() .read_line(&mut buffer) .expect("Cannot accept user input."); let word = buffer.trim(); @@ -1508,7 +1514,7 @@ fn enter_seed_phrase_dialog() -> Result { { phrase.push(word.to_string()); i += 1; - if i > 18 { + if i > mnemonic_length { break; } } else { diff --git a/neptune-core/src/state/wallet/mod.rs b/neptune-core/src/state/wallet/mod.rs index 314cdaebe..e6e5a3249 100644 --- a/neptune-core/src/state/wallet/mod.rs +++ b/neptune-core/src/state/wallet/mod.rs @@ -35,8 +35,6 @@ mod tests { use tasm_lib::prelude::Digest; use tasm_lib::prelude::Tip5; use tasm_lib::triton_vm::prelude::BFieldElement; - use tasm_lib::triton_vm::prelude::XFieldElement; - use tasm_lib::twenty_first::math::x_field_element::EXTENSION_DEGREE; use tracing_test::traced_test; use unlocked_utxo::UnlockedUtxo; @@ -61,6 +59,7 @@ mod tests { use crate::state::transaction::tx_creation_config::TxCreationConfig; use crate::state::transaction::tx_proving_capability::TxProvingCapability; use crate::state::wallet::expected_utxo::UtxoNotifier; + use crate::state::wallet::secret_key_material::BField32Bytes; use crate::state::wallet::secret_key_material::SecretKeyMaterial; use crate::state::wallet::transaction_output::TxOutput; use crate::state::wallet::transaction_output::TxOutputList; @@ -1151,18 +1150,18 @@ mod tests { proptest::proptest! { #[test] fn master_seed_is_not_sender_randomness( - secret in proptest_arbitrary_interop::arb::() + secret in proptest_arbitrary_interop::arb::() ) { let secret_as_digest = Digest::new( [ - secret.coefficients.to_vec(), - vec![BFieldElement::new(0); Digest::LEN - EXTENSION_DEGREE], + secret.0.to_vec(), + vec![BFieldElement::new(0); Digest::LEN - 4], ] .concat() .try_into() .unwrap(), ); - let wallet = WalletEntropy::new(SecretKeyMaterial(secret)); + let wallet = WalletEntropy::new(SecretKeyMaterial::V1(secret)); assert_ne!( wallet.generate_sender_randomness(BlockHeight::genesis(), random()), secret_as_digest diff --git a/neptune-core/src/state/wallet/secret_key_material.rs b/neptune-core/src/state/wallet/secret_key_material.rs index 3c52d5a62..6bc982db0 100644 --- a/neptune-core/src/state/wallet/secret_key_material.rs +++ b/neptune-core/src/state/wallet/secret_key_material.rs @@ -1,8 +1,23 @@ +use std::fmt::Display; +use std::ops::Add; +use std::ops::AddAssign; +use std::ops::Div; +use std::ops::Mul; +use std::ops::MulAssign; +use std::ops::Neg; +use std::ops::Sub; +use std::ops::SubAssign; + use anyhow::Result; use bip39::Mnemonic; +use bip39::MnemonicType; use itertools::Itertools; +use num_traits::ConstOne; use num_traits::ConstZero; +use num_traits::One; use num_traits::Zero; +use rand::distr::Distribution; +use rand::distr::StandardUniform; use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; @@ -10,19 +25,35 @@ use serde::Deserialize; use serde::Serialize; use strum::Display; use tasm_lib::triton_vm::prelude::BFieldElement; -use tasm_lib::triton_vm::prelude::XFieldElement; use tasm_lib::twenty_first::prelude::Polynomial; -use tasm_lib::twenty_first::xfe; use zeroize::Zeroize; +use crate::prelude::triton_vm::prelude::FiniteField; +use crate::prelude::triton_vm::prelude::XFieldElement; +use crate::prelude::twenty_first::bfe_vec; +use crate::prelude::twenty_first::bfieldcodec_derive::BFieldCodec; +use crate::prelude::twenty_first::math::traits::CyclicGroupGenerator; +use crate::prelude::twenty_first::math::traits::ModPowU64; +use crate::prelude::twenty_first::math::traits::PrimitiveRootOfUnity; +use crate::prelude::twenty_first::prelude::Inverse; +use crate::prelude::twenty_first::prelude::ModPowU32; +use crate::prelude::twenty_first::xfe; + /// Holds the secret seed of a wallet. -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(untagged)] #[cfg_attr(any(test, feature = "arbitrary-impls"), derive(arbitrary::Arbitrary))] -pub struct SecretKeyMaterial(pub(crate) XFieldElement); +pub enum SecretKeyMaterial { + V0(XFieldElement), + V1(BField32Bytes), +} impl Zeroize for SecretKeyMaterial { fn zeroize(&mut self) { - self.0 = XFieldElement::zero(); + match self { + SecretKeyMaterial::V0(x) => *x = XFieldElement::zero(), + SecretKeyMaterial::V1(x) => *x = BField32Bytes::zero(), + }; } } @@ -67,6 +98,14 @@ pub enum ShamirSecretSharingError { } impl SecretKeyMaterial { + /// The version of the secret key material. + pub fn version(&self) -> u8 { + match self { + SecretKeyMaterial::V0(_) => 0, // mnemonic with 18 words + SecretKeyMaterial::V1(_) => 1, // mnemonic with 24 words + } + } + /// Split the secret across n shares such that combining any t of them /// yields the secret again. /// @@ -98,18 +137,42 @@ impl SecretKeyMaterial { } let mut rng = StdRng::from_seed(seed); - let polynomial_coefficients = (0..t) - .map(|i| if i == 0 { self.0 } else { rng.random() }) - .collect_vec(); - - let evaluation_indices = (1..=n).collect_vec(); - let evaluation_points = evaluation_indices.iter().map(|i| xfe!(*i)).collect_vec(); - let secret_shares = - Polynomial::new(polynomial_coefficients).batch_evaluate(&evaluation_points); - Ok(evaluation_indices - .into_iter() - .zip(secret_shares.into_iter().map(SecretKeyMaterial)) - .collect_vec()) + match self { + SecretKeyMaterial::V0(sk) => { + let polynomial_coefficients = (0..t) + .map(|i| if i == 0 { *sk } else { rng.random() }) + .collect_vec(); + + let evaluation_indices = (1..=n).collect_vec(); + let evaluation_points = evaluation_indices.iter().map(|i| xfe!(*i)).collect_vec(); + let secret_shares = Polynomial::new(polynomial_coefficients) + .batch_evaluate(&evaluation_points) + .iter() + .map(|e| Self::V0(*e)) + .collect_vec(); + Ok(evaluation_indices + .into_iter() + .zip(secret_shares) + .collect_vec()) + } + SecretKeyMaterial::V1(sk) => { + let polynomial_coefficients = (0..t) + .map(|i| if i == 0 { *sk } else { rng.random() }) + .collect_vec(); + + let evaluation_indices = (1..=n).collect_vec(); + let evaluation_points = evaluation_indices + .iter() + .map(|i| BField32Bytes::new_const(BFieldElement::from(*i as u64))) + .collect_vec(); + let secret_shares = Polynomial::new(polynomial_coefficients) + .batch_evaluate(&evaluation_points) + .iter() + .map(|e| Self::V1(*e)) + .collect_vec(); + Ok(evaluation_indices.into_iter().zip(secret_shares).collect()) + } + } } /// Combine a quorum of Shamir secret shares into one. @@ -125,66 +188,443 @@ impl SecretKeyMaterial { let mut indices = shares.iter().map(|(i, _)| *i).collect_vec(); - let ordinates = indices.iter().map(|i| xfe!(*i)).collect_vec(); - indices.sort(); - indices.dedup(); - if indices.len() != ordinates.len() { - return Err(ShamirSecretSharingError::DuplicateIndex); - } - if ordinates.contains(&XFieldElement::ZERO) { - return Err(ShamirSecretSharingError::InvalidShare); - } + match shares[0].1 { + SecretKeyMaterial::V0(_) => { + if shares + .iter() + .any(|(_, y)| !matches!(y, SecretKeyMaterial::V0(_))) + { + return Err(ShamirSecretSharingError::InconsistentShares); + } - let abscissae = shares.into_iter().map(|(_, y)| y.0).collect_vec(); - let polynomial = Polynomial::interpolate(&ordinates, &abscissae); - if polynomial.degree() > 0 && polynomial.degree() as usize >= t { - return Err(ShamirSecretSharingError::InconsistentShares); - } + let ordinates = indices + .iter() + .map(|i| XFieldElement::new_const(BFieldElement::from(*i as u64))) + .collect_vec(); + indices.sort(); + indices.dedup(); + if indices.len() != ordinates.len() { + return Err(ShamirSecretSharingError::DuplicateIndex); + } + if ordinates.contains(&XFieldElement::ZERO) { + return Err(ShamirSecretSharingError::InvalidShare); + } + let abscissae = shares + .into_iter() + .map(|(_, y)| { + if let SecretKeyMaterial::V0(x) = y { + x + } else { + unreachable!() + } + }) + .collect_vec(); + let polynomial = Polynomial::interpolate(&ordinates, &abscissae); + if polynomial.degree() > 0 && polynomial.degree() as usize >= t { + return Err(ShamirSecretSharingError::InconsistentShares); + } + + let p0 = polynomial.evaluate(XFieldElement::ZERO); + Ok(SecretKeyMaterial::V0(p0)) + } + + SecretKeyMaterial::V1(_) => { + if shares + .iter() + .any(|(_, y)| !matches!(y, SecretKeyMaterial::V1(_))) + { + return Err(ShamirSecretSharingError::InconsistentShares); + } - let p0 = polynomial.evaluate(XFieldElement::ZERO); - Ok(SecretKeyMaterial(p0)) + let ordinates = indices + .iter() + .map(|i| BField32Bytes::new_const(BFieldElement::from(*i as u64))) + .collect_vec(); + indices.sort(); + indices.dedup(); + if indices.len() != ordinates.len() { + return Err(ShamirSecretSharingError::DuplicateIndex); + } + if ordinates.contains(&BField32Bytes::ZERO) { + return Err(ShamirSecretSharingError::InvalidShare); + } + + let abscissae = shares + .into_iter() + .map(|(_, y)| { + if let SecretKeyMaterial::V1(x) = y { + x + } else { + unreachable!() + } + }) + .collect_vec(); + let polynomial = Polynomial::interpolate(&ordinates, &abscissae); + if polynomial.degree() > 0 && polynomial.degree() as usize >= t { + return Err(ShamirSecretSharingError::InconsistentShares); + } + + let p0 = polynomial.evaluate(BField32Bytes::ZERO); + Ok(SecretKeyMaterial::V1(p0)) + } + } } /// Convert a seed phrase into [`SecretKeyMaterial`]. /// /// The returned secret key material is wrapped in a `Result`, which is - /// `Err` if the words are not 18 valid BIP-39 words. + /// `Err` if the words are not 24 or 18 valid BIP-39 words. pub fn from_phrase(phrase: &[String]) -> Result { - let mnemonic = Mnemonic::from_phrase(&phrase.iter().join(" "), bip39::Language::English)?; - let secret_seed: [u8; 24] = mnemonic.entropy().try_into()?; - let xfe = XFieldElement::new( - secret_seed - .chunks(8) - .map(|ch| u64::from_le_bytes(ch.try_into().unwrap())) - .map(BFieldElement::new) - .collect_vec() - .try_into() - .unwrap(), - ); - Ok(Self(xfe)) + let mnemonic = Mnemonic::from_phrase(&phrase.join(" "), bip39::Language::English)?; + match MnemonicType::for_word_count(phrase.len())? { + MnemonicType::Words18 => { + let secret_seed: [u8; 24] = mnemonic.entropy().try_into()?; + let xfe = XFieldElement::new( + secret_seed + .chunks(8) + .map(|ch| u64::from_le_bytes(ch.try_into().unwrap())) + .map(BFieldElement::new) + .collect_vec() + .try_into() + .unwrap(), + ); + Ok(Self::V0(xfe)) + } + MnemonicType::Words24 => { + let secret_seed: [u8; 32] = mnemonic.entropy().try_into()?; + let xfe = BField32Bytes::new( + secret_seed + .chunks(8) + .map(|ch| u64::from_le_bytes(ch.try_into().unwrap())) + .map(BFieldElement::new) + .collect_vec() + .try_into() + .unwrap(), + ); + Ok(Self::V1(xfe)) + } + _ => unreachable!(), + } } - /// Convert the secret key material into a BIP-39 phrase consisting of 18 - /// words (for 192 bits of entropy). + /// Convert the secret key material into a BIP-39 phrase consisting of 24 + /// words (for 256 bits of entropy). pub fn to_phrase(&self) -> Vec { - let entropy = self - .0 - .coefficients - .iter() - .flat_map(|bfe| bfe.value().to_le_bytes()) - .collect_vec(); - assert_eq!( - entropy.len(), - 24, - "Entropy for secret seed does not consist of 24 bytes." + match self { + SecretKeyMaterial::V0(sk) => { + let entropy = sk + .coefficients + .iter() + .flat_map(|bfe| bfe.value().to_le_bytes()) + .collect_vec(); + assert_eq!( + entropy.len(), + 24, + "Entropy for secret seed does not consist of 24 bytes." + ); + let mnemonic = Mnemonic::from_entropy(&entropy, bip39::Language::English) + .expect("Wrong entropy length (should be 24 bytes)."); + mnemonic + .phrase() + .split(' ') + .map(|s| s.to_string()) + .collect_vec() + } + SecretKeyMaterial::V1(sk) => { + let entropy = + sk.0.iter() + .flat_map(|bfe| bfe.value().to_le_bytes()) + .collect_vec(); + assert_eq!( + entropy.len(), + 32, + "Entropy for secret seed does not consist of 32 bytes." + ); + let mnemonic = Mnemonic::from_entropy(&entropy, bip39::Language::English) + .expect("Wrong entropy length (should be 32 bytes)."); + mnemonic + .phrase() + .split(' ') + .map(|s| s.to_string()) + .collect_vec() + } + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, BFieldCodec)] +#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(arbitrary::Arbitrary))] +pub struct BField32Bytes(pub(crate) [BFieldElement; 4]); + +impl Distribution for StandardUniform { + fn sample(&self, rng: &mut R) -> BField32Bytes { + BField32Bytes(rng.random()) + } +} + +impl Display for BField32Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl ConstOne for BField32Bytes { + const ONE: Self = Self([ + BFieldElement::ONE, + BFieldElement::ZERO, + BFieldElement::ZERO, + BFieldElement::ZERO, + ]); +} + +impl One for BField32Bytes { + fn one() -> Self { + Self::ONE + } +} + +impl Mul for BField32Bytes { + type Output = Self; + + #[allow(clippy::many_single_char_names)] + #[inline] + fn mul(self, other: Self) -> Self { + // (a x^3 + b x^2 + c x + d) * (e x^3 + f x^2 + g x + h) mod (x^4 + x + 1) + let [d, c, b, a] = self.0; // c0..c3 + let [h, g, f, e] = other.0; + + // Raw (before reduction) + let u0 = d * h; // x^0 + let u1 = d * g + c * h; // x^1 + let u2 = d * f + c * g + b * h; // x^2 + let u3 = d * e + c * f + b * g + a * h; // x^3 + let u4 = c * e + b * f + a * g; // x^4 + let u5 = b * e + a * f; // x^5 + let u6 = a * e; // x^6 + + // Reduction inline using x^4 = -x -1: + // x^4 -> -x -1 + // x^5 -> -x^2 - x + // x^6 -> -x^3 - x^2 + + let r0 = u0 - u4 - u5 - u6; // constant + let r1 = u1 - u4 - u5; // x^1 + let r2 = u2 - u5 - u6; // x^2 + let r3 = u3 - u6; // x^3 + + Self::new([r0, r1, r2, r3]) + } +} + +impl Sub for BField32Bytes { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + (-rhs) + } +} + +impl Div for BField32Bytes { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, rhs: Self) -> Self::Output { + rhs.inverse() * self + } +} + +impl Neg for BField32Bytes { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(self.0.map(Neg::neg)) + } +} + +impl AddAssign for BField32Bytes { + fn add_assign(&mut self, rhs: Self) { + for i in 0..self.0.len() { + self.0[i] += rhs.0[i] + } + } +} + +impl MulAssign for BField32Bytes { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl SubAssign for BField32Bytes { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl CyclicGroupGenerator for BField32Bytes { + fn get_cyclic_group_elements(&self, max: Option) -> Vec { + let mut val = *self; + let mut ret: Vec = vec![Self::one()]; + + loop { + ret.push(val); + val *= *self; + if val.is_one() || max.is_some() && ret.len() >= max.unwrap() { + break; + } + } + ret + } +} + +impl PrimitiveRootOfUnity for BField32Bytes { + fn primitive_root_of_unity(n: u64) -> Option { + let b_root = BFieldElement::primitive_root_of_unity(n); + Some(Self([ + b_root?, + BFieldElement::ZERO, + BFieldElement::ZERO, + BFieldElement::ZERO, + ])) + } +} + +impl Inverse for BField32Bytes { + fn inverse(&self) -> Self { + assert!( + !self.is_zero(), + "Cannot invert the zero element in the extension field." ); - let mnemonic = Mnemonic::from_entropy(&entropy, bip39::Language::English) - .expect("Wrong entropy length (should be 24 bytes)."); - mnemonic - .phrase() - .split(' ') - .map(|s| s.to_string()) - .collect_vec() + let self_as_poly: Polynomial = Polynomial::new(self.0.to_vec()); + let (_, a, _) = Polynomial::::xgcd(self_as_poly, Self::shah_polynomial()); + a.into() + } +} + +impl ModPowU32 for BField32Bytes { + fn mod_pow_u32(&self, exp: u32) -> Self { + self.mod_pow_u64(u64::from(exp)) + } +} + +impl ModPowU64 for BField32Bytes { + #[inline] + fn mod_pow_u64(&self, exponent: u64) -> Self { + let mut x = *self; + let mut result = Self::one(); + let mut i = exponent; + + while i > 0 { + if i & 1 == 1 { + result *= x; + } + + x *= x; + i >>= 1; + } + + result + } +} + +impl From for BField32Bytes { + fn from(value: u64) -> Self { + BField32Bytes::new_const(value.into()) + } +} +impl FiniteField for BField32Bytes {} + +impl Mul for BFieldElement { + type Output = BField32Bytes; + + #[inline] + fn mul(self, other: BField32Bytes) -> BField32Bytes { + let coefficients = other.0.map(|c| c * self); + BField32Bytes(coefficients) + } +} + +impl Mul for BField32Bytes { + type Output = Self; + + #[inline] + fn mul(self, other: BFieldElement) -> Self { + let coefficients = self.0.map(|c| c * other); + Self(coefficients) + } +} + +impl MulAssign for BField32Bytes { + #[inline] + fn mul_assign(&mut self, rhs: BFieldElement) { + *self = *self * rhs; + } +} + +impl From> for BField32Bytes { + fn from(poly: Polynomial<'_, BFieldElement>) -> Self { + let (_, rem) = poly.naive_divide(&Self::shah_polynomial()); + let mut xfe = [BFieldElement::ZERO; 4]; + + let Ok(rem_degree) = usize::try_from(rem.degree()) else { + return Self::ZERO; + }; + xfe[..=rem_degree].copy_from_slice(&rem.coefficients()[..=rem_degree]); + + BField32Bytes(xfe) + } +} + +impl Zero for BField32Bytes { + fn zero() -> Self { + Self::ZERO + } + + fn is_zero(&self) -> bool { + self == &Self::ZERO + } +} + +impl Add for BField32Bytes { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + let mut res = self; + for (i, x) in res.0.iter_mut().enumerate() { + *x += rhs.0[i]; + } + res + } +} + +impl ConstZero for BField32Bytes { + const ZERO: Self = Self([BFieldElement::ZERO; 4]); +} + +impl> From<[T; 4]> for BField32Bytes { + fn from(value: [T; 4]) -> Self { + Self(value.map(|e| e.into())) + } +} + +impl BField32Bytes { + #[inline] + pub fn shah_polynomial() -> Polynomial<'static, BFieldElement> { + // todo hduoc: check + // x^4 + x + 1 + Polynomial::new(bfe_vec![1, 1, 0, 0, 1]) + } + const fn new_const(e: BFieldElement) -> Self { + Self([ + e, + BFieldElement::ZERO, + BFieldElement::ZERO, + BFieldElement::ZERO, + ]) + } + + fn new(elements: [BFieldElement; 4]) -> Self { + Self(elements) } } @@ -201,9 +641,9 @@ mod tests { proptest::proptest! { #[test] fn phrase_conversion_works( - secret in proptest_arbitrary_interop::arb::() + secret in proptest_arbitrary_interop::arb::() ) { - let wallet_secret = SecretKeyMaterial(secret); + let wallet_secret = SecretKeyMaterial::V1(secret); let phrase = wallet_secret.to_phrase(); let wallet_again = SecretKeyMaterial::from_phrase(&phrase).unwrap(); let phrase_again = wallet_again.to_phrase(); @@ -215,7 +655,7 @@ mod tests { #[test] fn bad_phrase_conversion_fails() { - let wallet_secret = SecretKeyMaterial(rng().random()); + let wallet_secret = SecretKeyMaterial::V1(rng().random()); let mut phrase = wallet_secret.to_phrase(); phrase.push("blank".to_string()); assert!(SecretKeyMaterial::from_phrase(&phrase).is_err()); @@ -239,10 +679,10 @@ mod tests { fn happy_path_all_shares( #[strategy(2usize..20)] n: usize, #[strategy(2usize..=#n)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares = secret_key .share_shamir(t, n, seed) .expect("sharing on happy path should succeed"); @@ -256,11 +696,11 @@ mod tests { fn happy_path_t_shares( #[strategy(2usize..20)] n: usize, #[strategy(2usize..=#n)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], #[strategy(sample::subsequence((0..#n).collect_vec(), #t))] indices: Vec, ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares = secret_key .share_shamir(t, n, seed) .expect("sharing on happy path should succeed"); @@ -275,10 +715,10 @@ mod tests { fn catch_quorum_too_small( #[strategy(2usize..20)] n: usize, #[strategy(0usize..=1)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); prop_assert_eq!( secret_key.share_shamir(t, n, seed), Err(ShamirSecretSharingError::QuorumTooSmall) @@ -289,10 +729,10 @@ mod tests { fn catch_impossible_recombination( #[strategy(2usize..20)] n: usize, #[strategy(#n+1..30)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); prop_assert_eq!( secret_key.share_shamir(t, n, seed), Err(ShamirSecretSharingError::ImpossibleRecombination) @@ -303,10 +743,10 @@ mod tests { fn catch_not_enough_shares_to_split( #[strategy(Just(0usize))] n: usize, #[strategy(2usize..10)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); prop_assert_eq!( secret_key.share_shamir(t, n, seed), Err(ShamirSecretSharingError::NotEnoughSharesToSplit) @@ -318,10 +758,10 @@ mod tests { #[strategy(2usize..20)] n: usize, #[strategy(2usize..=#n)] t: usize, #[strategy(sample::subsequence((0..#n).collect_vec(), #t - 1))] indices: Vec, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares = secret_key .share_shamir(t, n, seed) .expect("sharing on happy path should succeed"); @@ -336,11 +776,11 @@ mod tests { fn catch_invalid_share( #[strategy(2usize..20)] n: usize, #[strategy(2usize..=#n)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], #[strategy(sample::subsequence((0..#n).collect_vec(), #t - 1))] indices: Vec, ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares = secret_key .share_shamir(t, n, seed) .expect("sharing on happy path should succeed"); @@ -358,11 +798,11 @@ mod tests { #[strategy(2usize..20)] n: usize, #[strategy(2usize..=#n)] t: usize, #[strategy(0usize..#t - 1)] dup_ind: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed: [u8; 32], #[strategy(sample::subsequence((0..#t).collect_vec(), #t - 1))] indices: Vec, ) { - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares = secret_key .share_shamir(t, n, seed) .expect("sharing on happy path should succeed"); @@ -380,7 +820,7 @@ mod tests { fn catch_inconsistent_shares( #[strategy(3usize..20)] n: usize, #[strategy(2usize..#n)] t: usize, - #[strategy(arb())] s: XFieldElement, + #[strategy(arb())] s: BField32Bytes, #[strategy([arb(); 32])] seed_a: [u8; 32], #[strategy([arb(); 32])] seed_b: [u8; 32], #[strategy(sample::subsequence((0..#n).collect_vec(), #t + 1))] indices: Vec, @@ -392,7 +832,7 @@ mod tests { // nothing to test here if the sharings are identical prop_assume!(seed_a != seed_b); - let secret_key = SecretKeyMaterial(s); + let secret_key = SecretKeyMaterial::V1(s); let shares_a = secret_key .share_shamir(t, n, seed_a) .expect("sharing on happy path should succeed"); @@ -415,4 +855,19 @@ mod tests { ); } } + + mod secret_key_material { + use crate::state::wallet::secret_key_material::SecretKeyMaterial; + + #[test] + fn test_parse_json() { + let sk: SecretKeyMaterial = serde_json::from_str("[10885651799413792391,15419758986129414034,2225506014986644298,13704052757432991042]").unwrap(); + assert!(matches!(sk, SecretKeyMaterial::V1(_))); + let sk2: SecretKeyMaterial = serde_json::from_str( + "{\"coefficients\":[5223899872919692492,9765490249295514317,5978636154531078456]}", + ) + .unwrap(); + assert!(matches!(sk2, SecretKeyMaterial::V0(_))); + } + } } diff --git a/neptune-core/src/state/wallet/wallet_entropy.rs b/neptune-core/src/state/wallet/wallet_entropy.rs index 6a5219f39..abbe0439b 100644 --- a/neptune-core/src/state/wallet/wallet_entropy.rs +++ b/neptune-core/src/state/wallet/wallet_entropy.rs @@ -1,3 +1,10 @@ +use super::address::ReceivingAddress; +use crate::prelude::triton_vm::prelude::XFieldElement; +use crate::prelude::twenty_first::xfe; +use crate::protocol::consensus::block::block_height::BlockHeight; +use crate::state::wallet::address::generation_address; +use crate::state::wallet::address::symmetric_key; +use crate::state::wallet::secret_key_material::SecretKeyMaterial; use anyhow::Result; use serde::Deserialize; use serde::Serialize; @@ -5,17 +12,9 @@ use tasm_lib::prelude::Tip5; use tasm_lib::twenty_first::bfe_vec; use tasm_lib::twenty_first::math::b_field_element::BFieldElement; use tasm_lib::twenty_first::math::bfield_codec::BFieldCodec; -use tasm_lib::twenty_first::math::x_field_element::XFieldElement; use tasm_lib::twenty_first::tip5::digest::Digest; -use tasm_lib::twenty_first::xfe; use zeroize::ZeroizeOnDrop; -use super::address::ReceivingAddress; -use crate::protocol::consensus::block::block_height::BlockHeight; -use crate::state::wallet::address::generation_address; -use crate::state::wallet::address::symmetric_key; -use crate::state::wallet::secret_key_material::SecretKeyMaterial; - /// The wallet's one source of randomness, from which all keys are derived. /// /// This struct wraps around [`SecretKeyMaterial`], which contains the secret @@ -34,7 +33,7 @@ impl WalletEntropy { /// Create a `WalletEntropy` object with a fixed digest pub fn devnet_wallet() -> Self { - let secret_seed = SecretKeyMaterial(xfe!([ + let secret_seed = SecretKeyMaterial::V0(xfe!([ 12063201067205522823_u64, 1529663126377206632_u64, 2090171368883726200_u64, @@ -74,7 +73,7 @@ impl WalletEntropy { // in case you don't know with what counter you made the address let key_seed = Tip5::hash_varlen( &[ - self.secret_seed.0.encode(), + self.encoded_secret_seed(), bfe_vec![generation_address::GENERATION_FLAG, index], ] .concat(), @@ -91,7 +90,7 @@ impl WalletEntropy { pub fn nth_symmetric_key(&self, index: u64) -> symmetric_key::SymmetricKey { let key_seed = Tip5::hash_varlen( &[ - self.secret_seed.0.encode(), + self.encoded_secret_seed(), bfe_vec![symmetric_key::SYMMETRIC_KEY_FLAG, index], ] .concat(), @@ -129,7 +128,7 @@ impl WalletEntropy { const SEED_FLAG: u64 = 0x2315439570c4a85fu64; Tip5::hash_varlen( &[ - self.secret_seed.0.encode(), + self.encoded_secret_seed(), bfe_vec![SEED_FLAG, block_height], ] .concat(), @@ -154,7 +153,7 @@ impl WalletEntropy { const SENDER_RANDOMNESS_FLAG: u64 = 0x5e116e1270u64; Tip5::hash_varlen( &[ - self.secret_seed.0.encode(), + self.encoded_secret_seed(), bfe_vec![SENDER_RANDOMNESS_FLAG, block_height], receiver_digest.encode(), ] @@ -162,12 +161,19 @@ impl WalletEntropy { ) } - /// Convert a secret seed phrase (list of 18 valid BIP-39 words) to a + /// Convert a secret seed phrase (list of 18 or 24 valid BIP-39 words) to a /// [`WalletEntropy`] object pub fn from_phrase(phrase: &[String]) -> Result { let key = SecretKeyMaterial::from_phrase(phrase)?; Ok(Self::new(key)) } + + pub fn encoded_secret_seed(&self) -> Vec { + match &self.secret_seed { + SecretKeyMaterial::V0(sk) => sk.encode(), + SecretKeyMaterial::V1(sk) => sk.encode(), + } + } } impl From for WalletEntropy { @@ -203,7 +209,7 @@ mod tests { pub(crate) fn new_pseudorandom(seed: [u8; 32]) -> Self { let mut rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(seed); Self { - secret_seed: SecretKeyMaterial(rand::Rng::random(&mut rng)), + secret_seed: SecretKeyMaterial::V1(rand::Rng::random(&mut rng)), } } } diff --git a/neptune-core/src/state/wallet/wallet_file.rs b/neptune-core/src/state/wallet/wallet_file.rs index 70eadb126..6594c68d5 100644 --- a/neptune-core/src/state/wallet/wallet_file.rs +++ b/neptune-core/src/state/wallet/wallet_file.rs @@ -20,7 +20,6 @@ pub const WALLET_SECRET_FILE_NAME: &str = "wallet.dat"; pub const WALLET_OUTGOING_SECRETS_FILE_NAME: &str = "outgoing_randomness.dat"; pub const WALLET_INCOMING_SECRETS_FILE_NAME: &str = "incoming_randomness.dat"; const STANDARD_WALLET_NAME: &str = "standard_wallet"; -const STANDARD_WALLET_VERSION: u8 = 0; pub const WALLET_DB_NAME: &str = "wallet"; pub const WALLET_OUTPUT_COUNT_DB_NAME: &str = "wallout_output_count_db"; @@ -151,12 +150,12 @@ impl WalletFile { Self { name: STANDARD_WALLET_NAME.to_string(), secret_seed, - version: STANDARD_WALLET_VERSION, + version: secret_seed.version(), } } fn new_random() -> Self { - Self::new(SecretKeyMaterial(rng().random())) + Self::new(SecretKeyMaterial::V1(rng().random())) } pub fn entropy(&self) -> WalletEntropy {