mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-13 19:49:29 +00:00
Merge branch 'main' into marvin/signature-bip340-fixes
This commit is contained in:
commit
a572817305
49
Cargo.lock
generated
49
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
"")
|
||||
|
||||
@ -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 "$@"
|
||||
|
||||
@ -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 owner’s 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]
|
||||
> We’ll simulate transferring to someone else by creating a new private account we own and treating it as if it belonged to another user.
|
||||
> We’ll 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 recipient’s 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
24
flake.lock
generated
@ -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": {
|
||||
|
||||
22
flake.nix
22
flake.nix
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
25
indexer_ffi/Cargo.toml
Normal 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
12
indexer_ffi/build.rs
Normal 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");
|
||||
}
|
||||
2
indexer_ffi/cbindgen.toml
Normal file
2
indexer_ffi/cbindgen.toml
Normal file
@ -0,0 +1,2 @@
|
||||
language = "C" # For increased compatibility
|
||||
no_includes = true
|
||||
76
indexer_ffi/indexer_ffi.h
Normal file
76
indexer_ffi/indexer_ffi.h
Normal 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);
|
||||
100
indexer_ffi/src/api/lifecycle.rs
Normal file
100
indexer_ffi/src/api/lifecycle.rs
Normal 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
|
||||
}
|
||||
14
indexer_ffi/src/api/memory.rs
Normal file
14
indexer_ffi/src/api/memory.rs
Normal 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) });
|
||||
}
|
||||
5
indexer_ffi/src/api/mod.rs
Normal file
5
indexer_ffi/src/api/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub use result::PointerResult;
|
||||
|
||||
pub mod lifecycle;
|
||||
pub mod memory;
|
||||
pub mod result;
|
||||
29
indexer_ffi/src/api/result.rs
Normal file
29
indexer_ffi/src/api/result.rs
Normal 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
22
indexer_ffi/src/errors.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
86
indexer_ffi/src/indexer.rs
Normal file
86
indexer_ffi/src/indexer.rs
Normal 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
8
indexer_ffi/src/lib.rs
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
220
integration_tests/src/setup.rs
Normal file
220
integration_tests/src/setup.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
296
integration_tests/src/test_context_ffi.rs
Normal file
296
integration_tests/src/test_context_ffi.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
287
integration_tests/tests/indexer_ffi.rs
Normal file
287
integration_tests/tests/indexer_ffi.rs
Normal 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(())
|
||||
}
|
||||
@ -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?;
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user