Merge branch 'main' into schouhy/add-wallet-deploy-command

This commit is contained in:
Sergio Chouhy 2025-12-10 09:58:45 -03:00
commit 9637b8b666
39 changed files with 2420 additions and 438 deletions

View File

@ -30,16 +30,25 @@ use crate::{
pub struct SequencerClient {
pub client: reqwest::Client,
pub sequencer_addr: String,
pub basic_auth: Option<(String, Option<String>)>,
}
impl SequencerClient {
pub fn new(sequencer_addr: String) -> Result<Self> {
Self::new_with_auth(sequencer_addr, None)
}
pub fn new_with_auth(
sequencer_addr: String,
basic_auth: Option<(String, Option<String>)>,
) -> Result<Self> {
Ok(Self {
client: Client::builder()
//Add more fiedls if needed
.timeout(std::time::Duration::from_secs(60))
.build()?,
sequencer_addr,
basic_auth,
})
}
@ -51,13 +60,16 @@ impl SequencerClient {
let request =
rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload);
let call_builder = self.client.post(&self.sequencer_addr);
let mut call_builder = self.client.post(&self.sequencer_addr);
if let Some((username, password)) = &self.basic_auth {
call_builder = call_builder.basic_auth(username, password.as_deref());
}
let call_res = call_builder.json(&request).send().await?;
let response_vall = call_res.json::<Value>().await?;
// TODO: Actually why we need separation of `result` and `error` in rpc response?
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct SequencerRpcResponse {

Binary file not shown.

View File

@ -2,6 +2,7 @@ use std::{
collections::HashMap,
path::PathBuf,
pin::Pin,
str::FromStr,
time::{Duration, Instant},
};
@ -86,9 +87,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
#[nssa_integration_test]
pub async fn test_success_move_to_another_account() {
info!("########## test_success_move_to_another_account ##########");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: ChainIndex::root(),
}));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let wallet_config = fetch_config().await.unwrap();
@ -292,9 +291,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -305,9 +302,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -318,9 +313,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -356,8 +349,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -375,11 +368,14 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16
// bytes) ] First byte of the data equal to 1 means it's a token holding account
assert_eq!(supply_acc.data[0], 1);
assert_eq!(supply_acc.data.as_ref()[0], 1);
// Bytes from 1 to 33 represent the id of the token this account is associated with.
// In this example, this is a token account of the newly created token, so it is expected
// to be equal to the account_id of the token definition account.
assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes());
assert_eq!(
&supply_acc.data.as_ref()[1..33],
definition_account_id.to_bytes()
);
assert_eq!(
u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()),
37
@ -441,20 +437,18 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account. All accounts are owned except
/// definition.
/// test executes a private token transfer to a new account. All accounts are private owned
/// except definition which is public.
#[nssa_integration_test]
pub async fn test_success_token_program_private_owned() {
info!("########## test_success_token_program_private_owned ##########");
pub async fn test_success_token_program_private_owned_supply() {
info!("########## test_success_token_program_private_owned_supply ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -465,9 +459,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -478,9 +470,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -518,8 +508,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -602,19 +592,104 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await);
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account.
/// This test creates a new private token using the token program. All accounts are private
/// owned except supply which is public.
#[nssa_integration_test]
pub async fn test_success_token_program_private_claiming_path() {
info!("########## test_success_token_program_private_claiming_path ##########");
pub async fn test_success_token_program_private_owned_definition() {
info!("########## test_success_token_program_private_owned_definition ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
// Create new account for the token definition (private)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: Some(ChainIndex::root()),
},
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for the token supply holder (public)
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
cci: Some(ChainIndex::root()),
},
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: make_private_account_input_from_str(
&definition_account_id.to_string(),
),
supply_account_id: make_public_account_input_from_str(&supply_account_id.to_string()),
name: "A NAME".to_string(),
total_supply: 37,
};
wallet::cli::execute_subcommand(Command::Token(subcommand))
.await
.unwrap();
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
.await
.unwrap();
let new_commitment1 = wallet_storage
.get_private_account_commitment(&definition_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await);
// Check the status of the token definition account is the expected after the execution
let supply_acc = seq_client
.get_account(supply_account_id.to_string())
.await
.unwrap()
.account;
assert_eq!(supply_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
supply_acc.data.as_ref(),
&[
1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239,
84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
}
/// This test creates a new private token using the token program. All accounts are private
/// owned.
#[nssa_integration_test]
pub async fn test_success_token_program_private_owned_definition_and_supply() {
info!(
"########## test_success_token_program_private_owned_definition_and_supply ##########"
);
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (private)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: Some(ChainIndex::root()),
},
)))
.await
@ -627,7 +702,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
cci: Some(ChainIndex::root()),
},
)))
.await
@ -635,13 +710,105 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
else {
panic!("invalid subcommand return value");
};
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: make_private_account_input_from_str(
&definition_account_id.to_string(),
),
supply_account_id: make_private_account_input_from_str(&supply_account_id.to_string()),
name: "A NAME".to_string(),
total_supply: 37,
};
wallet::cli::execute_subcommand(Command::Token(subcommand))
.await
.unwrap();
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
.await
.unwrap();
let new_commitment1 = wallet_storage
.get_private_account_commitment(&definition_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await);
let new_commitment2 = wallet_storage
.get_private_account_commitment(&supply_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await);
let definition_acc = wallet_storage
.get_account_private(&definition_account_id)
.unwrap();
let supply_acc = wallet_storage
.get_account_private(&supply_account_id)
.unwrap();
assert_eq!(definition_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
assert_eq!(supply_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
supply_acc.data.as_ref(),
&[
1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239,
84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account.
#[nssa_integration_test]
pub async fn test_success_token_program_private_claiming_path() {
info!("########## test_success_token_program_private_claiming_path ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for the token supply holder (private)
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for receiving a token transaction
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -679,8 +846,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -755,9 +922,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -768,9 +933,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -781,9 +944,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -821,8 +982,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -897,9 +1058,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -910,9 +1069,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -923,9 +1080,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -963,8 +1118,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -1126,9 +1281,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
);
let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: ChainIndex::root(),
}));
let command =
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None }));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
@ -1468,7 +1622,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
data_changer.id(),
vec![account_id],
vec![],
(),
vec![0],
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
@ -1485,7 +1639,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
.account;
assert_eq!(post_state_account.program_owner, data_changer.id());
assert_eq!(post_state_account.balance, 0);
assert_eq!(post_state_account.data, vec![0]);
assert_eq!(post_state_account.data.as_ref(), &[0]);
assert_eq!(post_state_account.nonce, 0);
info!("Success!");
@ -1494,9 +1648,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
#[nssa_integration_test]
pub async fn test_authenticated_transfer_initialize_function() {
info!("########## test initialize account for authenticated transfer ##########");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: ChainIndex::root(),
}));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let SubcommandReturnValue::RegisterAccount { account_id } =
wallet::cli::execute_subcommand(command).await.unwrap()
else {
@ -1594,9 +1746,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: winner_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -1674,6 +1824,217 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
info!("Success!");
}
#[nssa_integration_test]
pub async fn test_keys_restoration() {
info!("########## test_keys_restoration ##########");
let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::root()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id1,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::from_str("/0").unwrap()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id2,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&from.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id1.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 100,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&from.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id2.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 101,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let from: AccountId = ACC_SENDER.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::root()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id3,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::from_str("/0").unwrap()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id4,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&from.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id3.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 102,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&from.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id4.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 103,
});
wallet::cli::execute_subcommand(command).await.unwrap();
info!("########## PREPARATION END ##########");
wallet::cli::execute_keys_restoration("test_pass".to_string(), 10)
.await
.unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone())
.await
.unwrap();
let acc1 = wallet_storage
.storage
.user_data
.private_key_tree
.get_node(to_account_id1)
.expect("Acc 1 should be restored");
let acc2 = wallet_storage
.storage
.user_data
.private_key_tree
.get_node(to_account_id2)
.expect("Acc 2 should be restored");
let _ = wallet_storage
.storage
.user_data
.public_key_tree
.get_node(to_account_id3)
.expect("Acc 3 should be restored");
let _ = wallet_storage
.storage
.user_data
.public_key_tree
.get_node(to_account_id4)
.expect("Acc 4 should be restored");
assert_eq!(
acc1.value.1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(
acc2.value.1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(acc1.value.1.balance, 100);
assert_eq!(acc2.value.1.balance, 101);
info!("########## TREE CHECKS END ##########");
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&to_account_id1.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id2.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 10,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&to_account_id3.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id4.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 11,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let wallet_config = fetch_config().await.unwrap();
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone())
.await
.unwrap();
let comm1 = wallet_storage
.get_private_account_commitment(&to_account_id1)
.expect("Acc 1 commitment should exist");
let comm2 = wallet_storage
.get_private_account_commitment(&to_account_id2)
.expect("Acc 2 commitment should exist");
assert!(verify_commitment_is_in_state(comm1, &seq_client).await);
assert!(verify_commitment_is_in_state(comm2, &seq_client).await);
let acc3 = seq_client
.get_account_balance(to_account_id3.to_string())
.await
.expect("Acc 3 must be present in public state");
let acc4 = seq_client
.get_account_balance(to_account_id4.to_string())
.await
.expect("Acc 4 must be present in public state");
assert_eq!(acc3.balance, 91);
assert_eq!(acc4.balance, 114);
info!("Success!");
}
println!("{function_map:#?}");
function_map

View File

@ -8,7 +8,8 @@ use nssa::{
public_transaction as putx,
};
use nssa_core::{
MembershipProof, NullifierPublicKey, account::AccountWithMetadata,
MembershipProof, NullifierPublicKey,
account::{AccountWithMetadata, data::Data},
encryption::IncomingViewingPublicKey,
};
use sequencer_core::config::{AccountInitialData, CommitmentsInitialData, SequencerConfig};
@ -90,7 +91,7 @@ impl TpsTestManager {
balance: 100,
nonce: 0xdeadbeef,
program_owner: Program::authenticated_transfer_program().id(),
data: vec![],
data: Data::default(),
};
let initial_commitment = CommitmentsInitialData {
npk: sender_npk,
@ -129,7 +130,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
balance: 100,
nonce: 0xdeadbeef,
program_owner: program.id(),
data: vec![],
data: Data::default(),
},
true,
AccountId::from(&sender_npk),

View File

@ -16,6 +16,7 @@ bip39.workspace = true
hmac-sha512.workspace = true
thiserror.workspace = true
nssa-core = { path = "../nssa/core", features = ["host"] }
itertools.workspace = true
[dependencies.common]
path = "../common"

View File

@ -1,8 +1,9 @@
use std::{fmt::Display, str::FromStr};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize, Hash)]
pub struct ChainIndex(Vec<u32>);
#[derive(thiserror::Error, Debug)]
@ -77,12 +78,82 @@ impl ChainIndex {
ChainIndex(chain)
}
pub fn previous_in_line(&self) -> Option<ChainIndex> {
let mut chain = self.0.clone();
if let Some(last_p) = chain.last_mut() {
*last_p = last_p.checked_sub(1)?;
}
Some(ChainIndex(chain))
}
pub fn parent(&self) -> Option<ChainIndex> {
if self.0.is_empty() {
None
} else {
Some(ChainIndex(self.0[..(self.0.len() - 1)].to_vec()))
}
}
pub fn nth_child(&self, child_id: u32) -> ChainIndex {
let mut chain = self.0.clone();
chain.push(child_id);
ChainIndex(chain)
}
pub fn depth(&self) -> u32 {
self.0.iter().map(|cci| cci + 1).sum()
}
fn collapse_back(&self) -> Option<Self> {
let mut res = self.parent()?;
let last_mut = res.0.last_mut()?;
*last_mut += *(self.0.last()?) + 1;
Some(res)
}
fn shuffle_iter(&self) -> impl Iterator<Item = ChainIndex> {
self.0
.iter()
.permutations(self.0.len())
.unique()
.map(|item| ChainIndex(item.into_iter().cloned().collect()))
}
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = ChainIndex> {
let mut stack = vec![ChainIndex(vec![0; depth])];
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
while let Some(id) = stack.pop() {
if let Some(collapsed_id) = id.collapse_back() {
for id in collapsed_id.shuffle_iter() {
stack.push(id.clone());
cumulative_stack.push(id);
}
}
}
cumulative_stack.into_iter().unique()
}
pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator<Item = ChainIndex> {
let mut stack = vec![ChainIndex(vec![0; depth])];
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
while let Some(id) = stack.pop() {
if let Some(collapsed_id) = id.collapse_back() {
for id in collapsed_id.shuffle_iter() {
stack.push(id.clone());
cumulative_stack.push(id);
}
}
}
cumulative_stack.into_iter().rev().unique()
}
}
#[cfg(test)]
@ -145,4 +216,83 @@ mod tests {
assert_eq!(string_index, "/5/7/8".to_string());
}
#[test]
fn test_prev_in_line() {
let chain_id = ChainIndex(vec![1, 7, 3]);
let prev_chain_id = chain_id.previous_in_line().unwrap();
assert_eq!(prev_chain_id, ChainIndex(vec![1, 7, 2]))
}
#[test]
fn test_prev_in_line_no_prev() {
let chain_id = ChainIndex(vec![1, 7, 0]);
let prev_chain_id = chain_id.previous_in_line();
assert_eq!(prev_chain_id, None)
}
#[test]
fn test_parent() {
let chain_id = ChainIndex(vec![1, 7, 3]);
let parent_chain_id = chain_id.parent().unwrap();
assert_eq!(parent_chain_id, ChainIndex(vec![1, 7]))
}
#[test]
fn test_parent_no_parent() {
let chain_id = ChainIndex(vec![]);
let parent_chain_id = chain_id.parent();
assert_eq!(parent_chain_id, None)
}
#[test]
fn test_parent_root() {
let chain_id = ChainIndex(vec![1]);
let parent_chain_id = chain_id.parent().unwrap();
assert_eq!(parent_chain_id, ChainIndex::root())
}
#[test]
fn test_collapse_back() {
let chain_id = ChainIndex(vec![1, 1]);
let collapsed = chain_id.collapse_back().unwrap();
assert_eq!(collapsed, ChainIndex(vec![3]))
}
#[test]
fn test_collapse_back_one() {
let chain_id = ChainIndex(vec![1]);
let collapsed = chain_id.collapse_back();
assert_eq!(collapsed, None)
}
#[test]
fn test_collapse_back_root() {
let chain_id = ChainIndex(vec![]);
let collapsed = chain_id.collapse_back();
assert_eq!(collapsed, None)
}
#[test]
fn test_shuffle() {
for id in ChainIndex::chain_ids_at_depth(5) {
println!("{id}");
}
}
}

View File

@ -1,5 +1,10 @@
use std::collections::{BTreeMap, HashMap};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use anyhow::Result;
use common::sequencer_client::SequencerClient;
use serde::{Deserialize, Serialize};
use crate::key_management::{
@ -15,6 +20,8 @@ pub mod keys_private;
pub mod keys_public;
pub mod traits;
pub const DEPTH_SOFT_CAP: u32 = 20;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KeyTree<N: KeyNode> {
pub key_map: BTreeMap<ChainIndex, N>,
@ -96,21 +103,53 @@ impl<N: KeyNode> KeyTree<N> {
}
}
pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option<nssa::AccountId> {
let father_keys = self.key_map.get(&parent_cci)?;
pub fn generate_new_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(parent_cci)?;
let next_child_id = self
.find_next_last_child_of_id(&parent_cci)
.find_next_last_child_of_id(parent_cci)
.expect("Can be None only if parent is not present");
let next_cci = parent_cci.nth_child(next_child_id);
let child_keys = father_keys.nth_child(next_child_id);
let child_keys = parent_keys.nth_child(next_child_id);
let account_id = child_keys.account_id();
self.key_map.insert(next_cci.clone(), child_keys);
self.account_id_map.insert(account_id, next_cci);
self.account_id_map.insert(account_id, next_cci.clone());
Some(account_id)
Some((account_id, next_cci))
}
fn find_next_slot_layered(&self) -> ChainIndex {
let mut depth = 1;
'outer: loop {
for chain_id in ChainIndex::chain_ids_at_depth_rev(depth) {
if !self.key_map.contains_key(&chain_id) {
break 'outer chain_id;
}
}
depth += 1;
}
}
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
let child_id = *chain_index.chain().last()?;
let child_keys = parent_keys.nth_child(child_id);
let account_id = child_keys.account_id();
self.key_map.insert(chain_index.clone(), child_keys);
self.account_id_map.insert(account_id, chain_index.clone());
Some((account_id, chain_index.clone()))
}
pub fn generate_new_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
self.fill_node(&self.find_next_slot_layered())
}
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
@ -129,11 +168,164 @@ impl<N: KeyNode> KeyTree<N> {
self.account_id_map.insert(account_id, chain_index.clone());
self.key_map.insert(chain_index, node);
}
pub fn remove(&mut self, addr: nssa::AccountId) -> Option<N> {
let chain_index = self.account_id_map.remove(&addr).unwrap();
self.key_map.remove(&chain_index)
}
/// Populates tree with children.
///
/// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// Tree must be empty before start
pub fn generate_tree_for_depth(&mut self, depth: u32) {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
self.generate_new_node(&curr_id);
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
}
}
impl KeyTree<ChildKeysPrivate> {
/// Cleanup of all non-initialized accounts in a private tree
///
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// If account is default, removes them.
///
/// Chain must be parsed for accounts beforehand
///
/// Fast, leaves gaps between accounts
pub fn cleanup_tree_remove_uninit_for_depth(&mut self, depth: u32) {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
if let Some(node) = self.key_map.get(&curr_id)
&& node.value.1 == nssa::Account::default()
&& curr_id != ChainIndex::root()
{
let addr = node.account_id();
self.remove(addr);
}
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
}
/// Cleanup of non-initialized accounts in a private tree
///
/// If account is default, removes them, stops at first non-default account.
///
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
///
/// Chain must be parsed for accounts beforehand
///
/// Slow, maintains tree consistency.
pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) {
'outer: for i in (1..(depth as usize)).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id) {
if node.value.1 == nssa::Account::default() {
let addr = node.account_id();
self.remove(addr);
} else {
break 'outer;
}
}
}
}
}
}
impl KeyTree<ChildKeysPublic> {
/// Cleanup of all non-initialized accounts in a public tree
///
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// If account is default, removes them.
///
/// Fast, leaves gaps between accounts
pub async fn cleanup_tree_remove_ininit_for_depth(
&mut self,
depth: u32,
client: Arc<SequencerClient>,
) -> Result<()> {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
if let Some(node) = self.key_map.get(&curr_id) {
let address = node.account_id();
let node_acc = client.get_account(address.to_string()).await?.account;
if node_acc == nssa::Account::default() && curr_id != ChainIndex::root() {
self.remove(address);
}
}
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
Ok(())
}
/// Cleanup of non-initialized accounts in a public tree
///
/// If account is default, removes them, stops at first non-default account.
///
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
///
/// Slow, maintains tree consistency.
pub async fn cleanup_tree_remove_uninit_layered(
&mut self,
depth: u32,
client: Arc<SequencerClient>,
) -> Result<()> {
'outer: for i in (1..(depth as usize)).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id) {
let address = node.account_id();
let node_acc = client.get_account(address.to_string()).await?.account;
if node_acc == nssa::Account::default() {
let addr = node.account_id();
self.remove(addr);
} else {
break 'outer;
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::{collections::HashSet, str::FromStr};
use nssa::AccountId;
@ -162,7 +354,7 @@ mod tests {
fn test_small_key_tree() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
let mut tree = KeyTreePrivate::new(&seed_holder);
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -170,7 +362,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -183,12 +375,12 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -201,7 +393,7 @@ mod tests {
fn test_key_tree_can_not_make_child_keys() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
let mut tree = KeyTreePrivate::new(&seed_holder);
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -209,7 +401,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -222,7 +414,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap());
let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap());
assert_eq!(key_opt, None);
}
@ -239,7 +431,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -252,7 +444,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -265,7 +457,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 2);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -279,7 +471,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/0").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -293,7 +485,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/1").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -307,7 +499,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/2").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
assert!(
@ -321,4 +513,94 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
}
#[test]
fn test_tree_balancing_automatic() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
for _ in 0..100 {
tree.generate_new_node_layered().unwrap();
}
let next_slot = tree.find_next_slot_layered();
assert_eq!(next_slot, ChainIndex::from_str("/0/0/2/1").unwrap());
}
#[test]
fn test_cleanup() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePrivate::new(&seed_holder);
tree.generate_tree_for_depth(10);
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1").unwrap())
.unwrap();
acc.value.1.balance = 2;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/2").unwrap())
.unwrap();
acc.value.1.balance = 3;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
acc.value.1.balance = 5;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
acc.value.1.balance = 6;
tree.cleanup_tree_remove_uninit_layered(10);
let mut key_set_res = HashSet::new();
key_set_res.insert("/0".to_string());
key_set_res.insert("/1".to_string());
key_set_res.insert("/2".to_string());
key_set_res.insert("/".to_string());
key_set_res.insert("/0/0".to_string());
key_set_res.insert("/0/1".to_string());
key_set_res.insert("/1/0".to_string());
let mut key_set = HashSet::new();
for key in tree.key_map.keys() {
key_set.insert(key.to_string());
}
assert_eq!(key_set, key_set_res);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/1").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 2);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/2").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 3);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 5);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 6);
}
}

View File

@ -89,9 +89,18 @@ impl NSSAUserData {
/// Returns the account_id of new account
pub fn generate_new_public_transaction_private_key(
&mut self,
parent_cci: ChainIndex,
) -> nssa::AccountId {
self.public_key_tree.generate_new_node(parent_cci).unwrap()
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures
@ -113,9 +122,18 @@ impl NSSAUserData {
/// Returns the account_id of new account
pub fn generate_new_privacy_preserving_transaction_key_chain(
&mut self,
parent_cci: ChainIndex,
) -> nssa::AccountId {
self.private_key_tree.generate_new_node(parent_cci).unwrap()
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.generate_new_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.generate_new_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures
@ -169,16 +187,8 @@ mod tests {
fn test_new_account() {
let mut user_data = NSSAUserData::default();
let account_id_pub =
user_data.generate_new_public_transaction_private_key(ChainIndex::root());
let account_id_private =
user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root());
let is_private_key_generated = user_data
.get_pub_account_signing_key(&account_id_pub)
.is_some();
assert!(is_private_key_generated);
let (account_id_private, _) = user_data
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some();

View File

@ -6,14 +6,17 @@ edition = "2024"
[dependencies]
risc0-zkvm = { version = "3.0.3", features = ['std'] }
serde = { version = "1.0", default-features = false }
thiserror = { version = "2.0.12", optional = true }
thiserror = { version = "2.0.12" }
bytemuck = { version = "1.13", optional = true }
chacha20 = { version = "0.9", default-features = false }
k256 = { version = "0.13.3", optional = true }
base58 = { version = "0.2.0", optional = true }
anyhow = { version = "1.0.98", optional = true }
borsh.workspace = true
borsh = "1.5.7"
[dev-dependencies]
serde_json = "1.0.81"
[features]
default = []
host = ["thiserror", "bytemuck", "k256", "base58", "anyhow"]
host = ["dep:bytemuck", "dep:k256", "dep:base58", "dep:anyhow"]

View File

@ -4,12 +4,14 @@ use std::{fmt::Display, str::FromStr};
#[cfg(feature = "host")]
use base58::{FromBase58, ToBase58};
use borsh::{BorshDeserialize, BorshSerialize};
pub use data::Data;
use serde::{Deserialize, Serialize};
use crate::program::ProgramId;
pub mod data;
pub type Nonce = u128;
pub type Data = Vec<u8>;
/// Account to be used both in public and private contexts
#[derive(
@ -139,7 +141,10 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: b"testing_account_with_metadata_constructor".to_vec(),
data: b"testing_account_with_metadata_constructor"
.to_vec()
.try_into()
.unwrap(),
nonce: 0xdeadbeef,
};
let fingerprint = AccountId::new([8; 32]);

View File

@ -0,0 +1,174 @@
use std::ops::Deref;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB
#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct Data(Vec<u8>);
impl Data {
pub fn into_inner(self) -> Vec<u8> {
self.0
}
#[cfg(feature = "host")]
pub fn from_cursor(
cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<Self, crate::error::NssaCoreError> {
use std::io::Read as _;
let mut u32_bytes = [0u8; 4];
cursor.read_exact(&mut u32_bytes)?;
let data_length = u32::from_le_bytes(u32_bytes);
if data_length as usize > DATA_MAX_LENGTH_IN_BYTES {
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidData, DataTooBigError).into(),
);
}
let mut data = vec![0; data_length as usize];
cursor.read_exact(&mut data)?;
Ok(Self(data))
}
}
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
#[error("data length exceeds maximum allowed length of {DATA_MAX_LENGTH_IN_BYTES} bytes")]
pub struct DataTooBigError;
impl From<Data> for Vec<u8> {
fn from(data: Data) -> Self {
data.0
}
}
impl TryFrom<Vec<u8>> for Data {
type Error = DataTooBigError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() > DATA_MAX_LENGTH_IN_BYTES {
Err(DataTooBigError)
} else {
Ok(Self(value))
}
}
}
impl Deref for Data {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8]> for Data {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
/// Data deserialization visitor.
///
/// Compared to a simple deserialization into a `Vec<u8>`, this visitor enforces
/// early length check defined by [`DATA_MAX_LENGTH_IN_BYTES`].
struct DataVisitor;
impl<'de> serde::de::Visitor<'de> for DataVisitor {
type Value = Data;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"a byte array with length not exceeding {} bytes",
DATA_MAX_LENGTH_IN_BYTES
)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut vec =
Vec::with_capacity(seq.size_hint().unwrap_or(0).min(DATA_MAX_LENGTH_IN_BYTES));
while let Some(value) = seq.next_element()? {
if vec.len() >= DATA_MAX_LENGTH_IN_BYTES {
return Err(serde::de::Error::custom(DataTooBigError));
}
vec.push(value);
}
Ok(Data(vec))
}
}
deserializer.deserialize_seq(DataVisitor)
}
}
impl BorshDeserialize for Data {
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
// Implementation adapted from `impl BorshDeserialize for Vec<T>`
let len = u32::deserialize_reader(reader)?;
match len {
0 => Ok(Self::default()),
len if len as usize > DATA_MAX_LENGTH_IN_BYTES => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
DataTooBigError,
)),
len => {
let vec_bytes = u8::vec_from_reader(len, reader)?
.expect("can't be None in current borsh crate implementation");
Ok(Self(vec_bytes))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_max_length_allowed() {
let max_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES];
let result = Data::try_from(max_vec);
assert!(result.is_ok());
}
#[test]
fn test_data_too_big_error() {
let big_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let result = Data::try_from(big_vec);
assert!(matches!(result, Err(DataTooBigError)));
}
#[test]
fn test_borsh_deserialize_exceeding_limit_error() {
let too_big_data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let mut serialized = Vec::new();
<_ as BorshSerialize>::serialize(&too_big_data, &mut serialized).unwrap();
let result = <Data as BorshDeserialize>::deserialize(&mut serialized.as_ref());
assert!(result.is_err());
}
#[test]
fn test_json_deserialize_exceeding_limit_error() {
let data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let json = serde_json::to_string(&data).unwrap();
let result: Result<Data, _> = serde_json::from_str(&json);
assert!(result.is_err());
}
}

View File

@ -54,7 +54,7 @@ mod tests {
Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 12345678901234567890,
data: b"test data".to_vec(),
data: b"test data".to_vec().try_into().unwrap(),
nonce: 18446744073709551614,
},
true,
@ -64,7 +64,7 @@ mod tests {
Account {
program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
balance: 123123123456456567112,
data: b"test data".to_vec(),
data: b"test data".to_vec().try_into().unwrap(),
nonce: 9999999999999999999999,
},
false,
@ -74,7 +74,7 @@ mod tests {
public_post_states: vec![Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 100,
data: b"post state data".to_vec(),
data: b"post state data".to_vec().try_into().unwrap(),
nonce: 18446744073709551615,
}],
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],

View File

@ -26,12 +26,14 @@ impl Account {
bytes.extend_from_slice(&self.nonce.to_le_bytes());
let data_length: u32 = self.data.len() as u32;
bytes.extend_from_slice(&data_length.to_le_bytes());
bytes.extend_from_slice(self.data.as_slice());
bytes.extend_from_slice(self.data.as_ref());
bytes
}
#[cfg(feature = "host")]
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaCoreError> {
use crate::account::data::Data;
let mut u32_bytes = [0u8; 4];
let mut u128_bytes = [0u8; 16];
@ -51,10 +53,7 @@ impl Account {
let nonce = u128::from_le_bytes(u128_bytes);
// data
cursor.read_exact(&mut u32_bytes)?;
let data_length = u32::from_le_bytes(u32_bytes);
let mut data = vec![0; data_length as usize];
cursor.read_exact(&mut data)?;
let data = Data::from_cursor(cursor)?;
Ok(Self {
program_owner,
@ -149,7 +148,7 @@ mod tests {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 123456789012345678901234567890123456,
nonce: 42,
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
};
// program owner || balance || nonce || data_len || data
@ -210,7 +209,7 @@ mod tests {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 123456789012345678901234567890123456,
nonce: 42,
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
};
let bytes = account.to_bytes();
let mut cursor = Cursor::new(bytes.as_ref());

View File

@ -1,6 +1,8 @@
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
use crate::account::AccountId;
use crate::account::{Account, AccountWithMetadata};
pub type ProgramId = [u32; 8];
@ -12,12 +14,50 @@ pub struct ProgramInput<T> {
pub instruction: T,
}
/// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA).
///
/// Each program can derive up to `2^256` unique account IDs by choosing different
/// seeds. PDAs allow programs to control namespaced account identifiers without
/// collisions between programs.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct PdaSeed([u8; 32]);
impl PdaSeed {
pub fn new(value: [u8; 32]) -> Self {
Self(value)
}
}
#[cfg(feature = "host")]
impl From<(&ProgramId, &PdaSeed)> for AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256};
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
let mut bytes = [0; 96];
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..].copy_from_slice(&value.1.0);
AccountId::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct ChainedCall {
pub program_id: ProgramId,
pub instruction_data: InstructionData,
pub pre_states: Vec<AccountWithMetadata>,
pub pda_seeds: Vec<PdaSeed>,
}
/// Represents the final state of an `Account` after a program execution.
@ -164,8 +204,19 @@ pub fn validate_execution(
}
// 7. Total balance is preserved
let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum();
let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum();
let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else {
return false;
};
let Some(total_balance_post_states) =
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
else {
return false;
};
if total_balance_pre_states != total_balance_post_states {
return false;
}
@ -173,6 +224,33 @@ pub fn validate_execution(
true
}
/// Representation of a number as `lo + hi * 2^128`.
#[derive(PartialEq, Eq)]
struct WrappedBalanceSum {
lo: u128,
hi: u128,
}
impl WrappedBalanceSum {
/// Constructs a [`WrappedBalanceSum`] from an iterator of balances.
///
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
/// expected in practical scenarios.
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
let mut wrapped = WrappedBalanceSum { lo: 0, hi: 0 };
for balance in balances {
let (new_sum, did_overflow) = wrapped.lo.overflowing_add(balance);
if did_overflow {
wrapped.hi = wrapped.hi.checked_add(1)?;
}
wrapped.lo = new_sum;
}
Some(wrapped)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -182,7 +260,7 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};
@ -197,7 +275,7 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};
@ -212,7 +290,7 @@ mod tests {
let mut account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};

View File

@ -1578,6 +1578,7 @@ dependencies = [
"chacha20",
"risc0-zkvm",
"serde",
"thiserror",
]
[[package]]

View File

@ -0,0 +1,78 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
};
/// Initializes a default account under the ownership of this program.
/// This is achieved by a noop.
fn initialize_account(pre_state: AccountWithMetadata) {
let account_to_claim = pre_state.account.clone();
let is_authorized = pre_state.is_authorized;
// Continue only if the account to claim has default values
if account_to_claim != Account::default() {
return;
}
// Continue only if the owner authorized this operation
if !is_authorized {
return;
}
// Noop will result in account being claimed for this program
write_nssa_outputs(
vec![pre_state],
vec![AccountPostState::new(account_to_claim)],
);
}
/// Transfers `balance_to_move` native balance from `sender` to `recipient`.
fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) {
// Continue only if the sender has authorized this operation
if !sender.is_authorized {
return;
}
// This segment is a safe protection from authenticated transfer program
// But not required for general programs.
// Continue only if the sender has enough balance
// if sender.account.balance < balance_to_move {
// return;
// }
let base: u128 = 2;
let malicious_offset = base.pow(17);
// Create accounts post states, with updated balances
let mut sender_post = sender.account.clone();
let mut recipient_post = recipient.account.clone();
sender_post.balance -= balance_to_move + malicious_offset;
recipient_post.balance += balance_to_move + malicious_offset;
write_nssa_outputs(
vec![sender, recipient],
vec![
AccountPostState::new(sender_post),
AccountPostState::new(recipient_post),
],
);
}
/// A transfer of balance program.
/// To be used both in public and private contexts.
fn main() {
// Read input accounts.
let ProgramInput {
pre_states,
instruction: balance_to_move,
} = read_nssa_inputs();
match (pre_states.as_slice(), balance_to_move) {
([account_to_claim], 0) => initialize_account(account_to_claim.clone()),
([sender, recipient], balance_to_move) => {
transfer(sender.clone(), recipient.clone(), balance_to_move)
}
_ => panic!("invalid params"),
}
}

View File

@ -63,7 +63,11 @@ fn main() {
let mut pinata_post = pinata.account.clone();
let mut winner_post = winner.account.clone();
pinata_post.balance -= PRIZE;
pinata_post.data = data.next_data().to_vec();
pinata_post.data = data
.next_data()
.to_vec()
.try_into()
.expect("33 bytes should fit into Data");
winner_post.balance += PRIZE;
write_nssa_outputs(

View File

@ -0,0 +1,113 @@
use nssa_core::{
account::Data,
program::{
AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
},
};
use risc0_zkvm::{
serde::to_vec,
sha::{Impl, Sha256},
};
const PRIZE: u128 = 150;
type Instruction = u128;
struct Challenge {
difficulty: u8,
seed: [u8; 32],
}
impl Challenge {
fn new(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), 33);
let difficulty = bytes[0];
assert!(difficulty <= 32);
let mut seed = [0; 32];
seed.copy_from_slice(&bytes[1..]);
Self { difficulty, seed }
}
// Checks if the leftmost `self.difficulty` number of bytes of SHA256(self.data || solution) are
// zero.
fn validate_solution(&self, solution: Instruction) -> bool {
let mut bytes = [0; 32 + 16];
bytes[..32].copy_from_slice(&self.seed);
bytes[32..].copy_from_slice(&solution.to_le_bytes());
let digest: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap();
let difficulty = self.difficulty as usize;
digest[..difficulty].iter().all(|&b| b == 0)
}
fn next_data(self) -> Data {
let mut result = [0; 33];
result[0] = self.difficulty;
result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes());
result.to_vec().try_into().expect("should fit")
}
}
/// A pinata program
fn main() {
// Read input accounts.
// It is expected to receive three accounts: [pinata_definition, pinata_token_holding,
// winner_token_holding]
let ProgramInput {
pre_states,
instruction: solution,
} = read_nssa_inputs::<Instruction>();
let [
pinata_definition,
pinata_token_holding,
winner_token_holding,
] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let data = Challenge::new(&pinata_definition.account.data);
if !data.validate_solution(solution) {
return;
}
let mut pinata_definition_post = pinata_definition.account.clone();
let pinata_token_holding_post = pinata_token_holding.account.clone();
let winner_token_holding_post = winner_token_holding.account.clone();
pinata_definition_post.data = data.next_data();
let mut instruction_data: [u8; 23] = [0; 23];
instruction_data[0] = 1;
instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes());
// Flip authorization to true for chained call
let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone();
pinata_token_holding_for_chain_call.is_authorized = true;
let chained_calls = vec![ChainedCall {
program_id: pinata_token_holding_post.program_owner,
instruction_data: to_vec(&instruction_data).unwrap(),
pre_states: vec![
pinata_token_holding_for_chain_call,
winner_token_holding.clone(),
],
pda_seeds: vec![PdaSeed::new([0; 32])],
}];
write_nssa_outputs_with_chained_call(
vec![
pinata_definition,
pinata_token_holding,
winner_token_holding,
],
vec![
AccountPostState::new(pinata_definition_post),
AccountPostState::new(pinata_token_holding_post),
AccountPostState::new(winner_token_holding_post),
],
chained_calls,
);
}

View File

@ -1,15 +1,14 @@
use std::collections::HashSet;
use risc0_zkvm::{guest::env, serde::to_vec};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme,
Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
account::{Account, AccountId, AccountWithMetadata},
compute_digest_for_path,
encryption::Ciphertext,
program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution},
};
use risc0_zkvm::{guest::env, serde::to_vec};
fn main() {
let PrivacyPreservingCircuitInput {

View File

@ -1,36 +1,35 @@
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data},
account::{Account, AccountId, AccountWithMetadata, Data, data::DATA_MAX_LENGTH_IN_BYTES},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
},
};
// The token program has three functions:
// 1. New token definition.
// Arguments to this function are:
// * Two **default** accounts: [definition_account, holding_account].
// The first default account will be initialized with the token definition account values. The second account will
// be initialized to a token holding account for the new token, holding the entire total supply.
// * An instruction data of 23-bytes, indicating the total supply and the token name, with
// the following layout:
// [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
// 2. Token transfer
// Arguments to this function are:
// 1. New token definition. Arguments to this function are:
// * Two **default** accounts: [definition_account, holding_account]. The first default account
// will be initialized with the token definition account values. The second account will be
// initialized to a token holding account for the new token, holding the entire total supply.
// * An instruction data of 23-bytes, indicating the total supply and the token name, with the
// following layout: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] The
// name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
// 2. Token transfer Arguments to this function are:
// * Two accounts: [sender_account, recipient_account].
// * An instruction data byte string of length 23, indicating the total supply with the following layout
// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
// 3. Initialize account with zero balance
// Arguments to this function are:
// * An instruction data byte string of length 23, indicating the total supply with the
// following layout [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00
// || 0x00 || 0x00].
// 3. Initialize account with zero balance Arguments to this function are:
// * Two accounts: [definition_account, account_to_initialize].
// * An dummy byte string of length 23, with the following layout
// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00].
// * An dummy byte string of length 23, with the following layout [0x02 || 0x00 || 0x00 || 0x00
// || ... || 0x00 || 0x00].
const TOKEN_DEFINITION_TYPE: u8 = 0;
const TOKEN_DEFINITION_DATA_SIZE: usize = 23;
const _: () = assert!(TOKEN_DEFINITION_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES);
const TOKEN_HOLDING_TYPE: u8 = 1;
const TOKEN_HOLDING_DATA_SIZE: usize = 49;
const _: () = assert!(TOKEN_HOLDING_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES);
struct TokenDefinition {
account_type: u8,
@ -45,12 +44,15 @@ struct TokenHolding {
}
impl TokenDefinition {
fn into_data(self) -> Vec<u8> {
fn into_data(self) -> Data {
let mut bytes = [0; TOKEN_DEFINITION_DATA_SIZE];
bytes[0] = self.account_type;
bytes[1..7].copy_from_slice(&self.name);
bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes());
bytes.into()
bytes
.to_vec()
.try_into()
.expect("23 bytes should fit into Data")
}
fn parse(data: &[u8]) -> Option<Self> {
@ -84,8 +86,9 @@ impl TokenHolding {
fn parse(data: &[u8]) -> Option<Self> {
if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE {
None
} else {
return None;
}
let account_type = data[0];
let definition_id = AccountId::new(
data[1..33]
@ -103,14 +106,16 @@ impl TokenHolding {
account_type,
})
}
}
fn into_data(self) -> Data {
let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE];
bytes[0] = self.account_type;
bytes[1..33].copy_from_slice(&self.definition_id.to_bytes());
bytes[33..].copy_from_slice(&self.balance.to_le_bytes());
bytes.into()
bytes
.to_vec()
.try_into()
.expect("33 bytes should fit into Data")
}
}
@ -223,7 +228,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<AccountPostStat
panic!("Only uninitialized accounts can be initialized");
}
// TODO: We should check that this is an account owned by the token program.
// TODO: #212 We should check that this is an account owned by the token program.
// This check can't be done here since the ID of the program is known only after compiling it
//
// Check definition account is valid
@ -249,7 +254,7 @@ fn main() {
instruction,
} = read_nssa_inputs::<Instruction>();
let (pre_states, post_states) = match instruction[0] {
let post_states = match instruction[0] {
0 => {
// Parse instruction
let total_supply = u128::from_le_bytes(
@ -263,8 +268,7 @@ fn main() {
assert_ne!(name, [0; 6]);
// Execute
let post_states = new_definition(&pre_states, name, total_supply);
(pre_states, post_states)
new_definition(&pre_states, name, total_supply)
}
1 => {
// Parse instruction
@ -279,14 +283,14 @@ fn main() {
assert_eq!(name, [0; 6]);
// Execute
let post_states = transfer(&pre_states, balance_to_move);
(pre_states, post_states)
transfer(&pre_states, balance_to_move)
}
2 => {
// Initialize account
assert_eq!(instruction[1..], [0; 22]);
let post_states = initialize_account(&pre_states);
(pre_states, post_states)
if instruction[1..] != [0; 22] {
panic!("Invalid instruction for initialize account");
}
initialize_account(&pre_states)
}
_ => panic!("Invalid instruction"),
};
@ -402,15 +406,15 @@ mod tests {
let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
let [definition_account, holding_account] = post_states.try_into().ok().unwrap();
assert_eq!(
definition_account.account().data,
vec![
definition_account.account().data.as_ref(),
&[
0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
]
);
assert_eq!(
holding_account.account().data,
vec![
holding_account.account().data.as_ref(),
&[
1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
@ -460,7 +464,9 @@ mod tests {
AccountWithMetadata {
account: Account {
// First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts
data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE],
data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE]
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -482,7 +488,7 @@ mod tests {
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1],
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -504,7 +510,7 @@ mod tests {
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1],
data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -525,7 +531,7 @@ mod tests {
let pre_states = vec![
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -533,10 +539,12 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1]
data: [1]
.into_iter()
.chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1])
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -553,10 +561,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -564,7 +574,7 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -582,10 +592,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: false,
@ -593,7 +605,7 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -609,10 +621,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -621,10 +635,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 255
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(255))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -634,15 +650,15 @@ mod tests {
let post_states = transfer(&pre_states, 11);
let [sender_post, recipient_post] = post_states.try_into().ok().unwrap();
assert_eq!(
sender_post.account().data,
vec![
sender_post.account().data.as_ref(),
[
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, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
assert_eq!(
recipient_post.account().data,
vec![
recipient_post.account().data.as_ref(),
[
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, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
@ -655,10 +671,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Definition ID with
data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16]
data: [0; TOKEN_DEFINITION_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(1000))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: false,
@ -672,10 +690,13 @@ mod tests {
];
let post_states = initialize_account(&pre_states);
let [definition, holding] = post_states.try_into().ok().unwrap();
assert_eq!(definition.account().data, pre_states[0].account.data);
assert_eq!(
holding.account().data,
vec![
definition.account().data.as_ref(),
pre_states[0].account.data.as_ref()
);
assert_eq!(
holding.account().data.as_ref(),
[
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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]

View File

@ -95,7 +95,7 @@ impl Proof {
mod tests {
use nssa_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
account::{Account, AccountId, AccountWithMetadata},
account::{Account, AccountId, AccountWithMetadata, data::Data},
};
use super::*;
@ -134,14 +134,14 @@ mod tests {
program_owner: program.id(),
balance: 100 - balance_to_move,
nonce: 1,
data: vec![],
data: Data::default(),
};
let expected_recipient_post = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let expected_sender_pre = sender.clone();
@ -191,7 +191,7 @@ mod tests {
balance: 100,
nonce: 0xdeadbeef,
program_owner: program.id(),
data: vec![],
data: Data::default(),
},
true,
AccountId::from(&sender_keys.npk()),

View File

@ -7,7 +7,7 @@ use serde::Serialize;
use crate::{
error::NssaError,
program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF},
program_methods::{AUTHENTICATED_TRANSFER_ELF, MODIFIED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF},
};
/// Maximum number of cycles for a public execution.
@ -95,6 +95,12 @@ impl Program {
// `program_methods`
Self::new(TOKEN_ELF.to_vec()).unwrap()
}
pub fn modified_transfer_program() -> Self {
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of
// `program_methods`
Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap()
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
@ -104,6 +110,11 @@ impl Program {
// `program_methods`
Self::new(PINATA_ELF.to_vec()).unwrap()
}
pub fn pinata_token() -> Self {
use crate::program_methods::PINATA_TOKEN_ELF;
Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file")
}
}
#[cfg(test)]

View File

@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution},
};
use sha2::{Digest, digest::FixedOutput};
@ -107,12 +107,13 @@ impl PublicTransaction {
program_id: message.program_id,
instruction_data: message.instruction_data.clone(),
pre_states: input_pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([initial_call]);
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut chain_calls_counter = 0;
while let Some(chained_call) = chained_calls.pop_front() {
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded);
}
@ -125,6 +126,9 @@ impl PublicTransaction {
let mut program_output =
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
let authorized_pdas =
self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds);
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coinicide with the values in the public
@ -137,8 +141,11 @@ impl PublicTransaction {
return Err(NssaError::InvalidProgramBehavior);
}
// Check that authorization flags are consistent with the provided ones
if pre.is_authorized && !signer_account_ids.contains(&account_id) {
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let is_authorized = signer_account_ids.contains(&account_id)
|| authorized_pdas.contains(&account_id);
if pre.is_authorized != is_authorized {
return Err(NssaError::InvalidProgramBehavior);
}
}
@ -176,7 +183,7 @@ impl PublicTransaction {
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front(new_call);
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter += 1;
@ -184,6 +191,21 @@ impl PublicTransaction {
Ok(state_diff)
}
fn compute_authorized_pdas(
&self,
caller_program_id: &Option<ProgramId>,
pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> {
if let Some(caller_program_id) = caller_program_id {
pda_seeds
.iter()
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed)))
.collect()
} else {
HashSet::new()
}
}
}
#[cfg(test)]

View File

@ -234,11 +234,25 @@ impl V02State {
program_owner: Program::pinata().id(),
balance: 1500,
// Difficulty: 3
data: vec![3; 33],
data: vec![3; 33].try_into().expect("should fit"),
nonce: 0,
},
);
}
pub fn add_pinata_token_program(&mut self, account_id: AccountId) {
self.insert_program(Program::pinata_token());
self.public_state.insert(
account_id,
Account {
program_owner: Program::pinata_token().id(),
// Difficulty: 3
data: vec![3; 33].try_into().expect("should fit"),
..Account::default()
},
);
}
}
#[cfg(test)]
@ -248,9 +262,9 @@ pub mod tests {
use nssa_core::{
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar},
program::ProgramId,
program::{PdaSeed, ProgramId},
};
use crate::{
@ -491,7 +505,7 @@ pub mod tests {
..Account::default()
};
let account_with_default_values_except_data = Account {
data: vec![0xca, 0xfe],
data: vec![0xca, 0xfe].try_into().unwrap(),
..Account::default()
};
self.force_insert_account(
@ -716,7 +730,8 @@ pub mod tests {
program_id
);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0])
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
@ -1013,7 +1028,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_private_account_keys_2();
@ -1037,7 +1052,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
nonce: 0xcafecafe,
balance: sender_private_account.balance - balance_to_move,
data: vec![],
data: Data::default(),
},
);
@ -1079,7 +1094,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_public_account_keys_1();
let recipient_initial_balance = 400;
@ -1112,7 +1127,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
nonce: 0xcafecafe,
balance: sender_private_account.balance - balance_to_move,
data: vec![],
data: Data::default(),
},
);
@ -1234,7 +1249,7 @@ pub mod tests {
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(()).unwrap(),
&Program::serialize_instruction(vec![0]).unwrap(),
&[0],
&[],
&[],
@ -1245,6 +1260,34 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
#[test]
fn test_data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() {
let program = Program::data_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
balance: 0,
..Account::default()
},
true,
AccountId::new([0; 32]),
);
let large_data: Vec<u8> = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1];
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(large_data).unwrap(),
&[0],
&[],
&[],
&[],
&program,
);
assert!(matches!(result, Err(NssaError::ProgramProveFailed(_))));
}
#[test]
fn test_extra_output_program_should_fail_in_privacy_preserving_circuit() {
let program = Program::extra_output_program();
@ -1678,7 +1721,7 @@ pub mod tests {
let private_account_2 = AccountWithMetadata::new(
Account {
// Non default data
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
..Account::default()
},
false,
@ -1967,7 +2010,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_private_account_keys_2();
@ -1993,7 +2036,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100 - balance_to_move,
nonce: 0xcafecafe,
data: vec![],
data: Data::default(),
};
let tx = private_balance_transfer_for_tests(
@ -2093,14 +2136,18 @@ pub mod tests {
let key = PrivateKey::try_new([1; 32]).unwrap();
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
let to = AccountId::new([2; 32]);
let initial_balance = 100;
let initial_balance = 1000;
let initial_data = [(from, initial_balance), (to, 0)];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from_key = key;
let amount: u128 = 0;
let instruction: (u128, ProgramId, u32) =
(amount, Program::authenticated_transfer_program().id(), 2);
let amount: u128 = 37;
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
2,
None,
);
let expected_to_post = Account {
program_owner: Program::authenticated_transfer_program().id(),
@ -2140,10 +2187,11 @@ pub mod tests {
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from_key = key;
let amount: u128 = 0;
let instruction: (u128, ProgramId, u32) = (
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
MAX_NUMBER_CHAINED_CALLS as u32 + 1,
None,
);
let message = public_transaction::Message::try_new(
@ -2164,6 +2212,48 @@ pub mod tests {
));
}
#[test]
fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
let chain_caller = Program::chain_caller();
let pda_seed = PdaSeed::new([37; 32]);
let from = AccountId::from((&chain_caller.id(), &pda_seed));
let to = AccountId::new([2; 32]);
let initial_balance = 1000;
let initial_data = [(from, initial_balance), (to, 0)];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let amount: u128 = 58;
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
1,
Some(pda_seed),
);
let expected_to_post = Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: amount, // The `chain_caller` chains the program twice
..Account::default()
};
let message = public_transaction::Message::try_new(
chain_caller.id(),
vec![to, from], // The chain_caller program permutes the account order in the chain
// call
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
let from_post = state.get_account_by_id(&from);
let to_post = state.get_account_by_id(&to);
assert_eq!(from_post.balance, initial_balance - amount);
assert_eq!(to_post, expected_to_post);
}
#[test]
fn test_claiming_mechanism_within_chain_call() {
// This test calls the authenticated transfer program through the chain_caller program.
@ -2195,8 +2285,12 @@ pub mod tests {
// The transaction executes the chain_caller program, which internally calls the
// authenticated_transfer program
let instruction: (u128, ProgramId, u32) =
(amount, Program::authenticated_transfer_program().id(), 1);
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
1,
None,
);
let message = public_transaction::Message::try_new(
chain_caller.id(),
vec![to, from], // The chain_caller program permutes the account order in the chain
@ -2216,6 +2310,86 @@ pub mod tests {
assert_eq!(to_post, expected_to_post);
}
#[test]
fn test_pda_mechanism_with_pinata_token_program() {
let pinata_token = Program::pinata_token();
let token = Program::token();
let pinata_definition_id = AccountId::new([1; 32]);
let pinata_token_definition_id = AccountId::new([2; 32]);
// Total supply of pinata token will be in an account under a PDA.
let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32])));
let winner_token_holding_id = AccountId::new([3; 32]);
let mut expected_winner_account_data = [0; 49];
expected_winner_account_data[0] = 1;
expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value());
expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes());
let expected_winner_token_holding_post = Account {
program_owner: token.id(),
data: expected_winner_account_data.to_vec().try_into().unwrap(),
..Account::default()
};
let mut state = V02State::new_with_genesis_accounts(&[], &[]);
state.add_pinata_token_program(pinata_definition_id);
// Execution of the token program to create new token for the pinata token
// definition and supply accounts
let total_supply: u128 = 10_000_000;
// instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
let mut instruction: [u8; 23] = [0; 23];
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
instruction[17..].copy_from_slice(b"PINATA");
let message = public_transaction::Message::try_new(
token.id(),
vec![pinata_token_definition_id, pinata_token_holding_id],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
// Execution of the token program transfer just to initialize the winner token account
let mut instruction: [u8; 23] = [0; 23];
instruction[0] = 2;
let message = public_transaction::Message::try_new(
token.id(),
vec![pinata_token_definition_id, winner_token_holding_id],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
// Submit a solution to the pinata program to claim the prize
let solution: u128 = 989106;
let message = public_transaction::Message::try_new(
pinata_token.id(),
vec![
pinata_definition_id,
pinata_token_holding_id,
winner_token_holding_id,
],
vec![],
solution,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
let winner_token_holding_post = state.get_account_by_id(&winner_token_holding_id);
assert_eq!(
winner_token_holding_post,
expected_winner_token_holding_post
);
}
#[test]
fn test_claiming_mechanism_cannot_claim_initialied_accounts() {
let claimer = Program::claimer();
@ -2241,4 +2415,70 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)))
}
/// This test ensures that even if a malicious program tries to perform overflow of balances
/// it will not be able to break the balance validation.
#[test]
fn test_malicious_program_cannot_break_balance_validation() {
let sender_key = PrivateKey::try_new([37; 32]).unwrap();
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
let sender_init_balance: u128 = 10;
let recipient_key = PrivateKey::try_new([42; 32]).unwrap();
let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key));
let recipient_init_balance: u128 = 10;
let mut state = V02State::new_with_genesis_accounts(
&[
(sender_id, sender_init_balance),
(recipient_id, recipient_init_balance),
],
&[],
);
state.insert_program(Program::modified_transfer_program());
let balance_to_move: u128 = 4;
let sender =
AccountWithMetadata::new(state.get_account_by_id(&sender_id.clone()), true, sender_id);
let sender_nonce = sender.account.nonce;
let _recipient =
AccountWithMetadata::new(state.get_account_by_id(&recipient_id), false, sender_id);
let message = public_transaction::Message::try_new(
Program::modified_transfer_program().id(),
vec![sender_id, recipient_id],
vec![sender_nonce],
balance_to_move,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
let tx = PublicTransaction::new(message, witness_set);
let res = state.transition_from_public_transaction(&tx);
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
let sender_post = state.get_account_by_id(&sender_id);
let recipient_post = state.get_account_by_id(&recipient_id);
let expected_sender_post = {
let mut this = state.get_account_by_id(&sender_id);
this.balance = sender_init_balance;
this.nonce = 0;
this
};
let expected_recipient_post = {
let mut this = state.get_account_by_id(&sender_id);
this.balance = recipient_init_balance;
this.nonce = 0;
this
};
assert!(expected_sender_post == sender_post);
assert!(expected_recipient_post == recipient_post);
}
}

View File

@ -1583,6 +1583,7 @@ dependencies = [
"chacha20",
"risc0-zkvm",
"serde",
"thiserror",
]
[[package]]

View File

@ -1,47 +1,54 @@
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
};
use risc0_zkvm::serde::to_vec;
type Instruction = (u128, ProgramId, u32);
type Instruction = (u128, ProgramId, u32, Option<PdaSeed>);
/// A program that calls another program `num_chain_calls` times.
/// It permutes the order of the input accounts on the subsequent call
/// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program
fn main() {
let ProgramInput {
pre_states,
instruction: (balance, program_id, num_chain_calls),
instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed),
} = read_nssa_inputs::<Instruction>();
let [sender_pre, receiver_pre] = match pre_states.try_into() {
let [recipient_pre, sender_pre] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let instruction_data = to_vec(&balance).unwrap();
let mut chained_call = vec![
ChainedCall {
program_id,
instruction_data: instruction_data.clone(),
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
};
num_chain_calls as usize - 1
];
let mut running_recipient_pre = recipient_pre.clone();
let mut running_sender_pre = sender_pre.clone();
chained_call.push(ChainedCall {
program_id,
instruction_data,
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
});
if pda_seed.is_some() {
running_sender_pre.is_authorized = true;
}
let mut chained_calls = Vec::new();
for _i in 0..num_chain_calls {
let new_chained_call = ChainedCall {
program_id: auth_transfer_id,
instruction_data: instruction_data.clone(),
pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], // <- Account order permutation here
pda_seeds: pda_seed.iter().cloned().collect(),
};
chained_calls.push(new_chained_call);
running_sender_pre.account.balance -= balance;
running_recipient_pre.account.balance += balance;
}
write_nssa_outputs_with_chained_call(
vec![sender_pre.clone(), receiver_pre.clone()],
vec![sender_pre.clone(), recipient_pre.clone()],
vec![
AccountPostState::new(sender_pre.account),
AccountPostState::new(receiver_pre.account),
AccountPostState::new(recipient_pre.account),
],
chained_call,
chained_calls,
);
}

View File

@ -1,9 +1,10 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = ();
type Instruction = Vec<u8>;
/// A program that modifies the account data by setting bytes sent in instruction.
fn main() {
let ProgramInput { pre_states, .. } = read_nssa_inputs::<Instruction>();
let ProgramInput { pre_states, instruction: data } = read_nssa_inputs::<Instruction>();
let [pre] = match pre_states.try_into() {
Ok(array) => array,
@ -12,7 +13,7 @@ fn main() {
let account_pre = &pre.account;
let mut account_post = account_pre.clone();
account_post.data.push(0);
account_post.data = data.try_into().expect("provided data should fit into data limit");
write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]);
}

View File

@ -23,6 +23,7 @@ itertools.workspace = true
sha2.workspace = true
futures.workspace = true
async-stream = "0.3.6"
indicatif = { version = "0.18.3", features = ["improved_unicode"] }
[dependencies.key_protocol]
path = "../key_protocol"

View File

@ -8,6 +8,7 @@ use key_protocol::{
},
key_protocol_core::NSSAUserData,
};
use log::debug;
use nssa::program::Program;
use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig};
@ -127,7 +128,7 @@ impl WalletChainStore {
account_id: nssa::AccountId,
account: nssa_core::account::Account,
) {
println!("inserting at address {account_id}, this account {account:?}");
debug!("inserting at address {account_id}, this account {account:?}");
let entry = self
.user_data
@ -263,6 +264,7 @@ mod tests {
seq_poll_max_retries: 10,
seq_block_poll_max_amount: 100,
initial_accounts: create_initial_accounts(),
basic_auth: None,
}
}

View File

@ -96,13 +96,13 @@ pub enum NewSubcommand {
Public {
#[arg(long)]
/// Chain index of a parent node
cci: ChainIndex,
cci: Option<ChainIndex>,
},
/// Register new private account
Private {
#[arg(long)]
/// Chain index of a parent node
cci: ChainIndex,
cci: Option<ChainIndex>,
},
}
@ -113,9 +113,11 @@ impl WalletSubcommand for NewSubcommand {
) -> Result<SubcommandReturnValue> {
match self {
NewSubcommand::Public { cci } => {
let account_id = wallet_core.create_new_account_public(cci);
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
println!("Generated new account with account_id Public/{account_id}");
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
let path = wallet_core.store_persistent_data().await?;
@ -124,7 +126,7 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
NewSubcommand::Private { cci } => {
let account_id = wallet_core.create_new_account_private(cci);
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
let (key, _) = wallet_core
.storage
@ -133,7 +135,7 @@ impl WalletSubcommand for NewSubcommand {
.unwrap();
println!(
"Generated new account with account_id Private/{}",
"Generated new account with account_id Private/{} at path {chain_index}",
account_id.to_bytes().to_base58()
);
println!("With npk {}", hex::encode(key.nullifer_public_key.0));

View File

@ -9,10 +9,6 @@ use crate::{
/// Represents generic config CLI subcommand
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigSubcommand {
/// Command to explicitly setup config and storage
///
/// Does nothing in case if both already present
Setup {},
/// Getter of config fields
Get { key: String },
/// Setter of config fields
@ -27,11 +23,6 @@ impl WalletSubcommand for ConfigSubcommand {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
ConfigSubcommand::Setup {} => {
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
}
ConfigSubcommand::Get { key } => match key.as_str() {
"all" => {
let config_str =
@ -73,6 +64,13 @@ impl WalletSubcommand for ConfigSubcommand {
"initial_accounts" => {
println!("{:#?}", wallet_core.storage.wallet_config.initial_accounts);
}
"basic_auth" => {
if let Some(basic_auth) = &wallet_core.storage.wallet_config.basic_auth {
println!("{basic_auth}");
} else {
println!("Not set");
}
}
_ => {
println!("Unknown field");
}
@ -99,6 +97,9 @@ impl WalletSubcommand for ConfigSubcommand {
wallet_core.storage.wallet_config.seq_block_poll_max_amount =
value.parse()?;
}
"basic_auth" => {
wallet_core.storage.wallet_config.basic_auth = Some(value.parse()?);
}
"initial_accounts" => {
anyhow::bail!("Setting this field from wallet is not supported");
}
@ -141,6 +142,9 @@ impl WalletSubcommand for ConfigSubcommand {
"initial_accounts" => {
println!("List of initial accounts' keys(both public and private)");
}
"basic_auth" => {
println!("Basic authentication credentials for sequencer HTTP requests");
}
_ => {
println!("Unknown field");
}

View File

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{io::Write, path::PathBuf};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
@ -15,7 +15,7 @@ use crate::{
token::TokenProgramAgnosticSubcommand,
},
},
helperfunctions::fetch_config,
helperfunctions::{fetch_config, fetch_persistent_storage, merge_auth_config},
};
pub mod account;
@ -53,27 +53,21 @@ pub enum Command {
/// Command to setup config, get and set config fields
#[command(subcommand)]
Config(ConfigSubcommand),
/// Restoring keys from given password at given `depth`
///
/// !!!WARNING!!! will rewrite current storage
RestoreKeys {
#[arg(short, long)]
/// Indicates, how deep in tree accounts may be. Affects command complexity.
depth: u32,
},
/// Deploy a program
DeployProgram { binary_filepath: PathBuf },
}
/// Represents overarching CLI command for a wallet with setup included
#[derive(Debug, Subcommand, Clone)]
#[clap(about)]
pub enum OverCommand {
/// Represents CLI command for a wallet
#[command(subcommand)]
Command(Command),
/// Setup of a storage. Initializes rots for public and private trees from `password`.
Setup {
#[arg(short, long)]
password: String,
},
}
/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config
///
/// All account adresses must be valid 32 byte base58 strings.
/// All account addresses must be valid 32 byte base58 strings.
///
/// All account account_ids must be provided as {privacy_prefix}/{account_id},
/// where valid options for `privacy_prefix` is `Public` and `Private`
@ -83,9 +77,12 @@ pub struct Args {
/// Continious run flag
#[arg(short, long)]
pub continuous_run: bool,
/// Basic authentication in the format `user` or `user:password`
#[arg(long)]
pub auth: Option<String>,
/// Wallet command
#[command(subcommand)]
pub command: Option<OverCommand>,
pub command: Option<Command>,
}
#[derive(Debug, Clone)]
@ -98,7 +95,22 @@ pub enum SubcommandReturnValue {
}
pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValue> {
execute_subcommand_with_auth(command, None).await
}
pub async fn execute_subcommand_with_auth(
command: Command,
auth: Option<String>,
) -> Result<SubcommandReturnValue> {
if fetch_persistent_storage().await.is_err() {
println!("Persistent storage not found, need to execute setup");
let password = read_password_from_stdin()?;
execute_setup_with_auth(password, auth.clone()).await?;
}
let wallet_config = fetch_config().await?;
let wallet_config = merge_auth_config(wallet_config, auth.clone())?;
let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?;
let subcommand_ret = match command {
@ -158,6 +170,12 @@ pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValu
.handle_subcommand(&mut wallet_core)
.await?
}
Command::RestoreKeys { depth } => {
let password = read_password_from_stdin()?;
execute_keys_restoration_with_auth(password, depth, auth).await?;
SubcommandReturnValue::Empty
}
Command::DeployProgram { binary_filepath } => {
let bytecode: Vec<u8> = std::fs::read(&binary_filepath).context(format!(
"Failed to read program binary at {}",
@ -179,7 +197,11 @@ pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValu
}
pub async fn execute_continuous_run() -> Result<()> {
execute_continuous_run_with_auth(None).await
}
pub async fn execute_continuous_run_with_auth(auth: Option<String>) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?;
loop {
@ -197,11 +219,90 @@ pub async fn execute_continuous_run() -> Result<()> {
}
}
pub fn read_password_from_stdin() -> Result<String> {
let mut password = String::new();
print!("Input password: ");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut password)?;
Ok(password.trim().to_string())
}
pub async fn execute_setup(password: String) -> Result<()> {
execute_setup_with_auth(password, None).await
}
pub async fn execute_setup_with_auth(password: String, auth: Option<String>) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?;
wallet_core.store_persistent_data().await?;
Ok(())
}
pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()> {
execute_keys_restoration_with_auth(password, depth, None).await
}
pub async fn execute_keys_restoration_with_auth(
password: String,
depth: u32,
auth: Option<String>,
) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let mut wallet_core =
WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?;
wallet_core
.storage
.user_data
.public_key_tree
.generate_tree_for_depth(depth);
println!("Public tree generated");
wallet_core
.storage
.user_data
.private_key_tree
.generate_tree_for_depth(depth);
println!("Private tree generated");
wallet_core
.storage
.user_data
.public_key_tree
.cleanup_tree_remove_uninit_layered(depth, wallet_core.sequencer_client.clone())
.await?;
println!("Public tree cleaned up");
let last_block = wallet_core
.sequencer_client
.get_last_block()
.await?
.last_block;
println!("Last block is {last_block}");
wallet_core.sync_to_block(last_block).await?;
println!("Private tree clean up start");
wallet_core
.storage
.user_data
.private_key_tree
.cleanup_tree_remove_uninit_layered(depth);
println!("Private tree cleaned up");
wallet_core.store_persistent_data().await?;
Ok(())
}

View File

@ -197,6 +197,7 @@ async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId)
let account = wallet.get_account_public(pinata_account_id).await?;
let data: [u8; 33] = account
.data
.as_ref()
.try_into()
.map_err(|_| anyhow::Error::msg("invalid pinata account data"))?;

View File

@ -14,8 +14,6 @@ use crate::{
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramAgnosticSubcommand {
/// Produce a new token
///
/// Currently the only supported privacy options is for public definition
New {
/// definition_account_id - valid 32 byte base58 string with privacy prefix
#[arg(long)]
@ -72,8 +70,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
let underlying_subcommand = match (definition_addr_privacy, supply_addr_privacy) {
(AccountPrivacyKind::Public, AccountPrivacyKind::Public) => {
TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::CreateNewToken {
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id,
supply_account_id,
name,
@ -82,8 +80,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
)
}
(AccountPrivacyKind::Public, AccountPrivacyKind::Private) => {
TokenProgramSubcommand::Private(
TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned {
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
@ -92,14 +90,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
)
}
(AccountPrivacyKind::Private, AccountPrivacyKind::Private) => {
// ToDo: maybe implement this one. It is not immediately clear why
// definition should be private.
anyhow::bail!("Unavailable privacy pairing")
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
},
)
}
(AccountPrivacyKind::Private, AccountPrivacyKind::Public) => {
// ToDo: Probably valid. If definition is not public, but supply is it is
// very suspicious.
anyhow::bail!("Unavailable privacy pairing")
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
},
)
}
};
@ -202,6 +210,9 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
/// Represents generic CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommand {
/// Creation of new token
#[command(subcommand)]
Create(CreateNewTokenProgramSubcommand),
/// Public execution
#[command(subcommand)]
Public(TokenProgramSubcommandPublic),
@ -219,17 +230,6 @@ pub enum TokenProgramSubcommand {
/// Represents generic public CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommandPublic {
// Create a new token using the token program
CreateNewToken {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
// Transfer tokens using the token program
TransferToken {
#[arg(short, long)]
@ -244,17 +244,6 @@ pub enum TokenProgramSubcommandPublic {
/// Represents generic private CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommandPrivate {
// Create a new token using the token program
CreateNewTokenPrivateOwned {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
// Transfer tokens using the token program
TransferTokenPrivateOwned {
#[arg(short, long)]
@ -320,35 +309,69 @@ pub enum TokenProgramSubcommandShielded {
},
}
/// Represents generic initialization subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum CreateNewTokenProgramSubcommand {
/// Create a new token using the token program
///
/// Definition - public, supply - public
NewPublicDefPublicSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - public, supply - private
NewPublicDefPrivateSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - private, supply - public
NewPrivateDefPublicSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - private, supply - private
NewPrivateDefPrivateSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
}
impl WalletSubcommand for TokenProgramSubcommandPublic {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommandPublic::CreateNewToken {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!();
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
Token(wallet_core)
.send_new_definition(
definition_account_id.parse().unwrap(),
supply_account_id.parse().unwrap(),
name_bytes,
total_supply,
)
.await?;
Ok(SubcommandReturnValue::Empty)
}
TokenProgramSubcommandPublic::TransferToken {
sender_account_id,
recipient_account_id,
@ -373,54 +396,6 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_supply) = Token(wallet_core)
.send_new_definition_private_owned(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_supply, supply_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
TokenProgramSubcommandPrivate::TransferTokenPrivateOwned {
sender_account_id,
recipient_account_id,
@ -657,12 +632,195 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
}
}
impl WalletSubcommand for CreateNewTokenProgramSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, [secret_definition, secret_supply]) = Token(wallet_core)
.send_new_definition_private_owned_definiton_and_supply(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![
(secret_definition, definition_account_id),
(secret_supply, supply_account_id),
];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_definition) = Token(wallet_core)
.send_new_definition_private_owned_definiton(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_definition, definition_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_supply) = Token(wallet_core)
.send_new_definition_private_owned_supply(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_supply, supply_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!();
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
Token(wallet_core)
.send_new_definition(
definition_account_id.parse().unwrap(),
supply_account_id.parse().unwrap(),
name_bytes,
total_supply,
)
.await?;
Ok(SubcommandReturnValue::Empty)
}
}
}
}
impl WalletSubcommand for TokenProgramSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommand::Create(creation_subcommand) => {
creation_subcommand.handle_subcommand(wallet_core).await
}
TokenProgramSubcommand::Private(private_subcommand) => {
private_subcommand.handle_subcommand(wallet_core).await
}

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use key_protocol::key_management::{
KeyChain,
key_tree::{
@ -6,6 +8,49 @@ use key_protocol::key_management::{
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicAuth {
pub username: String,
pub password: Option<String>,
}
impl std::fmt::Display for BasicAuth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.username)?;
if let Some(password) = &self.password {
write!(f, ":{password}")?;
}
Ok(())
}
}
impl FromStr for BasicAuth {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse = || {
let mut parts = s.splitn(2, ':');
let username = parts.next()?;
let password = parts.next().filter(|p| !p.is_empty());
if parts.next().is_some() {
return None;
}
Some((username, password))
};
let (username, password) = parse().ok_or_else(|| {
anyhow::anyhow!("Invalid auth format. Expected 'user' or 'user:password'")
})?;
Ok(Self {
username: username.to_string(),
password: password.map(|p| p.to_string()),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitialAccountDataPublic {
pub account_id: String,
@ -143,6 +188,8 @@ pub struct WalletConfig {
pub seq_block_poll_max_amount: u64,
/// Initial accounts for wallet
pub initial_accounts: Vec<InitialAccountData>,
/// Basic authentication credentials
pub basic_auth: Option<BasicAuth>,
}
impl Default for WalletConfig {
@ -154,6 +201,7 @@ impl Default for WalletConfig {
seq_tx_poll_max_blocks: 5,
seq_poll_max_retries: 5,
seq_block_poll_max_amount: 100,
basic_auth: None,
initial_accounts: {
let init_acc_json = r#"
[

View File

@ -12,7 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{
HOME_DIR_ENV_VAR,
config::{
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
BasicAuth, InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig,
},
};
@ -89,6 +89,23 @@ pub async fn fetch_config() -> Result<WalletConfig> {
Ok(config)
}
/// Parse CLI auth string and merge with config auth, prioritizing CLI
pub fn merge_auth_config(
mut config: WalletConfig,
cli_auth: Option<String>,
) -> Result<WalletConfig> {
if let Some(auth_str) = cli_auth {
let cli_auth_config: BasicAuth = auth_str.parse()?;
if config.basic_auth.is_some() {
println!("Warning: CLI auth argument takes precedence over config basic-auth");
}
config.basic_auth = Some(cli_auth_config);
}
Ok(config)
}
/// Fetch data stored at home
///
/// File must be created through setup beforehand.

View File

@ -47,7 +47,14 @@ pub struct WalletCore {
impl WalletCore {
pub async fn start_from_config_update_chain(config: WalletConfig) -> Result<Self> {
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
let basic_auth = config
.basic_auth
.as_ref()
.map(|auth| (auth.username.clone(), auth.password.clone()));
let client = Arc::new(SequencerClient::new_with_auth(
config.sequencer_addr.clone(),
basic_auth,
)?);
let tx_poller = TxPoller::new(config.clone(), client.clone());
let PersistentStorage {
@ -69,7 +76,14 @@ impl WalletCore {
config: WalletConfig,
password: String,
) -> Result<Self> {
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
let basic_auth = config
.basic_auth
.as_ref()
.map(|auth| (auth.username.clone(), auth.password.clone()));
let client = Arc::new(SequencerClient::new_with_auth(
config.sequencer_addr.clone(),
basic_auth,
)?);
let tx_poller = TxPoller::new(config.clone(), client.clone());
let storage = WalletChainStore::new_storage(config, password)?;
@ -112,13 +126,19 @@ impl WalletCore {
Ok(config_path)
}
pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId {
pub fn create_new_account_public(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
.user_data
.generate_new_public_transaction_private_key(chain_index)
}
pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId {
pub fn create_new_account_private(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
.user_data
.generate_new_privacy_preserving_transaction_key_chain(chain_index)
@ -306,11 +326,14 @@ impl WalletCore {
}
let before_polling = std::time::Instant::now();
let num_of_blocks = block_id - self.last_synced_block;
println!("Syncing to block {block_id}. Blocks to sync: {num_of_blocks}");
let poller = self.poller.clone();
let mut blocks =
std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id));
let bar = indicatif::ProgressBar::new(num_of_blocks);
while let Some(block) = blocks.try_next().await? {
for tx in block.transactions {
let nssa_tx = NSSATransaction::try_from(&tx)?;
@ -319,7 +342,9 @@ impl WalletCore {
self.last_synced_block = block.block_id;
self.store_persistent_data().await?;
bar.inc(1);
}
bar.finish();
println!(
"Synced to block {block_id} in {:?}",
@ -379,7 +404,7 @@ impl WalletCore {
.collect::<Vec<_>>();
for (affected_account_id, new_acc) in affected_accounts {
println!(
info!(
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
);
self.storage

View File

@ -1,7 +1,7 @@
use anyhow::Result;
use clap::{CommandFactory as _, Parser as _};
use tokio::runtime::Builder;
use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_setup, execute_subcommand};
use wallet::cli::{Args, execute_continuous_run_with_auth, execute_subcommand_with_auth};
pub const NUM_THREADS: usize = 2;
@ -10,7 +10,6 @@ pub const NUM_THREADS: usize = 2;
// file path?
// TODO #172: Why it requires config as env var while sequencer_runner accepts as
// argument?
// TODO #171: Running pinata doesn't give output about transaction hash and etc.
fn main() -> Result<()> {
let runtime = Builder::new_multi_thread()
.worker_threads(NUM_THREADS)
@ -23,16 +22,11 @@ fn main() -> Result<()> {
env_logger::init();
runtime.block_on(async move {
if let Some(over_command) = args.command {
match over_command {
OverCommand::Command(command) => {
let _output = execute_subcommand(command).await?;
if let Some(command) = args.command {
let _output = execute_subcommand_with_auth(command, args.auth).await?;
Ok(())
}
OverCommand::Setup { password } => execute_setup(password).await,
}
} else if args.continuous_run {
execute_continuous_run().await
execute_continuous_run_with_auth(args.auth).await
} else {
let help = Args::command().render_long_help();
println!("{help}");

View File

@ -38,7 +38,7 @@ impl Token<'_> {
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
}
pub async fn send_new_definition_private_owned(
pub async fn send_new_definition_private_owned_supply(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
@ -61,11 +61,66 @@ impl Token<'_> {
let first = secrets
.into_iter()
.next()
.expect("expected recipient's secret");
.expect("expected supply's secret");
(resp, first)
})
}
pub async fn send_new_definition_private_owned_definiton(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
name: [u8; 6],
total_supply: u128,
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
let (instruction_data, program) = token_program_preparation_definition(name, total_supply);
self.0
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(supply_account_id),
],
&instruction_data,
&program,
)
.await
.map(|(resp, secrets)| {
let first = secrets
.into_iter()
.next()
.expect("expected definition's secret");
(resp, first)
})
}
pub async fn send_new_definition_private_owned_definiton_and_supply(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
name: [u8; 6],
total_supply: u128,
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
let (instruction_data, program) = token_program_preparation_definition(name, total_supply);
self.0
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
],
&instruction_data,
&program,
)
.await
.map(|(resp, secrets)| {
let mut iter = secrets.into_iter();
let first = iter.next().expect("expected definition's secret");
let second = iter.next().expect("expected supply's secret");
(resp, [first, second])
})
}
pub async fn send_transfer_transaction(
&self,
sender_account_id: AccountId,