Merge pull request #104 from vacp2p/Pravdyvy/key-protocol-update-public-part

Key protocol update public part
This commit is contained in:
Pravdyvy 2025-09-02 17:10:02 +03:00 committed by GitHub
commit c297186aee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 391 additions and 738 deletions

View File

@ -4,8 +4,7 @@ members = [
"integration_tests",
"sequencer_runner",
"storage",
"accounts",
"utxo",
"key_protocol",
"sequencer_rpc",
"mempool",
"wallet",

View File

@ -1,3 +0,0 @@
// TODO: Consider wrapping `AccountAddress` in a struct.
pub type AccountAddress = [u8; 32];

View File

@ -1,221 +0,0 @@
use std::collections::HashMap;
use anyhow::Result;
use common::{merkle_tree_public::TreeHashType, transaction::Tag};
use k256::AffinePoint;
use log::info;
use nssa::Address;
use serde::{Deserialize, Serialize};
use utxo::utxo_core::UTXO;
use crate::key_management::{
constants_types::{CipherText, Nonce},
ephemeral_key_holder::EphemeralKeyHolder,
AddressKeyHolder,
};
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug)]
pub struct Account {
pub key_holder: AddressKeyHolder,
pub address: Address,
pub balance: u64,
pub utxos: HashMap<TreeHashType, UTXO>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AccountForSerialization {
pub key_holder: AddressKeyHolder,
pub address: String,
pub balance: u64,
pub utxos: HashMap<String, UTXO>,
}
impl From<Account> for AccountForSerialization {
fn from(value: Account) -> Self {
AccountForSerialization {
key_holder: value.key_holder,
balance: value.balance,
address: value.address.to_string(),
utxos: value
.utxos
.into_iter()
.map(|(key, val)| (hex::encode(key), val))
.collect(),
}
}
}
impl From<AccountForSerialization> for Account {
fn from(value: AccountForSerialization) -> Self {
let public_key =
nssa::PublicKey::new_from_private_key(value.key_holder.get_pub_account_signing_key());
let address = nssa::Address::from(&public_key);
Account {
key_holder: value.key_holder,
address,
balance: value.balance,
utxos: value
.utxos
.into_iter()
.map(|(key, val)| (hex::decode(key).unwrap().try_into().unwrap(), val))
.collect(),
}
}
}
impl Serialize for Account {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let account_for_serialization: AccountForSerialization = From::from(self.clone());
account_for_serialization.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Account {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let account_for_serialization = <AccountForSerialization>::deserialize(deserializer)?;
Ok(account_for_serialization.into())
}
}
impl Account {
pub fn new() -> Self {
let key_holder = AddressKeyHolder::new_os_random();
let public_key =
nssa::PublicKey::new_from_private_key(key_holder.get_pub_account_signing_key());
let address = nssa::Address::from(&public_key);
let balance = 0;
let utxos = HashMap::new();
Self {
key_holder,
address,
balance,
utxos,
}
}
pub fn new_with_balance(balance: u64) -> Self {
let key_holder = AddressKeyHolder::new_os_random();
let public_key =
nssa::PublicKey::new_from_private_key(key_holder.get_pub_account_signing_key());
let address = nssa::Address::from(&public_key);
let utxos = HashMap::new();
Self {
key_holder,
address,
balance,
utxos,
}
}
pub fn encrypt_data(
ephemeral_key_holder: &EphemeralKeyHolder,
viewing_public_key_receiver: AffinePoint,
data: &[u8],
) -> (CipherText, Nonce) {
ephemeral_key_holder.encrypt_data(viewing_public_key_receiver, data)
}
pub fn decrypt_data(
&self,
ephemeral_public_key_sender: AffinePoint,
ciphertext: CipherText,
nonce: Nonce,
) -> Result<Vec<u8>, aes_gcm::Error> {
self.key_holder
.decrypt_data(ephemeral_public_key_sender, ciphertext, nonce)
}
pub fn add_new_utxo_outputs(&mut self, utxos: Vec<UTXO>) -> Result<()> {
for utxo in utxos {
if self.utxos.contains_key(&utxo.hash) {
return Err(anyhow::anyhow!("UTXO already exists"));
}
self.utxos.insert(utxo.hash, utxo);
}
Ok(())
}
pub fn update_public_balance(&mut self, new_balance: u64) {
self.balance = new_balance;
}
pub fn add_asset<Asset: Serialize>(
&mut self,
asset: Asset,
amount: u128,
privacy_flag: bool,
) -> Result<()> {
let asset_utxo = UTXO::new(
*self.address.value(),
serde_json::to_vec(&asset)?,
amount,
privacy_flag,
);
self.utxos.insert(asset_utxo.hash, asset_utxo);
Ok(())
}
pub fn log(&self) {
info!("Keys generated");
info!("Account address is {:?}", hex::encode(self.address));
info!("Account balance is {:?}", self.balance);
}
pub fn make_tag(&self) -> Tag {
self.address.value()[0]
}
}
impl Default for Account {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_dummy_utxo(address: TreeHashType, amount: u128) -> UTXO {
UTXO::new(address, vec![], amount, false)
}
#[test]
fn test_new_account() {
let account = Account::new();
assert_eq!(account.balance, 0);
}
#[test]
fn test_add_new_utxo_outputs() {
let mut account = Account::new();
let utxo1 = generate_dummy_utxo(*account.address.value(), 100);
let utxo2 = generate_dummy_utxo(*account.address.value(), 200);
let result = account.add_new_utxo_outputs(vec![utxo1.clone(), utxo2.clone()]);
assert!(result.is_ok());
assert_eq!(account.utxos.len(), 2);
}
#[test]
fn test_update_public_balance() {
let mut account = Account::new();
account.update_public_balance(500);
assert_eq!(account.balance, 500);
}
}

View File

@ -1,2 +0,0 @@
pub mod account_core;
pub mod key_management;

View File

@ -68,6 +68,8 @@ pub enum ExecutionFailureKind {
DecodeError(String),
#[error("Inputs amounts does not match outputs")]
AmountMismatchError,
#[error("Accounts key not found")]
KeyNotFoundError,
#[error("Sequencer client error: {0:?}")]
SequencerClientError(#[from] SequencerClientError),
#[error("Insufficient gas for operation")]

View File

@ -36,5 +36,5 @@ path = "../wallet"
[dependencies.common]
path = "../common"
[dependencies.accounts]
path = "../accounts"
[dependencies.key_protocol]
path = "../key_protocol"

View File

@ -1,45 +1,113 @@
{
"home": "./node",
"override_rust_log": null,
"sequencer_addr": "http://127.0.0.1:3040",
"seq_poll_timeout_secs": 10,
"initial_accounts": [
{
"address": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
"balance": 10000,
"key_holder": {
"address": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
"nullifer_public_key": "03A340BECA9FAAB444CED0140681D72EA1318B5C611704FEE017DA9836B17DB718",
"pub_account_signing_key": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"top_secret_key_holder": {
"secret_spending_key": "7BC46784DB1BC67825D8F029436846712BFDF9B5D79EA3AB11D39A52B9B229D4"
"home": "./node",
"override_rust_log": null,
"sequencer_addr": "http://127.0.0.1:3040",
"seq_poll_timeout_secs": 10,
"initial_accounts": [
{
"address": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
"pub_sign_key": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 10000,
"nonce": 0,
"data": []
}
},
"utxo_secret_key_holder": {
"nullifier_secret_key": "BB54A8D3C9C51B82C431082D1845A74677B0EF829A11B517E1D9885DE3139506",
"viewing_secret_key": "AD923E92F6A5683E30140CEAB2702AFB665330C1EE4EFA70FAF29767B6B52BAF"
},
"viewing_public_key": "0361220C5D277E7A1709340FD31A52600C1432B9C45B9BCF88A43581D58824A8B6"
},
"utxos": {}
},
{
"address": "4d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766",
"balance": 20000,
"key_holder": {
"address": "4d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766",
"nullifer_public_key": "02172F50274DE67C4087C344F5D58E11DF761D90285B095060E0994FAA6BCDE271",
"pub_account_signing_key": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
"top_secret_key_holder": {
"secret_spending_key": "80A186737C8D38B4288A03F0F589957D9C040D79C19F3E0CC4BA80F8494E5179"
},
"utxo_secret_key_holder": {
"nullifier_secret_key": "746928E63F0984F6F4818933493CE9C067562D9CB932FDC06D82C86CDF6D7122",
"viewing_secret_key": "89176CF4BC9E673807643FD52110EF99D4894335AFB10D881AC0B5041FE1FCB7"
},
"viewing_public_key": "026072A8F83FEC3472E30CDD4767683F30B91661D25B1040AD9A5FC2E01D659F99"
},
"utxos": {}
}
]
{
"address": "4d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766",
"pub_sign_key": [
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 20000,
"nonce": 0,
"data": []
}
}
]
}

View File

@ -156,7 +156,9 @@ pub async fn test_failure() {
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
wallet::execute_subcommand(command).await.unwrap();
let failed_send = wallet::execute_subcommand(command).await;
assert!(failed_send.is_err());
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;

View File

@ -1,5 +1,5 @@
[package]
name = "accounts"
name = "key_protocol"
version = "0.1.0"
edition = "2021"
@ -17,9 +17,7 @@ hex.workspace = true
aes-gcm.workspace = true
lazy_static.workspace = true
tiny-keccak.workspace = true
[dependencies.utxo]
path = "../utxo"
nssa-core = { path = "../nssa/core" }
[dependencies.common]
path = "../common"

View File

@ -1,15 +1,15 @@
use std::collections::HashMap;
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit};
use constants_types::{CipherText, Nonce};
use elliptic_curve::point::AffineCoordinates;
use k256::AffinePoint;
use log::info;
use rand::{rngs::OsRng, Rng};
use secret_holders::{SeedHolder, TopSecretKeyHolder, UTXOSecretKeyHolder};
use serde::{Deserialize, Serialize};
use crate::account_core::PublicKey;
use crate::key_protocol_core::PublicKey;
pub type PublicAccountSigningKey = [u8; 32];
use nssa::{self};
pub mod constants_types;
pub mod ephemeral_key_holder;
@ -17,15 +17,16 @@ pub mod secret_holders;
#[derive(Serialize, Deserialize, Clone, Debug)]
///Entrypoint to key management
pub struct AddressKeyHolder {
pub struct KeyChain {
top_secret_key_holder: TopSecretKeyHolder,
pub utxo_secret_key_holder: UTXOSecretKeyHolder,
pub_account_signing_key: nssa::PrivateKey,
///Map for all users accounts
pub_account_signing_keys: HashMap<nssa::Address, nssa::PrivateKey>,
pub nullifer_public_key: PublicKey,
pub viewing_public_key: PublicKey,
}
impl AddressKeyHolder {
impl KeyChain {
pub fn new_os_random() -> Self {
//Currently dropping SeedHolder at the end of initialization.
//Now entirely sure if we need it in the future.
@ -37,26 +38,50 @@ impl AddressKeyHolder {
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
let viewing_public_key = utxo_secret_key_holder.generate_viewing_public_key();
let mut rng = OsRng;
let pub_account_signing_key = loop {
match nssa::PrivateKey::try_new(rng.gen()) {
Ok(key) => break key,
Err(_) => continue,
}
};
Self {
top_secret_key_holder,
utxo_secret_key_holder,
nullifer_public_key,
viewing_public_key,
pub_account_signing_keys: HashMap::new(),
}
}
pub fn new_os_random_with_accounts(accounts: HashMap<nssa::Address, nssa::PrivateKey>) -> Self {
//Currently dropping SeedHolder at the end of initialization.
//Now entirely sure if we need it in the future.
let seed_holder = SeedHolder::new_os_random();
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
let utxo_secret_key_holder = top_secret_key_holder.produce_utxo_secret_holder();
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
let viewing_public_key = utxo_secret_key_holder.generate_viewing_public_key();
Self {
top_secret_key_holder,
utxo_secret_key_holder,
nullifer_public_key,
viewing_public_key,
pub_account_signing_key,
pub_account_signing_keys: accounts,
}
}
pub fn generate_new_private_key(&mut self) -> nssa::Address {
let private_key = nssa::PrivateKey::new_os_random();
let address = nssa::Address::from(&nssa::PublicKey::new_from_private_key(&private_key));
self.pub_account_signing_keys.insert(address, private_key);
address
}
/// Returns the signing key for public transaction signatures
pub fn get_pub_account_signing_key(&self) -> &nssa::PrivateKey {
&self.pub_account_signing_key
pub fn get_pub_account_signing_key(
&self,
address: &nssa::Address,
) -> Option<&nssa::PrivateKey> {
self.pub_account_signing_keys.get(address)
}
pub fn calculate_shared_secret_receiver(
@ -127,8 +152,8 @@ mod tests {
#[test]
fn test_new_os_random() {
// Ensure that a new AddressKeyHolder instance can be created without errors.
let address_key_holder = AddressKeyHolder::new_os_random();
// Ensure that a new KeyChain instance can be created without errors.
let address_key_holder = KeyChain::new_os_random();
// Check that key holder fields are initialized with expected types
assert!(!Into::<bool>::into(
@ -141,7 +166,7 @@ mod tests {
#[test]
fn test_calculate_shared_secret_receiver() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Generate a random ephemeral public key sender
let scalar = Scalar::random(&mut OsRng);
@ -157,7 +182,7 @@ mod tests {
#[test]
fn test_decrypt_data() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Generate an ephemeral key and shared secret
let ephemeral_public_key_sender =
@ -188,8 +213,8 @@ mod tests {
#[test]
fn test_new_os_random_initialization() {
// Ensure that AddressKeyHolder is initialized correctly
let address_key_holder = AddressKeyHolder::new_os_random();
// Ensure that KeyChain is initialized correctly
let address_key_holder = KeyChain::new_os_random();
// Check that key holder fields are initialized with expected types and values
assert!(!Into::<bool>::into(
@ -202,7 +227,7 @@ mod tests {
#[test]
fn test_calculate_shared_secret_with_identity_point() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Use identity point as ephemeral public key
let identity_point = AffinePoint::identity();
@ -217,7 +242,7 @@ mod tests {
#[test]
#[should_panic]
fn test_decrypt_data_with_incorrect_nonce() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Generate ephemeral public key and shared secret
let scalar = Scalar::random(OsRng);
@ -250,7 +275,7 @@ mod tests {
#[test]
#[should_panic]
fn test_decrypt_data_with_incorrect_ciphertext() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Generate ephemeral public key and shared secret
let scalar = Scalar::random(OsRng);
@ -285,7 +310,7 @@ mod tests {
#[test]
fn test_encryption_decryption_round_trip() {
let address_key_holder = AddressKeyHolder::new_os_random();
let address_key_holder = KeyChain::new_os_random();
// Generate ephemeral key and shared secret
let scalar = Scalar::random(OsRng);
@ -303,7 +328,7 @@ mod tests {
.encrypt(nonce, plaintext.as_ref())
.expect("encryption failure");
// Decrypt the data using the `AddressKeyHolder` instance
// Decrypt the data using the `KeyChain` instance
let decrypted_data = address_key_holder
.decrypt_data(
ephemeral_public_key_sender,
@ -318,9 +343,15 @@ mod tests {
#[test]
fn test_get_public_account_signing_key() {
let address_key_holder = AddressKeyHolder::new_os_random();
let signing_key = address_key_holder.get_pub_account_signing_key();
assert_eq!(signing_key, &address_key_holder.pub_account_signing_key);
let mut address_key_holder = KeyChain::new_os_random();
let address = address_key_holder.generate_new_private_key();
let is_private_key_generated = address_key_holder
.get_pub_account_signing_key(&address)
.is_some();
assert!(is_private_key_generated);
}
#[test]
@ -333,13 +364,7 @@ mod tests {
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
let viewing_public_key = utxo_secret_key_holder.generate_viewing_public_key();
let mut rng = OsRng;
let pub_account_signing_key = loop {
match nssa::PrivateKey::try_new(rng.gen()) {
Ok(key) => break key,
Err(_) => continue,
}
};
let pub_account_signing_key = nssa::PrivateKey::new_os_random();
let public_key = nssa::PublicKey::new_from_private_key(&pub_account_signing_key);

View File

@ -0,0 +1,72 @@
use std::collections::HashMap;
use anyhow::Result;
use k256::AffinePoint;
use serde::{Deserialize, Serialize};
use crate::key_management::KeyChain;
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NSSAUserData {
pub key_holder: KeyChain,
}
impl NSSAUserData {
pub fn new() -> Self {
let key_holder = KeyChain::new_os_random();
Self { key_holder }
}
fn valid_key_transaction_pairing_check(
accounts_keys_map: &HashMap<nssa::Address, nssa::PrivateKey>,
) -> bool {
let mut check_res = true;
for (addr, key) in accounts_keys_map {
if &nssa::Address::from(&nssa::PublicKey::new_from_private_key(key)) != addr {
check_res = false;
}
}
check_res
}
pub fn new_with_accounts(
accounts_keys: HashMap<nssa::Address, nssa::PrivateKey>,
) -> Result<Self> {
if !Self::valid_key_transaction_pairing_check(&accounts_keys) {
anyhow::bail!("Key transaction pairing check not satisfied, there is addresses, which is not derived from keys");
}
let key_holder = KeyChain::new_os_random_with_accounts(accounts_keys);
Ok(Self { key_holder })
}
pub fn generate_new_account(&mut self) -> nssa::Address {
self.key_holder.generate_new_private_key()
}
pub fn get_account_signing_key(&self, address: &nssa::Address) -> Option<&nssa::PrivateKey> {
self.key_holder.get_pub_account_signing_key(address)
}
}
impl Default for NSSAUserData {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_account() {
let mut user_data = NSSAUserData::new();
let _addr = user_data.generate_new_account();
}
}

2
key_protocol/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod key_management;
pub mod key_protocol_core;

View File

@ -648,9 +648,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
"serde",
@ -2785,12 +2785,11 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
@ -2800,15 +2799,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c"
dependencies = [
"num-conv",
"time-core",

View File

@ -1,5 +1,7 @@
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::signature::PublicKey;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
@ -57,6 +59,28 @@ impl Display for Address {
}
}
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let hex_string = self.to_string();
hex_string.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let hex_string = String::deserialize(deserializer)?;
Address::from_str(&hex_string).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use crate::{Address, address::AddressError};

View File

@ -1,3 +1,4 @@
use rand::{Rng, rngs::OsRng};
use serde::{Deserialize, Serialize};
use crate::error::NssaError;
@ -8,6 +9,17 @@ use crate::error::NssaError;
pub struct PrivateKey([u8; 32]);
impl PrivateKey {
pub fn new_os_random() -> Self {
let mut rng = OsRng;
loop {
match Self::try_new(rng.r#gen()) {
Ok(key) => break key,
Err(_) => continue,
};
}
}
fn is_valid_key(value: [u8; 32]) -> bool {
secp256k1::SecretKey::from_byte_array(value).is_ok()
}
@ -33,4 +45,9 @@ mod tests {
let key = PrivateKey::try_new([1; 32]).unwrap();
assert_eq!(key.value(), &key.0);
}
#[test]
fn test_produce_key() {
let _key = PrivateKey::new_os_random();
}
}

View File

@ -22,8 +22,8 @@ path = "../storage"
[dependencies.mempool]
path = "../mempool"
[dependencies.accounts]
path = "../accounts"
[dependencies.key_protocol]
path = "../key_protocol"
[dependencies.common]
path = "../common"

View File

@ -22,8 +22,8 @@ tokio.workspace = true
[dependencies.mempool]
path = "../mempool"
[dependencies.accounts]
path = "../accounts"
[dependencies.key_protocol]
path = "../key_protocol"
[dependencies.sequencer_core]
path = "../sequencer_core"

View File

@ -1,17 +0,0 @@
[package]
name = "utxo"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow.workspace = true
serde_json.workspace = true
env_logger.workspace = true
log.workspace = true
serde.workspace = true
sha2.workspace = true
hex.workspace = true
rand.workspace = true
[dependencies.common]
path = "../common"

View File

@ -1 +0,0 @@
pub mod utxo_core;

View File

@ -1,162 +0,0 @@
use anyhow::Result;
use common::{merkle_tree_public::TreeHashType, AccountId};
use log::info;
use rand::{rngs::OsRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{digest::FixedOutput, Digest};
///Raw asset data
pub type Asset = Vec<u8>;
pub type Randomness = [u8; 32];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
///Container for raw utxo payload
pub struct UTXO {
pub hash: TreeHashType,
pub owner: AccountId,
pub asset: Asset,
// TODO: change to u256
pub amount: u128,
pub privacy_flag: bool,
pub randomness: Randomness,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UTXOPayload {
pub owner: AccountId,
pub asset: Asset,
// TODO: change to u256
pub amount: u128,
pub privacy_flag: bool,
pub randomness: Randomness,
}
impl UTXOPayload {
fn to_bytes(&self) -> Vec<u8> {
let mut result = Vec::new();
result.extend_from_slice(&self.owner);
result.extend_from_slice(&self.asset);
result.extend_from_slice(&self.amount.to_be_bytes());
result.push(self.privacy_flag as u8);
result.extend_from_slice(&self.randomness);
result
}
}
impl UTXO {
pub fn new(owner: AccountId, asset: Asset, amount: u128, privacy_flag: bool) -> Self {
let mut randomness = Randomness::default();
OsRng.fill_bytes(&mut randomness);
let payload = UTXOPayload {
owner,
asset,
amount,
privacy_flag,
randomness,
};
Self::create_utxo_from_payload(payload)
}
pub fn create_utxo_from_payload(payload_with_asset: UTXOPayload) -> Self {
let mut hasher = sha2::Sha256::new();
hasher.update(payload_with_asset.to_bytes());
let hash = <TreeHashType>::from(hasher.finalize_fixed());
Self {
hash,
owner: payload_with_asset.owner,
asset: payload_with_asset.asset,
amount: payload_with_asset.amount,
privacy_flag: payload_with_asset.privacy_flag,
randomness: payload_with_asset.randomness,
}
}
pub fn interpret_asset<'de, ToInterpret: Deserialize<'de>>(&'de self) -> Result<ToInterpret> {
Ok(serde_json::from_slice(&self.asset)?)
}
pub fn into_payload(&self) -> UTXOPayload {
UTXOPayload {
owner: self.owner,
asset: self.asset.clone(),
amount: self.amount,
privacy_flag: self.privacy_flag,
randomness: self.randomness,
}
}
pub fn log(&self) {
info!("UTXO hash is {:?}", hex::encode(self.hash));
info!("UTXO owner is {:?}", hex::encode(self.owner));
info!("UTXO asset is {:?}", hex::encode(self.asset.clone()));
info!("UTXO amount is {:?}", self.amount);
info!("UTXO privacy_flag is {:?}", self.privacy_flag);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct TestAsset {
id: u32,
name: String,
}
fn sample_account() -> AccountId {
AccountId::default()
}
fn sample_payload() -> UTXOPayload {
UTXOPayload {
owner: sample_account(),
asset: serde_json::to_vec(&TestAsset {
id: 1,
name: "Test".to_string(),
})
.unwrap(),
amount: 10,
privacy_flag: false,
randomness: Randomness::default(),
}
}
#[test]
fn test_create_utxo_from_payload() {
let payload = sample_payload();
let utxo = UTXO::create_utxo_from_payload(payload.clone());
// Ensure hash is created and the UTXO fields are correctly assigned
assert_eq!(utxo.owner, payload.owner);
assert_eq!(utxo.asset, payload.asset);
}
#[test]
fn test_interpret_asset() {
let payload = sample_payload();
let utxo = UTXO::create_utxo_from_payload(payload);
// Interpret asset as TestAsset
let interpreted: TestAsset = utxo.interpret_asset().unwrap();
assert_eq!(
interpreted,
TestAsset {
id: 1,
name: "Test".to_string()
}
);
}
#[test]
fn test_interpret_invalid_asset() {
let mut payload = sample_payload();
payload.asset = vec![0, 1, 2, 3]; // Invalid data for deserialization
let utxo = UTXO::create_utxo_from_payload(payload);
// This should fail because the asset is not valid JSON for TestAsset
let result: Result<TestAsset> = utxo.interpret_asset();
assert!(result.is_err());
}
}

View File

@ -22,12 +22,10 @@ risc0-zkvm = "3.0.3"
hex.workspace = true
actix-rt.workspace = true
clap.workspace = true
nssa-core = { path = "../nssa/core" }
[dependencies.accounts]
path = "../accounts"
[dependencies.utxo]
path = "../utxo"
[dependencies.key_protocol]
path = "../key_protocol"
[dependencies.nssa]
path = "../nssa"

View File

@ -1,112 +0,0 @@
use accounts::account_core::Account;
use nssa::Address;
use std::collections::HashMap;
pub struct WalletAccountsStore {
pub accounts: HashMap<Address, Account>,
}
impl WalletAccountsStore {
pub fn new() -> Self {
Self {
accounts: HashMap::new(),
}
}
pub fn register_account(&mut self, account: Account) {
self.accounts.insert(account.address, account);
}
pub fn unregister_account(&mut self, account_addr: Address) {
self.accounts.remove(&account_addr);
}
}
impl Default for WalletAccountsStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use accounts::account_core::Account;
/// Helper function to create a sample account
fn create_sample_account(balance: u64) -> Account {
Account::new_with_balance(balance)
}
fn pad_to_32(slice: &[u8]) -> [u8; 32] {
let mut padded = [0u8; 32];
let len = slice.len().min(32);
padded[..len].copy_from_slice(&slice[..len]);
padded
}
#[test]
fn test_create_empty_store() {
let store = WalletAccountsStore::new();
assert!(store.accounts.is_empty());
}
#[test]
fn test_register_account() {
let mut store = WalletAccountsStore::new();
let account = create_sample_account(100);
let account_addr = account.address;
store.register_account(account);
assert_eq!(store.accounts.len(), 1);
let stored_account = store.accounts.get(&account_addr).unwrap();
assert_eq!(stored_account.balance, 100);
}
#[test]
fn test_unregister_account() {
let mut store = WalletAccountsStore::new();
let account = create_sample_account(100);
let account_addr = account.address;
store.register_account(account);
assert_eq!(store.accounts.len(), 1);
store.unregister_account(account_addr);
assert!(store.accounts.is_empty());
}
#[test]
fn test_unregister_nonexistent_account() {
let mut store = WalletAccountsStore::new();
let account_addr: [u8; 32] = pad_to_32("nonexistent".to_string().as_bytes());
store.unregister_account(Address::new(account_addr));
assert!(store.accounts.is_empty());
}
#[test]
fn test_register_multiple_accounts() {
let mut store = WalletAccountsStore::new();
let account1 = create_sample_account(100);
let account2 = create_sample_account(200);
let address_1 = account1.address;
let address_2 = account2.address;
store.register_account(account1);
store.register_account(account2);
assert_eq!(store.accounts.len(), 2);
let stored_account1 = store.accounts.get(&address_1).unwrap();
let stored_account2 = store.accounts.get(&address_2).unwrap();
assert_eq!(stored_account1.balance, 100);
assert_eq!(stored_account2.balance, 200);
}
}

View File

@ -1,54 +1,30 @@
use std::collections::HashMap;
use accounts::account_core::Account;
use anyhow::Result;
use common::merkle_tree_public::merkle_tree::UTXOCommitmentsMerkleTree;
use nssa::Address;
use serde::{Deserialize, Serialize};
use key_protocol::key_protocol_core::NSSAUserData;
use crate::config::WalletConfig;
pub mod accounts_store;
#[derive(Deserialize, Serialize)]
pub struct AccMap {
pub acc_map: HashMap<String, Account>,
}
impl From<HashMap<[u8; 32], Account>> for AccMap {
fn from(value: HashMap<[u8; 32], Account>) -> Self {
AccMap {
acc_map: value
.into_iter()
.map(|(key, val)| (hex::encode(key), val))
.collect(),
}
}
}
impl From<AccMap> for HashMap<[u8; 32], Account> {
fn from(value: AccMap) -> Self {
value
.acc_map
.into_iter()
.map(|(key, val)| (hex::decode(key).unwrap().try_into().unwrap(), val))
.collect()
}
}
pub struct WalletChainStore {
pub acc_map: HashMap<Address, Account>,
pub user_data: NSSAUserData,
pub utxo_commitments_store: UTXOCommitmentsMerkleTree,
pub wallet_config: WalletConfig,
}
impl WalletChainStore {
pub fn new(config: WalletConfig) -> Result<Self> {
let acc_map = HashMap::new();
let accounts_keys: HashMap<nssa::Address, nssa::PrivateKey> = config
.initial_accounts
.clone()
.into_iter()
.map(|init_acc_data| (init_acc_data.address, init_acc_data.pub_sign_key))
.collect();
let utxo_commitments_store = UTXOCommitmentsMerkleTree::new(vec![]);
Ok(Self {
acc_map,
user_data: NSSAUserData::new_with_accounts(accounts_keys)?,
utxo_commitments_store,
wallet_config: config,
})
@ -57,48 +33,33 @@ impl WalletChainStore {
#[cfg(test)]
mod tests {
use crate::config::InitialAccountData;
use super::*;
use accounts::account_core::Account;
use std::path::PathBuf;
use tempfile::tempdir;
fn create_initial_accounts() -> Vec<Account> {
fn create_initial_accounts() -> Vec<InitialAccountData> {
let initial_acc1 = serde_json::from_str(r#"{
"address": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
"balance": 100,
"nonce": 0,
"key_holder": {
"nullifer_public_key": "03A340BECA9FAAB444CED0140681D72EA1318B5C611704FEE017DA9836B17DB718",
"pub_account_signing_key": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"top_secret_key_holder": {
"secret_spending_key": "7BC46784DB1BC67825D8F029436846712BFDF9B5D79EA3AB11D39A52B9B229D4"
},
"utxo_secret_key_holder": {
"nullifier_secret_key": "BB54A8D3C9C51B82C431082D1845A74677B0EF829A11B517E1D9885DE3139506",
"viewing_secret_key": "AD923E92F6A5683E30140CEAB2702AFB665330C1EE4EFA70FAF29767B6B52BAF"
},
"viewing_public_key": "0361220C5D277E7A1709340FD31A52600C1432B9C45B9BCF88A43581D58824A8B6"
},
"utxos": {}
"pub_sign_key": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"account": {
"program_owner": [0,0,0,0,0,0,0,0],
"balance": 100,
"nonce": 0,
"data": []
}
}"#).unwrap();
let initial_acc2 = serde_json::from_str(r#"{
"address": "4d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766",
"balance": 200,
"nonce": 0,
"key_holder": {
"nullifer_public_key": "02172F50274DE67C4087C344F5D58E11DF761D90285B095060E0994FAA6BCDE271",
"pub_account_signing_key": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
"top_secret_key_holder": {
"secret_spending_key": "80A186737C8D38B4288A03F0F589957D9C040D79C19F3E0CC4BA80F8494E5179"
},
"utxo_secret_key_holder": {
"nullifier_secret_key": "746928E63F0984F6F4818933493CE9C067562D9CB932FDC06D82C86CDF6D7122",
"viewing_secret_key": "89176CF4BC9E673807643FD52110EF99D4894335AFB10D881AC0B5041FE1FCB7"
},
"viewing_public_key": "026072A8F83FEC3472E30CDD4767683F30B91661D25B1040AD9A5FC2E01D659F99"
},
"utxos": {}
"pub_sign_key": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
"account": {
"program_owner": [0,0,0,0,0,0,0,0],
"balance": 100,
"nonce": 0,
"data": []
}
}"#).unwrap();
let initial_accounts = vec![initial_acc1, initial_acc2];
@ -125,7 +86,6 @@ mod tests {
let store = WalletChainStore::new(config.clone()).unwrap();
assert!(store.acc_map.is_empty());
assert_eq!(
store.utxo_commitments_store.get_root().unwrap_or([0; 32]),
[0; 32]

View File

@ -1,7 +1,12 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use accounts::account_core::Account;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitialAccountData {
pub address: nssa::Address,
pub account: nssa_core::account::Account,
pub pub_sign_key: nssa::PrivateKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GasConfig {
@ -32,5 +37,5 @@ pub struct WalletConfig {
///Sequencer polling duration for new blocks in seconds
pub seq_poll_timeout_secs: u64,
///Initial accounts for wallet
pub initial_accounts: Vec<Account>,
pub initial_accounts: Vec<InitialAccountData>,
}

View File

@ -1,10 +1,12 @@
use std::{fs::File, io::BufReader, path::PathBuf, str::FromStr};
use accounts::account_core::Account;
use anyhow::Result;
use nssa::Address;
use crate::{config::WalletConfig, HOME_DIR_ENV_VAR};
use crate::{
config::{InitialAccountData, WalletConfig},
HOME_DIR_ENV_VAR,
};
///Get home dir for wallet. Env var `NSSA_WALLET_HOME_DIR` must be set before execution to succeed.
pub fn get_home() -> Result<PathBuf> {
@ -28,7 +30,7 @@ pub fn produce_account_addr_from_hex(hex_str: String) -> Result<Address> {
///Fetch list of accounts stored at `NSSA_WALLET_HOME_DIR/curr_accounts.json`
///
/// If file not present, it is considered as empty list of persistent accounts
pub fn fetch_persistent_accounts() -> Result<Vec<Account>> {
pub fn fetch_persistent_accounts() -> Result<Vec<InitialAccountData>> {
let home = get_home()?;
let accs_path = home.join("curr_accounts.json");

View File

@ -5,7 +5,6 @@ use common::{
ExecutionFailureKind,
};
use accounts::account_core::Account;
use anyhow::Result;
use chain_storage::WalletChainStore;
use config::WalletConfig;
@ -13,10 +12,9 @@ use log::info;
use nssa::Address;
use clap::{Parser, Subcommand};
use nssa_core::account::Account;
use crate::helperfunctions::{
fetch_config, fetch_persistent_accounts, produce_account_addr_from_hex,
};
use crate::helperfunctions::{fetch_config, produce_account_addr_from_hex};
pub const HOME_DIR_ENV_VAR: &str = "NSSA_WALLET_HOME_DIR";
pub const BLOCK_GEN_DELAY_SECS: u64 = 20;
@ -27,41 +25,32 @@ pub mod helperfunctions;
pub struct WalletCore {
pub storage: WalletChainStore,
pub wallet_config: WalletConfig,
pub sequencer_client: Arc<SequencerClient>,
}
impl WalletCore {
pub async fn start_from_config_update_chain(config: WalletConfig) -> Result<Self> {
pub fn start_from_config_update_chain(config: WalletConfig) -> Result<Self> {
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
let mut storage = WalletChainStore::new(config.clone())?;
for acc in config.clone().initial_accounts {
storage.acc_map.insert(acc.address, acc);
}
//Persistent accounts take precedence for initial accounts
let persistent_accounts = fetch_persistent_accounts()?;
for acc in persistent_accounts {
storage.acc_map.insert(acc.address, acc);
}
let storage = WalletChainStore::new(config)?;
Ok(Self {
storage,
wallet_config: config.clone(),
sequencer_client: client.clone(),
})
}
pub async fn create_new_account(&mut self) -> Address {
let account = Account::new();
account.log();
pub fn create_new_account(&mut self) -> Address {
self.storage.user_data.generate_new_account()
}
let addr = account.address;
self.storage.acc_map.insert(account.address, account);
addr
pub fn search_for_initial_account(&self, acc_addr: Address) -> Option<Account> {
for initial_acc in &self.storage.wallet_config.initial_accounts {
if initial_acc.address == acc_addr {
return Some(initial_acc.account.clone());
}
}
None
}
pub async fn send_public_native_token_transfer(
@ -71,27 +60,36 @@ impl WalletCore {
to: Address,
balance_to_move: u128,
) -> Result<SendTxResponse, ExecutionFailureKind> {
let account = self.storage.acc_map.get(&from);
let account = self.search_for_initial_account(from);
if let Some(account) = account {
let addresses = vec![from, to];
let nonces = vec![nonce];
let program_id = nssa::program::Program::authenticated_transfer_program().id();
let message = nssa::public_transaction::Message::try_new(
program_id,
addresses,
nonces,
balance_to_move,
)
.unwrap();
if account.balance >= balance_to_move {
let addresses = vec![from, to];
let nonces = vec![nonce];
let program_id = nssa::program::Program::authenticated_transfer_program().id();
let message = nssa::public_transaction::Message::try_new(
program_id,
addresses,
nonces,
balance_to_move,
)
.unwrap();
let signing_key = account.key_holder.get_pub_account_signing_key();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let signing_key = self.storage.user_data.get_account_signing_key(&from);
let tx = nssa::PublicTransaction::new(message, witness_set);
if let Some(signing_key) = signing_key {
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
Ok(self.sequencer_client.send_tx(tx).await?)
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self.sequencer_client.send_tx(tx).await?)
} else {
Err(ExecutionFailureKind::KeyNotFoundError)
}
} else {
Err(ExecutionFailureKind::InsufficientFundsError)
}
} else {
Err(ExecutionFailureKind::AmountMismatchError)
}
@ -141,7 +139,7 @@ pub async fn execute_subcommand(command: Command) -> Result<()> {
let from = produce_account_addr_from_hex(from)?;
let to = produce_account_addr_from_hex(to)?;
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?;
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)?;
let res = wallet_core
.send_public_native_token_transfer(from, nonce, to, amount)