From 7833b0a3b89e02463ddb28069c100b9854a6f3d3 Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:21:55 +0300 Subject: [PATCH 1/6] wallet-core: add eip2334 module --- wallet-core/src/keys/eip2334.rs | 121 ++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 wallet-core/src/keys/eip2334.rs diff --git a/wallet-core/src/keys/eip2334.rs b/wallet-core/src/keys/eip2334.rs new file mode 100644 index 0000000000..25d37bf3c4 --- /dev/null +++ b/wallet-core/src/keys/eip2334.rs @@ -0,0 +1,121 @@ +// 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: u64) -> BlsSecretKey { + let path = index_to_path(index as usize); + + BlsSecretKey::from( + 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: u64, +) -> (BlsSecretKey, BlsPublicKey) { + let path = index_to_path(index as usize); + + let sk = BlsSecretKey::from( + 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. +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); + } +} From dfb88c64d0b4a036f65a15dd65ccc0c627741b3a Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:23:05 +0300 Subject: [PATCH 2/6] wallet-core: move old derivation into legacy module - Re-export legacy module to keep same interface for now - Make eip2334 public --- wallet-core/src/keys/legacy.rs | 128 +++++++++++++++++++++++++++++++++ wallet-core/src/keys/mod.rs | 126 +++----------------------------- 2 files changed, 137 insertions(+), 117 deletions(-) create mode 100644 wallet-core/src/keys/legacy.rs 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) -} From 670403ad9a2de66478710d77c9a4627a5704f9f5 Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:27:46 +0300 Subject: [PATCH 3/6] wallet-core: add legacy_to_eip_migration transaction --- wallet-core/src/transaction.rs | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index e91b78b96d..92b801d6d6 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,49 @@ 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. +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 _s, eip_2333_moonlight_receiver_pk) = + keys::eip2334::derive_bls_key_pair(&eip_master_sk, index as u64); + _s.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 +} From 2c998be2febbe82df71abc4dd2a6639ea180df9f Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:28:36 +0300 Subject: [PATCH 4/6] wallet-core: add test for deriving eip sk --- wallet-core/tests/keys.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/wallet-core/tests/keys.rs b/wallet-core/tests/keys.rs index bce0af43d4..79980770de 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 u64).to_bytes(), + sk_bytes + ); } From 97caa6291b06a7af2656edc046cf93eb0e0c5417 Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:43:44 +0100 Subject: [PATCH 5/6] wallet-core: change derive arg index to usize - Fix clippy lints --- wallet-core/src/keys/eip2334.rs | 20 ++++++++------------ wallet-core/src/transaction.rs | 9 ++++++--- wallet-core/tests/keys.rs | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/wallet-core/src/keys/eip2334.rs b/wallet-core/src/keys/eip2334.rs index 25d37bf3c4..277ed344ff 100644 --- a/wallet-core/src/keys/eip2334.rs +++ b/wallet-core/src/keys/eip2334.rs @@ -53,13 +53,10 @@ pub(crate) fn index_to_path(index: usize) -> String { /// This function panics when invariants are violated, which should never /// happen. #[must_use] -pub fn derive_bls_sk(master_sk: &BlsSecretKey, index: u64) -> BlsSecretKey { - let path = index_to_path(index as usize); +pub fn derive_bls_sk(master_sk: &BlsSecretKey, index: usize) -> BlsSecretKey { + let path = index_to_path(index); - BlsSecretKey::from( - eip2333::derive_bls_sk(master_sk, &path) - .expect("Should always succeed"), - ) + eip2333::derive_bls_sk(master_sk, &path).expect("Should always succeed") } /// Generates the [`BlsSecretKey`] & [`BlsPublicKey`] pair from the given master @@ -74,14 +71,12 @@ pub fn derive_bls_sk(master_sk: &BlsSecretKey, index: u64) -> BlsSecretKey { #[must_use] pub fn derive_bls_key_pair( master_sk: &BlsSecretKey, - index: u64, + index: usize, ) -> (BlsSecretKey, BlsPublicKey) { - let path = index_to_path(index as usize); + let path = index_to_path(index); - let sk = BlsSecretKey::from( - eip2333::derive_bls_sk(master_sk, &path) - .expect("Should always succeed"), - ); + let sk = eip2333::derive_bls_sk(master_sk, &path) + .expect("Should always succeed"); let pk = BlsPublicKey::from(&sk); (sk, pk) @@ -92,6 +87,7 @@ pub fn derive_bls_key_pair( /// 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) } diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index 92b801d6d6..06c02a1644 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -885,6 +885,9 @@ fn withdraw_to_moonlight( /// # 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, @@ -900,9 +903,9 @@ pub fn legacy_to_eip_migration( // 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 _s, eip_2333_moonlight_receiver_pk) = - keys::eip2334::derive_bls_key_pair(&eip_master_sk, index as u64); - _s.zeroize(); + 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; diff --git a/wallet-core/tests/keys.rs b/wallet-core/tests/keys.rs index 79980770de..1dc4605796 100644 --- a/wallet-core/tests/keys.rs +++ b/wallet-core/tests/keys.rs @@ -99,7 +99,7 @@ fn test_derive_bls_sk() { eip2333::derive_master_sk(&SEED).expect("Should always succeed"); assert_eq!( - eip2334::derive_bls_sk(&master_sk, INDEX as u64).to_bytes(), + eip2334::derive_bls_sk(&master_sk, INDEX as usize).to_bytes(), sk_bytes ); } From d47c83038bf69b3eb55ee8cdcf5d27cad2c29b8d Mon Sep 17 00:00:00 2001 From: Neotamandua <107320179+Neotamandua@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:25:10 +0100 Subject: [PATCH 6/6] wallet-core: update changelog --- wallet-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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