Merge branch 'main' into marvin/signature-bip340-fixes

This commit is contained in:
jonesmarvin8 2026-04-30 12:19:52 -04:00
commit a572817305
110 changed files with 3324 additions and 1233 deletions

49
Cargo.lock generated
View File

@ -1111,7 +1111,7 @@ dependencies = [
"log",
"num",
"pin-project-lite",
"rand 0.9.2",
"rand 0.9.3",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
@ -2506,7 +2506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986"
dependencies = [
"portable-atomic",
"rand 0.9.2",
"rand 0.9.3",
"web-time",
]
@ -3478,6 +3478,16 @@ dependencies = [
"url",
]
[[package]]
name = "indexer_ffi"
version = "0.1.0"
dependencies = [
"cbindgen",
"indexer_service",
"log",
"tokio",
]
[[package]]
name = "indexer_service"
version = "0.1.0"
@ -3589,6 +3599,7 @@ dependencies = [
"env_logger",
"futures",
"hex",
"indexer_ffi",
"indexer_service",
"indexer_service_rpc",
"key_protocol",
@ -3838,7 +3849,7 @@ dependencies = [
"jsonrpsee-types",
"parking_lot",
"pin-project",
"rand 0.9.2",
"rand 0.9.3",
"rustc-hash",
"serde",
"serde_json",
@ -4055,7 +4066,7 @@ dependencies = [
"oco_ref",
"or_poisoned",
"paste",
"rand 0.9.2",
"rand 0.9.3",
"reactive_graph",
"rustc-hash",
"rustc_version",
@ -5717,7 +5728,7 @@ dependencies = [
"futures-util",
"opentelemetry",
"percent-encoding",
"rand 0.9.2",
"rand 0.9.3",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
@ -6101,7 +6112,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
dependencies = [
"bitflags 2.11.0",
"num-traits",
"rand 0.9.2",
"rand 0.9.3",
"rand_chacha 0.9.0",
"rand_xorshift",
"unarray",
@ -6134,7 +6145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@ -6147,7 +6158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@ -6214,7 +6225,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"rand 0.9.3",
"ring",
"rustc-hash",
"rustls",
@ -6302,9 +6313,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
@ -6606,7 +6617,7 @@ dependencies = [
"elf",
"lazy_static",
"postcard",
"rand 0.9.2",
"rand 0.9.3",
"risc0-zkp",
"risc0-zkvm-platform",
"ruint",
@ -6702,7 +6713,7 @@ dependencies = [
"hex",
"lazy-regex",
"metal",
"rand 0.9.2",
"rand 0.9.3",
"rayon",
"risc0-circuit-recursion-sys",
"risc0-core",
@ -6746,7 +6757,7 @@ dependencies = [
"num-traits",
"paste",
"postcard",
"rand 0.9.2",
"rand 0.9.3",
"rayon",
"ringbuffer",
"risc0-binfmt",
@ -6853,7 +6864,7 @@ dependencies = [
"ndarray",
"parking_lot",
"paste",
"rand 0.9.2",
"rand 0.9.3",
"rand_core 0.9.5",
"rayon",
"risc0-core",
@ -6891,7 +6902,7 @@ dependencies = [
"num-traits",
"object",
"prost 0.13.5",
"rand 0.9.2",
"rand 0.9.3",
"rayon",
"risc0-binfmt",
"risc0-build",
@ -6979,7 +6990,7 @@ dependencies = [
"futures",
"light-poseidon",
"quote",
"rand 0.9.2",
"rand 0.9.3",
"syn 1.0.109",
"thiserror 2.0.18",
"tiny-keccak",
@ -7030,7 +7041,7 @@ dependencies = [
"borsh",
"proptest",
"rand 0.8.5",
"rand 0.9.2",
"rand 0.9.3",
"ruint-macro",
"serde_core",
"valuable",
@ -8722,7 +8733,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand 0.9.2",
"rand 0.9.3",
"sha1",
"thiserror 2.0.18",
"utf-8",

View File

@ -38,6 +38,7 @@ members = [
"examples/program_deployment/methods/guest",
"bedrock_client",
"testnet_initial_state",
"indexer_ffi",
]
[workspace.dependencies]
@ -57,6 +58,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
indexer_service_rpc = { path = "indexer/service/rpc" }
wallet = { path = "wallet" }
wallet-ffi = { path = "wallet-ffi", default-features = false }
indexer_ffi = { path = "indexer_ffi" }
clock_core = { path = "programs/clock/core" }
token_core = { path = "programs/token/core" }
token_program = { path = "programs/token" }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,7 +3,7 @@ use nssa::AccountId;
use crate::{
HashType,
block::{Block, HashableBlockData},
transaction::NSSATransaction,
transaction::{NSSATransaction, clock_invocation},
};
// Helpers
@ -15,7 +15,7 @@ pub fn sequencer_sign_key_for_testing() -> nssa::PrivateKey {
// Dummy producers
/// Produce dummy block with.
/// Produce dummy block with provided transactions + clock transaction an the end.
///
/// `id` - block id, provide zero for genesis.
///
@ -26,8 +26,12 @@ pub fn sequencer_sign_key_for_testing() -> nssa::PrivateKey {
pub fn produce_dummy_block(
id: u64,
prev_hash: Option<HashType>,
transactions: Vec<NSSATransaction>,
mut transactions: Vec<NSSATransaction>,
) -> Block {
transactions.push(NSSATransaction::Public(clock_invocation(
id.saturating_mul(100),
)));
let block_data = HashableBlockData {
block_id: id,
prev_block_hash: prev_hash.unwrap_or_default(),

View File

@ -93,6 +93,12 @@ Only `Public/2gJJjtG9UivBGEhA1Jz6waZQx1cwfYupC5yvKEweHaeH` is used for completio
exec zsh
```
> **Note:** After updating the completion script, re-run step 1 to copy the new file, then rebuild the cache:
> ```sh
> cp _wallet ~/.oh-my-zsh/custom/plugins/wallet/
> rm -rf ~/.zcompdump* && exec zsh
> ```
### Requirements
The completion script calls `wallet account list` to dynamically fetch account IDs. Ensure the `wallet` command is in your `$PATH`.
@ -197,8 +203,7 @@ wallet account get --account-id <TAB>
2. Rebuild the completion cache:
```sh
rm -f ~/.zcompdump*
exec zsh
rm -rf ~/.zcompdump* && exec zsh
```
### Account IDs not completing

View File

@ -46,7 +46,7 @@ _wallet() {
cword=$COMP_CWORD
}
local commands="auth-transfer chain-info account pinata token amm check-health config restore-keys deploy-program help"
local commands="auth-transfer chain-info account pinata token amm ata check-health config restore-keys deploy-program help"
# Find the main command and subcommand by scanning words before the cursor.
# Global options that take a value are skipped along with their argument.
@ -127,10 +127,10 @@ _wallet() {
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount)
--to-npk | --to-vpk | --to-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --to-identifier --amount" -- "$cur"))
;;
esac
;;
@ -187,11 +187,11 @@ _wallet() {
sync-private)
;; # no options
new)
# `account new` is itself a subcommand: public | private
# `account new` is itself a subcommand: public | private-accounts-key
local new_subcmd=""
for ((i = subcmd_idx + 1; i < cword; i++)); do
case "${words[$i]}" in
public | private)
public | private-accounts-key)
new_subcmd="${words[$i]}"
break
;;
@ -199,13 +199,26 @@ _wallet() {
done
if [[ -z "$new_subcmd" ]]; then
COMPREPLY=($(compgen -W "public private" -- "$cur"))
COMPREPLY=($(compgen -W "public private-accounts-key" -- "$cur"))
else
case "$prev" in
--cci | -l | --label)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci -l --label" -- "$cur"))
case "$new_subcmd" in
public)
case "$prev" in
--cci | -l | --label)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci -l --label" -- "$cur"))
;;
esac
;;
private-accounts-key)
case "$prev" in
--cci)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci" -- "$cur"))
;;
esac
;;
esac
fi
@ -289,10 +302,10 @@ _wallet() {
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount)
--to-npk | --to-vpk | --to-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --to-identifier --amount" -- "$cur"))
;;
esac
;;
@ -331,10 +344,10 @@ _wallet() {
--holder-label)
_wallet_complete_account_label "$cur"
;;
--holder-npk | --holder-vpk | --amount)
--holder-npk | --holder-vpk | --holder-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --holder-identifier --amount" -- "$cur"))
;;
esac
;;
@ -344,7 +357,7 @@ _wallet() {
amm)
case "$subcmd" in
"")
COMPREPLY=($(compgen -W "new swap add-liquidity remove-liquidity help" -- "$cur"))
COMPREPLY=($(compgen -W "new swap-exact-input swap-exact-output add-liquidity remove-liquidity help" -- "$cur"))
;;
new)
case "$prev" in
@ -373,7 +386,7 @@ _wallet() {
;;
esac
;;
swap)
swap-exact-input)
case "$prev" in
--user-holding-a)
_wallet_complete_account_id "$cur"
@ -394,6 +407,15 @@ _wallet() {
;;
esac
;;
swap-exact-output)
case "$prev" in
--user-holding-a | --user-holding-b | --exact-amount-out | --max-amount-in | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --exact-amount-out --max-amount-in --token-definition" -- "$cur"))
;;
esac
;;
add-liquidity)
case "$prev" in
--user-holding-a)
@ -451,6 +473,68 @@ _wallet() {
esac
;;
ata)
case "$subcmd" in
"")
COMPREPLY=($(compgen -W "address create send burn list help" -- "$cur"))
;;
address)
case "$prev" in
--owner | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
create)
case "$prev" in
--owner)
_wallet_complete_account_id "$cur"
;;
--token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
send)
case "$prev" in
--from)
_wallet_complete_account_id "$cur"
;;
--to | --token-definition | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --token-definition --to --amount" -- "$cur"))
;;
esac
;;
burn)
case "$prev" in
--holder)
_wallet_complete_account_id "$cur"
;;
--token-definition | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--holder --token-definition --amount" -- "$cur"))
;;
esac
;;
list)
case "$prev" in
--owner | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
esac
;;
config)
case "$subcmd" in
"")

View File

@ -24,6 +24,7 @@ _wallet() {
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM program interaction subcommand'
'ata:Associated Token Account program interaction subcommand'
'check-health:Check the wallet can connect to the node and builtin local programs match the remote versions'
'config:Command to setup config, get and set config fields'
'restore-keys:Restoring keys from given password at given depth'
@ -52,6 +53,9 @@ _wallet() {
amm)
_wallet_amm
;;
ata)
_wallet_ata
;;
config)
_wallet_config
;;
@ -72,7 +76,7 @@ _wallet() {
# auth-transfer subcommand
_wallet_auth_transfer() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -91,16 +95,17 @@ _wallet_auth_transfer() {
init)
_arguments \
'--account-id[Account ID to initialize]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
'--account-label[Account label (alternative to --account-id)]:label:'
;;
send)
_arguments \
'--from[Source account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--from-label[From account label (alternative to --from)]:label:' \
'--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-label[To account label (alternative to --to)]:label:' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--to-identifier[Identifier for the recipient private account]:identifier:' \
'--amount[Amount of native tokens to send]:amount:'
;;
esac
@ -111,7 +116,7 @@ _wallet_auth_transfer() {
# chain-info subcommand
_wallet_chain_info() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -144,7 +149,7 @@ _wallet_chain_info() {
# account subcommand
_wallet_account() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -169,7 +174,7 @@ _wallet_account() {
'(-r --raw)'{-r,--raw}'[Get raw account data]' \
'(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/vpk for private accounts)]' \
'(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
'--account-label[Account label (alternative to --account-id)]:label:'
;;
list|ls)
_arguments \
@ -181,19 +186,27 @@ _wallet_account() {
'*:: :->new_args'
case $state in
account_type)
compadd public private
compadd public private-accounts-key
;;
new_args)
_arguments \
'--cci[Chain index of a parent node]:chain_index:' \
'(-l --label)'{-l,--label}'[Label to assign to the new account]:label:'
case $line[1] in
public)
_arguments \
'--cci[Chain index of a parent node]:chain_index:' \
'(-l --label)'{-l,--label}'[Label to assign to the new account]:label:'
;;
private-accounts-key)
_arguments \
'--cci[Chain index of a parent node]:chain_index:'
;;
esac
;;
esac
;;
label)
_arguments \
'(-a --account-id)'{-a,--account-id}'[Account ID to label]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' \
'--account-label[Account label (alternative to --account-id)]:label:' \
'(-l --label)'{-l,--label}'[The label to assign to the account]:label:'
;;
esac
@ -204,7 +217,7 @@ _wallet_account() {
# pinata subcommand
_wallet_pinata() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -222,7 +235,7 @@ _wallet_pinata() {
claim)
_arguments \
'--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels'
'--to-label[To account label (alternative to --to)]:label:'
;;
esac
;;
@ -255,36 +268,38 @@ _wallet_token() {
'--name[Token name]:name:' \
'--total-supply[Total supply of tokens to mint]:total_supply:' \
'--definition-account-id[Account ID for token definition]:definition_account:_wallet_account_ids' \
'--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:_wallet_account_labels' \
'--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:' \
'--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' \
'--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:_wallet_account_labels'
'--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:'
;;
send)
_arguments \
'--from[Source holding account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--from-label[From account label (alternative to --from)]:label:' \
'--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-label[To account label (alternative to --to)]:label:' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--to-identifier[Identifier for the recipient private account]:identifier:' \
'--amount[Amount of tokens to send]:amount:'
;;
burn)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--definition-label[Definition account label (alternative to --definition)]:label:' \
'--holder[Holder account ID]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--holder-label[Holder account label (alternative to --holder)]:label:' \
'--amount[Amount of tokens to burn]:amount:'
;;
mint)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--definition-label[Definition account label (alternative to --definition)]:label:' \
'--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--holder-label[Holder account label (alternative to --holder)]:label:' \
'--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \
'--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \
'--holder-identifier[Identifier for the holder private account]:identifier:' \
'--amount[Amount of tokens to mint]:amount:'
;;
esac
@ -295,7 +310,7 @@ _wallet_token() {
# amm subcommand
_wallet_amm() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -304,7 +319,8 @@ _wallet_amm() {
subcommand)
subcommands=(
'new:Create a new liquidity pool'
'swap:Swap tokens using the AMM'
'swap-exact-input:Swap specifying exact input amount'
'swap-exact-output:Swap specifying exact output amount'
'add-liquidity:Add liquidity to an existing pool'
'remove-liquidity:Remove liquidity from a pool'
'help:Print this message or the help of the given subcommand(s)'
@ -316,32 +332,40 @@ _wallet_amm() {
new)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--balance-a[Amount of token A to deposit]:balance_a:' \
'--balance-b[Amount of token B to deposit]:balance_b:'
;;
swap)
swap-exact-input)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--amount-in[Amount of tokens to swap]:amount_in:' \
'--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \
'--token-definition[Definition ID of the token being provided]:token_def:'
;;
swap-exact-output)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:' \
'--user-holding-b[User token B holding account ID]:holding_b:' \
'--exact-amount-out[Exact amount of tokens expected out]:exact_amount_out:' \
'--max-amount-in[Maximum tokens to spend]:max_amount_in:' \
'--token-definition[Definition ID of the token being provided]:token_def:'
;;
add-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--max-amount-a[Maximum amount of token A to deposit]:max_amount_a:' \
'--max-amount-b[Maximum amount of token B to deposit]:max_amount_b:' \
'--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:'
@ -349,11 +373,11 @@ _wallet_amm() {
remove-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--balance-lp[Amount of LP tokens to burn]:balance_lp:' \
'--min-amount-a[Minimum token A to receive]:min_amount_a:' \
'--min-amount-b[Minimum token B to receive]:min_amount_b:'
@ -363,6 +387,61 @@ _wallet_amm() {
esac
}
# ata subcommand
_wallet_ata() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'address:Derive and print the Associated Token Account address (local only)'
'create:Create (or idempotently no-op) the Associated Token Account'
'send:Send tokens from owner ATA to a recipient token holding account'
'burn:Burn tokens from holder ATA'
'list:List all ATAs for a given owner across multiple token definitions'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'ata subcommands' subcommands
;;
args)
case $line[1] in
address)
_arguments \
'--owner[Owner account (no privacy prefix)]:owner:' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:'
;;
create)
_arguments \
'--owner[Owner account with privacy prefix]:owner:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:'
;;
send)
_arguments \
'--from[Sender account with privacy prefix]:from:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:' \
'--to[Recipient account (no privacy prefix)]:to:' \
'--amount[Amount of tokens to send]:amount:'
;;
burn)
_arguments \
'--holder[Holder account with privacy prefix]:holder:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:' \
'--amount[Amount of tokens to burn]:amount:'
;;
list)
_arguments \
'--owner[Owner account (no privacy prefix)]:owner:' \
'--token-definition[Token definition accounts (no privacy prefix)]:token_def:'
;;
esac
;;
esac
}
# config subcommand
_wallet_config() {
local -a subcommands
@ -435,6 +514,7 @@ _wallet_help() {
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM program interaction subcommand'
'ata:Associated Token Account program interaction subcommand'
'check-health:Check the wallet can connect to the node'
'config:Command to setup config, get and set config fields'
'restore-keys:Restoring keys from given password at given depth'
@ -468,25 +548,4 @@ _wallet_account_ids() {
_multi_parts / accounts
}
# Helper function to complete account labels
# Uses `wallet account list` to get available labels
_wallet_account_labels() {
local -a labels
local line
if command -v wallet &>/dev/null; then
while IFS= read -r line; do
local label
# Extract label from [...] at end of line
label="${line##*\[}"
label="${label%\]}"
[[ -n "$label" && "$label" != "$line" ]] && labels+=("$label")
done < <(wallet account list 2>/dev/null)
fi
if (( ${#labels} > 0 )); then
compadd -a labels
fi
}
_wallet "$@"

View File

@ -5,6 +5,7 @@ This tutorial walks through native token transfers between public and private ac
4. Private account creation.
5. Native token transfer from a public account to a private account.
6. Native token transfer from a public account to a private account owned by someone else.
7. Sending to a private accounts key from multiple independent senders.
---
@ -142,7 +143,7 @@ Account owned by authenticated-transfer program
> Private accounts are structurally identical to public accounts, but their values are stored off-chain. On-chain, only a 32-byte commitment is recorded.
> Transactions include encrypted private values so the owner can recover them, and the decryption keys are never shared.
> Private accounts use two keypairs: nullifier keys for privacy-preserving executions and viewing keys for encrypting and decrypting values.
> The private account ID is derived from the nullifier public key.
> The private account ID is derived from the nullifier public key and a numeric identifier: `SHA256(prefix || npk || identifier)`. The same `npk` paired with different identifiers yields different, independent account IDs.
> Private accounts can be initialized by anyone, but once initialized they can only be modified by the owners keys.
> Updates include a new commitment and a nullifier for the old state, which prevents linkage between versions.
@ -158,7 +159,9 @@ With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
```
> [!Tip]
> Focus on the account ID for now. The `npk` and `vpk` values are stored locally and used to build privacy-preserving transactions. The private account ID is derived from `npk`.
> Save this account ID. You will use it in later commands.
### b. Check the account status
Just like public accounts, new private accounts start out uninitialized:
@ -218,21 +221,23 @@ Account owned by authenticated-transfer program
## 6. Native token transfer from a public account to a private account owned by someone else
> [!Important]
> Well simulate transferring to someone else by creating a new private account we own and treating it as if it belonged to another user.
> Well simulate transferring to someone else by creating a new private accounts key and treating it as if it belonged to another user. When the recipient is someone else, you only have their `npk` and `vpk` — not an account ID.
### a. Create a new uninitialized private account
### a. Create a new private accounts key to simulate a foreign recipient
```bash
wallet account new private
wallet account new private-accounts-key
# Output:
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
Generated new private accounts key at path /1
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
```
> [!Tip]
> Ignore the private account ID here and use the `npk` and `vpk` values to send to a foreign private account.
> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account.
### b. Send 3 tokens using the recipients npk and vpk
```bash
wallet auth-transfer send \
@ -242,9 +247,74 @@ wallet auth-transfer send \
--amount 3
```
> [!Note]
> `--to-identifier` is omitted here. When omitted, the wallet picks a random identifier, which is usually fine. Use the flag explicitly when a specific identifier is required.
> [!Warning]
> This command creates a privacy-preserving transaction, which may take a few minutes. The updated values are encrypted and included in the transaction.
> Once accepted, the recipient must run `wallet account sync-private` to scan the chain for their encrypted updates and refresh local state.
> [!Note]
> You have seen transfers between two public accounts and from a public sender to a private recipient. Transfers from a private sender, whether to a public account or to another private account, follow the same pattern.
## 7. Sending to a private accounts key from multiple independent senders
> [!Important]
> A private accounts key (`npk` + `vpk`) can be shared with multiple senders. Each sender independently chooses an identifier; the recipient's account ID is derived from `(npk, identifier)`. Two senders using different identifiers produce two separate private accounts under the same key.
### a. Alice creates a private accounts key
```bash
wallet account new private-accounts-key
# Output:
Generated new private accounts key at path /2
With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345
With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c
```
Alice shares the `npk` and `vpk` values with Bob and Charlie out of band.
### b. Bob sends 10 tokens to Alice using identifier 1
```bash
wallet auth-transfer send \
--from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
--to-identifier 1 \
--amount 10
```
### c. Charlie sends 5 tokens to Alice using identifier 2
```bash
wallet auth-transfer send \
--from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
--to-identifier 2 \
--amount 5
```
> [!Note]
> Bob and Charlie each chose a different identifier. They do not need to coordinate — any two distinct values work.
### d. Alice syncs to discover the new accounts
```bash
wallet account sync-private
```
```bash
wallet account list
# Output (private account entries under key /2):
/2 Private/AliceBobAcctXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/2 Private/AliceCharlieAcctXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Alice now has two separate private accounts, one funded by Bob and one by Charlie, both controlled by the same key at path `/2`.
> [!Tip]
> Alice can check each account balance with `wallet account get --account-id Private/...`. Neither balance is visible on-chain.

24
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
"lastModified": 1769737823,
"narHash": "sha256-DrBaNpZ+sJ4stXm+0nBX7zqZT9t9P22zbk6m5YhQxS4=",
"lastModified": 1776396856,
"narHash": "sha256-aRJpIJUlZLaf06ekPvqjuU46zvO9K90IxJGpbqodkPs=",
"owner": "ipetkov",
"repo": "crane",
"rev": "b2f45c3830aa96b7456a4c4bc327d04d7a43e1ba",
"rev": "28462d6d55c33206ffa5a56c7907ca3125ed788f",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1770979891,
"narHash": "sha256-cvkVnE7btuFLzv70ORAZve9K1Huiplq0iECgXSXb0ZY=",
"lastModified": 1775835011,
"narHash": "sha256-SQDLyyRUa5J9QHjNiHbeZw4rQOZnTEo61TcaUpjtLBs=",
"owner": "logos-blockchain",
"repo": "logos-blockchain-circuits",
"rev": "ec7d298e5a3a0507bb8570df86cdf78dc452d024",
"rev": "d6cf41f66500d4afc157b4f43de0f0d5bfa01443",
"type": "github"
},
"original": {
@ -51,11 +51,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770019141,
"narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=",
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
@ -80,11 +80,11 @@
]
},
"locked": {
"lastModified": 1770088046,
"narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=",
"lastModified": 1776395632,
"narHash": "sha256-Mi1uF5f2FsdBIvy+v7MtsqxD3Xjhd0ARJdwoqqqPtJo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "71f9daa4e05e49c434d08627e755495ae222bc34",
"rev": "8087ff1f47fff983a1fba70fa88b759f2fd8ae97",
"type": "github"
},
"original": {

View File

@ -130,9 +130,26 @@
'';
}
);
indexerFfiPackage = craneLib.buildPackage (
commonArgs
// {
pname = "logos-execution-zone-indexer-ffi";
version = "0.1.0";
cargoExtraArgs = "-p indexer_ffi";
postInstall = ''
mkdir -p $out/include
cp indexer_ffi/indexer_ffi.h $out/include/
''
+ pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
install_name_tool -id @rpath/libindexer_ffi.dylib $out/lib/libindexer_ffi.dylib
'';
}
);
in
{
wallet = walletFfiPackage;
indexer = indexerFfiPackage;
default = walletFfiPackage;
}
);
@ -144,9 +161,14 @@
walletFfiShell = pkgs.mkShell {
inputsFrom = [ walletFfiPackage ];
};
indexerFfiPackage = self.packages.${system}.indexer;
indexerFfiShell = pkgs.mkShell {
inputsFrom = [ indexerFfiPackage ];
};
in
{
wallet = walletFfiShell;
indexer = indexerFfiShell;
default = walletFfiShell;
}
);

View File

@ -243,14 +243,9 @@ mod tests {
&sign_key,
);
let block_id = u64::try_from(i).unwrap();
let block_timestamp = block_id.saturating_mul(100);
let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp));
let next_block = common::test_utils::produce_dummy_block(
block_id,
Some(prev_hash),
vec![tx, clock_tx],
);
let next_block =
common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]);
prev_hash = next_block.header.hash;
storage

View File

@ -63,6 +63,7 @@ impl IndexerCore {
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
@ -70,8 +71,8 @@ impl IndexerCore {
nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect()
@ -143,23 +144,27 @@ impl IndexerCore {
l2_blocks_parsed_ids.sort_unstable();
info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids);
for l2_block in l2_block_vec {
// TODO: proper fix is to make the sequencer's genesis include a
// trailing `clock_invocation(0)` (and have the indexer's
// `open_db_with_genesis` not pre-apply state transitions) so the
// inscribed genesis can flow through `put_block` like any other
// block. For now we skip re-applying it.
//
// The channel-start (block_id == 1) is the sequencer's genesis
// inscription that we re-discover during initial search. The
// indexer already has its own locally-constructed genesis in
// the store from `open_db_with_genesis`, so re-applying the
// inscribed copy is both redundant and would fail the strict
// block validation in `put_block` (the inscribed genesis lacks
// the trailing clock invocation).
if l2_block.header.block_id != 1 {
self.store.put_block(l2_block.clone(), l1_header).await?;
}
for l2_block in l2_block_vec {
// TODO: proper fix is to make the sequencer's genesis include a
// trailing `clock_invocation(0)` (and have the indexer's
// `open_db_with_genesis` not pre-apply state transitions) so the
// inscribed genesis can flow through `put_block` like any other
// block. For now we skip re-applying it.
//
// The channel-start (block_id == 1) is the sequencer's genesis
// inscription that we re-discover during initial search. The
// indexer already has its own locally-constructed genesis in
// the store from `open_db_with_genesis`, so re-applying the
// inscribed copy is both redundant and would fail the strict
// block validation in `put_block` (the inscribed genesis lacks
// the trailing clock invocation).
if l2_block.header.block_id != 1 {
self
.store
.put_block(l2_block.clone(), l1_header)
.await
.inspect_err(|err| error!("Failed to put block with err {err:?}"))?;
}
yield Ok(l2_block);
}

View File

@ -6,7 +6,7 @@
clippy::integer_division_remainder_used,
reason = "Mock service uses intentional casts and format patterns for test data generation"
)]
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc, time::Duration};
use indexer_service_protocol::{
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
@ -19,15 +19,73 @@ use jsonrpsee::{
core::{SubscriptionResult, async_trait},
types::ErrorObjectOwned,
};
use tokio::sync::{RwLock, broadcast};
/// A mock implementation of the `IndexerService` RPC for testing purposes.
pub struct MockIndexerService {
const MOCK_GENESIS_TIMESTAMP_MS: u64 = 1_704_067_200_000;
const MOCK_BLOCK_INTERVAL_MS: u64 = 30_000;
struct MockState {
blocks: Vec<Block>,
accounts: HashMap<AccountId, Account>,
account_ids: Vec<AccountId>,
transactions: HashMap<HashType, (Transaction, BlockId)>,
}
/// A mock implementation of the `IndexerService` RPC for testing purposes.
pub struct MockIndexerService {
state: Arc<RwLock<MockState>>,
finalized_blocks_tx: broadcast::Sender<Block>,
}
impl MockIndexerService {
fn spawn_block_generation_task(
state: Arc<RwLock<MockState>>,
finalized_blocks_tx: broadcast::Sender<Block>,
) {
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(30)).await;
let new_block = {
let mut state = state.write().await;
let next_block_id = state
.blocks
.last()
.map_or(1, |block| block.header.block_id.saturating_add(1));
let prev_hash = state
.blocks
.last()
.map_or(HashType([0_u8; 32]), |block| block.header.hash);
let timestamp = state.blocks.last().map_or(
MOCK_GENESIS_TIMESTAMP_MS + MOCK_BLOCK_INTERVAL_MS,
|block| {
block
.header
.timestamp
.saturating_add(MOCK_BLOCK_INTERVAL_MS)
},
);
let block = build_mock_block(
next_block_id,
prev_hash,
timestamp,
&state.account_ids,
BedrockStatus::Finalized,
);
index_block_transactions(&mut state.transactions, &block);
state.blocks.push(block.clone());
block
};
let _res = finalized_blocks_tx.send(new_block);
}
});
}
#[must_use]
pub fn new_with_mock_blocks() -> Self {
let mut blocks = Vec::new();
@ -59,119 +117,38 @@ impl MockIndexerService {
let mut prev_hash = HashType([0_u8; 32]);
for block_id in 1..=100 {
let block_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = 0xff;
HashType(hash)
};
// Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and
// ProgramDeployment)
let num_txs = 2 + (block_id % 3);
let mut block_transactions = Vec::new();
for tx_idx in 0..num_txs {
let tx_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = tx_idx as u8;
HashType(hash)
};
// Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment
let tx = match (block_id + tx_idx) % 5 {
// Public transactions (most common)
0 | 1 => Transaction::Public(PublicTransaction {
hash: tx_hash,
message: PublicMessage {
program_id: ProgramId([1_u32; 8]),
account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
account_ids[(tx_idx as usize + 1) % account_ids.len()],
],
nonces: vec![block_id as u128, (block_id + 1) as u128],
instruction_data: vec![1, 2, 3, 4],
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: None,
},
}),
// PrivacyPreserving transactions
2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
hash: tx_hash,
message: PrivacyPreservingMessage {
public_account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
}],
encrypted_private_post_states: vec![EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![
0x01, 0x02, 0x03, 0x04,
]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
}],
new_commitments: vec![Commitment([block_id as u8; 32])],
new_nullifiers: vec![(
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: Some(indexer_service_protocol::Proof(vec![0; 32])),
},
}),
// ProgramDeployment transactions (rare)
_ => Transaction::ProgramDeployment(ProgramDeploymentTransaction {
hash: tx_hash,
message: ProgramDeploymentMessage {
bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic number */
},
}),
};
transactions.insert(tx_hash, (tx.clone(), block_id));
block_transactions.push(tx);
}
let block = Block {
header: BlockHeader {
block_id,
prev_block_hash: prev_hash,
hash: block_hash,
timestamp: 1_704_067_200_000 + (block_id * 12_000), // ~12 seconds per block
signature: Signature([0_u8; 64]),
},
body: BlockBody {
transactions: block_transactions,
},
bedrock_status: match block_id {
let block = build_mock_block(
block_id,
prev_hash,
MOCK_GENESIS_TIMESTAMP_MS + (block_id * MOCK_BLOCK_INTERVAL_MS),
&account_ids,
match block_id {
0..=5 => BedrockStatus::Finalized,
6..=8 => BedrockStatus::Safe,
_ => BedrockStatus::Pending,
},
bedrock_parent_id: MantleMsgId([0; 32]),
};
);
prev_hash = block_hash;
index_block_transactions(&mut transactions, &block);
prev_hash = block.header.hash;
blocks.push(block);
}
Self {
let state = Arc::new(RwLock::new(MockState {
blocks,
accounts,
account_ids,
transactions,
}));
let (finalized_blocks_tx, _) = broadcast::channel(32);
Self::spawn_block_generation_task(Arc::clone(&state), finalized_blocks_tx.clone());
Self {
state,
finalized_blocks_tx,
}
}
}
@ -183,21 +160,45 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
subscription_sink: jsonrpsee::PendingSubscriptionSink,
) -> SubscriptionResult {
let sink = subscription_sink.accept().await?;
for block in self
.blocks
.iter()
.filter(|b| b.bedrock_status == BedrockStatus::Finalized)
{
let initial_finalized_blocks: Vec<Block> = {
let state = self.state.read().await;
state
.blocks
.iter()
.filter(|b| b.bedrock_status == BedrockStatus::Finalized)
.cloned()
.collect()
};
for block in &initial_finalized_blocks {
let json = serde_json::value::to_raw_value(block).unwrap();
sink.send(json).await?;
}
let mut receiver = self.finalized_blocks_tx.subscribe();
loop {
match receiver.recv().await {
Ok(block) => {
let json = serde_json::value::to_raw_value(&block).unwrap();
sink.send(json).await?;
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => break,
}
}
Ok(())
}
async fn get_last_finalized_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
self.blocks
.last()
.map(|bl| bl.header.block_id)
self.state
.read()
.await
.blocks
.iter()
.rev()
.find(|block| block.bedrock_status == BedrockStatus::Finalized)
.map(|block| block.header.block_id)
.ok_or_else(|| {
ErrorObjectOwned::owned(-32001, "Last block not found".to_owned(), None::<()>)
})
@ -205,6 +206,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
async fn get_block_by_id(&self, block_id: BlockId) -> Result<Option<Block>, ErrorObjectOwned> {
Ok(self
.state
.read()
.await
.blocks
.iter()
.find(|b| b.header.block_id == block_id)
@ -216,6 +220,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
block_hash: HashType,
) -> Result<Option<Block>, ErrorObjectOwned> {
Ok(self
.state
.read()
.await
.blocks
.iter()
.find(|b| b.header.hash == block_hash)
@ -223,7 +230,10 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
}
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> {
self.accounts
self.state
.read()
.await
.accounts
.get(&account_id)
.cloned()
.ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>))
@ -233,7 +243,13 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
&self,
tx_hash: HashType,
) -> Result<Option<Transaction>, ErrorObjectOwned> {
Ok(self.transactions.get(&tx_hash).map(|(tx, _)| tx.clone()))
Ok(self
.state
.read()
.await
.transactions
.get(&tx_hash)
.map(|(tx, _)| tx.clone()))
}
async fn get_blocks(
@ -241,15 +257,17 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
before: Option<BlockId>,
limit: u64,
) -> Result<Vec<Block>, ErrorObjectOwned> {
let state = self.state.read().await;
let start_id = before.map_or_else(
|| self.blocks.len(),
|| state.blocks.len(),
|id| usize::try_from(id.saturating_sub(1)).expect("u64 should fit in usize"),
);
let result = (1..=start_id)
.rev()
.take(limit as usize)
.map_while(|block_id| self.blocks.get(block_id - 1).cloned())
.map_while(|block_id| state.blocks.get(block_id - 1).cloned())
.collect();
Ok(result)
@ -261,20 +279,24 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
offset: u64,
limit: u64,
) -> Result<Vec<Transaction>, ErrorObjectOwned> {
let mut account_txs: Vec<_> = self
.transactions
.values()
.filter(|(tx, _)| match tx {
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
Transaction::PrivacyPreserving(priv_tx) => {
priv_tx.message.public_account_ids.contains(&account_id)
}
Transaction::ProgramDeployment(_) => false,
})
.collect();
let mut account_txs: Vec<(Transaction, BlockId)> = {
let state = self.state.read().await;
state
.transactions
.values()
.filter(|(tx, _)| match tx {
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
Transaction::PrivacyPreserving(priv_tx) => {
priv_tx.message.public_account_ids.contains(&account_id)
}
Transaction::ProgramDeployment(_) => false,
})
.cloned()
.collect()
};
// Sort by block ID descending (most recent first)
account_txs.sort_by_key(|b| std::cmp::Reverse(b.1));
account_txs.sort_by_key(|(_, block_id)| std::cmp::Reverse(*block_id));
let start = offset as usize;
if start >= account_txs.len() {
@ -293,3 +315,123 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
Ok(())
}
}
fn build_mock_block(
block_id: BlockId,
prev_hash: HashType,
timestamp: u64,
account_ids: &[AccountId],
bedrock_status: BedrockStatus,
) -> Block {
let block_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = 0xff;
HashType(hash)
};
// Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment)
let num_txs = 2 + (block_id % 3);
let mut block_transactions = Vec::new();
for tx_idx in 0..num_txs {
let tx_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = tx_idx as u8;
HashType(hash)
};
// Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment
let tx = match (block_id + tx_idx) % 5 {
// Public transactions (most common)
0 | 1 => Transaction::Public(PublicTransaction {
hash: tx_hash,
message: PublicMessage {
program_id: ProgramId([1_u32; 8]),
account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
account_ids[(tx_idx as usize + 1) % account_ids.len()],
],
nonces: vec![block_id as u128, (block_id + 1) as u128],
instruction_data: vec![1, 2, 3, 4],
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: None,
},
}),
// PrivacyPreserving transactions
2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
hash: tx_hash,
message: PrivacyPreservingMessage {
public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
}],
encrypted_private_post_states: vec![EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![
0x01, 0x02, 0x03, 0x04,
]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
}],
new_commitments: vec![Commitment([block_id as u8; 32])],
new_nullifiers: vec![(
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: Some(indexer_service_protocol::Proof(vec![0; 32])),
},
}),
// ProgramDeployment transactions (rare)
_ => Transaction::ProgramDeployment(ProgramDeploymentTransaction {
hash: tx_hash,
message: ProgramDeploymentMessage {
bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic
* number */
},
}),
};
block_transactions.push(tx);
}
Block {
header: BlockHeader {
block_id,
prev_block_hash: prev_hash,
hash: block_hash,
timestamp,
signature: Signature([0_u8; 64]),
},
body: BlockBody {
transactions: block_transactions,
},
bedrock_status,
bedrock_parent_id: MantleMsgId([0; 32]),
}
}
fn index_block_transactions(
transactions: &mut HashMap<HashType, (Transaction, BlockId)>,
block: &Block,
) {
for tx in &block.body.transactions {
let tx_hash = match tx {
Transaction::Public(public_tx) => public_tx.hash,
Transaction::PrivacyPreserving(private_tx) => private_tx.hash,
Transaction::ProgramDeployment(deployment_tx) => deployment_tx.hash,
};
transactions.insert(tx_hash, (tx.clone(), block.header.block_id));
}
}

25
indexer_ffi/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
edition = "2024"
license = { workspace = true }
name = "indexer_ffi"
version = "0.1.0"
[dependencies]
indexer_service.workspace = true
log = { workspace = true }
tokio = { features = ["rt-multi-thread"], workspace = true }
[build-dependencies]
cbindgen = "0.29"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
name = "indexer_ffi"
[lints]
workspace = true
[package.metadata.cargo-machete]
ignored = [
"cbindgen",
] # machete does not recognize this for build dep and complains.

12
indexer_ffi/build.rs Normal file
View File

@ -0,0 +1,12 @@
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
println!("cargo:rerun-if-changed=src/");
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate bindings")
.write_to_file("indexer_ffi.h");
}

View File

@ -0,0 +1,2 @@
language = "C" # For increased compatibility
no_includes = true

76
indexer_ffi/indexer_ffi.h Normal file
View File

@ -0,0 +1,76 @@
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef enum OperationStatus {
Ok = 0,
NullPointer = 1,
InitializationError = 2,
} OperationStatus;
typedef struct IndexerServiceFFI {
void *indexer_handle;
void *runtime;
} IndexerServiceFFI;
/**
* Simple wrapper around a pointer to a value or an error.
*
* Pointer is not guaranteed. You should check the error field before
* dereferencing the pointer.
*/
typedef struct PointerResult_IndexerServiceFFI__OperationStatus {
struct IndexerServiceFFI *value;
enum OperationStatus error;
} PointerResult_IndexerServiceFFI__OperationStatus;
typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult;
/**
* Creates and starts an indexer based on the provided
* configuration file path.
*
* # Arguments
*
* - `config_path`: A pointer to a string representing the path to the configuration file.
* - `port`: Number representing a port, on which indexers RPC will start.
*
* # Returns
*
* An `InitializedIndexerServiceFFIResult` containing either a pointer to the
* initialized `IndexerServiceFFI` or an error code.
*/
InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port);
/**
* Stops and frees the resources associated with the given indexer service.
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped.
*
* # Returns
*
* An `OperationStatus` indicating success or failure.
*
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - The `IndexerServiceFFI` instance was created by this library
* - The pointer will not be used after this function returns
*/
enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer);
/**
* # Safety
* It's up to the caller to pass a proper pointer, if somehow from c/c++ side
* this is called with a type which doesn't come from a returned `CString` it
* will cause a segfault.
*/
void free_cstring(char *block);
bool is_ok(const enum OperationStatus *self);
bool is_error(const enum OperationStatus *self);

View File

@ -0,0 +1,100 @@
use std::{ffi::c_char, path::PathBuf};
use tokio::runtime::Runtime;
use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus};
pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, OperationStatus>;
/// Creates and starts an indexer based on the provided
/// configuration file path.
///
/// # Arguments
///
/// - `config_path`: A pointer to a string representing the path to the configuration file.
/// - `port`: Number representing a port, on which indexers RPC will start.
///
/// # Returns
///
/// An `InitializedIndexerServiceFFIResult` containing either a pointer to the
/// initialized `IndexerServiceFFI` or an error code.
#[unsafe(no_mangle)]
pub extern "C" fn start_indexer(
config_path: *const c_char,
port: u16,
) -> InitializedIndexerServiceFFIResult {
setup_indexer(config_path, port).map_or_else(
InitializedIndexerServiceFFIResult::from_error,
InitializedIndexerServiceFFIResult::from_value,
)
}
/// Initializes and starts an indexer based on the provided
/// configuration file path.
///
/// # Arguments
///
/// - `config_path`: A pointer to a string representing the path to the configuration file.
/// - `port`: Number representing a port, on which indexers RPC will start.
///
/// # Returns
///
/// A `Result` containing either the initialized `IndexerServiceFFI` or an
/// error code.
fn setup_indexer(
config_path: *const c_char,
port: u16,
) -> Result<IndexerServiceFFI, OperationStatus> {
let user_config_path = PathBuf::from(
unsafe { std::ffi::CStr::from_ptr(config_path) }
.to_str()
.map_err(|e| {
log::error!("Could not convert the config path to string: {e}");
OperationStatus::InitializationError
})?,
);
let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| {
log::error!("Failed to read config: {e}");
OperationStatus::InitializationError
})?;
let rt = Runtime::new().unwrap();
let indexer_handle = rt
.block_on(indexer_service::run_server(config, port))
.map_err(|e| {
log::error!("Could not start indexer service: {e}");
OperationStatus::InitializationError
})?;
Ok(IndexerServiceFFI::new(indexer_handle, rt))
}
/// Stops and frees the resources associated with the given indexer service.
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped.
///
/// # Returns
///
/// An `OperationStatus` indicating success or failure.
///
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - The `IndexerServiceFFI` instance was created by this library
/// - The pointer will not be used after this function returns
#[unsafe(no_mangle)]
pub unsafe extern "C" fn stop_indexer(indexer: *mut IndexerServiceFFI) -> OperationStatus {
if indexer.is_null() {
log::error!("Attempted to stop a null indexer pointer. This is a bug. Aborting.");
return OperationStatus::NullPointer;
}
let indexer = unsafe { Box::from_raw(indexer) };
drop(indexer);
OperationStatus::Ok
}

View File

@ -0,0 +1,14 @@
use std::ffi::{CString, c_char};
/// # Safety
/// It's up to the caller to pass a proper pointer, if somehow from c/c++ side
/// this is called with a type which doesn't come from a returned `CString` it
/// will cause a segfault.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_cstring(block: *mut c_char) {
if block.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
drop(unsafe { CString::from_raw(block) });
}

View File

@ -0,0 +1,5 @@
pub use result::PointerResult;
pub mod lifecycle;
pub mod memory;
pub mod result;

View File

@ -0,0 +1,29 @@
/// Simple wrapper around a pointer to a value or an error.
///
/// Pointer is not guaranteed. You should check the error field before
/// dereferencing the pointer.
#[repr(C)]
pub struct PointerResult<Type, Error> {
pub value: *mut Type,
pub error: Error,
}
impl<Type, Error: Default> PointerResult<Type, Error> {
pub fn from_pointer(pointer: *mut Type) -> Self {
Self {
value: pointer,
error: Error::default(),
}
}
pub fn from_value(value: Type) -> Self {
Self::from_pointer(Box::into_raw(Box::new(value)))
}
pub const fn from_error(error: Error) -> Self {
Self {
value: std::ptr::null_mut(),
error,
}
}
}

22
indexer_ffi/src/errors.rs Normal file
View File

@ -0,0 +1,22 @@
#[derive(Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub enum OperationStatus {
#[default]
Ok = 0x0,
NullPointer = 0x1,
InitializationError = 0x2,
}
impl OperationStatus {
#[must_use]
#[unsafe(no_mangle)]
pub extern "C" fn is_ok(&self) -> bool {
*self == Self::Ok
}
#[must_use]
#[unsafe(no_mangle)]
pub extern "C" fn is_error(&self) -> bool {
!self.is_ok()
}
}

View File

@ -0,0 +1,86 @@
use std::{ffi::c_void, net::SocketAddr};
use indexer_service::IndexerHandle;
use tokio::runtime::Runtime;
#[repr(C)]
pub struct IndexerServiceFFI {
indexer_handle: *mut c_void,
runtime: *mut c_void,
}
impl IndexerServiceFFI {
pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self {
Self {
// Box the complex types and convert to opaque pointers
indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::<c_void>(),
runtime: Box::into_raw(Box::new(runtime)).cast::<c_void>(),
}
}
/// Helper to take ownership back.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub unsafe fn into_parts(self) -> (Box<IndexerHandle>, Box<Runtime>) {
let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::<IndexerHandle>()) };
let runtime = unsafe { Box::from_raw(self.runtime.cast::<Runtime>()) };
(indexer_handle, runtime)
}
/// Helper to get indexer handle addr.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn addr(&self) -> SocketAddr {
let indexer_handle = unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
.as_ref()
.expect("Indexer Handle must be non-null pointer")
};
indexer_handle.addr()
}
/// Helper to get indexer handle addr.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn handle(&self) -> &IndexerHandle {
unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
.as_ref()
.expect("Indexer Handle must be non-null pointer")
}
}
}
// Implement Drop to prevent memory leaks
impl Drop for IndexerServiceFFI {
fn drop(&mut self) {
let Self {
indexer_handle,
runtime,
} = self;
if indexer_handle.is_null() {
log::error!("Attempted to drop a null indexer pointer. This is a bug");
}
if runtime.is_null() {
log::error!("Attempted to drop a null tokio runtime pointer. This is a bug");
}
drop(unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) });
drop(unsafe { Box::from_raw(runtime.cast::<Runtime>()) });
}
}

8
indexer_ffi/src/lib.rs Normal file
View File

@ -0,0 +1,8 @@
#![allow(clippy::undocumented_unsafe_blocks, reason = "It is an FFI")]
pub use errors::OperationStatus;
pub use indexer::IndexerServiceFFI;
pub mod api;
mod errors;
mod indexer;

View File

@ -22,6 +22,7 @@ ata_core.workspace = true
indexer_service_rpc.workspace = true
sequencer_service_rpc = { workspace = true, features = ["client"] }
wallet-ffi.workspace = true
indexer_ffi.workspace = true
testnet_initial_state.workspace = true
url.workspace = true

View File

@ -60,11 +60,11 @@ impl InitialData {
let mut private_charlie_key_chain = KeyChain::new_os_random();
let mut private_charlie_account_id =
AccountId::from(&private_charlie_key_chain.nullifier_public_key);
AccountId::from((&private_charlie_key_chain.nullifier_public_key, 0));
let mut private_david_key_chain = KeyChain::new_os_random();
let mut private_david_account_id =
AccountId::from(&private_david_key_chain.nullifier_public_key);
AccountId::from((&private_david_key_chain.nullifier_public_key, 0));
// Ensure consistent ordering
if private_charlie_account_id > private_david_account_id {
@ -139,11 +139,10 @@ impl InitialData {
})
})
.chain(self.private_accounts.iter().map(|(key_chain, account)| {
let account_id = AccountId::from(&key_chain.nullifier_public_key);
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account_id,
account: account.clone(),
key_chain: key_chain.clone(),
identifier: 0,
}))
}))
.collect()

View File

@ -1,12 +1,12 @@
//! This library contains common code for integration tests.
use std::{net::SocketAddr, path::PathBuf, sync::LazyLock};
use std::sync::LazyLock;
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result};
use common::{HashType, transaction::NSSATransaction};
use futures::FutureExt as _;
use indexer_service::IndexerHandle;
use log::{debug, error, warn};
use log::{debug, error};
use nssa::{AccountId, PrivacyPreservingTransaction};
use nssa_core::Commitment;
use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _};
@ -14,9 +14,13 @@ use sequencer_service::SequencerHandle;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::{WalletCore, config::WalletConfigOverrides};
use wallet::WalletCore;
use crate::setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet};
pub mod config;
pub mod setup;
pub mod test_context_ffi;
// TODO: Remove this and control time from tests
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
@ -67,13 +71,13 @@ impl TestContext {
debug!("Test context setup");
let (bedrock_compose, bedrock_addr) = Self::setup_bedrock_node().await?;
let (bedrock_compose, bedrock_addr) = setup_bedrock_node().await?;
let (indexer_handle, temp_indexer_dir) = Self::setup_indexer(bedrock_addr, &initial_data)
let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr, &initial_data)
.await
.context("Failed to setup Indexer")?;
let (sequencer_handle, temp_sequencer_dir) = Self::setup_sequencer(
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
sequencer_partial_config,
bedrock_addr,
indexer_handle.addr(),
@ -83,7 +87,7 @@ impl TestContext {
.context("Failed to setup Sequencer")?;
let (wallet, temp_wallet_dir, wallet_password) =
Self::setup_wallet(sequencer_handle.addr(), &initial_data)
setup_wallet(sequencer_handle.addr(), &initial_data)
.await
.context("Failed to setup wallet")?;
@ -112,165 +116,6 @@ impl TestContext {
})
}
async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let bedrock_compose_path =
PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml");
let mut compose = DockerCompose::with_auto_client(&[bedrock_compose_path])
.await
.context("Failed to setup docker compose for Bedrock")?
// Setting port to 0 to avoid conflicts between parallel tests, actual port will be retrieved after container is up
.with_env("PORT", "0");
#[expect(
clippy::items_after_statements,
reason = "This is more readable is this function used just after its definition"
)]
async fn up_and_retrieve_port(compose: &mut DockerCompose) -> Result<u16> {
compose
.up()
.await
.context("Failed to bring up Bedrock services")?;
let container = compose
.service(BEDROCK_SERVICE_WITH_OPEN_PORT)
.with_context(|| {
format!(
"Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`"
)
})?;
let ports = container.ports().await.with_context(|| {
format!(
"Failed to get ports for Bedrock service container `{}`",
container.id()
)
})?;
ports
.map_to_host_port_ipv4(BEDROCK_SERVICE_PORT)
.with_context(|| {
format!(
"Failed to retrieve host port of {BEDROCK_SERVICE_PORT} container \
port for container `{}`, existing ports: {ports:?}",
container.id()
)
})
}
let mut port = None;
let mut attempt = 0_u32;
let max_attempts = 5_u32;
while port.is_none() && attempt < max_attempts {
attempt = attempt
.checked_add(1)
.expect("We check that attempt < max_attempts, so this won't overflow");
match up_and_retrieve_port(&mut compose).await {
Ok(p) => {
port = Some(p);
}
Err(err) => {
warn!(
"Failed to bring up Bedrock services: {err:?}, attempt {attempt}/{max_attempts}"
);
}
}
}
let Some(port) = port else {
bail!("Failed to bring up Bedrock services after {max_attempts} attempts");
};
let addr = SocketAddr::from(([127, 0, 0, 1], port));
Ok((compose, addr))
}
async fn setup_indexer(
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(IndexerHandle, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
debug!(
"Using temp indexer home at {}",
temp_indexer_dir.path().display()
);
let indexer_config = config::indexer_config(
bedrock_addr,
temp_indexer_dir.path().to_owned(),
initial_data,
)
.context("Failed to create Indexer config")?;
indexer_service::run_server(indexer_config, 0)
.await
.context("Failed to run Indexer Service")
.map(|handle| (handle, temp_indexer_dir))
}
async fn setup_sequencer(
partial: config::SequencerPartialConfig,
bedrock_addr: SocketAddr,
indexer_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(SequencerHandle, TempDir)> {
let temp_sequencer_dir =
tempfile::tempdir().context("Failed to create temp dir for sequencer home")?;
debug!(
"Using temp sequencer home at {}",
temp_sequencer_dir.path().display()
);
let config = config::sequencer_config(
partial,
temp_sequencer_dir.path().to_owned(),
bedrock_addr,
indexer_addr,
initial_data,
)
.context("Failed to create Sequencer config")?;
let sequencer_handle = sequencer_service::run(config, 0).await?;
Ok((sequencer_handle, temp_sequencer_dir))
}
async fn setup_wallet(
sequencer_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(WalletCore, TempDir, String)> {
let config = config::wallet_config(sequencer_addr, initial_data)
.context("Failed to create Wallet config")?;
let config_serialized =
serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?;
let temp_wallet_dir =
tempfile::tempdir().context("Failed to create temp dir for wallet home")?;
let config_path = temp_wallet_dir.path().join("wallet_config.json");
std::fs::write(&config_path, config_serialized)
.context("Failed to write wallet config in temp dir")?;
let storage_path = temp_wallet_dir.path().join("storage.json");
let config_overrides = WalletConfigOverrides::default();
let wallet_password = "test_pass".to_owned();
let (wallet, _mnemonic) = WalletCore::new_init_storage(
config_path,
storage_path,
Some(config_overrides),
&wallet_password,
)
.context("Failed to init wallet")?;
wallet
.store_persistent_data()
.await
.context("Failed to store wallet persistent data")?;
Ok((wallet, temp_wallet_dir, wallet_password))
}
/// Get reference to the wallet.
#[must_use]
pub const fn wallet(&self) -> &WalletCore {

View File

@ -0,0 +1,220 @@
use std::{
ffi::{CString, c_char},
fs::File,
io::Write as _,
net::SocketAddr,
path::PathBuf,
};
use anyhow::{Context as _, Result, bail};
use indexer_ffi::{IndexerServiceFFI, api::lifecycle::InitializedIndexerServiceFFIResult};
use indexer_service::IndexerHandle;
use log::{debug, warn};
use sequencer_service::SequencerHandle;
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::{WalletCore, config::WalletConfigOverrides};
use crate::{BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, config};
unsafe extern "C" {
fn start_indexer(config_path: *const c_char, port: u16) -> InitializedIndexerServiceFFIResult;
}
pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let bedrock_compose_path = PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml");
let mut compose = DockerCompose::with_auto_client(&[bedrock_compose_path])
.await
.context("Failed to setup docker compose for Bedrock")?
// Setting port to 0 to avoid conflicts between parallel tests, actual port will be retrieved after container is up
.with_env("PORT", "0");
#[expect(
clippy::items_after_statements,
reason = "This is more readable is this function used just after its definition"
)]
async fn up_and_retrieve_port(compose: &mut DockerCompose) -> Result<u16> {
compose
.up()
.await
.context("Failed to bring up Bedrock services")?;
let container = compose
.service(BEDROCK_SERVICE_WITH_OPEN_PORT)
.with_context(|| {
format!(
"Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`"
)
})?;
let ports = container.ports().await.with_context(|| {
format!(
"Failed to get ports for Bedrock service container `{}`",
container.id()
)
})?;
ports
.map_to_host_port_ipv4(BEDROCK_SERVICE_PORT)
.with_context(|| {
format!(
"Failed to retrieve host port of {BEDROCK_SERVICE_PORT} container \
port for container `{}`, existing ports: {ports:?}",
container.id()
)
})
}
let mut port = None;
let mut attempt = 0_u32;
let max_attempts = 5_u32;
while port.is_none() && attempt < max_attempts {
attempt = attempt
.checked_add(1)
.expect("We check that attempt < max_attempts, so this won't overflow");
match up_and_retrieve_port(&mut compose).await {
Ok(p) => {
port = Some(p);
}
Err(err) => {
warn!(
"Failed to bring up Bedrock services: {err:?}, attempt {attempt}/{max_attempts}"
);
}
}
}
let Some(port) = port else {
bail!("Failed to bring up Bedrock services after {max_attempts} attempts");
};
let addr = SocketAddr::from(([127, 0, 0, 1], port));
Ok((compose, addr))
}
pub(crate) async fn setup_indexer(
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(IndexerHandle, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
debug!(
"Using temp indexer home at {}",
temp_indexer_dir.path().display()
);
let indexer_config = config::indexer_config(
bedrock_addr,
temp_indexer_dir.path().to_owned(),
initial_data,
)
.context("Failed to create Indexer config")?;
indexer_service::run_server(indexer_config, 0)
.await
.context("Failed to run Indexer Service")
.map(|handle| (handle, temp_indexer_dir))
}
pub(crate) async fn setup_sequencer(
partial: config::SequencerPartialConfig,
bedrock_addr: SocketAddr,
indexer_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(SequencerHandle, TempDir)> {
let temp_sequencer_dir =
tempfile::tempdir().context("Failed to create temp dir for sequencer home")?;
debug!(
"Using temp sequencer home at {}",
temp_sequencer_dir.path().display()
);
let config = config::sequencer_config(
partial,
temp_sequencer_dir.path().to_owned(),
bedrock_addr,
indexer_addr,
initial_data,
)
.context("Failed to create Sequencer config")?;
let sequencer_handle = sequencer_service::run(config, 0).await?;
Ok((sequencer_handle, temp_sequencer_dir))
}
pub(crate) async fn setup_wallet(
sequencer_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(WalletCore, TempDir, String)> {
let config = config::wallet_config(sequencer_addr, initial_data)
.context("Failed to create Wallet config")?;
let config_serialized =
serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?;
let temp_wallet_dir =
tempfile::tempdir().context("Failed to create temp dir for wallet home")?;
let config_path = temp_wallet_dir.path().join("wallet_config.json");
std::fs::write(&config_path, config_serialized)
.context("Failed to write wallet config in temp dir")?;
let storage_path = temp_wallet_dir.path().join("storage.json");
let config_overrides = WalletConfigOverrides::default();
let wallet_password = "test_pass".to_owned();
let (wallet, _mnemonic) = WalletCore::new_init_storage(
config_path,
storage_path,
Some(config_overrides),
&wallet_password,
)
.context("Failed to init wallet")?;
wallet
.store_persistent_data()
.await
.context("Failed to store wallet persistent data")?;
Ok((wallet, temp_wallet_dir, wallet_password))
}
pub(crate) fn setup_indexer_ffi(
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(IndexerServiceFFI, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
debug!(
"Using temp indexer home at {}",
temp_indexer_dir.path().display()
);
let indexer_config = config::indexer_config(
bedrock_addr,
temp_indexer_dir.path().to_owned(),
initial_data,
)
.context("Failed to create Indexer config")?;
let config_json = serde_json::to_vec(&indexer_config)?;
let config_path = temp_indexer_dir.path().join("indexer_config.json");
let mut file = File::create(config_path.as_path())?;
file.write_all(&config_json)?;
file.flush()?;
let res =
// SAFETY: lib function ensures validity of value.
unsafe { start_indexer(CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) };
if res.error.is_error() {
anyhow::bail!("Indexer FFI error {:?}", res.error);
}
Ok((
// SAFETY: lib function ensures validity of value.
unsafe { std::ptr::read(res.value) },
temp_indexer_dir,
))
}

View File

@ -0,0 +1,296 @@
use std::sync::Arc;
use anyhow::{Context as _, Result};
use futures::FutureExt as _;
use indexer_ffi::IndexerServiceFFI;
use indexer_service_rpc::RpcClient as _;
use log::{debug, error};
use nssa::AccountId;
use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _};
use sequencer_service::SequencerHandle;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::WalletCore;
use crate::{
BEDROCK_SERVICE_WITH_OPEN_PORT, LOGGER, TestContextBuilder, config,
setup::{setup_bedrock_node, setup_indexer_ffi, setup_sequencer, setup_wallet},
};
/// Test context which sets up a sequencer, indexer through ffi and a wallet for integration tests.
///
/// It's memory and logically safe to create multiple instances of this struct in parallel tests,
/// as each instance uses its own temporary directories for sequencer and wallet data.
// NOTE: Order of fields is important for proper drop order.
pub struct TestContextFFI {
sequencer_client: SequencerClient,
indexer_client: IndexerClient,
wallet: WalletCore,
wallet_password: String,
/// Optional to move out value in Drop.
sequencer_handle: Option<SequencerHandle>,
bedrock_compose: DockerCompose,
_temp_indexer_dir: TempDir,
_temp_sequencer_dir: TempDir,
_temp_wallet_dir: TempDir,
}
#[expect(
clippy::multiple_inherent_impl,
reason = "It is more natural to have this implementation here"
)]
impl TestContextBuilder {
pub fn build_ffi(
self,
runtime: &Arc<tokio::runtime::Runtime>,
) -> Result<(TestContextFFI, IndexerServiceFFI)> {
TestContextFFI::new_configured(
self.sequencer_partial_config.unwrap_or_default(),
&self.initial_data.unwrap_or_else(|| {
config::InitialData::with_two_public_and_two_private_initialized_accounts()
}),
runtime,
)
}
}
impl TestContextFFI {
/// Create new test context.
pub fn new(runtime: &Arc<tokio::runtime::Runtime>) -> Result<(Self, IndexerServiceFFI)> {
Self::builder().build_ffi(runtime)
}
#[must_use]
pub const fn builder() -> TestContextBuilder {
TestContextBuilder::new()
}
fn new_configured(
sequencer_partial_config: config::SequencerPartialConfig,
initial_data: &config::InitialData,
runtime: &Arc<tokio::runtime::Runtime>,
) -> Result<(Self, IndexerServiceFFI)> {
// Ensure logger is initialized only once
*LOGGER;
debug!("Test context setup");
let (bedrock_compose, bedrock_addr) = runtime.block_on(setup_bedrock_node())?;
let (indexer_ffi, temp_indexer_dir) =
setup_indexer_ffi(bedrock_addr, initial_data).context("Failed to setup Indexer")?;
let (sequencer_handle, temp_sequencer_dir) = runtime
.block_on(setup_sequencer(
sequencer_partial_config,
bedrock_addr,
// SAFETY: addr is valid if indexer_ffi is valid.
unsafe { indexer_ffi.addr() },
initial_data,
))
.context("Failed to setup Sequencer")?;
let (wallet, temp_wallet_dir, wallet_password) = runtime
.block_on(setup_wallet(sequencer_handle.addr(), initial_data))
.context("Failed to setup wallet")?;
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
.context("Failed to convert sequencer addr to URL")?;
let indexer_url = config::addr_to_url(
config::UrlProtocol::Ws,
// SAFETY: addr is valid if indexer_ffi is valid.
unsafe { indexer_ffi.addr() },
)
.context("Failed to convert indexer addr to URL")?;
let sequencer_client = SequencerClientBuilder::default()
.build(sequencer_url)
.context("Failed to create sequencer client")?;
let indexer_client = runtime
.block_on(IndexerClient::new(&indexer_url))
.context("Failed to create indexer client")?;
Ok((
Self {
sequencer_client,
indexer_client,
wallet,
wallet_password,
bedrock_compose,
sequencer_handle: Some(sequencer_handle),
_temp_indexer_dir: temp_indexer_dir,
_temp_sequencer_dir: temp_sequencer_dir,
_temp_wallet_dir: temp_wallet_dir,
},
indexer_ffi,
))
}
/// Get reference to the wallet.
#[must_use]
pub const fn wallet(&self) -> &WalletCore {
&self.wallet
}
#[must_use]
pub fn wallet_password(&self) -> &str {
&self.wallet_password
}
/// Get mutable reference to the wallet.
pub const fn wallet_mut(&mut self) -> &mut WalletCore {
&mut self.wallet
}
/// Get reference to the sequencer client.
#[must_use]
pub const fn sequencer_client(&self) -> &SequencerClient {
&self.sequencer_client
}
/// Get reference to the indexer client.
#[must_use]
pub const fn indexer_client(&self) -> &IndexerClient {
&self.indexer_client
}
/// Get existing public account IDs in the wallet.
#[must_use]
pub fn existing_public_accounts(&self) -> Vec<AccountId> {
self.wallet
.storage()
.user_data
.public_account_ids()
.collect()
}
/// Get existing private account IDs in the wallet.
#[must_use]
pub fn existing_private_accounts(&self) -> Vec<AccountId> {
self.wallet
.storage()
.user_data
.private_account_ids()
.collect()
}
pub fn get_last_block_sequencer(&self, runtime: &Arc<tokio::runtime::Runtime>) -> Result<u64> {
Ok(runtime.block_on(self.sequencer_client.get_last_block_id())?)
}
pub fn get_last_block_indexer(&self, runtime: &Arc<tokio::runtime::Runtime>) -> Result<u64> {
Ok(runtime.block_on(self.indexer_client.get_last_finalized_block_id())?)
}
}
impl Drop for TestContextFFI {
fn drop(&mut self) {
let Self {
sequencer_handle,
bedrock_compose,
_temp_indexer_dir: _,
_temp_sequencer_dir: _,
_temp_wallet_dir: _,
sequencer_client: _,
indexer_client: _,
wallet: _,
wallet_password: _,
} = self;
let sequencer_handle = sequencer_handle
.take()
.expect("Sequencer handle should be present in TestContext drop");
if !sequencer_handle.is_healthy() {
let Err(err) = sequencer_handle
.failed()
.now_or_never()
.expect("Sequencer handle should not be running");
error!(
"Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}"
);
}
let container = bedrock_compose
.service(BEDROCK_SERVICE_WITH_OPEN_PORT)
.unwrap_or_else(|| {
panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`")
});
let output = std::process::Command::new("docker")
.args(["inspect", "-f", "{{.State.Running}}", container.id()])
.output()
.expect("Failed to execute docker inspect command to check if Bedrock container is still running");
let stdout = String::from_utf8(output.stdout)
.expect("Failed to parse docker inspect output as String");
if stdout.trim() != "true" {
error!(
"Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}",
container.id()
);
}
}
}
/// A test context with ffi to be used in normal #[test] tests.
pub struct BlockingTestContextFFI {
ctx: Option<TestContextFFI>,
runtime: Arc<tokio::runtime::Runtime>,
indexer_ffi: IndexerServiceFFI,
}
impl BlockingTestContextFFI {
pub fn new() -> Result<Self> {
let runtime = tokio::runtime::Runtime::new().unwrap();
let runtime_wrapped = Arc::new(runtime);
let (ctx, indexer_ffi) = TestContextFFI::new(&runtime_wrapped)?;
Ok(Self {
ctx: Some(ctx),
runtime: runtime_wrapped,
indexer_ffi,
})
}
#[must_use]
pub const fn ctx(&self) -> &TestContextFFI {
self.ctx.as_ref().expect("TestContext is set")
}
#[must_use]
pub const fn ctx_mut(&mut self) -> &mut TestContextFFI {
self.ctx.as_mut().expect("TestContext is set")
}
#[must_use]
pub const fn runtime(&self) -> &Arc<tokio::runtime::Runtime> {
&self.runtime
}
#[must_use]
pub fn runtime_clone(&self) -> Arc<tokio::runtime::Runtime> {
Arc::<tokio::runtime::Runtime>::clone(&self.runtime)
}
}
impl Drop for BlockingTestContextFFI {
fn drop(&mut self) {
let Self {
ctx,
runtime,
indexer_ffi,
} = self;
// Ensure async cleanup of TestContext by blocking on its drop in the runtime.
runtime.block_on(async {
if let Some(ctx) = ctx.take() {
drop(ctx);
}
});
let indexer_handle =
// SAFETY: lib function ensures validity of value.
unsafe { indexer_ffi.handle() };
if !indexer_handle.is_healthy() {
error!("Indexer handle has unexpectedly stopped before TestContext drop");
}
}
}

View File

@ -4,7 +4,7 @@
)]
use anyhow::Result;
use integration_tests::TestContext;
use integration_tests::{TestContext, format_private_account_id};
use log::info;
use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
@ -70,34 +70,29 @@ async fn new_public_account_with_label() -> Result<()> {
}
#[test]
async fn new_private_account_with_label() -> Result<()> {
async fn add_label_to_existing_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let account_id = ctx.existing_private_accounts()[0];
let label = "my-test-private-account".to_owned();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: Some(label.clone()),
}));
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_private_account_id(account_id)),
account_label: None,
label: label.clone(),
});
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
execute_subcommand(ctx.wallet_mut(), command).await?;
// Extract the account_id from the result
let wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } = result else {
panic!("Expected RegisterAccount return value")
};
// Verify the label was stored
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the new account");
.expect("Label should be stored for the account");
assert_eq!(stored_label.to_string(), label);
info!("Successfully created private account with label");
info!("Successfully set label on existing private account");
Ok(())
}

View File

@ -133,6 +133,7 @@ async fn amm_public() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 7,
};
@ -162,6 +163,7 @@ async fn amm_public() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 7,
};
@ -550,6 +552,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
@ -574,6 +577,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;

View File

@ -268,6 +268,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)
@ -500,6 +501,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)
@ -614,6 +616,7 @@ async fn burn_via_ata_private_owner() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)

View File

@ -30,6 +30,7 @@ async fn private_transfer_to_owned_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -71,6 +72,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
amount: 100,
});
@ -121,6 +123,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -170,12 +173,11 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
@ -186,6 +188,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -236,6 +239,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -280,6 +284,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
amount: 100,
});
@ -336,12 +341,11 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
};
// Get the newly created account's keys
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send transfer using nullifier and viewing public keys
@ -352,6 +356,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -455,6 +460,7 @@ async fn private_transfer_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -527,3 +533,112 @@ async fn initialize_private_account_using_label() -> Result<()> {
Ok(())
}
#[test]
async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Both transfers below will target this same node with distinct identifiers.
let chain_index = ctx.wallet_mut().create_private_accounts_key(None);
let (npk, vpk) = {
let node = ctx
.wallet()
.storage()
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("node was just inserted");
let key_chain = &node.value.0;
(
key_chain.nullifier_public_key,
key_chain.viewing_public_key.clone(),
)
};
let npk_hex = hex::encode(npk.0);
let vpk_hex = hex::encode(vpk.0);
let identifier_1 = 1_u128;
let identifier_2 = 2_u128;
let sender_0: AccountId = ctx.existing_public_accounts()[0];
let sender_1: AccountId = ctx.existing_public_accounts()[1];
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_0)),
from_label: None,
to: None,
to_label: None,
to_npk: Some(npk_hex.clone()),
to_vpk: Some(vpk_hex.clone()),
to_identifier: Some(identifier_1),
amount: 100,
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_1)),
from_label: None,
to: None,
to_label: None,
to_npk: Some(npk_hex),
to_vpk: Some(vpk_hex),
to_identifier: Some(identifier_2),
amount: 200,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::SyncPrivate {}),
)
.await?;
// Both accounts must be discovered with the correct balances.
let account_id_1 = AccountId::from((&npk, identifier_1));
let acc_1 = ctx
.wallet()
.get_account_private(account_id_1)
.context("account for identifier 1 not found after sync")?;
assert_eq!(acc_1.balance, 100);
let account_id_2 = AccountId::from((&npk, identifier_2));
let acc_2 = ctx
.wallet()
.get_account_private(account_id_2)
.context("account for identifier 2 not found after sync")?;
assert_eq!(acc_2.balance, 200);
// Both account ids must resolve to the same key node.
let tree = &ctx.wallet().storage().user_data.private_key_tree;
let ci_1 = tree
.account_id_map
.get(&account_id_1)
.context("account_id_1 missing from private_key_tree.account_id_map")?;
let ci_2 = tree
.account_id_map
.get(&account_id_2)
.context("account_id_2 missing from private_key_tree.account_id_map")?;
assert_eq!(
ci_1, ci_2,
"identifiers 1 and 2 under the same NPK must share a single chain_index"
);
assert_eq!(
ci_1, &chain_index,
"both accounts must resolve to the key node created at the start of the test"
);
info!("Successfully transferred to two distinct identifiers under the same NPK");
Ok(())
}

View File

@ -23,6 +23,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -81,6 +82,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -119,6 +121,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 1_000_000,
});
@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -193,6 +197,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -278,6 +283,7 @@ async fn successful_transfer_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -325,6 +331,7 @@ async fn successful_transfer_using_to_label() -> Result<()> {
to_label: Some(label),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});

View File

@ -14,7 +14,6 @@ use integration_tests::{
};
use log::info;
use nssa::AccountId;
use tokio::test;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
/// Maximum time to wait for the indexer to catch up to the sequencer.
@ -53,7 +52,7 @@ async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 {
})
}
#[test]
#[tokio::test]
async fn indexer_test_run() -> Result<()> {
let ctx = TestContext::new().await?;
@ -70,7 +69,7 @@ async fn indexer_test_run() -> Result<()> {
Ok(())
}
#[test]
#[tokio::test]
async fn indexer_block_batching() -> Result<()> {
let ctx = TestContext::new().await?;
@ -101,7 +100,7 @@ async fn indexer_block_batching() -> Result<()> {
Ok(())
}
#[test]
#[tokio::test]
async fn indexer_state_consistency() -> Result<()> {
let mut ctx = TestContext::new().await?;
@ -112,6 +111,7 @@ async fn indexer_state_consistency() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -148,6 +148,7 @@ async fn indexer_state_consistency() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -204,7 +205,7 @@ async fn indexer_state_consistency() -> Result<()> {
Ok(())
}
#[test]
#[tokio::test]
async fn indexer_state_consistency_with_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
@ -234,6 +235,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
to_label: Some(to_label_str),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});

View File

@ -0,0 +1,287 @@
#![expect(
clippy::shadow_unrelated,
clippy::tests_outside_test_module,
reason = "We don't care about these in tests"
)]
use anyhow::{Context as _, Result};
use indexer_service_rpc::RpcClient as _;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id,
test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state,
};
use log::info;
use nssa::AccountId;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
/// Maximum time to wait for the indexer to catch up to the sequencer.
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000;
#[test]
fn indexer_test_run_ffi() -> Result<()> {
let blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime();
// RUN OBSERVATION
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?;
info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 1);
Ok(())
}
#[test]
fn indexer_ffi_block_batching() -> Result<()> {
let blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime();
let ctx = blocking_ctx.ctx();
// WAIT
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
let last_block_indexer = runtime_wrapped
.block_on(ctx.indexer_client().get_last_finalized_block_id())
.unwrap();
info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 1);
// Getting wide batch to fit all blocks (from latest backwards)
let mut block_batch = runtime_wrapped
.block_on(ctx.indexer_client().get_blocks(None, 100))
.unwrap();
// Reverse to check chain consistency from oldest to newest
block_batch.reverse();
// Checking chain consistency
let mut prev_block_hash = block_batch.first().unwrap().header.hash;
for block in &block_batch[1..] {
assert_eq!(block.header.prev_block_hash, prev_block_hash);
info!("Block {} chain-consistent", block.header.block_id);
prev_block_hash = block.header.hash;
}
Ok(())
}
#[test]
fn indexer_ffi_state_consistency() -> Result<()> {
let mut blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime_clone();
let ctx = blocking_ctx.ctx_mut();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
info!("Checking correct balance move");
let acc_1_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc_2_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
let from: AccountId = ctx.existing_private_accounts()[0];
let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
let new_commitment1 = ctx
.wallet()
.get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?;
let commitment_check1 = runtime_wrapped.block_on(verify_commitment_is_in_state(
new_commitment1,
ctx.sequencer_client(),
));
assert!(commitment_check1);
let new_commitment2 = ctx
.wallet()
.get_private_account_commitment(to)
.context("Failed to get private account commitment for receiver")?;
let commitment_check2 = runtime_wrapped.block_on(verify_commitment_is_in_state(
new_commitment2,
ctx.sequencer_client(),
));
assert!(commitment_check2);
info!("Successfully transferred privately to owned account");
// WAIT
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
let acc1_ind_state = runtime_wrapped.block_on(
ctx.indexer_client()
.get_account(ctx.existing_public_accounts()[0].into()),
)?;
let acc2_ind_state = runtime_wrapped.block_on(
ctx.indexer_client()
.get_account(ctx.existing_public_accounts()[1].into()),
)?;
info!("Checking correct state transition");
let acc1_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc2_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
assert_eq!(acc1_ind_state, acc1_seq_state.into());
assert_eq!(acc2_ind_state, acc2_seq_state.into());
// ToDo: Check private state transition
Ok(())
}
#[test]
fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
let mut blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime_clone();
let ctx = blocking_ctx.ctx_mut();
// Assign labels to both accounts
let from_label = "idx-sender-label".to_owned();
let to_label_str = "idx-receiver-label".to_owned();
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
label: from_label.clone(),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
label: to_label_str.clone(),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
// Send using labels instead of account IDs
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(from_label),
to: None,
to_label: Some(to_label_str),
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
let acc_1_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc_2_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
let acc1_ind_state = runtime_wrapped.block_on(
ctx.indexer_client()
.get_account(ctx.existing_public_accounts()[0].into()),
)?;
let acc1_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
assert_eq!(acc1_ind_state, acc1_seq_state.into());
info!("Indexer state is consistent after label-based transfer");
Ok(())
}

View File

@ -59,12 +59,11 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
@ -75,6 +74,7 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -151,6 +151,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -163,6 +164,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 101,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -203,6 +205,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 102,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -215,6 +218,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 103,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -259,16 +263,16 @@ async fn restore_keys_from_seed() -> Result<()> {
.expect("Acc 4 should be restored");
assert_eq!(
acc1.value.1.program_owner,
acc1.value.1[0].1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(
acc2.value.1.program_owner,
acc2.value.1[0].1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(acc1.value.1.balance, 100);
assert_eq!(acc2.value.1.balance, 101);
assert_eq!(acc1.value.1[0].1.balance, 100);
assert_eq!(acc2.value.1[0].1.balance, 101);
info!("Tree checks passed, testing restored accounts can transact");
@ -280,6 +284,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 10,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -291,6 +296,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 11,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;

View File

@ -134,6 +134,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -227,6 +228,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount,
};
@ -372,6 +374,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -566,6 +569,7 @@ async fn create_token_with_private_definition() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount_public,
};
@ -614,6 +618,7 @@ async fn create_token_with_private_definition() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount_private,
};
@ -756,6 +761,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -887,6 +893,7 @@ async fn shielded_token_transfer() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -1013,6 +1020,7 @@ async fn deshielded_token_transfer() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -1131,12 +1139,11 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
};
// Get keys for foreign mint (claiming path)
let (holder_keys, _) = ctx
let (holder_keys, _, holder_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(recipient_account_id)
.cloned()
.context("Failed to get private account keys")?;
// Mint using claiming path (foreign account)
@ -1148,6 +1155,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
holder_label: None,
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)),
holder_identifier: Some(holder_identifier),
amount: mint_amount,
};
@ -1351,6 +1359,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;

View File

@ -220,14 +220,17 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
data: Data::default(),
},
true,
AccountId::from(&sender_npk),
AccountId::from((&sender_npk, 0)),
);
let recipient_nsk = [2; 32];
let recipient_vsk = [99; 32];
let recipient_vpk = ViewingPublicKey::from_scalar(recipient_vsk);
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
let recipient_pre =
AccountWithMetadata::new(Account::default(), false, AccountId::from(&recipient_npk));
let recipient_pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from((&recipient_npk, 0)),
);
let eph_holder_from = EphemeralKeyHolder::new(&sender_npk);
let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_vpk);
@ -249,7 +252,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)],
vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)],
vec![sender_nsk],
vec![Some(proof)],
&program.into(),

View File

@ -26,7 +26,7 @@ use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir;
use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, WalletHandle, error,
FfiTransferResult, FfiU128, WalletHandle, error,
};
unsafe extern "C" {
@ -53,6 +53,11 @@ unsafe extern "C" {
out_account_id: *mut FfiBytes32,
) -> error::WalletFfiError;
fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle,
out_keys: *mut FfiPrivateAccountKeys,
) -> error::WalletFfiError;
fn wallet_ffi_list_accounts(
handle: *mut WalletHandle,
out_list: *mut FfiAccountList,
@ -116,6 +121,7 @@ unsafe extern "C" {
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
@ -132,6 +138,7 @@ unsafe extern "C" {
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
@ -260,33 +267,28 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests";
let n_accounts = 10;
// Create `n_accounts` private accounts with wallet FFI
let new_private_account_ids_ffi = unsafe {
let mut account_ids = Vec::new();
// Create `n_accounts` receiving keys with wallet FFI
let new_npks_ffi = unsafe {
let mut npks = Vec::new();
let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?;
for _ in 0..n_accounts {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
account_ids.push(out_account_id.data);
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
npks.push(out_keys.nullifier_public_key.data);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
}
wallet_ffi_destroy(wallet_ffi_handle);
account_ids
npks
};
// All returned IDs must be unique and non-zero
assert_eq!(new_private_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate private account IDs returned"
);
// All returned NPKs must be unique and non-zero
assert_eq!(new_npks_ffi.len(), n_accounts);
let unique: HashSet<_> = new_npks_ffi.iter().collect();
assert_eq!(unique.len(), n_accounts, "Duplicate NPKs returned");
assert!(
new_private_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
new_npks_ffi.iter().all(|id| *id != [0_u8; 32]),
"Zero NPK returned"
);
Ok(())
@ -294,46 +296,35 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
#[test]
fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let mut out_private_account_id = FfiBytes32::from_bytes([0; 32]);
let home = tempfile::tempdir()?;
// Create a private account with the wallet FFI and save it
unsafe {
// Create a receiving key and save
let first_npk = unsafe {
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_private_account_id);
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let npk = out_keys.nullifier_public_key.data;
wallet_ffi_free_private_account_keys(&raw mut out_keys);
wallet_ffi_save(wallet_ffi_handle);
wallet_ffi_destroy(wallet_ffi_handle);
}
let private_account_keys = unsafe {
let wallet_ffi_handle = load_existing_ffi_wallet(home.path())?;
let mut private_account = FfiAccount::default();
let result = wallet_ffi_get_account_private(
wallet_ffi_handle,
&raw const out_private_account_id,
&raw mut private_account,
);
assert_eq!(result, error::WalletFfiError::Success);
let mut out_keys = FfiPrivateAccountKeys::default();
let result = wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_private_account_id,
&raw mut out_keys,
);
assert_eq!(result, error::WalletFfiError::Success);
wallet_ffi_destroy(wallet_ffi_handle);
out_keys
npk
};
assert_eq!(
nssa::AccountId::from(&private_account_keys.npk()),
out_private_account_id.into()
// After loading, creating a new key should yield a different NPK (state was persisted)
let second_npk = unsafe {
let wallet_ffi_handle = load_existing_ffi_wallet(home.path())?;
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let npk = out_keys.nullifier_public_key.data;
wallet_ffi_free_private_account_keys(&raw mut out_keys);
wallet_ffi_destroy(wallet_ffi_handle);
npk
};
assert_ne!(first_npk, [0_u8; 32], "First NPK should be non-zero");
assert_ne!(second_npk, [0_u8; 32], "Second NPK should be non-zero");
assert_ne!(
first_npk, second_npk,
"Keys should differ after state was persisted"
);
Ok(())
@ -344,22 +335,22 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
let password = "password_for_tests";
// Create the wallet FFI and track which account IDs were created as public/private
let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
let (wallet_ffi_handle, created_public_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?;
let mut public_ids: Vec<[u8; 32]> = Vec::new();
let mut private_ids: Vec<[u8; 32]> = Vec::new();
// Create 5 public accounts and 5 private accounts, recording their IDs
// Create 5 public accounts and 5 receiving keys
for _ in 0..5 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id);
public_ids.push(out_account_id.data);
wallet_ffi_create_account_private(handle, &raw mut out_account_id);
private_ids.push(out_account_id.data);
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(handle, &raw mut out_keys);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
}
(handle, public_ids, private_ids)
(handle, public_ids)
};
// Get the account list with FFI method
@ -382,31 +373,19 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
.filter(|e| e.is_public)
.map(|e| e.account_id.data)
.collect();
let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter()
.filter(|e| !e.is_public)
.map(|e| e.account_id.data)
.collect();
for id in &created_public_ids {
assert!(
listed_public_ids.contains(id),
"Created public account not found in list with is_public=true"
);
}
for id in &created_private_ids {
assert!(
listed_private_ids.contains(id),
"Created private account not found in list with is_public=false"
);
}
// Total listed accounts must be at least the number we created
// Total listed accounts must be at least the number of public accounts created
// (receiving keys without synced accounts don't appear in the list)
assert!(
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(),
"Listed account count ({}) is less than the number of created accounts ({})",
wallet_ffi_account_list.count >= created_public_ids.len(),
"Listed account count ({}) is less than the number of created public accounts ({})",
wallet_ffi_account_list.count,
created_public_ids.len() + created_private_ids.len()
created_public_ids.len()
);
unsafe {
@ -710,25 +689,13 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// Create a new uninitialized public account
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
// Create a new private account
let mut out_account_id = FfiBytes32::default();
unsafe {
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
}
// Check its program owner is the default program id
let account: Account = unsafe {
let mut out_account = FfiAccount::default();
wallet_ffi_get_account_private(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_account,
);
(&out_account).try_into().unwrap()
};
assert_eq!(account.program_owner, DEFAULT_PROGRAM_ID);
// Call the init funciton
// Call the init function
let mut transfer_result = FfiTransferResult::default();
unsafe {
wallet_ffi_register_private_account(
@ -832,24 +799,24 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_keys,
);
(out_account_id, out_keys)
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
(to, out_keys)
};
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_shielded(
wallet_ffi_handle,
&raw const from,
&raw const to_keys,
&raw const to_identifier,
&raw const amount,
&raw mut transfer_result,
);
@ -966,25 +933,25 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_keys,
);
(out_account_id, out_keys)
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
(to, out_keys)
};
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_private(
wallet_ffi_handle,
&raw const from,
&raw const to_keys,
&raw const to_identifier,
&raw const amount,
&raw mut transfer_result,
);

View File

@ -1,23 +1,24 @@
use k256::{Scalar, elliptic_curve::PrimeField as _};
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey};
use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey};
use serde::{Deserialize, Serialize};
use crate::key_management::{
KeyChain,
key_tree::traits::KeyNode,
key_tree::traits::KeyTreeNode,
secret_holders::{PrivateKeyHolder, SecretSpendingKey},
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChildKeysPrivate {
pub value: (KeyChain, nssa::Account),
pub value: (KeyChain, Vec<(Identifier, nssa::Account)>),
pub ccc: [u8; 32],
/// Can be [`None`] if root.
pub cci: Option<u32>,
}
impl KeyNode for ChildKeysPrivate {
fn root(seed: [u8; 64]) -> Self {
impl ChildKeysPrivate {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, b"LEE_master_priv");
let ssk = SecretSpendingKey(
@ -46,14 +47,15 @@ impl KeyNode for ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
nssa::Account::default(),
vec![],
),
ccc,
cci: None,
}
}
fn nth_child(&self, cci: u32) -> Self {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
#[expect(clippy::arithmetic_side_effects, reason = "TODO: fix later")]
let parent_pt =
Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into())
@ -95,43 +97,27 @@ impl KeyNode for ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
nssa::Account::default(),
vec![],
),
ccc,
cci: Some(cci),
}
}
fn chain_code(&self) -> &[u8; 32] {
&self.ccc
}
fn child_index(&self) -> Option<u32> {
self.cci
}
fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from(&self.value.0.nullifier_public_key)
}
}
#[expect(
clippy::single_char_lifetime_names,
reason = "TODO add meaningful name"
)]
impl<'a> From<&'a ChildKeysPrivate> for &'a (KeyChain, nssa::Account) {
fn from(value: &'a ChildKeysPrivate) -> Self {
&value.value
impl KeyTreeNode for ChildKeysPrivate {
fn from_seed(seed: [u8; 64]) -> Self {
Self::root(seed)
}
}
#[expect(
clippy::single_char_lifetime_names,
reason = "TODO add meaningful name"
)]
impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
fn from(value: &'a mut ChildKeysPrivate) -> Self {
&mut value.value
fn derive_child(&self, cci: u32) -> Self {
self.nth_child(cci)
}
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.value.1.iter().map(|(identifier, _)| {
nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier))
})
}
}

View File

@ -1,7 +1,7 @@
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyNode;
use crate::key_management::key_tree::traits::KeyTreeNode;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChildKeysPublic {
@ -13,32 +13,8 @@ pub struct ChildKeysPublic {
}
impl ChildKeysPublic {
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
// Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`.
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
.expect("32 bytes, within curve order");
let pk = sk.public_key();
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else {
// Harden.
hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.csk.value());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
}
impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let csk = nssa::PrivateKey::try_new(
@ -58,7 +34,8 @@ impl KeyNode for ChildKeysPublic {
}
}
fn nth_child(&self, cci: u32) -> Self {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
let hash_value = self.compute_hash_value(cci);
let csk = nssa::PrivateKey::try_new({
@ -90,17 +67,33 @@ impl KeyNode for ChildKeysPublic {
}
}
fn chain_code(&self) -> &[u8; 32] {
&self.ccc
}
fn child_index(&self) -> Option<u32> {
self.cci
}
fn account_id(&self) -> nssa::AccountId {
#[must_use]
pub fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from(&self.cpk)
}
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
// Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`.
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
.expect("32 bytes, within curve order");
let pk = sk.public_key();
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else {
// Harden.
hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.csk.value());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
}
#[expect(
@ -113,6 +106,20 @@ impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
}
}
impl KeyTreeNode for ChildKeysPublic {
fn from_seed(seed: [u8; 64]) -> Self {
Self::root(seed)
}
fn derive_child(&self, cci: u32) -> Self {
self.nth_child(cci)
}
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
std::iter::once(self.account_id())
}
}
#[cfg(test)]
mod tests {
use nssa::{PrivateKey, PublicKey};

View File

@ -2,12 +2,13 @@ use std::collections::BTreeMap;
use anyhow::Result;
use nssa::{Account, AccountId};
use nssa_core::Identifier;
use serde::{Deserialize, Serialize};
use crate::key_management::{
key_tree::{
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
traits::KeyNode,
traits::KeyTreeNode,
},
secret_holders::SeedHolder,
};
@ -20,7 +21,7 @@ pub mod traits;
pub const DEPTH_SOFT_CAP: u32 = 20;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KeyTree<N: KeyNode> {
pub struct KeyTree<N: KeyTreeNode> {
pub key_map: BTreeMap<ChainIndex, N>,
pub account_id_map: BTreeMap<nssa::AccountId, ChainIndex>,
}
@ -28,7 +29,7 @@ pub struct KeyTree<N: KeyNode> {
pub type KeyTreePublic = KeyTree<ChildKeysPublic>;
pub type KeyTreePrivate = KeyTree<ChildKeysPrivate>;
impl<N: KeyNode> KeyTree<N> {
impl<N: KeyTreeNode> KeyTree<N> {
#[must_use]
pub fn new(seed: &SeedHolder) -> Self {
let seed_fit: [u8; 64] = seed
@ -37,29 +38,62 @@ impl<N: KeyNode> KeyTree<N> {
.try_into()
.expect("SeedHolder seed is 64 bytes long");
let root_keys = N::root(seed_fit);
let account_id = root_keys.account_id();
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]);
let account_id_map = BTreeMap::from_iter([(account_id, ChainIndex::root())]);
let root_keys = N::from_seed(seed_fit);
let account_id_map = root_keys
.account_ids()
.map(|id| (id, ChainIndex::root()))
.collect();
Self {
key_map,
key_map: BTreeMap::from_iter([(ChainIndex::root(), root_keys)]),
account_id_map,
}
}
pub fn new_from_root(root: N) -> Self {
let account_id_map = BTreeMap::from_iter([(root.account_id(), ChainIndex::root())]);
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root)]);
let account_id_map = root
.account_ids()
.map(|id| (id, ChainIndex::root()))
.collect();
Self {
key_map,
key_map: BTreeMap::from_iter([(ChainIndex::root(), root)]),
account_id_map,
}
}
// ToDo: Add function to create a tree from list of nodes with consistency check.
pub fn generate_new_node(&mut self, parent_cci: &ChainIndex) -> Option<ChainIndex> {
let parent_keys = self.key_map.get(parent_cci)?;
let next_child_id = self
.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 = parent_keys.derive_child(next_child_id);
let account_ids = child_keys.account_ids();
for account_id in account_ids {
self.account_id_map.insert(account_id, next_cci.clone());
}
self.key_map.insert(next_cci.clone(), child_keys);
Some(next_cci)
}
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<ChainIndex> {
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
let child_id = *chain_index.chain().last()?;
let child_keys = parent_keys.derive_child(child_id);
let account_ids = child_keys.account_ids();
for account_id in account_ids {
self.account_id_map.insert(account_id, chain_index.clone());
}
self.key_map.insert(chain_index.clone(), child_keys);
Some(chain_index.clone())
}
#[must_use]
pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option<u32> {
@ -102,25 +136,6 @@ impl<N: KeyNode> KeyTree<N> {
}
}
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)
.expect("Can be None only if parent is not present");
let next_cci = parent_cci.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.clone());
Some((account_id, next_cci))
}
fn find_next_slot_layered(&self) -> ChainIndex {
let mut depth = 1;
@ -134,44 +149,10 @@ impl<N: KeyNode> KeyTree<N> {
}
}
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)> {
pub fn generate_new_node_layered(&mut self) -> Option<ChainIndex> {
self.fill_node(&self.find_next_slot_layered())
}
#[must_use]
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get(chain_id)
}
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get_mut(chain_id)
}
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: 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)?;
self.key_map.remove(&chain_index)
}
/// Populates tree with children.
///
/// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) <
@ -194,37 +175,50 @@ impl<N: KeyNode> KeyTree<N> {
}
}
}
}
impl KeyTree<ChildKeysPrivate> {
/// 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) {
let depth = usize::try_from(depth).expect("Depth is expected to fit in usize");
'outer: for i in (1..depth).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;
}
}
}
}
#[must_use]
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get(chain_id)
}
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get_mut(chain_id)
}
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: 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)?;
self.key_map.remove(&chain_index)
}
}
impl KeyTree<ChildKeysPublic> {
/// Generate a new public key node, returning the account ID and chain index.
pub fn generate_new_public_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<(nssa::AccountId, ChainIndex)> {
let cci = self.generate_new_node(parent_cci)?;
let node = self.key_map.get(&cci)?;
let account_id = node.account_ids().next()?;
Some((account_id, cci))
}
/// Generate a new public key node using layered placement, returning the account ID and chain
/// index.
pub fn generate_new_public_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
let cci = self.generate_new_node_layered()?;
let node = self.key_map.get(&cci)?;
let account_id = node.account_ids().next()?;
Some((account_id, cci))
}
/// Cleanup of non-initialized accounts in a public tree.
///
/// If account is default, removes them, stops at first non-default account.
@ -259,6 +253,65 @@ impl KeyTree<ChildKeysPublic> {
}
}
impl KeyTree<ChildKeysPrivate> {
pub fn create_private_accounts_key_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<ChainIndex> {
self.generate_new_node(parent_cci)
}
pub fn create_private_accounts_key_node_layered(&mut self) -> Option<ChainIndex> {
self.generate_new_node_layered()
}
/// Register an additional identifier on an existing private key node, inserting the derived
/// `AccountId` into `account_id_map`. Returns `None` if the node does not exist or the
/// `AccountId` is already registered.
pub fn register_identifier_on_node(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
let node = self.key_map.get(cci)?;
let account_id = nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
if self.account_id_map.contains_key(&account_id) {
return None;
}
self.account_id_map.insert(account_id, cci.clone());
Some(account_id)
}
/// Cleanup of non-initialized accounts in a private tree.
///
/// If account has no synced entries, removes it, stops at first initialized account.
///
/// Walks through tree in layers 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) {
let depth = usize::try_from(depth).expect("Depth is expected to fit in usize");
'outer: for i in (1..depth).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).cloned() {
if node.value.1.is_empty() {
let account_ids = node.account_ids();
self.key_map.remove(&id);
for addr in account_ids {
self.account_id_map.remove(&addr);
}
} else {
break 'outer;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about this in tests")]
@ -478,25 +531,59 @@ mod tests {
.key_map
.get_mut(&ChainIndex::from_str("/1").unwrap())
.unwrap();
acc.value.1.balance = 2;
acc.value.1.push((
0,
nssa::Account {
balance: 2,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/2").unwrap())
.unwrap();
acc.value.1.balance = 3;
acc.value.1.push((
0,
nssa::Account {
balance: 3,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
acc.value.1.balance = 5;
acc.value.1.push((
0,
nssa::Account {
balance: 5,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
acc.value.1.balance = 6;
acc.value.1.push((
0,
nssa::Account {
balance: 6,
..nssa::Account::default()
},
));
// Update account_id_map for nodes that now have entries
for chain_index_str in ["/1", "/2", "/0/1", "/1/0"] {
let id = ChainIndex::from_str(chain_index_str).unwrap();
if let Some(node) = tree.key_map.get(&id) {
for account_id in node.account_ids() {
tree.account_id_map.insert(account_id, id.clone());
}
}
}
tree.cleanup_tree_remove_uninit_layered(10);
@ -518,15 +605,15 @@ mod tests {
assert_eq!(key_set, key_set_res);
let acc = &tree.key_map[&ChainIndex::from_str("/1").unwrap()];
assert_eq!(acc.value.1.balance, 2);
assert_eq!(acc.value.1[0].1.balance, 2);
let acc = &tree.key_map[&ChainIndex::from_str("/2").unwrap()];
assert_eq!(acc.value.1.balance, 3);
assert_eq!(acc.value.1[0].1.balance, 3);
let acc = &tree.key_map[&ChainIndex::from_str("/0/1").unwrap()];
assert_eq!(acc.value.1.balance, 5);
assert_eq!(acc.value.1[0].1.balance, 5);
let acc = &tree.key_map[&ChainIndex::from_str("/1/0").unwrap()];
assert_eq!(acc.value.1.balance, 6);
assert_eq!(acc.value.1[0].1.balance, 6);
}
}

View File

@ -1,15 +1,8 @@
/// Trait, that reperesents a Node in hierarchical key tree.
pub trait KeyNode {
/// Tree root node.
fn root(seed: [u8; 64]) -> Self;
/// `cci`'s child of node.
pub trait KeyTreeNode: Sized {
#[must_use]
fn nth_child(&self, cci: u32) -> Self;
fn chain_code(&self) -> &[u8; 32];
fn child_index(&self) -> Option<u32>;
fn account_id(&self) -> nssa::AccountId;
fn from_seed(seed: [u8; 64]) -> Self;
#[must_use]
fn derive_child(&self, cci: u32) -> Self;
#[must_use]
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId>;
}

View File

@ -172,11 +172,12 @@ mod tests {
// /0/0
key_tree_private.generate_new_node_layered().unwrap();
// /2
let (second_child_id, _) = key_tree_private.generate_new_node_layered().unwrap();
let second_chain_index = key_tree_private.generate_new_node_layered().unwrap();
key_tree_private
.get_node(second_child_id)
.unwrap()
.key_map
.get(&second_chain_index)
.expect("Node was just inserted")
.value
.0
.clone()

View File

@ -2,6 +2,8 @@ use std::collections::BTreeMap;
use anyhow::Result;
use k256::AffinePoint;
use nssa::{Account, AccountId};
use nssa_core::Identifier;
use serde::{Deserialize, Serialize};
use crate::key_management::{
@ -12,13 +14,18 @@ use crate::key_management::{
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserPrivateAccountData {
pub key_chain: KeyChain,
pub accounts: Vec<(Identifier, Account)>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NSSAUserData {
/// Default public accounts.
pub default_pub_account_signing_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
/// Default private accounts.
pub default_user_private_accounts:
BTreeMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
pub default_user_private_accounts: BTreeMap<AccountId, UserPrivateAccountData>,
/// Tree of public keys.
pub public_key_tree: KeyTreePublic,
/// Tree of private keys.
@ -42,13 +49,16 @@ impl NSSAUserData {
}
fn valid_private_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
accounts_keys_map: &BTreeMap<AccountId, UserPrivateAccountData>,
) -> bool {
let mut check_res = true;
for (account_id, (key, _)) in accounts_keys_map {
let expected_account_id = nssa::AccountId::from(&key.nullifier_public_key);
if expected_account_id != *account_id {
println!("{expected_account_id}, {account_id}");
for (account_id, entry) in accounts_keys_map {
let any_match = entry.accounts.iter().any(|(identifier, _)| {
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier))
== *account_id
});
if !any_match {
println!("No matching entry found for account_id {account_id}");
check_res = false;
}
}
@ -57,10 +67,7 @@ impl NSSAUserData {
pub fn new_with_accounts(
default_accounts_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
default_accounts_key_chains: BTreeMap<
nssa::AccountId,
(KeyChain, nssa_core::account::Account),
>,
default_accounts_key_chains: BTreeMap<AccountId, UserPrivateAccountData>,
public_key_tree: KeyTreePublic,
private_key_tree: KeyTreePrivate,
) -> Result<Self> {
@ -94,11 +101,11 @@ impl NSSAUserData {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_node(&parent_cci)
.generate_new_public_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_node_layered()
.generate_new_public_node_layered()
.expect("Search for new node slot failed"),
}
}
@ -114,50 +121,61 @@ impl NSSAUserData {
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
}
/// Generated new private key for privacy preserving transactions.
///
/// Returns the `account_id` of new account.
pub fn generate_new_privacy_preserving_transaction_key_chain(
&mut self,
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
/// Creates a new receiving key node and returns its `ChainIndex`.
pub fn create_private_accounts_key(&mut self, parent_cci: Option<ChainIndex>) -> ChainIndex {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.generate_new_node(&parent_cci)
.create_private_accounts_key_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.generate_new_node_layered()
.create_private_accounts_key_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures.
/// Registers an additional identifier on an existing private key node, deriving and recording
/// the corresponding `AccountId`. Returns `None` if the node does not exist or the identifier
/// is already registered.
pub fn register_identifier_on_private_key_chain(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
self.private_key_tree
.register_identifier_on_node(cci, identifier)
}
/// Returns the key chain and account data for the given private account ID.
#[must_use]
pub fn get_private_account(
&self,
account_id: nssa::AccountId,
) -> Option<&(KeyChain, nssa_core::account::Account)> {
self.default_user_private_accounts
.get(&account_id)
.or_else(|| self.private_key_tree.get_node(account_id).map(Into::into))
}
/// Returns the signing key for public transaction signatures.
pub fn get_private_account_mut(
&mut self,
account_id: &nssa::AccountId,
) -> Option<&mut (KeyChain, nssa_core::account::Account)> {
// First seek in defaults
if let Some(key) = self.default_user_private_accounts.get_mut(account_id) {
Some(key)
// Then seek in tree
} else {
self.private_key_tree
.get_node_mut(*account_id)
.map(Into::into)
) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> {
// Check default accounts
if let Some(entry) = self.default_user_private_accounts.get(&account_id) {
for (identifier, account) in &entry.accounts {
let expected_id =
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((entry.key_chain.clone(), account.clone(), *identifier));
}
}
return None;
}
// Check tree
if let Some(node) = self.private_key_tree.get_node(account_id) {
let key_chain = &node.value.0;
for (identifier, account) in &node.value.1 {
let expected_id =
nssa::AccountId::from((&key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((key_chain.clone(), account.clone(), *identifier));
}
}
}
None
}
pub fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
@ -200,16 +218,15 @@ mod tests {
fn new_account() {
let mut user_data = NSSAUserData::default();
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();
let chain_index = user_data.create_private_accounts_key(Some(ChainIndex::root()));
let is_key_chain_generated = user_data
.private_key_tree
.key_map
.contains_key(&chain_index);
assert!(is_key_chain_generated);
let account_id_private_str = account_id_private.to_string();
println!("{account_id_private_str:#?}");
let key_chain = &user_data.get_private_account(account_id_private).unwrap().0;
let key_chain = &user_data.private_key_tree.key_map[&chain_index].value.0;
println!("{key_chain:#?}");
}
}

View File

@ -10,7 +10,7 @@ use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::{NullifierPublicKey, NullifierSecretKey, program::ProgramId};
use crate::{NullifierSecretKey, program::ProgramId};
pub mod data;
@ -26,9 +26,9 @@ impl Nonce {
}
#[must_use]
pub fn private_account_nonce_init(npk: &NullifierPublicKey) -> Self {
pub fn private_account_nonce_init(account_id: &AccountId) -> Self {
let mut bytes: [u8; 64] = [0_u8; 64];
bytes[..32].copy_from_slice(&npk.0);
bytes[..32].copy_from_slice(account_id.value());
let result: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap();
let result = result.first_chunk::<16>().unwrap();
@ -306,8 +306,8 @@ mod tests {
#[test]
fn initialize_private_nonce() {
let npk = NullifierPublicKey([42; 32]);
let nonce = Nonce::private_account_nonce_init(&npk);
let account_id = AccountId::new([42; 32]);
let nonce = Nonce::private_account_nonce_init(&account_id);
let expected_nonce = Nonce(37_937_661_125_547_691_021_612_781_941_709_513_486);
assert_eq!(nonce, expected_nonce);
}

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::{
Commitment, CommitmentSetDigest, MembershipProof, Nullifier, NullifierPublicKey,
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
@ -19,8 +19,8 @@ pub struct PrivacyPreservingCircuitInput {
/// - `2` - private account without authentication
/// - `3` - private PDA account
pub visibility_mask: Vec<u8>,
/// Public keys of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
/// Public keys and identifiers of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
/// Nullifier secret keys for authorized private accounts.
pub private_account_nsks: Vec<NullifierSecretKey>,
/// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts.
@ -57,7 +57,7 @@ mod tests {
use super::*;
use crate::{
Commitment, Nullifier, NullifierPublicKey,
Commitment, Nullifier,
account::{Account, AccountId, AccountWithMetadata, Nonce},
};
@ -94,12 +94,12 @@ mod tests {
}],
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],
new_commitments: vec![Commitment::new(
&NullifierPublicKey::from(&[1; 32]),
&AccountId::new([1; 32]),
&Account::default(),
)],
new_nullifiers: vec![(
Nullifier::for_account_update(
&Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()),
&Commitment::new(&AccountId::new([2; 32]), &Account::default()),
&[1; 32],
),
[0xab; 32],

View File

@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use crate::{NullifierPublicKey, account::Account};
use crate::account::{Account, AccountId};
/// A commitment to all zero data.
/// ```python
@ -49,16 +49,16 @@ impl std::fmt::Debug for Commitment {
}
impl Commitment {
/// Generates the commitment to a private account owned by user for npk:
/// SHA256( `Comm_DS` || npk || `program_owner` || balance || nonce || SHA256(data)).
/// Generates the commitment to a private account owned by user for `account_id`:
/// SHA256( `Comm_DS` || `account_id` || `program_owner` || balance || nonce || SHA256(data)).
#[must_use]
pub fn new(npk: &NullifierPublicKey, account: &Account) -> Self {
pub fn new(account_id: &AccountId, account: &Account) -> Self {
const COMMITMENT_PREFIX: &[u8; 32] =
b"/LEE/v0.3/Commitment/\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let mut bytes = Vec::new();
bytes.extend_from_slice(COMMITMENT_PREFIX);
bytes.extend_from_slice(&npk.to_byte_array());
bytes.extend_from_slice(account_id.value());
let account_bytes_with_hashed_data = {
let mut this = Vec::new();
for word in &account.program_owner {
@ -115,14 +115,15 @@ mod tests {
use risc0_zkvm::sha::{Impl, Sha256 as _};
use crate::{
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, NullifierPublicKey, account::Account,
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH,
account::{Account, AccountId},
};
#[test]
fn nothing_up_my_sleeve_dummy_commitment() {
let default_account = Account::default();
let npk_null = NullifierPublicKey([0; 32]);
let expected_dummy_commitment = Commitment::new(&npk_null, &default_account);
let account_id_null = AccountId::new([0; 32]);
let expected_dummy_commitment = Commitment::new(&account_id_null, &default_account);
assert_eq!(DUMMY_COMMITMENT, expected_dummy_commitment);
}

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey};
use crate::{Commitment, account::Account};
use crate::{Commitment, Identifier, account::Account};
#[cfg(feature = "host")]
pub mod shared_key_derivation;
@ -40,11 +40,14 @@ impl EncryptionScheme {
#[must_use]
pub fn encrypt(
account: &Account,
identifier: Identifier,
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Ciphertext {
let mut buffer = account.to_bytes();
// Plaintext: identifier (16 bytes, little-endian) || account bytes
let mut buffer = identifier.to_le_bytes().to_vec();
buffer.extend_from_slice(&account.to_bytes());
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
Ciphertext(buffer)
}
@ -86,12 +89,17 @@ impl EncryptionScheme {
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Option<Account> {
) -> Option<(Identifier, Account)> {
use std::io::Cursor;
let mut buffer = ciphertext.0.clone();
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
let mut cursor = Cursor::new(buffer.as_slice());
if buffer.len() < 16 {
return None;
}
let identifier = Identifier::from_le_bytes(buffer[..16].try_into().unwrap());
let mut cursor = Cursor::new(&buffer[16..]);
Account::from_cursor(&mut cursor)
.inspect_err(|err| {
println!(
@ -104,5 +112,6 @@ impl EncryptionScheme {
);
})
.ok()
.map(|account| (identifier, account))
}
}

View File

@ -9,7 +9,7 @@ pub use commitment::{
compute_digest_for_path,
};
pub use encryption::{EncryptionScheme, SharedSecretKey};
pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey};
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
pub mod account;
mod circuit_io;

View File

@ -4,18 +4,24 @@ use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId};
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
pub type Identifier = u128;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Hash))]
pub struct NullifierPublicKey(pub [u8; 32]);
impl From<&NullifierPublicKey> for AccountId {
fn from(value: &NullifierPublicKey) -> Self {
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
fn from(value: (&NullifierPublicKey, Identifier)) -> Self {
let (npk, identifier) = value;
let mut bytes = [0; 64];
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
let mut bytes = [0; 80];
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
bytes[32..].copy_from_slice(&value.0);
bytes[32..64].copy_from_slice(&npk.0);
bytes[64..80].copy_from_slice(&identifier.to_le_bytes());
Self::new(
Impl::hash_bytes(&bytes)
.as_bytes()
@ -85,10 +91,10 @@ impl Nullifier {
/// Computes a nullifier for an account initialization.
#[must_use]
pub fn for_account_initialization(npk: &NullifierPublicKey) -> Self {
pub fn for_account_initialization(account_id: &AccountId) -> Self {
const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00";
let mut bytes = INIT_PREFIX.to_vec();
bytes.extend_from_slice(&npk.to_byte_array());
bytes.extend_from_slice(account_id.value());
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
}
}
@ -111,7 +117,7 @@ mod tests {
#[test]
fn constructor_for_account_initialization() {
let npk = NullifierPublicKey([
let account_id = AccountId::new([
112, 188, 193, 129, 150, 55, 228, 67, 88, 168, 29, 151, 5, 92, 23, 190, 17, 162, 164,
255, 29, 105, 42, 186, 43, 11, 157, 168, 132, 225, 17, 163,
]);
@ -119,7 +125,7 @@ mod tests {
149, 59, 95, 181, 2, 194, 20, 143, 72, 233, 104, 243, 59, 70, 67, 243, 110, 77, 109,
132, 139, 111, 51, 125, 128, 92, 107, 46, 252, 4, 20, 149,
]);
let nullifier = Nullifier::for_account_initialization(&npk);
let nullifier = Nullifier::for_account_initialization(&account_id);
assert_eq!(nullifier, expected_nullifier);
}
@ -145,11 +151,46 @@ mod tests {
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
139, 72, 194, 222, 215, 187, 147, 56, 55, 35, 222, 205, 156, 12, 204, 227, 166, 44, 30,
81, 186, 14, 167, 234, 28, 236, 32, 213, 125, 251, 193, 233,
165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50,
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
]);
let account_id = AccountId::from(&npk);
let account_id = AccountId::from((&npk, 0));
assert_eq!(account_id, expected_account_id);
}
#[test]
fn account_id_from_nullifier_public_key_identifier_1() {
let nsk = [
57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30,
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95,
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
]);
let account_id = AccountId::from((&npk, 1));
assert_eq!(account_id, expected_account_id);
}
#[test]
fn account_id_from_nullifier_public_key_byte_asymmetric_identifier() {
let identifier: u128 = 0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210;
let nsk = [
57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30,
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174,
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
]);
let account_id = AccountId::from((&npk, identifier));
assert_eq!(account_id, expected_account_id);
}

View File

@ -913,18 +913,6 @@ mod tests {
assert_ne!(private_id, public_id);
}
/// A private PDA address differs from a standard private account address at the same `npk`,
/// because the private PDA formula includes `program_id` and `seed`.
#[test]
fn for_private_pda_differs_from_standard_private() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let standard_private_id = AccountId::from(&npk);
assert_ne!(private_pda_id, standard_private_id);
}
// ---- compute_public_authorized_pdas tests ----
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.

View File

@ -2,8 +2,8 @@ use std::collections::{HashMap, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::AccountWithMetadata,
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
};
@ -68,7 +68,7 @@ pub fn execute_and_prove(
pre_states: Vec<AccountWithMetadata>,
instruction_data: InstructionData,
visibility_mask: Vec<u8>,
private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
private_account_nsks: Vec<NullifierSecretKey>,
private_account_membership_proofs: Vec<Option<MembershipProof>>,
program_with_dependencies: &ProgramWithDependencies,
@ -213,11 +213,8 @@ mod tests {
AccountId::new([0; 32]),
);
let recipient = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&recipient_keys.npk()),
);
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
@ -231,7 +228,7 @@ mod tests {
let expected_recipient_post = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
data: Data::default(),
};
@ -244,7 +241,7 @@ mod tests {
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![(recipient_keys.npk(), shared_secret)],
vec![(recipient_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
@ -261,7 +258,7 @@ mod tests {
assert_eq!(output.new_nullifiers.len(), 1);
assert_eq!(output.ciphertexts.len(), 1);
let recipient_post = EncryptionScheme::decrypt(
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0],
&shared_secret,
&output.new_commitments[0],
@ -286,27 +283,24 @@ mod tests {
data: Data::default(),
},
true,
AccountId::from(&sender_keys.npk()),
AccountId::from((&sender_keys.npk(), 0)),
);
let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
let recipient = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&recipient_keys.npk()),
);
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
let mut commitment_set = CommitmentSet::with_capacity(2);
commitment_set.extend(std::slice::from_ref(&commitment_sender));
let expected_new_nullifiers = vec![
(
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
commitment_set.digest(),
),
(
Nullifier::for_account_initialization(&recipient_keys.npk()),
Nullifier::for_account_initialization(&recipient_account_id),
DUMMY_COMMITMENT_HASH,
),
];
@ -322,12 +316,12 @@ mod tests {
let expected_private_account_2 = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
..Default::default()
};
let expected_new_commitments = vec![
Commitment::new(&sender_keys.npk(), &expected_private_account_1),
Commitment::new(&recipient_keys.npk(), &expected_private_account_2),
Commitment::new(&sender_account_id, &expected_private_account_1),
Commitment::new(&recipient_account_id, &expected_private_account_2),
];
let esk_1 = [3; 32];
@ -341,8 +335,8 @@ mod tests {
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
(sender_keys.npk(), 0, shared_secret_1),
(recipient_keys.npk(), 0, shared_secret_2),
],
vec![sender_keys.nsk],
vec![commitment_set.get_proof_for(&commitment_sender), None],
@ -357,7 +351,7 @@ mod tests {
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
assert_eq!(output.ciphertexts.len(), 2);
let sender_post = EncryptionScheme::decrypt(
let (_identifier, sender_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0],
&shared_secret_1,
&expected_new_commitments[0],
@ -366,7 +360,7 @@ mod tests {
.unwrap();
assert_eq!(sender_post, expected_private_account_1);
let recipient_post = EncryptionScheme::decrypt(
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[1],
&shared_secret_2,
&expected_new_commitments[1],
@ -382,7 +376,7 @@ mod tests {
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&account_keys.npk()),
AccountId::from((&account_keys.npk(), 0)),
);
let validity_window_chain_caller = Program::validity_window_chain_caller();
@ -409,7 +403,7 @@ mod tests {
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&program_with_deps,

View File

@ -168,9 +168,11 @@ pub mod tests {
let encrypted_private_post_states = Vec::new();
let new_commitments = vec![Commitment::new(&npk2, &account2)];
let account_id2 = nssa_core::account::AccountId::from((&npk2, 0));
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
let old_commitment = Commitment::new(&npk1, &account1);
let account_id1 = nssa_core::account::AccountId::from((&npk1, 0));
let old_commitment = Commitment::new(&account_id1, &account1);
let new_nullifiers = vec![(
Nullifier::for_account_update(&old_commitment, &nsk1),
[0; 32],
@ -245,11 +247,12 @@ pub mod tests {
let npk = NullifierPublicKey::from(&[1; 32]);
let vpk = ViewingPublicKey::from_scalar([2; 32]);
let account = Account::default();
let commitment = Commitment::new(&npk, &account);
let account_id = nssa_core::account::AccountId::from((&npk, 0));
let commitment = Commitment::new(&account_id, &account);
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &vpk);
let epk = EphemeralPublicKey::from_scalar(esk);
let ciphertext = EncryptionScheme::encrypt(&account, &shared_secret, &commitment, 2);
let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2);
let encrypted_account_data =
EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone());

View File

@ -459,7 +459,8 @@ pub mod tests {
#[must_use]
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
let commitment = Commitment::new(&keys.npk(), account);
let account_id = AccountId::from((&keys.npk(), 0));
let commitment = Commitment::new(&account_id, account);
self.private_state.0.extend(&[commitment]);
self
}
@ -617,13 +618,13 @@ pub mod tests {
..Account::default()
};
let npk1 = keys1.npk();
let npk2 = keys2.npk();
let account_id1 = AccountId::from((&keys1.npk(), 0));
let account_id2 = AccountId::from((&keys2.npk(), 0));
let init_commitment1 = Commitment::new(&npk1, &account);
let init_commitment2 = Commitment::new(&npk2, &account);
let init_nullifier1 = Nullifier::for_account_initialization(&npk1);
let init_nullifier2 = Nullifier::for_account_initialization(&npk2);
let init_commitment1 = Commitment::new(&account_id1, &account);
let init_commitment2 = Commitment::new(&account_id2, &account);
let init_nullifier1 = Nullifier::for_account_initialization(&account_id1);
let init_nullifier2 = Nullifier::for_account_initialization(&account_id2);
let initial_private_accounts = vec![
(init_commitment1, init_nullifier1),
@ -1283,7 +1284,8 @@ pub mod tests {
let sender_nonce = sender.account.nonce;
let recipient = AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
let recipient =
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk());
@ -1293,7 +1295,7 @@ pub mod tests {
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![(recipient_keys.npk(), shared_secret)],
vec![(recipient_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
@ -1320,11 +1322,15 @@ pub mod tests {
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = Program::authenticated_transfer_program();
let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account);
let sender_pre =
AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
);
let recipient_pre =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let esk_1 = [3; 32];
let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk());
@ -1339,8 +1345,8 @@ pub mod tests {
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
(sender_keys.npk(), 0, shared_secret_1),
(recipient_keys.npk(), 0, shared_secret_2),
],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment), None],
@ -1372,9 +1378,13 @@ pub mod tests {
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = Program::authenticated_transfer_program();
let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account);
let sender_pre =
AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
);
let recipient_pre = AccountWithMetadata::new(
state.get_account_by_id(*recipient_account_id),
false,
@ -1389,7 +1399,7 @@ pub mod tests {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 0],
vec![(sender_keys.npk(), shared_secret)],
vec![(sender_keys.npk(), 0, shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
@ -1476,8 +1486,10 @@ pub mod tests {
&state,
);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let expected_new_commitment_1 = Commitment::new(
&sender_keys.npk(),
&sender_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
@ -1486,15 +1498,15 @@ pub mod tests {
},
);
let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
let expected_new_commitment_2 = Commitment::new(
&recipient_keys.npk(),
&recipient_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
balance: balance_to_move,
..Account::default()
},
@ -1553,8 +1565,9 @@ pub mod tests {
&state,
);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let expected_new_commitment = Commitment::new(
&sender_keys.npk(),
&sender_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
@ -1563,7 +1576,7 @@ pub mod tests {
},
);
let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
@ -1895,10 +1908,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let result = execute_and_prove(
vec![private_account_1, private_account_2],
@ -1907,10 +1920,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -1933,7 +1948,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
@ -1941,6 +1956,7 @@ pub mod tests {
// Setting only one key for an execution with two private accounts.
let private_account_keys = [(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
)];
let result = execute_and_prove(
@ -1968,10 +1984,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting no second commitment proof.
let private_account_membership_proofs = [Some((0, vec![]))];
@ -1982,10 +1998,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2009,10 +2027,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting no auth key for an execution with one non default private accounts.
let private_account_nsks = [];
@ -2023,10 +2041,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2050,20 +2070,22 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let private_account_keys = [
// First private account is the sender
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
// Second private account is the recipient
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
];
@ -2098,7 +2120,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2107,7 +2129,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2117,10 +2139,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2144,7 +2168,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2153,7 +2177,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2163,10 +2187,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2190,7 +2216,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2199,7 +2225,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2209,10 +2235,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2236,7 +2264,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2245,7 +2273,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2255,10 +2283,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2283,13 +2313,13 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account::default(),
// This should be set to false in normal circumstances
true,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2299,10 +2329,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2341,7 +2373,7 @@ pub mod tests {
vec![public_account_1, private_pda_account],
Program::serialize_instruction(10_u128).unwrap(),
visibility_mask.to_vec(),
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2370,7 +2402,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, u128::MAX, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2408,7 +2440,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![3],
vec![(npk_b, shared_secret)],
vec![(npk_b, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2442,7 +2474,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, u128::MAX, shared_secret)],
vec![],
vec![None],
&program_with_deps,
@ -2479,7 +2511,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program_with_deps,
@ -2515,7 +2547,7 @@ pub mod tests {
vec![pre_a, pre_b],
Program::serialize_instruction(seed).unwrap(),
vec![3, 3],
vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)],
vec![(keys_a.npk(), 0, shared_a), (keys_b.npk(), 0, shared_b)],
vec![],
vec![None, None],
&program.into(),
@ -2559,7 +2591,7 @@ pub mod tests {
vec![owned_pre_state],
Program::serialize_instruction(()).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2580,10 +2612,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let result = execute_and_prove(
vec![private_account_1, private_account_2],
@ -2592,10 +2624,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2619,24 +2653,27 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting three private account keys for a circuit execution with only two private
// accounts.
let private_account_keys = [
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[57; 32], &sender_keys.vpk()),
),
];
@ -2665,10 +2702,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting two private account keys for a circuit execution with only one non default
// private account (visibility mask equal to 1 means that auth keys are expected).
@ -2682,10 +2719,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2764,7 +2803,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let visibility_mask = [1, 1];
@ -2776,8 +2815,8 @@ pub mod tests {
Program::serialize_instruction(100_u128).unwrap(),
visibility_mask.to_vec(),
vec![
(sender_keys.npk(), shared_secret),
(sender_keys.npk(), shared_secret),
(sender_keys.npk(), 0, shared_secret),
(sender_keys.npk(), 0, shared_secret),
],
private_account_nsks.to_vec(),
private_account_membership_proofs.to_vec(),
@ -3091,14 +3130,16 @@ pub mod tests {
balance: 100,
..Account::default()
};
let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
let mut state = V03State::new_with_genesis_accounts(
&[],
vec![(sender_commitment.clone(), sender_init_nullifier)],
0,
);
let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk());
let sender_pre =
AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0));
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
let recipient_account_id =
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
@ -3112,7 +3153,7 @@ pub mod tests {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(37_u128).unwrap(),
vec![1, 0],
vec![(sender_keys.npk(), shared_secret)],
vec![(sender_keys.npk(), 0, shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
@ -3164,7 +3205,7 @@ pub mod tests {
..Account::default()
},
true,
&from_keys.npk(),
(&from_keys.npk(), 0),
);
let to_account = AccountWithMetadata::new(
Account {
@ -3172,13 +3213,15 @@ pub mod tests {
..Account::default()
},
true,
&to_keys.npk(),
(&to_keys.npk(), 0),
);
let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account);
let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account);
let from_init_nullifier = Nullifier::for_account_initialization(&from_keys.npk());
let to_init_nullifier = Nullifier::for_account_initialization(&to_keys.npk());
let from_account_id = AccountId::from((&from_keys.npk(), 0));
let to_account_id = AccountId::from((&to_keys.npk(), 0));
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id);
let mut state = V03State::new_with_genesis_accounts(
&[],
vec![
@ -3217,21 +3260,21 @@ pub mod tests {
nonce: from_new_nonce,
..from_account.account.clone()
};
let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post);
let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post);
let to_expected_post = Account {
balance: u128::from(number_of_calls) * amount,
nonce: to_new_nonce,
..to_account.account.clone()
};
let to_expected_commitment = Commitment::new(&to_keys.npk(), &to_expected_post);
let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post);
// Act
let (output, proof) = execute_and_prove(
vec![to_account, from_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1, 1],
vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
vec![(from_keys.npk(), 0, to_ss), (to_keys.npk(), 0, from_ss)],
vec![from_keys.nsk, to_keys.nsk],
vec![
state.get_proof_for_commitment(&from_commitment),
@ -3483,7 +3526,7 @@ pub mod tests {
// Create an authorized private account with default values (new account being initialized)
let authorized_account =
AccountWithMetadata::new(Account::default(), true, &private_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let program = Program::authenticated_transfer_program();
@ -3500,7 +3543,7 @@ pub mod tests {
vec![authorized_account],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![private_keys.nsk],
vec![None],
&program.into(),
@ -3522,7 +3565,8 @@ pub mod tests {
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
assert!(result.is_ok());
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -3536,7 +3580,7 @@ pub mod tests {
// operate them without the corresponding private keys, so unauthorized private claiming
// remains allowed.
let unauthorized_account =
AccountWithMetadata::new(Account::default(), false, &private_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0));
let program = Program::claimer();
let esk = [5; 32];
@ -3547,7 +3591,7 @@ pub mod tests {
vec![unauthorized_account],
Program::serialize_instruction(0_u128).unwrap(),
vec![2],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -3569,7 +3613,8 @@ pub mod tests {
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -3582,7 +3627,7 @@ pub mod tests {
// Step 1: Create a new private account with authorization
let authorized_account =
AccountWithMetadata::new(Account::default(), true, &private_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let claimer_program = Program::claimer();
@ -3598,7 +3643,7 @@ pub mod tests {
vec![authorized_account.clone()],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![private_keys.nsk],
vec![None],
&claimer_program.into(),
@ -3624,7 +3669,8 @@ pub mod tests {
);
// Verify the account is now initialized (nullifier exists)
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
// Prepare new state of account
@ -3643,7 +3689,7 @@ pub mod tests {
vec![account_metadata],
Program::serialize_instruction(()).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret2)],
vec![(private_keys.npk(), 0, shared_secret2)],
vec![private_keys.nsk],
vec![None],
&noop_program.into(),
@ -3711,7 +3757,7 @@ pub mod tests {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
@ -3721,6 +3767,7 @@ pub mod tests {
vec![1],
vec![(
sender_keys.npk(),
0,
SharedSecretKey::new(&[3; 32], &sender_keys.vpk()),
)],
vec![sender_keys.nsk],
@ -3737,7 +3784,7 @@ pub mod tests {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
// Change data but don't claim (false) - should fail
let new_data = vec![1, 2, 3, 4, 5];
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
@ -3748,6 +3795,7 @@ pub mod tests {
vec![1],
vec![(
sender_keys.npk(),
0,
SharedSecretKey::new(&[3; 32], &sender_keys.vpk()),
)],
vec![sender_keys.nsk],
@ -3777,11 +3825,12 @@ pub mod tests {
sender_keys.account_id(),
);
let recipient_account =
AccountWithMetadata::new(Account::default(), true, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0));
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient_commitment =
Commitment::new(&recipient_keys.npk(), &recipient_account.account);
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_keys.npk());
Commitment::new(&recipient_account_id, &recipient_account.account);
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
let state = V03State::new_with_genesis_accounts(
&[(sender_account.account_id, sender_account.account.balance)],
vec![(recipient_commitment.clone(), recipient_init_nullifier)],
@ -3804,7 +3853,7 @@ pub mod tests {
vec![sender_account, recipient_account],
Program::serialize_instruction(instruction).unwrap(),
vec![0, 1],
vec![(recipient_keys.npk(), recipient)],
vec![(recipient_keys.npk(), 0, recipient)],
vec![recipient_keys.nsk],
vec![state.get_proof_for_commitment(&recipient_commitment)],
&program_with_deps,
@ -3939,7 +3988,7 @@ pub mod tests {
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let esk = [3; 32];
@ -3954,7 +4003,7 @@ pub mod tests {
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),
@ -4008,7 +4057,7 @@ pub mod tests {
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let esk = [3; 32];
@ -4023,7 +4072,7 @@ pub mod tests {
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),

View File

@ -4,9 +4,9 @@ use std::{
};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof,
Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier,
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
program::{
@ -17,6 +17,8 @@ use nssa_core::{
};
use risc0_zkvm::{guest::env, serde::to_vec};
const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX;
/// State of the involved accounts before and after program execution.
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
@ -54,7 +56,7 @@ impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
@ -67,7 +69,7 @@ impl ExecutionState {
let mut keys_iter = private_account_keys.iter();
for (pos, &mask) in visibility_mask.iter().enumerate() {
if matches!(mask, 1..=3) {
let (npk, _) = keys_iter.next().unwrap_or_else(|| {
let (npk, _, _) = keys_iter.next().unwrap_or_else(|| {
panic!(
"private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})"
)
@ -487,7 +489,7 @@ fn resolve_authorization_and_record_bindings(
fn compute_circuit_output(
execution_state: ExecutionState,
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
) -> PrivacyPreservingCircuitOutput {
@ -523,16 +525,18 @@ fn compute_circuit_output(
output.public_post_states.push(post_state);
}
1 | 2 => {
let Some((npk, shared_secret)) = private_keys_iter.next() else {
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
assert_eq!(
AccountId::from(npk),
pre_state.account_id,
"AccountId mismatch"
assert_ne!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
);
let account_id = AccountId::from((npk, *identifier));
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
let (new_nullifier, new_nonce) = if account_visibility_mask == 1 {
// Private account with authentication
@ -560,7 +564,7 @@ fn compute_circuit_output(
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
&account_id,
nsk,
);
@ -590,9 +594,9 @@ fn compute_circuit_output(
"Membership proof must be None for unauthorized accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
let nullifier = Nullifier::for_account_initialization(&account_id);
let new_nonce = Nonce::private_account_nonce_init(npk);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
@ -603,11 +607,12 @@ fn compute_circuit_output(
post_with_updated_nonce.nonce = new_nonce;
// Compute commitment
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce);
// Encrypt and push post state
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
*identifier,
shared_secret,
&commitment_post,
output_index,
@ -628,10 +633,15 @@ fn compute_circuit_output(
// `private_pda_bound_positions` check) guarantees that every mask-3
// position has been through at least one such binding, so this
// branch can safely use the wallet npk without re-verifying.
let Some((npk, shared_secret)) = private_keys_iter.next() else {
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
assert_eq!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}."
);
let (new_nullifier, new_nonce) = if pre_state.is_authorized {
// Existing private PDA with authentication (like mask 1)
let Some(nsk) = private_nsks_iter.next() else {
@ -650,7 +660,7 @@ fn compute_circuit_output(
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
&pre_state.account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
@ -677,8 +687,8 @@ fn compute_circuit_output(
"Membership proof must be None for new accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
let new_nonce = Nonce::private_account_nonce_init(npk);
let nullifier = Nullifier::for_account_initialization(&pre_state.account_id);
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
output.new_nullifiers.push(new_nullifier);
@ -686,10 +696,12 @@ fn compute_circuit_output(
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
let commitment_post =
Commitment::new(&pre_state.account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
PRIVATE_PDA_FIXED_IDENTIFIER,
shared_secret,
&commitment_post,
output_index,
@ -726,7 +738,7 @@ fn compute_circuit_output(
fn compute_nullifier_and_set_digest(
membership_proof_opt: Option<&MembershipProof>,
pre_account: &Account,
npk: &NullifierPublicKey,
account_id: &AccountId,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
membership_proof_opt.as_ref().map_or_else(
@ -738,12 +750,12 @@ fn compute_nullifier_and_set_digest(
);
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
let nullifier = Nullifier::for_account_initialization(account_id);
(nullifier, DUMMY_COMMITMENT_HASH)
},
|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, pre_account);
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier

View File

@ -110,6 +110,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
@ -117,8 +118,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect()
@ -1107,10 +1108,14 @@ mod tests {
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = execute_and_prove(
vec![AccountWithMetadata::new(Account::default(), true, &npk)],
vec![AccountWithMetadata::new(
Account::default(),
true,
(&npk, 0),
)],
Program::serialize_instruction(0_u128).unwrap(),
vec![1],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![nsk],
vec![None],
&Program::authenticated_transfer_program().into(),

View File

@ -1,6 +1,9 @@
use std::{path::Path, sync::Arc};
use common::block::Block;
use common::{
block::Block,
transaction::{NSSATransaction, clock_invocation},
};
use nssa::V03State;
use rocksdb::{
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options,
@ -169,22 +172,52 @@ impl RocksDBIO {
for block in self.get_block_batch_seq(
start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id,
)? {
for transaction in block.body.transactions {
transaction
.transaction_stateless_check()
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction pre check failed with err {err:?}"
))
})?
.execute_check_on_state(
&mut breakpoint,
let expected_clock =
NSSATransaction::Public(clock_invocation(block.header.timestamp));
if let Some((clock_tx, user_txs)) = block.body.transactions.split_last() {
if *clock_tx != expected_clock {
return Err(DbError::db_interaction_error(
"Last transaction in block must be the clock invocation for the block timestamp"
.to_owned(),
));
}
for transaction in user_txs {
transaction
.clone()
.transaction_stateless_check()
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction pre check failed with err {err:?}"
))
})?
.execute_check_on_state(
&mut breakpoint,
block.header.block_id,
block.header.timestamp,
)
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction execution failed with err {err:?}"
))
})?;
}
let NSSATransaction::Public(clock_public_tx) = clock_tx else {
return Err(DbError::db_interaction_error(
"Clock invocation must be a public transaction".to_owned(),
));
};
breakpoint
.transition_from_public_transaction(
clock_public_tx,
block.header.block_id,
block.header.timestamp,
)
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction execution failed with err {err:?}"
"clock transaction execution failed with err {err:?}"
))
})?;
}
@ -213,6 +246,7 @@ fn closest_breakpoint_id(block_id: u64) -> u64 {
#[expect(clippy::shadow_unrelated, reason = "Fine for tests")]
#[cfg(test)]
mod tests {
use common::test_utils::produce_dummy_block;
use nssa::{AccountId, PublicKey};
use tempfile::tempdir;
@ -302,7 +336,7 @@ mod tests {
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
dbio.put_block(&block, [1; 32]).unwrap();
@ -369,11 +403,7 @@ mod tests {
1,
&sign_key,
);
let block = common::test_utils::produce_dummy_block(
(i + 1).into(),
Some(prev_hash),
vec![transfer_tx],
);
let block = produce_dummy_block((i + 1).into(), Some(prev_hash), vec![transfer_tx]);
dbio.put_block(&block, [i; 32]).unwrap();
}
@ -439,7 +469,7 @@ mod tests {
let prev_hash = last_block.header.hash;
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
let control_hash1 = block.header.hash;
@ -451,7 +481,7 @@ mod tests {
let prev_hash = last_block.header.hash;
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]);
let control_hash2 = block.header.hash;
@ -466,7 +496,7 @@ mod tests {
let control_tx_hash1 = transfer_tx.hash();
let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]);
dbio.put_block(&block, [3; 32]).unwrap();
let last_id = dbio.get_meta_last_block_in_db().unwrap();
@ -478,7 +508,7 @@ mod tests {
let control_tx_hash2 = transfer_tx.hash();
let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
dbio.put_block(&block, [4; 32]).unwrap();
let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap();
@ -526,7 +556,7 @@ mod tests {
let prev_hash = last_block.header.hash;
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]);
block_res.push(block.clone());
dbio.put_block(&block, [1; 32]).unwrap();
@ -537,7 +567,7 @@ mod tests {
let prev_hash = last_block.header.hash;
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]);
block_res.push(block.clone());
dbio.put_block(&block, [2; 32]).unwrap();
@ -549,7 +579,7 @@ mod tests {
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]);
block_res.push(block.clone());
dbio.put_block(&block, [3; 32]).unwrap();
@ -560,7 +590,7 @@ mod tests {
let transfer_tx =
common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key);
let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
block_res.push(block.clone());
dbio.put_block(&block, [4; 32]).unwrap();
@ -633,11 +663,7 @@ mod tests {
tx_hash_res.push(transfer_tx1.hash().0);
tx_hash_res.push(transfer_tx2.hash().0);
let block = common::test_utils::produce_dummy_block(
2,
Some(prev_hash),
vec![transfer_tx1, transfer_tx2],
);
let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]);
dbio.put_block(&block, [1; 32]).unwrap();
@ -652,11 +678,7 @@ mod tests {
tx_hash_res.push(transfer_tx1.hash().0);
tx_hash_res.push(transfer_tx2.hash().0);
let block = common::test_utils::produce_dummy_block(
3,
Some(prev_hash),
vec![transfer_tx1, transfer_tx2],
);
let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]);
dbio.put_block(&block, [2; 32]).unwrap();
@ -671,11 +693,7 @@ mod tests {
tx_hash_res.push(transfer_tx1.hash().0);
tx_hash_res.push(transfer_tx2.hash().0);
let block = common::test_utils::produce_dummy_block(
4,
Some(prev_hash),
vec![transfer_tx1, transfer_tx2],
);
let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx1, transfer_tx2]);
dbio.put_block(&block, [3; 32]).unwrap();
@ -687,7 +705,7 @@ mod tests {
common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key);
tx_hash_res.push(transfer_tx.hash().0);
let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]);
dbio.put_block(&block, [4; 32]).unwrap();

View File

@ -95,9 +95,16 @@ pub struct PublicAccountPrivateInitialData {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivateAccountPrivateInitialData {
pub account_id: nssa::AccountId,
pub account: nssa_core::account::Account,
pub key_chain: KeyChain,
pub identifier: nssa_core::Identifier,
}
impl PrivateAccountPrivateInitialData {
#[must_use]
pub fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
}
}
#[must_use]
@ -142,7 +149,6 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
vec![
PrivateAccountPrivateInitialData {
account_id: AccountId::from(&key_chain_1.nullifier_public_key),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_A_INITIAL_BALANCE,
@ -150,9 +156,9 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
nonce: 0.into(),
},
key_chain: key_chain_1,
identifier: 0,
},
PrivateAccountPrivateInitialData {
account_id: AccountId::from(&key_chain_2.nullifier_public_key),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_B_INITIAL_BALANCE,
@ -160,6 +166,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
nonce: 0.into(),
},
key_chain: key_chain_2,
identifier: 0,
},
]
}
@ -201,14 +208,15 @@ pub fn initial_state() -> V03State {
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
acc.program_owner = nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect();
@ -239,8 +247,8 @@ mod tests {
const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV";
const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo";
const PRIV_ACC_A_TEXT_ADDR: &str = "5ya25h4Xc9GAmrGB2WrTEnEWtQKJwRwQx3Xfo2tucNcE";
const PRIV_ACC_B_TEXT_ADDR: &str = "E8HwiTyQe4H9HK7icTvn95HQMnzx49mP9A2ddtMLpNaN";
const PRIV_ACC_A_TEXT_ADDR: &str = "4eGX3M3rgjHsme8n3sSp89af8JRZtYVTesbJjLqaX1VQ";
const PRIV_ACC_B_TEXT_ADDR: &str = "3m6HQmCgmAvsxZtxAHPqqEqoBG4335fCG8TzxigyW7rE";
#[test]
fn pub_state_consistency() {
@ -354,11 +362,11 @@ mod tests {
);
assert_eq!(
init_private_accs_keys[0].account_id.to_string(),
init_private_accs_keys[0].account_id().to_string(),
PRIV_ACC_A_TEXT_ADDR
);
assert_eq!(
init_private_accs_keys[1].account_id.to_string(),
init_private_accs_keys[1].account_id().to_string(),
PRIV_ACC_B_TEXT_ADDR
);

View File

@ -7,7 +7,10 @@ use nssa::AccountId;
use crate::{
block_on,
error::{print_error, WalletFfiError},
types::{FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, WalletHandle},
types::{
FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiPrivateAccountKeys,
WalletHandle,
},
wallet::get_wallet,
};
@ -59,10 +62,18 @@ pub unsafe extern "C" fn wallet_ffi_create_account_public(
WalletFfiError::Success
}
/// Create a new private account.
/// Create a new private account, storing a default account entry in local storage.
///
/// Private accounts use privacy-preserving transactions with nullifiers
/// and commitments.
/// This is the private-account equivalent of `wallet_ffi_create_account_public`.
/// It generates a key node, assigns a random identifier, and inserts a default
/// account record so the account can immediately be used with
/// `wallet_ffi_register_private_account`.
///
/// The identifier is chosen at random and is not encoded in the mnemonic seed.
/// Once the account is initialized, the identifier is embedded in the encrypted
/// transaction payload and can be recovered by running `sync-private` from the
/// same mnemonic. An account that was created locally but has never been initialized
/// cannot be recovered from the seed alone.
///
/// # Parameters
/// - `handle`: Valid wallet handle
@ -107,6 +118,78 @@ pub unsafe extern "C" fn wallet_ffi_create_account_private(
WalletFfiError::Success
}
/// Create a new private key node.
///
/// Returns the nullifier public key (npk) and viewing public key (vpk) to share with
/// senders. Account IDs are discovered later via sync when senders initialize accounts
/// under this key.
///
/// # Parameters
/// - `handle`: Valid wallet handle
/// - `out_keys`: Output pointer for the key data (npk + vpk)
///
/// # Returns
/// - `Success` on successful creation
/// - Error code on failure
///
/// # Memory
/// The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
///
/// # Safety
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
/// - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle,
out_keys: *mut FfiPrivateAccountKeys,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
if out_keys.is_null() {
print_error("Null output pointer for keys");
return WalletFfiError::NullPointer;
}
let mut wallet = match wrapper.core.lock() {
Ok(w) => w,
Err(e) => {
print_error(format!("Failed to lock wallet: {e}"));
return WalletFfiError::InternalError;
}
};
let chain_index = wallet.create_private_accounts_key(None);
let node = wallet
.storage()
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("Node was just inserted");
let key_chain = &node.value.0;
let npk_bytes = key_chain.nullifier_public_key.0;
let vpk_bytes = key_chain.viewing_public_key.to_bytes();
let vpk_len = vpk_bytes.len();
#[expect(
clippy::as_conversions,
reason = "We need to convert the boxed slice into a raw pointer for FFI"
)]
let vpk_ptr = Box::into_raw(vpk_bytes.to_vec().into_boxed_slice()) as *const u8;
unsafe {
(*out_keys).nullifier_public_key.data = npk_bytes;
(*out_keys).viewing_public_key = vpk_ptr;
(*out_keys).viewing_public_key_len = vpk_len;
}
WalletFfiError::Success
}
/// List all accounts in the wallet.
///
/// Returns both public and private accounts managed by this wallet.

View File

@ -116,7 +116,8 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys(
let account_id = AccountId::new(unsafe { (*account_id).data });
let Some((key_chain, _account)) = wallet.storage().user_data.get_private_account(account_id)
let Some((key_chain, _account, _identifier)) =
wallet.storage().user_data.get_private_account(account_id)
else {
print_error("Private account not found in wallet");
return WalletFfiError::AccountNotFound;

View File

@ -9,7 +9,7 @@ use crate::{
block_on,
error::{print_error, WalletFfiError},
map_execution_error,
types::{FfiBytes32, FfiTransferResult, WalletHandle},
types::{FfiBytes32, FfiTransferResult, FfiU128, WalletHandle},
wallet::get_wallet,
FfiPrivateAccountKeys,
};
@ -102,6 +102,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public(
/// - `handle`: Valid wallet handle
/// - `from`: Source account ID (must be owned by this wallet)
/// - `to_keys`: Destination account keys
/// - `to_identifier`: Identifier for the recipient's private account
/// - `amount`: Amount to transfer as little-endian [u8; 16]
/// - `out_result`: Output pointer for transfer result
///
@ -125,6 +126,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
@ -133,7 +135,12 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
Err(e) => return e,
};
if from.is_null() || to_keys.is_null() || amount.is_null() || out_result.is_null() {
if from.is_null()
|| to_keys.is_null()
|| to_identifier.is_null()
|| amount.is_null()
|| out_result.is_null()
{
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
@ -155,13 +162,18 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
return e;
}
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(
transfer.send_shielded_transfer_to_outer_account(from_id, to_npk, to_vpk, amount),
) {
match block_on(transfer.send_shielded_transfer_to_outer_account(
from_id,
to_npk,
to_vpk,
to_identifier,
amount,
)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
@ -271,6 +283,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded(
/// - `handle`: Valid wallet handle
/// - `from`: Source account ID (must be owned by this wallet)
/// - `to_keys`: Destination account keys
/// - `to_identifier`: Identifier for the recipient's private account
/// - `amount`: Amount to transfer as little-endian [u8; 16]
/// - `out_result`: Output pointer for transfer result
///
@ -294,6 +307,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
@ -302,7 +316,12 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
Err(e) => return e,
};
if from.is_null() || to_keys.is_null() || amount.is_null() || out_result.is_null() {
if from.is_null()
|| to_keys.is_null()
|| to_identifier.is_null()
|| amount.is_null()
|| out_result.is_null()
{
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
@ -324,12 +343,18 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
return e;
}
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_private_transfer_to_outer_account(from_id, to_npk, to_vpk, amount))
{
match block_on(transfer.send_private_transfer_to_outer_account(
from_id,
to_npk,
to_vpk,
to_identifier,
amount,
)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);

View File

@ -126,6 +126,24 @@ typedef struct FfiBytes32 {
uint8_t data[32];
} FfiBytes32;
/**
* Public keys for a private account (safe to expose).
*/
typedef struct FfiPrivateAccountKeys {
/**
* Nullifier public key (32 bytes).
*/
struct FfiBytes32 nullifier_public_key;
/**
* viewing public key (compressed secp256k1 point).
*/
const uint8_t *viewing_public_key;
/**
* Length of viewing public key (typically 33 bytes).
*/
uintptr_t viewing_public_key_len;
} FfiPrivateAccountKeys;
/**
* Single entry in the account list.
*/
@ -189,24 +207,6 @@ typedef struct FfiPublicAccountKey {
struct FfiBytes32 public_key;
} FfiPublicAccountKey;
/**
* Public keys for a private account (safe to expose).
*/
typedef struct FfiPrivateAccountKeys {
/**
* Nullifier public key (32 bytes).
*/
struct FfiBytes32 nullifier_public_key;
/**
* viewing public key (compressed secp256k1 point).
*/
const uint8_t *viewing_public_key;
/**
* Length of viewing public key (typically 33 bytes).
*/
uintptr_t viewing_public_key_len;
} FfiPrivateAccountKeys;
/**
* Result of a transfer operation.
*/
@ -243,10 +243,18 @@ enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle
struct FfiBytes32 *out_account_id);
/**
* Create a new private account.
* Create a new private account, storing a default account entry in local storage.
*
* Private accounts use privacy-preserving transactions with nullifiers
* and commitments.
* This is the private-account equivalent of `wallet_ffi_create_account_public`.
* It generates a key node, assigns a random identifier, and inserts a default
* account record so the account can immediately be used with
* `wallet_ffi_register_private_account`.
*
* The identifier is chosen at random and is not encoded in the mnemonic seed.
* Once the account is initialized, the identifier is embedded in the encrypted
* transaction payload and can be recovered by running `sync-private` from the
* same mnemonic. An account that was created locally but has never been initialized
* cannot be recovered from the seed alone.
*
* # Parameters
* - `handle`: Valid wallet handle
@ -263,6 +271,31 @@ enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle
enum WalletFfiError wallet_ffi_create_account_private(struct WalletHandle *handle,
struct FfiBytes32 *out_account_id);
/**
* Create a new private key node.
*
* Returns the nullifier public key (npk) and viewing public key (vpk) to share with
* senders. Account IDs are discovered later via sync when senders initialize accounts
* under this key.
*
* # Parameters
* - `handle`: Valid wallet handle
* - `out_keys`: Output pointer for the key data (npk + vpk)
*
* # Returns
* - `Success` on successful creation
* - Error code on failure
*
* # Memory
* The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
*
* # Safety
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
* - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
*/
enum WalletFfiError wallet_ffi_create_private_accounts_key(struct WalletHandle *handle,
struct FfiPrivateAccountKeys *out_keys);
/**
* List all accounts in the wallet.
*
@ -685,6 +718,7 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle,
* - `handle`: Valid wallet handle
* - `from`: Source account ID (must be owned by this wallet)
* - `to_keys`: Destination account keys
* - `to_identifier`: Identifier for the recipient's private account
* - `amount`: Amount to transfer as little-endian [u8; 16]
* - `out_result`: Output pointer for transfer result
*
@ -707,6 +741,7 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle,
enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle,
const struct FfiBytes32 *from,
const struct FfiPrivateAccountKeys *to_keys,
const struct FfiU128 *to_identifier,
const uint8_t (*amount)[16],
struct FfiTransferResult *out_result);
@ -753,6 +788,7 @@ enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle,
* - `handle`: Valid wallet handle
* - `from`: Source account ID (must be owned by this wallet)
* - `to_keys`: Destination account keys
* - `to_identifier`: Identifier for the recipient's private account
* - `amount`: Amount to transfer as little-endian [u8; 16]
* - `out_result`: Output pointer for transfer result
*
@ -775,6 +811,7 @@ enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle,
enum WalletFfiError wallet_ffi_transfer_private(struct WalletHandle *handle,
const struct FfiBytes32 *from,
const struct FfiPrivateAccountKeys *to_keys,
const struct FfiU128 *to_identifier,
const uint8_t (*amount)[16],
struct FfiTransferResult *out_result);

View File

@ -19,7 +19,8 @@
},
{
"Private": {
"account_id": "9DGDXnrNo4QhUUb2F8WDuDrPESja3eYDkZG5HkzvAvMC",
"account_id": "GoKB6RuE6pT2KxCqDXQqiCuuuYZaGdJNfctzyqRdGBCy",
"identifier": 0,
"account": {
"program_owner": [
0,
@ -214,7 +215,8 @@
},
{
"Private": {
"account_id": "A6AT9UvsgitUi8w4BH43n6DyX1bK37DtSCfjEWXQQUrQ",
"account_id": "BCdMnPkdH2DrVhe7cGdawkPU9iapsSboRvJpWX8pWnLq",
"identifier": 0,
"account": {
"program_owner": [
0,

View File

@ -7,7 +7,7 @@ use key_protocol::{
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
secret_holders::SeedHolder,
},
key_protocol_core::NSSAUserData,
key_protocol_core::{NSSAUserData, UserPrivateAccountData},
};
use log::debug;
use nssa::program::Program;
@ -70,15 +70,28 @@ impl WalletChainStore {
public_tree.insert(data.account_id, data.chain_index, data.data);
}
PersistentAccountData::Private(data) => {
private_tree.insert(data.account_id, data.chain_index, data.data);
let npk = data.data.value.0.nullifier_public_key;
let chain_index = data.chain_index;
for identifier in &data.identifiers {
let account_id = nssa::AccountId::from((&npk, *identifier));
private_tree
.account_id_map
.insert(account_id, chain_index.clone());
}
private_tree.key_map.insert(chain_index, data.data);
}
PersistentAccountData::Preconfigured(acc_data) => match acc_data {
InitialAccountData::Public(data) => {
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
}
InitialAccountData::Private(data) => {
private_init_acc_map
.insert(data.account_id, (data.key_chain, data.account));
private_init_acc_map.insert(
data.account_id(),
UserPrivateAccountData {
key_chain: data.key_chain,
accounts: vec![(data.identifier, data.account)],
},
);
}
},
}
@ -111,13 +124,20 @@ impl WalletChainStore {
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
}
InitialAccountData::Private(data) => {
let account_id = data.account_id();
let mut account = data.account;
// TODO: Program owner is only known after code is compiled and can't be set
// in the config. Therefore we overwrite it here on
// startup. Fix this when program id can be fetched
// from the node and queried from the wallet.
account.program_owner = Program::authenticated_transfer_program().id();
private_init_acc_map.insert(data.account_id, (data.key_chain, account));
private_init_acc_map.insert(
account_id,
UserPrivateAccountData {
key_chain: data.key_chain,
accounts: vec![(data.identifier, account)],
},
);
}
}
}
@ -170,28 +190,71 @@ impl WalletChainStore {
pub fn insert_private_account_data(
&mut self,
account_id: nssa::AccountId,
identifier: nssa_core::Identifier,
account: nssa_core::account::Account,
) {
debug!("inserting at address {account_id}, this account {account:?}");
let entry = self
// Update default accounts if present
if let Entry::Occupied(mut entry) = self
.user_data
.default_user_private_accounts
.entry(account_id)
.and_modify(|data| data.1 = account.clone());
{
let entry = entry.get_mut();
if let Some((_, acc)) = entry.accounts.iter_mut().find(|(id, _)| *id == identifier) {
*acc = account;
} else {
entry.accounts.push((identifier, account));
}
return;
}
if matches!(entry, Entry::Vacant(_)) {
self.user_data
// Otherwise update the private key tree
// Find the node by iterating all tree nodes for this account_id
let chain_index = self
.user_data
.private_key_tree
.account_id_map
.get(&account_id)
.cloned();
if let Some(chain_index) = chain_index {
// Node already in account_id_map — update its entry
if let Some(node) = self
.user_data
.private_key_tree
.account_id_map
.get(&account_id)
.map(|chain_index| {
.key_map
.get_mut(&chain_index)
{
if let Some((_, acc)) = node.value.1.iter_mut().find(|(id, _)| *id == identifier) {
*acc = account;
} else {
node.value.1.push((identifier, account));
}
}
} else {
// Node not yet in account_id_map — find it by checking all nodes
for (ci, node) in &mut self.user_data.private_key_tree.key_map {
let expected_id =
nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
if expected_id == account_id {
if let Some((_, acc)) =
node.value.1.iter_mut().find(|(id, _)| *id == identifier)
{
*acc = account;
} else {
node.value.1.push((identifier, account));
}
// Register in account_id_map
self.user_data
.private_key_tree
.key_map
.entry(chain_index.clone())
.and_modify(|data| data.value.1 = account)
});
.account_id_map
.insert(account_id, ci.clone());
break;
}
}
}
}
}
@ -199,7 +262,7 @@ impl WalletChainStore {
#[cfg(test)]
mod tests {
use key_protocol::key_management::key_tree::{
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, traits::KeyNode as _,
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
};
use super::*;
@ -228,7 +291,7 @@ mod tests {
data: public_data,
}),
PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate {
account_id: private_data.account_id(),
identifiers: vec![],
chain_index: ChainIndex::root(),
data: private_data,
})),

Some files were not shown because too many files have changed in this diff Show More