diff --git a/wallet-core/CHANGELOG.md b/wallet-core/CHANGELOG.md index dc34ffdd23..f4a592909d 100644 --- a/wallet-core/CHANGELOG.md +++ b/wallet-core/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.4.0] - 2025-11-06 +- Add EIP2334 deterministic account hierarchy derivation support [#3572] - Added support for generic TransactionData into FFI [#3750] ### Changed @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - First `dusk-wallet-core` release +[#3572]: https://github.com/dusk-network/rusk/issues/3572 [#3750]: https://github.com/dusk-network/rusk/issues/3750 [#3681]: https://github.com/dusk-network/rusk/issues/3681 [#3476]: https://github.com/dusk-network/rusk/issues/3476 diff --git a/wallet-core/src/keys/eip2334.rs b/wallet-core/src/keys/eip2334.rs new file mode 100644 index 0000000000..277ed344ff --- /dev/null +++ b/wallet-core/src/keys/eip2334.rs @@ -0,0 +1,117 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! This module defines the EIP2334 Deterministic Account Hierarchy path for BLS +//! keys as defined at + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; + +use crate::keys::eip2333; + +/// The base derivation path for Dusk EIP-2333 BLS12-381 derivation. +/// `m / purpose / coin_type / account / use` +/// +/// `purpose` is set to 12381 as per EIP-2334, which is the name of the curve. +/// `coin_type` type is set to 744. The number that Dusk uses. +/// `account` is incremented when the user wants to create a new account i.e., +/// get a new address. +/// `use` is **always** set to 0 for moonlight. It is set to 1 for staking keys +/// to separate them from moonlight keys. +pub const EIP_2334_BASE_PATH: &str = "m/12381/744/0/0"; + +/// Converts a given index nummber to the corresponding derivation path of +/// moonlight EIP-2333 BLS12-381 derivation. +pub(crate) fn index_to_path(index: usize) -> String { + let index_str = index.to_string(); + + // put the index at the correct position (account) + // m/12381/744/index/0 + let mut path_parts: Vec<&str> = EIP_2334_BASE_PATH.split('/').collect(); + path_parts[3] = &index_str; + + path_parts.join("/") +} + +/// Generates a [`BlsSecretKey`] from the master secret key and index. +/// +/// The key is generated through EIP-2333. +/// +/// When generating a new key pair, the [`derive_bls_key_pair`] function is +/// preferred to be used, as it generates both the secret and public key at +/// once. +/// +/// # Panics +/// +/// This function panics when invariants are violated, which should never +/// happen. +#[must_use] +pub fn derive_bls_sk(master_sk: &BlsSecretKey, index: usize) -> BlsSecretKey { + let path = index_to_path(index); + + eip2333::derive_bls_sk(master_sk, &path).expect("Should always succeed") +} + +/// Generates the [`BlsSecretKey`] & [`BlsPublicKey`] pair from the given master +/// secret key and index. +/// +/// The key is generated through EIP-2333. +/// +/// # Panics +/// +/// This function panics when invariants are violated, which should never +/// happen. +#[must_use] +pub fn derive_bls_key_pair( + master_sk: &BlsSecretKey, + index: usize, +) -> (BlsSecretKey, BlsPublicKey) { + let path = index_to_path(index); + + let sk = eip2333::derive_bls_sk(master_sk, &path) + .expect("Should always succeed"); + let pk = BlsPublicKey::from(&sk); + + (sk, pk) +} + +/// Generates a [`BlsPublicKey`] from the given [`BlsSecretKey`] +/// +/// When generating a new key pair, the [`derive_bls_key_pair`] function is +/// preferred to be used, as it generates both the secret and public key at +/// once. +#[must_use] +pub fn derive_bls_pk(sk: &BlsSecretKey) -> BlsPublicKey { + BlsPublicKey::from(sk) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test path to index conversion + #[test] + fn test_index_to_path_conversion() { + let path = "m/12381/744/0/0"; + + let indexes = index_to_path(0); + assert_eq!(indexes, path); + + let path = "m/12381/744/1/0"; + + let indexes = index_to_path(1); + assert_eq!(indexes, path); + + let path = "m/12381/744/150/0"; + + let indexes = index_to_path(150); + assert_eq!(indexes, path); + } +} diff --git a/wallet-core/src/keys/legacy.rs b/wallet-core/src/keys/legacy.rs new file mode 100644 index 0000000000..9365aa00ad --- /dev/null +++ b/wallet-core/src/keys/legacy.rs @@ -0,0 +1,128 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Module to generate phoenix and moonlight keys from a seed and index based on +//! the legacy key derivation method. + +use alloc::vec::Vec; +use core::ops::Range; + +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::transfer::phoenix::{ + PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, + ViewKey as PhoenixViewKey, +}; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha12Rng; +use sha2::{Digest, Sha256}; +use zeroize::Zeroize; + +use crate::Seed; + +/// Generates a [`BlsSecretKey`] from a seed and index. +/// +/// The randomness is generated using [`rng_with_index`]. +#[must_use] +pub fn derive_bls_sk(seed: &Seed, index: u8) -> BlsSecretKey { + // note that if we change the string used for the rng, all previously + // generated keys will become invalid + // NOTE: When breaking the keys, we will want to change the string too + BlsSecretKey::random(&mut rng_with_index(seed, index, b"SK")) +} + +/// Generates a [`BlsPublicKey`] from a seed and index. +/// +/// The randomness is generated using [`rng_with_index`]. +#[must_use] +pub fn derive_bls_pk(seed: &Seed, index: u8) -> BlsPublicKey { + let mut sk = derive_bls_sk(seed, index); + let pk = BlsPublicKey::from(&sk); + sk.zeroize(); + + pk +} + +/// Generates a [`PhoenixSecretKey`] from a seed and index. +/// +/// The randomness is generated using [`rng_with_index`]. +#[must_use] +pub fn derive_phoenix_sk(seed: &Seed, index: u8) -> PhoenixSecretKey { + // note that if we change the string used for the rng, all previously + // generated keys will become invalid + // NOTE: When breaking the keys, we will want to change the string too + PhoenixSecretKey::random(&mut rng_with_index(seed, index, b"SSK")) +} + +/// Generates multiple [`PhoenixSecretKey`] from a seed and a range of +/// indices. +/// +/// The randomness is generated using [`rng_with_index`]. +#[must_use] +pub fn derive_multiple_phoenix_sk( + seed: &Seed, + index_range: Range, +) -> Vec { + index_range + .map(|index| derive_phoenix_sk(seed, index)) + .collect() +} + +/// Generates a [`PhoenixPublicKey`] from its seed and index. +/// +/// First the [`PhoenixSecretKey`] is derived with [`derive_phoenix_sk`], +/// then the public key is generated from it and the secret key is +/// erased from memory. +#[must_use] +pub fn derive_phoenix_pk(seed: &Seed, index: u8) -> PhoenixPublicKey { + let mut sk = derive_phoenix_sk(seed, index); + let pk = PhoenixPublicKey::from(&sk); + sk.zeroize(); + + pk +} + +/// Generates a [`PhoenixViewKey`] from its seed and index. +/// +/// First the [`PhoenixSecretKey`] is derived with [`derive_phoenix_sk`], +/// then the view key is generated from it and the secret key is erased +/// from memory. +#[must_use] +pub fn derive_phoenix_vk(seed: &Seed, index: u8) -> PhoenixViewKey { + let mut sk = derive_phoenix_sk(seed, index); + let vk = PhoenixViewKey::from(&sk); + sk.zeroize(); + + vk +} + +/// Creates a secure RNG from a seed with embedded index and termination +/// constant. +/// +/// First the `seed` and then the little-endian representation of the key's +/// `index` are passed through SHA-256. A constant is then mixed in and the +/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is +/// subsequently used to generate the key. +#[must_use] +pub fn rng_with_index( + seed: &Seed, + index: u8, + termination: &[u8], +) -> ChaCha12Rng { + // NOTE: to not break the test-keys, we cast to a u64 here. Once we are + // ready to use the new keys, the index should not be cast to a u64 + // anymore. + let index = u64::from(index); + let mut hash = Sha256::new(); + + hash.update(seed); + hash.update(index.to_le_bytes()); + hash.update(termination); + + let hash = hash.finalize().into(); + ChaCha12Rng::from_seed(hash) +} diff --git a/wallet-core/src/keys/mod.rs b/wallet-core/src/keys/mod.rs index 67e31ca5c2..a536821228 100644 --- a/wallet-core/src/keys/mod.rs +++ b/wallet-core/src/keys/mod.rs @@ -7,121 +7,13 @@ //! Utilities to derive keys from the seed. pub mod eip2333; - -use alloc::vec::Vec; -use core::ops::Range; - -use dusk_core::signatures::bls::{ - PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +pub mod eip2334; +pub mod legacy; + +// Re-export all phoenix functions, as they are not influenced by EIP-2333 +// Temporarily Re-export bls functions as well, until we migrate consuming apps +// to using EIP-2334 +pub use legacy::{ + derive_bls_pk, derive_bls_sk, derive_multiple_phoenix_sk, + derive_phoenix_pk, derive_phoenix_sk, derive_phoenix_vk, }; -use dusk_core::transfer::phoenix::{ - PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, - ViewKey as PhoenixViewKey, -}; -use rand_chacha::rand_core::SeedableRng; -use rand_chacha::ChaCha12Rng; -use sha2::{Digest, Sha256}; -use zeroize::Zeroize; - -use crate::Seed; - -/// Generates a [`BlsSecretKey`] from a seed and index. -/// -/// The randomness is generated using [`rng_with_index`]. -#[must_use] -pub fn derive_bls_sk(seed: &Seed, index: u8) -> BlsSecretKey { - // note that if we change the string used for the rng, all previously - // generated keys will become invalid - // NOTE: When breaking the keys, we will want to change the string too - BlsSecretKey::random(&mut rng_with_index(seed, index, b"SK")) -} - -/// Generates a [`BlsPublicKey`] from a seed and index. -/// -/// The randomness is generated using [`rng_with_index`]. -#[must_use] -pub fn derive_bls_pk(seed: &Seed, index: u8) -> BlsPublicKey { - let mut sk = derive_bls_sk(seed, index); - let pk = BlsPublicKey::from(&sk); - sk.zeroize(); - - pk -} - -/// Generates a [`PhoenixSecretKey`] from a seed and index. -/// -/// The randomness is generated using [`rng_with_index`]. -#[must_use] -pub fn derive_phoenix_sk(seed: &Seed, index: u8) -> PhoenixSecretKey { - // note that if we change the string used for the rng, all previously - // generated keys will become invalid - // NOTE: When breaking the keys, we will want to change the string too - PhoenixSecretKey::random(&mut rng_with_index(seed, index, b"SSK")) -} - -/// Generates multiple [`PhoenixSecretKey`] from a seed and a range of indices. -/// -/// The randomness is generated using [`rng_with_index`]. -#[must_use] -pub fn derive_multiple_phoenix_sk( - seed: &Seed, - index_range: Range, -) -> Vec { - index_range - .map(|index| derive_phoenix_sk(seed, index)) - .collect() -} - -/// Generates a [`PhoenixPublicKey`] from its seed and index. -/// -/// First the [`PhoenixSecretKey`] is derived with [`derive_phoenix_sk`], then -/// the public key is generated from it and the secret key is erased from -/// memory. -#[must_use] -pub fn derive_phoenix_pk(seed: &Seed, index: u8) -> PhoenixPublicKey { - let mut sk = derive_phoenix_sk(seed, index); - let pk = PhoenixPublicKey::from(&sk); - sk.zeroize(); - - pk -} - -/// Generates a [`PhoenixViewKey`] from its seed and index. -/// -/// First the [`PhoenixSecretKey`] is derived with [`derive_phoenix_sk`], then -/// the view key is generated from it and the secret key is erased from memory. -#[must_use] -pub fn derive_phoenix_vk(seed: &Seed, index: u8) -> PhoenixViewKey { - let mut sk = derive_phoenix_sk(seed, index); - let vk = PhoenixViewKey::from(&sk); - sk.zeroize(); - - vk -} - -/// Creates a secure RNG from a seed with embedded index and termination -/// constant. -/// -/// First the `seed` and then the little-endian representation of the key's -/// `index` are passed through SHA-256. A constant is then mixed in and the -/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is -/// subsequently used to generate the key. -#[must_use] -pub fn rng_with_index( - seed: &Seed, - index: u8, - termination: &[u8], -) -> ChaCha12Rng { - // NOTE: to not break the test-keys, we cast to a u64 here. Once we are - // ready to use the new keys, the index should not be cast to a u64 - // anymore. - let index = u64::from(index); - let mut hash = Sha256::new(); - - hash.update(seed); - hash.update(index.to_le_bytes()); - hash.update(termination); - - let hash = hash.finalize().into(); - ChaCha12Rng::from_seed(hash) -} diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index e91b78b96d..06c02a1644 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -31,6 +31,8 @@ use ff::Field; use rand::{CryptoRng, RngCore}; use zeroize::Zeroize; +use crate::{keys, Seed}; + /// An unproven-transaction is nearly identical to a [`PhoenixTransaction`] with /// the only difference being that it carries a serialized [`TxCircuitVec`] /// instead of the proof bytes. @@ -875,3 +877,52 @@ fn withdraw_to_moonlight( gas_payment_token, ) } + +/// Creates a Moonlight transfer [`Transaction`], for transferring funds +/// from a legacy Moonlight account to the corresponding EIP-2333 derived +/// account based on the same seed. +/// +/// # Errors +/// The creation of this transaction doesn't error, but still returns a result +/// for the sake of API consistency. +/// +/// # Panics +/// This function will panic if the provided seed is smaller than 32 bytes. +pub fn legacy_to_eip_migration( + seed: &Seed, + index: u8, + transfer_value: u64, + gas_limit: u64, + gas_price: u64, + moonlight_nonce: u64, + chain_id: u8, +) -> Result { + // secret key based on the legacy method + let mut legacy_moonlight_sender_sk = + keys::legacy::derive_bls_sk(seed, index); + // public key based on the new EIP-2333 method to send the funds to + let mut eip_master_sk = keys::eip2333::derive_master_sk(seed) + .expect("Expect seed to be larger than 32 bytes"); + let (mut sk, eip_2333_moonlight_receiver_pk) = + keys::eip2334::derive_bls_key_pair(&eip_master_sk, index as usize); + sk.zeroize(); + eip_master_sk.zeroize(); + + let deposit = 0; + + let tx = moonlight( + &legacy_moonlight_sender_sk, + Some(eip_2333_moonlight_receiver_pk), + transfer_value, + deposit, + gas_limit, + gas_price, + moonlight_nonce, + chain_id, + None::, + ); + + legacy_moonlight_sender_sk.zeroize(); + + tx +} diff --git a/wallet-core/tests/keys.rs b/wallet-core/tests/keys.rs index bce0af43d4..1dc4605796 100644 --- a/wallet-core/tests/keys.rs +++ b/wallet-core/tests/keys.rs @@ -6,9 +6,10 @@ use dusk_bytes::Serializable; use dusk_wallet_core::keys::{ - derive_bls_sk, derive_multiple_phoenix_sk, derive_phoenix_pk, - derive_phoenix_sk, derive_phoenix_vk, + derive_multiple_phoenix_sk, derive_phoenix_pk, derive_phoenix_sk, + derive_phoenix_vk, }; +use dusk_wallet_core::keys::{eip2333, eip2334, legacy}; const SEED: [u8; 64] = [0; 64]; const INDEX: u8 = 42; @@ -76,12 +77,29 @@ fn test_derive_phoenix_vk() { } #[test] -fn test_derive_bls_sk() { +fn test_derive_legacy_bls_sk() { // it is important that we always derive the same key from a fixed seed let sk_bytes = [ 95, 35, 167, 191, 106, 171, 71, 158, 159, 39, 84, 1, 132, 238, 152, 235, 154, 5, 250, 158, 255, 195, 79, 95, 193, 58, 36, 189, 0, 99, 230, 86, ]; - assert_eq!(derive_bls_sk(&SEED, INDEX).to_bytes(), sk_bytes); + assert_eq!(legacy::derive_bls_sk(&SEED, INDEX).to_bytes(), sk_bytes); +} + +#[test] +fn test_derive_bls_sk() { + let sk_bytes = [ + 9, 195, 91, 35, 193, 184, 186, 70, 226, 2, 37, 105, 147, 84, 27, 127, + 49, 5, 50, 208, 253, 29, 118, 227, 116, 251, 81, 129, 181, 113, 136, + 85, + ]; + + let master_sk = + eip2333::derive_master_sk(&SEED).expect("Should always succeed"); + + assert_eq!( + eip2334::derive_bls_sk(&master_sk, INDEX as usize).to_bytes(), + sk_bytes + ); }