Merge branch 'main' into schouhy/adapt-sequencer-to-bedrock

This commit is contained in:
Sergio Chouhy 2026-01-26 11:16:36 -03:00
commit ded6e8123d
56 changed files with 2175 additions and 1158 deletions

View File

@ -99,7 +99,7 @@ jobs:
run: rustup install
- name: Install nextest
run: cargo install cargo-nextest
run: cargo install --locked cargo-nextest
- name: Run tests
env:

1111
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -79,9 +79,9 @@ base58 = "0.2.0"
itertools = "0.14.0"
url = "2.5.4"
common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
nomos-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
rocksdb = { version = "0.24.0", default-features = false, features = [
"snappy",

View File

@ -155,6 +155,8 @@ cargo install --path wallet --force
Run `wallet help` to check everything went well.
Some completion scripts exists, see the [completions](./completions/README.md) folder.
## Tutorial
This tutorial walks you through creating accounts and executing NSSA programs in both public and private contexts.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,5 +6,5 @@ edition = "2024"
[dependencies]
reqwest.workspace = true
anyhow.workspace = true
common-http-client.workspace = true
nomos-core.workspace = true
logos-blockchain-common-http-client.workspace = true
logos-blockchain-core.workspace = true

View File

@ -1,7 +1,6 @@
use anyhow::Result;
use common_http_client::CommonHttpClient;
pub use common_http_client::{BasicAuthCredentials, Error};
use nomos_core::mantle::SignedMantleTx;
pub use logos_blockchain_common_http_client::{BasicAuthCredentials, CommonHttpClient, Error};
use logos_blockchain_core::mantle::SignedMantleTx;
use reqwest::{Client, Url};
// Simple wrapper
@ -14,7 +13,7 @@ pub struct BedrockClient {
impl BedrockClient {
pub fn new(auth: Option<BasicAuthCredentials>, node_url: Url) -> Result<Self> {
let client = Client::builder()
//Add more fiedls if needed
//Add more fields if needed
.timeout(std::time::Duration::from_secs(60))
.build()?;

135
completions/README.md Normal file
View File

@ -0,0 +1,135 @@
# Wallet CLI Completion
Completion scripts for the LSSA `wallet` command.
## ZSH
Works with both vanilla zsh and oh-my-zsh.
### Features
- Full completion for all wallet subcommands
- Contextual option completion for each command
- Dynamic account ID completion via `wallet account list`
- Descriptions for all commands and options
Note that only accounts created by the user auto-complete.
Preconfigured accounts and accounts only with `/` (no number) are not completed.
e.g.:
```
▶ wallet account list
Preconfigured Public/Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw,
Preconfigured Public/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy,
Preconfigured Private/3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw,
Preconfigured Private/AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX,
/ Public/8DstRgMQrB2N9a7ymv98RDDbt8nctrP9ZzaNRSpKDZSu,
/0 Public/2gJJjtG9UivBGEhA1Jz6waZQx1cwfYupC5yvKEweHaeH,
/ Private/Bcv15B36bs1VqvQAdY6ZGFM1KioByNQQsB92KTNAx6u2
```
Only `Public/2gJJjtG9UivBGEhA1Jz6waZQx1cwfYupC5yvKEweHaeH` is used for completion.
### Supported Commands
| Command | Description |
|------------------------|-------------------------------------------------------------|
| `wallet auth-transfer` | Authenticated transfer (init, send) |
| `wallet chain-info` | Chain info queries (current-block-id, block, transaction) |
| `wallet account` | Account management (get, list, new, sync-private) |
| `wallet pinata` | Piñata faucet (claim) |
| `wallet token` | Token operations (new, send) |
| `wallet amm` | AMM operations (new, swap, add-liquidity, remove-liquidity) |
| `wallet check-health` | Health check |
### Installation
#### Vanilla Zsh
1. Create a completions directory:
```sh
mkdir -p ~/.zsh/completions
```
2. Copy the completion file:
```sh
cp ./zsh/_wallet ~/.zsh/completions/
```
3. Add to your `~/.zshrc` (before any `compinit` call, or add these lines if you don't have one):
```sh
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
```
4. Reload your shell:
```sh
exec zsh
```
#### Oh-My-Zsh
1. Create the plugin directory and copy the file:
```sh
mkdir -p ~/.oh-my-zsh/custom/plugins/wallet
cp _wallet ~/.oh-my-zsh/custom/plugins/wallet/
```
2. Add `wallet` to your plugins array in `~/.zshrc`:
```sh
plugins=(... wallet)
```
3. Reload your shell:
```sh
exec zsh
```
### Requirements
The completion script calls `wallet account list` to dynamically fetch account IDs. Ensure the `wallet` command is in your `$PATH`.
### Usage
```sh
# Main commands
wallet <TAB>
# Account subcommands
wallet account <TAB>
# Options for auth-transfer send
wallet auth-transfer send --<TAB>
# Account types when creating
wallet account new <TAB>
# Shows: public private
# Account IDs (fetched dynamically)
wallet account get --account-id <TAB>
# Shows: Public/... Private/...
```
## Troubleshooting
### Completions not appearing
1. Check that `compinit` is called in your `.zshrc`
2. Rebuild the completion cache:
```sh
rm -f ~/.zcompdump*
exec zsh
```
### Account IDs not completing
Ensure `wallet account list` works from your command line.

435
completions/zsh/_wallet Normal file
View File

@ -0,0 +1,435 @@
#compdef wallet
# Zsh completion script for the wallet CLI
# See instructions in ../README.md
_wallet() {
local -a commands
local -a subcommands
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'(-c --continuous-run)'{-c,--continuous-run}'[Continuous run flag]' \
'--auth[Basic authentication in the format user or user\:password]:auth:' \
'1: :->command' \
'*:: :->args'
case $state in
command)
commands=(
'auth-transfer:Authenticated transfer subcommand'
'chain-info:Generic chain info subcommand'
'account:Account view and sync subcommand'
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM 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'
'deploy-program:Deploy a program'
'help:Print help message or the help of the given subcommand(s)'
)
_describe -t commands 'wallet commands' commands
;;
args)
case $line[1] in
auth-transfer)
_wallet_auth_transfer
;;
chain-info)
_wallet_chain_info
;;
account)
_wallet_account
;;
pinata)
_wallet_pinata
;;
token)
_wallet_token
;;
amm)
_wallet_amm
;;
config)
_wallet_config
;;
restore-keys)
_wallet_restore_keys
;;
deploy-program)
_wallet_deploy_program
;;
help)
_wallet_help
;;
esac
;;
esac
}
# auth-transfer subcommand
_wallet_auth_transfer() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'init:Initialize account under authenticated transfer program'
'send:Send native tokens from one account to another with variable privacy'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'auth-transfer subcommands' subcommands
;;
args)
case $line[1] in
init)
_arguments \
'--account-id[Account ID to initialize]:account_id:_wallet_account_ids'
;;
send)
_arguments \
'--from[Source account ID]:from_account:_wallet_account_ids' \
'--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-ipk[Destination viewing public key (for foreign private accounts)]:ipk:' \
'--amount[Amount of native tokens to send]:amount:'
;;
esac
;;
esac
}
# chain-info subcommand
_wallet_chain_info() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'current-block-id:Get current block id from sequencer'
'block:Get block at id from sequencer'
'transaction:Get transaction at hash from sequencer'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'chain-info subcommands' subcommands
;;
args)
case $line[1] in
block)
_arguments \
'--id[Block ID to retrieve]:block_id:'
;;
transaction)
_arguments \
'--hash[Transaction hash to retrieve]:tx_hash:'
;;
esac
;;
esac
}
# account subcommand
_wallet_account() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'get:Get account data'
'list:List all accounts'
'ls:List all accounts (alias for list)'
'new:Produce new public or private account'
'sync-private:Sync private accounts'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'account subcommands' subcommands
;;
args)
case $line[1] in
get)
_arguments \
'(-r --raw)'{-r,--raw}'[Get raw account data]' \
'(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/ipk for private accounts)]' \
'(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids'
;;
list|ls)
_arguments \
'(-l --long)'{-l,--long}'[Display detailed account information]'
;;
new)
_arguments -C \
'1: :->account_type' \
'*:: :->new_args'
case $state in
account_type)
compadd public private
;;
new_args)
_arguments \
'--cci[Chain index of a parent node]:chain_index:'
;;
esac
;;
esac
;;
esac
}
# pinata subcommand
_wallet_pinata() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'claim:Claim tokens from the Piñata faucet'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'pinata subcommands' subcommands
;;
args)
case $line[1] in
claim)
_arguments \
'--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids'
;;
esac
;;
esac
}
# token subcommand
_wallet_token() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'new:Produce a new token'
'send:Send tokens from one account to another with variable privacy'
'burn:Burn tokens on holder, modify definition'
'mint:Mint tokens on holder, modify definition'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'token subcommands' subcommands
;;
args)
case $line[1] in
new)
_arguments \
'--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' \
'--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids'
;;
send)
_arguments \
'--from[Source holding account ID]:from_account:_wallet_account_ids' \
'--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-ipk[Destination viewing public key (for foreign private accounts)]:ipk:' \
'--amount[Amount of tokens to send]:amount:'
;;
burn)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--holder[Holder account ID]:holder_account:_wallet_account_ids' \
'--amount[Amount of tokens to burn]:amount:'
;;
mint)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \
'--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \
'--holder-ipk[Holder viewing public key (for foreign private accounts)]:ipk:' \
'--amount[Amount of tokens to mint]:amount:'
;;
esac
;;
esac
}
# amm subcommand
_wallet_amm() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'new:Create a new liquidity pool'
'swap:Swap tokens using the AMM'
'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)'
)
_describe -t subcommands 'amm subcommands' subcommands
;;
args)
case $line[1] in
new)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--balance-a[Amount of token A to deposit]:balance_a:' \
'--balance-b[Amount of token B to deposit]:balance_b:'
;;
swap)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--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:'
;;
add-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--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:'
;;
remove-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--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:'
;;
esac
;;
esac
}
# config subcommand
_wallet_config() {
local -a subcommands
local -a config_keys
config_keys=(
'all'
'override_rust_log'
'sequencer_addr'
'seq_poll_timeout_millis'
'seq_tx_poll_max_blocks'
'seq_poll_max_retries'
'seq_block_poll_max_amount'
'initial_accounts'
'basic_auth'
)
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'get:Getter of config fields'
'set:Setter of config fields'
'description:Prints description of corresponding field'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'config subcommands' subcommands
;;
args)
case $line[1] in
get|description)
compadd -a config_keys
;;
set)
_arguments \
'1:key:compadd -a config_keys' \
'2:value:'
;;
esac
;;
esac
}
# restore-keys subcommand
_wallet_restore_keys() {
_arguments \
'(-d --depth)'{-d,--depth}'[How deep in tree accounts may be]:depth:'
}
# deploy-program subcommand
_wallet_deploy_program() {
_arguments \
'1:binary filepath:_files'
}
# help subcommand
_wallet_help() {
local -a commands
commands=(
'auth-transfer:Authenticated transfer subcommand'
'chain-info:Generic chain info subcommand'
'account:Account view and sync subcommand'
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM 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'
'deploy-program:Deploy a program'
)
_describe -t commands 'wallet commands' commands
}
# Helper function to complete account IDs
# Uses `wallet account list` to get available accounts
# Only includes accounts with /N prefix (where N is a number)
_wallet_account_ids() {
local -a accounts
local line
# Try to get accounts from wallet account list command
# Filter to lines starting with /N (numbered accounts) and extract the account ID
if command -v wallet &>/dev/null; then
while IFS= read -r line; do
# Remove trailing comma if present and add to array
[[ -n "$line" ]] && accounts+=("${line%,}")
done < <(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}')
fi
# Provide type prefixes as fallback if command fails or returns nothing
if (( ${#accounts} == 0 )); then
compadd -S '' -- 'Public/' 'Private/'
return
fi
_multi_parts / accounts
}
_wallet "$@"

View File

@ -50,7 +50,7 @@ async fn main() {
wallet_core
.send_privacy_preserving_tx(
accounts,
&Program::serialize_instruction(greeting).unwrap(),
Program::serialize_instruction(greeting).unwrap(),
&program.into(),
)
.await

View File

@ -58,7 +58,7 @@ async fn main() {
wallet_core
.send_privacy_preserving_tx(
accounts,
&Program::serialize_instruction(instruction).unwrap(),
Program::serialize_instruction(instruction).unwrap(),
&program_with_dependencies,
)
.await

View File

@ -101,7 +101,7 @@ async fn main() {
wallet_core
.send_privacy_preserving_tx(
accounts,
&Program::serialize_instruction(instruction).unwrap(),
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
)
.await
@ -142,7 +142,7 @@ async fn main() {
wallet_core
.send_privacy_preserving_tx(
accounts,
&Program::serialize_instruction(instruction).unwrap(),
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
)
.await

View File

@ -11,11 +11,13 @@ use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::pinata::PinataProgramAgnosticSubcommand,
programs::{
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
},
};
#[test]
async fn claim_pinata_to_public_account() -> Result<()> {
async fn claim_pinata_to_existing_public_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let pinata_prize = 150;
@ -120,8 +122,26 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
anyhow::bail!("Expected RegisterAccount return value");
};
let winner_account_id_formatted = format_private_account_id(&winner_account_id.to_string());
// Initialize account under auth transfer program
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: winner_account_id_formatted.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let new_commitment = ctx
.wallet()
.get_private_account_commitment(&winner_account_id)
.context("Failed to get private account commitment")?;
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
// Claim pinata to the new private account
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: format_private_account_id(&winner_account_id.to_string()),
to: winner_account_id_formatted,
});
let pinata_balance_pre = ctx

View File

@ -235,16 +235,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
]],
);
let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(sender_npk.clone(), sender_ss),
(recipient_npk.clone(), recipient_ss),
],
&[sender_nsk],
&[Some(proof)],
vec![sender_nsk],
vec![Some(proof)],
&program.into(),
)
.unwrap();

View File

@ -24,8 +24,9 @@ risc0-binfmt = "3.0.2"
[dev-dependencies]
test_program_methods.workspace = true
hex-literal = "1.0.0"
env_logger.workspace = true
hex-literal = "1.0.0"
test-case = "3.3.1"
[features]
default = []

View File

@ -15,9 +15,8 @@ pub type Nonce = u128;
/// Account to be used both in public and private contexts
#[derive(
Clone, Default, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct Account {
pub program_owner: ProgramId,
pub balance: u128,
@ -25,8 +24,7 @@ pub struct Account {
pub nonce: Nonce,
}
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct AccountWithMetadata {
pub account: Account,
pub is_authorized: bool,
@ -45,6 +43,7 @@ impl AccountWithMetadata {
}
#[derive(
Debug,
Default,
Copy,
Clone,
@ -56,7 +55,7 @@ impl AccountWithMetadata {
BorshSerialize,
BorshDeserialize,
)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialOrd, Ord))]
#[cfg_attr(any(feature = "host", test), derive(PartialOrd, Ord))]
pub struct AccountId {
value: [u8; 32],
}

View File

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB
#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
pub struct Data(Vec<u8>);
impl Data {

View File

@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId};
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))]
pub struct NullifierPublicKey(pub [u8; 32]);
impl From<&NullifierPublicKey> for AccountId {

View File

@ -30,6 +30,20 @@ impl PdaSeed {
}
}
pub fn compute_authorized_pdas(
caller_program_id: Option<ProgramId>,
pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> {
caller_program_id
.map(|caller_program_id| {
pda_seeds
.iter()
.map(|pda_seed| AccountId::from((&caller_program_id, pda_seed)))
.collect()
})
.unwrap_or_default()
}
impl From<(&ProgramId, &PdaSeed)> for AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256};
@ -93,6 +107,13 @@ impl AccountPostState {
}
}
/// Creates a post state that requests ownership of the account
/// if the account's program owner is the default program ID.
pub fn new_claimed_if_default(account: Account) -> Self {
let claim = account.program_owner == DEFAULT_PROGRAM_ID;
Self { account, claim }
}
/// Returns `true` if this post state requests that the account
/// be claimed (owned) by the executing program.
pub fn requires_claim(&self) -> bool {
@ -108,6 +129,11 @@ impl AccountPostState {
pub fn account_mut(&mut self) -> &mut Account {
&mut self.account
}
/// Consumes the post state and returns the underlying account
pub fn into_account(self) -> Account {
self.account
}
}
#[derive(Serialize, Deserialize, Clone)]

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
account::AccountWithMetadata,
program::{InstructionData, ProgramId, ProgramOutput},
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
};
use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover};
@ -43,27 +43,44 @@ impl From<Program> for ProgramWithDependencies {
}
/// Generates a proof of the execution of a NSSA program inside the privacy preserving execution
/// circuit
/// circuit.
#[expect(clippy::too_many_arguments, reason = "TODO: fix later")]
pub fn execute_and_prove(
pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData,
visibility_mask: &[u8],
private_account_nonces: &[u128],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
pre_states: Vec<AccountWithMetadata>,
instruction_data: InstructionData,
visibility_mask: Vec<u8>,
private_account_nonces: Vec<u128>,
private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
private_account_nsks: Vec<NullifierSecretKey>,
private_account_membership_proofs: Vec<Option<MembershipProof>>,
program_with_dependencies: &ProgramWithDependencies,
) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> {
let mut program = &program_with_dependencies.program;
let dependencies = &program_with_dependencies.dependencies;
let mut instruction_data = instruction_data.clone();
let mut pre_states = pre_states.to_vec();
let ProgramWithDependencies {
program,
dependencies,
} = program_with_dependencies;
let mut env_builder = ExecutorEnv::builder();
let mut program_outputs = Vec::new();
for _i in 0..MAX_NUMBER_CHAINED_CALLS {
let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?;
let initial_call = ChainedCall {
program_id: program.id(),
instruction_data: instruction_data.clone(),
pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([(initial_call, program)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, program)) = chained_calls.pop_front() {
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded);
}
let inner_receipt = execute_and_prove_program(
program,
&chained_call.pre_states,
&chained_call.instruction_data,
)?;
let program_output: ProgramOutput = inner_receipt
.journal
@ -76,39 +93,23 @@ pub fn execute_and_prove(
// Prove circuit.
env_builder.add_assumption(inner_receipt);
// TODO: Remove when multi-chain calls are supported in the circuit
assert!(program_output.chained_calls.len() <= 1);
// TODO: Modify when multi-chain calls are supported in the circuit
if let Some(next_call) = program_output.chained_calls.first() {
program = dependencies
.get(&next_call.program_id)
for new_call in program_output.chained_calls.into_iter().rev() {
let next_program = dependencies
.get(&new_call.program_id)
.ok_or(NssaError::InvalidProgramBehavior)?;
instruction_data = next_call.instruction_data.clone();
// Build post states with metadata for next call
let mut post_states_with_metadata = Vec::new();
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states)
{
let mut post_with_metadata = pre.clone();
post_with_metadata.account = post.account().clone();
post_states_with_metadata.push(post_with_metadata);
}
pre_states = next_call.pre_states.clone();
} else {
break;
chained_calls.push_front((new_call, next_program));
}
chain_calls_counter += 1;
}
let circuit_input = PrivacyPreservingCircuitInput {
program_outputs,
visibility_mask: visibility_mask.to_vec(),
private_account_nonces: private_account_nonces.to_vec(),
private_account_keys: private_account_keys.to_vec(),
private_account_nsks: private_account_nsks.to_vec(),
private_account_membership_proofs: private_account_membership_proofs.to_vec(),
visibility_mask,
private_account_nonces,
private_account_keys,
private_account_nsks,
private_account_membership_proofs,
program_id: program_with_dependencies.program.id(),
};
@ -215,13 +216,13 @@ mod tests {
let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk());
let (output, proof) = execute_and_prove(
&[sender, recipient],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[0, 2],
&[0xdeadbeef],
&[(recipient_keys.npk(), shared_secret)],
&[],
&[None],
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![0xdeadbeef],
vec![(recipient_keys.npk(), shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
)
.unwrap();
@ -311,16 +312,16 @@ mod tests {
let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk());
let (output, proof) = execute_and_prove(
&[sender_pre.clone(), recipient],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![sender_pre.clone(), recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
],
&[sender_keys.nsk],
&[commitment_set.get_proof_for(&commitment_sender), None],
vec![sender_keys.nsk],
vec![commitment_set.get_proof_for(&commitment_sender), None],
&program.into(),
)
.unwrap();

View File

@ -226,6 +226,15 @@ mod tests {
}
}
pub fn changer_claimer() -> Self {
use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID};
Program {
id: CHANGER_CLAIMER_ID,
elf: CHANGER_CLAIMER_ELF.to_vec(),
}
}
pub fn noop() -> Self {
use test_program_methods::{NOOP_ELF, NOOP_ID};
@ -235,6 +244,17 @@ mod tests {
}
}
pub fn malicious_authorization_changer() -> Self {
use test_program_methods::{
MALICIOUS_AUTHORIZATION_CHANGER_ELF, MALICIOUS_AUTHORIZATION_CHANGER_ID,
};
Program {
id: MALICIOUS_AUTHORIZATION_CHANGER_ID,
elf: MALICIOUS_AUTHORIZATION_CHANGER_ELF.to_vec(),
}
}
pub fn modified_transfer_program() -> Self {
use test_program_methods::MODIFIED_TRANSFER_ELF;
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of

View File

@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest, digest::FixedOutput};
@ -119,7 +119,7 @@ impl PublicTransaction {
return Err(NssaError::MaxChainedCallsDepthExceeded);
}
// Check the `program_id` corresponds to a deployed program
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
@ -135,12 +135,14 @@ impl PublicTransaction {
chained_call.program_id, program_output
);
let authorized_pdas =
self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds);
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coinicide with the values in the public
// Check that the program output pre_states coincide with the values in the public
// state or with any modifications to those values during the chain of calls.
let expected_pre = state_diff
.get(&account_id)
@ -198,22 +200,23 @@ impl PublicTransaction {
chain_calls_counter += 1;
}
Ok(state_diff)
}
fn compute_authorized_pdas(
&self,
caller_program_id: &Option<ProgramId>,
pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> {
if let Some(caller_program_id) = caller_program_id {
pda_seeds
.iter()
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed)))
.collect()
} else {
HashSet::new()
// Check that all modified uninitialized accounts where claimed
for post in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some(post)
}) {
if post.program_owner == DEFAULT_PROGRAM_ID {
return Err(NssaError::InvalidProgramBehavior);
}
}
Ok(state_diff)
}
}

View File

@ -504,6 +504,7 @@ pub mod tests {
self.insert_program(Program::chain_caller());
self.insert_program(Program::amm());
self.insert_program(Program::claimer());
self.insert_program(Program::changer_claimer());
self
}
@ -865,13 +866,13 @@ pub mod tests {
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = circuit::execute_and_prove(
&[sender, recipient],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[0, 2],
&[0xdeadbeef],
&[(recipient_keys.npk(), shared_secret)],
&[],
&[None],
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![0xdeadbeef],
vec![(recipient_keys.npk(), shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
)
.unwrap();
@ -912,16 +913,16 @@ pub mod tests {
let epk_2 = EphemeralPublicKey::from_scalar(esk_2);
let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2],
&new_nonces,
&[
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
new_nonces.to_vec(),
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
],
&[sender_keys.nsk],
&[state.get_proof_for_commitment(&sender_commitment), None],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment), None],
&program.into(),
)
.unwrap();
@ -965,13 +966,13 @@ pub mod tests {
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 0],
&[new_nonce],
&[(sender_keys.npk(), shared_secret)],
&[sender_keys.nsk],
&[state.get_proof_for_commitment(&sender_commitment)],
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 0],
vec![new_nonce],
vec![(sender_keys.npk(), shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
)
.unwrap();
@ -1179,13 +1180,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(10u128).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(10u128).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1206,13 +1207,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(10u128).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(10u128).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1233,13 +1234,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(()).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(()).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1260,13 +1261,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(vec![0]).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(vec![0]).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1289,13 +1290,13 @@ pub mod tests {
let large_data: Vec<u8> = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1];
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(large_data).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(large_data).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.to_owned().into(),
);
@ -1316,13 +1317,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(()).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(()).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1352,13 +1353,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account_1, public_account_2],
&Program::serialize_instruction(()).unwrap(),
&[0, 0],
&[],
&[],
&[],
&[],
vec![public_account_1, public_account_2],
Program::serialize_instruction(()).unwrap(),
vec![0, 0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1379,13 +1380,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(()).unwrap(),
&[0],
&[],
&[],
&[],
&[],
vec![public_account],
Program::serialize_instruction(()).unwrap(),
vec![0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1415,13 +1416,13 @@ pub mod tests {
);
let result = execute_and_prove(
&[public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[0, 0],
&[],
&[],
&[],
&[],
vec![public_account_1, public_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![0, 0],
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1453,13 +1454,13 @@ pub mod tests {
// Setting only one visibility mask for a circuit execution with two pre_state accounts.
let visibility_mask = [0];
let result = execute_and_prove(
&[public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&visibility_mask,
&[],
&[],
&[],
&[],
vec![public_account_1, public_account_2],
Program::serialize_instruction(10u128).unwrap(),
visibility_mask.to_vec(),
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1486,11 +1487,11 @@ pub mod tests {
// Setting only one nonce for an execution with two private accounts.
let private_account_nonces = [0xdeadbeef1];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&private_account_nonces,
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
private_account_nonces.to_vec(),
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1500,8 +1501,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1530,13 +1531,13 @@ pub mod tests {
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
)];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
private_account_keys.to_vec(),
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1563,11 +1564,11 @@ pub mod tests {
// Setting no second commitment proof.
let private_account_membership_proofs = [Some((0, vec![]))];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1577,8 +1578,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&private_account_membership_proofs,
vec![sender_keys.nsk],
private_account_membership_proofs.to_vec(),
&program.into(),
);
@ -1605,11 +1606,11 @@ pub mod tests {
// Setting no auth key for an execution with one non default private accounts.
let private_account_nsks = [];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1619,8 +1620,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&private_account_nsks,
&[],
private_account_nsks.to_vec(),
vec![],
&program.into(),
);
@ -1663,13 +1664,13 @@ pub mod tests {
let private_account_nsks = [recipient_keys.nsk];
let private_account_membership_proofs = [Some((0, vec![]))];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&private_account_nsks,
&private_account_membership_proofs,
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
private_account_keys.to_vec(),
private_account_nsks.to_vec(),
private_account_membership_proofs.to_vec(),
&program.into(),
);
@ -1701,11 +1702,11 @@ pub mod tests {
);
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1715,8 +1716,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1749,11 +1750,11 @@ pub mod tests {
);
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1763,8 +1764,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1796,11 +1797,11 @@ pub mod tests {
);
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1810,8 +1811,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1843,11 +1844,11 @@ pub mod tests {
);
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1857,8 +1858,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1888,11 +1889,11 @@ pub mod tests {
);
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1902,8 +1903,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -1927,13 +1928,13 @@ pub mod tests {
let visibility_mask = [0, 3];
let result = execute_and_prove(
&[public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&visibility_mask,
&[],
&[],
&[],
&[],
vec![public_account_1, public_account_2],
Program::serialize_instruction(10u128).unwrap(),
visibility_mask.to_vec(),
vec![],
vec![],
vec![],
vec![],
&program.into(),
);
@ -1961,11 +1962,11 @@ pub mod tests {
// accounts.
let private_account_nonces = [0xdeadbeef1, 0xdeadbeef2, 0xdeadbeef3];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&private_account_nonces,
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
private_account_nonces.to_vec(),
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1975,8 +1976,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -2017,13 +2018,13 @@ pub mod tests {
),
];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&[1, 2],
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&[sender_keys.nsk],
&[Some((0, vec![]))],
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
vec![1, 2],
vec![0xdeadbeef1, 0xdeadbeef2],
private_account_keys.to_vec(),
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
@ -2053,11 +2054,11 @@ pub mod tests {
let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk];
let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))];
let result = execute_and_prove(
&[private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(),
&visibility_mask,
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1, private_account_2],
Program::serialize_instruction(10u128).unwrap(),
visibility_mask.to_vec(),
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(
sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -2067,8 +2068,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
),
],
&private_account_nsks,
&private_account_membership_proofs,
private_account_nsks.to_vec(),
private_account_membership_proofs.to_vec(),
&program.into(),
);
@ -2149,16 +2150,16 @@ pub mod tests {
let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))];
let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk());
let result = execute_and_prove(
&[private_account_1.clone(), private_account_1],
&Program::serialize_instruction(100u128).unwrap(),
&visibility_mask,
&[0xdeadbeef1, 0xdeadbeef2],
&[
vec![private_account_1.clone(), private_account_1],
Program::serialize_instruction(100u128).unwrap(),
visibility_mask.to_vec(),
vec![0xdeadbeef1, 0xdeadbeef2],
vec![
(sender_keys.npk(), shared_secret),
(sender_keys.npk(), shared_secret),
],
&private_account_nsks,
&private_account_membership_proofs,
private_account_nsks.to_vec(),
private_account_membership_proofs.to_vec(),
&program.into(),
);
@ -3941,8 +3942,9 @@ pub mod tests {
assert_eq!(to_post, expected_to_post);
}
#[test]
fn test_private_chained_call() {
#[test_case::test_case(1; "single call")]
#[test_case::test_case(2; "two calls")]
fn test_private_chained_call(number_of_calls: u32) {
// Arrange
let chain_caller = Program::chain_caller();
let auth_transfers = Program::authenticated_transfer_program();
@ -3978,7 +3980,7 @@ pub mod tests {
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
1,
number_of_calls,
None,
);
@ -3999,14 +4001,14 @@ pub mod tests {
let to_new_nonce = 0xdeadbeef2;
let from_expected_post = Account {
balance: initial_balance - amount,
balance: initial_balance - number_of_calls as u128 * amount,
nonce: from_new_nonce,
..from_account.account.clone()
};
let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post);
let to_expected_post = Account {
balance: amount,
balance: number_of_calls as u128 * amount,
nonce: to_new_nonce,
..to_account.account.clone()
};
@ -4014,13 +4016,13 @@ pub mod tests {
// Act
let (output, proof) = execute_and_prove(
&[to_account, from_account],
&Program::serialize_instruction(instruction).unwrap(),
&[1, 1],
&[from_new_nonce, to_new_nonce],
&[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
&[from_keys.nsk, to_keys.nsk],
&[
vec![to_account, from_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1, 1],
vec![from_new_nonce, to_new_nonce],
vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
vec![from_keys.nsk, to_keys.nsk],
vec![
state.get_proof_for_commitment(&from_commitment),
state.get_proof_for_commitment(&to_commitment),
],
@ -4255,13 +4257,13 @@ pub mod tests {
// Execute and prove the circuit with the authorized account but no commitment proof
let (output, proof) = execute_and_prove(
std::slice::from_ref(&authorized_account),
&Program::serialize_instruction(balance).unwrap(),
&[1],
&[nonce],
&[(private_keys.npk(), shared_secret)],
&[private_keys.nsk],
&[None],
vec![authorized_account],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![nonce],
vec![(private_keys.npk(), shared_secret)],
vec![private_keys.nsk],
vec![None],
&program.into(),
)
.unwrap();
@ -4308,13 +4310,13 @@ pub mod tests {
// Step 2: Execute claimer program to claim the account with authentication
let (output, proof) = execute_and_prove(
std::slice::from_ref(&authorized_account),
&Program::serialize_instruction(balance).unwrap(),
&[1],
&[nonce],
&[(private_keys.npk(), shared_secret)],
&[private_keys.nsk],
&[None],
vec![authorized_account.clone()],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![nonce],
vec![(private_keys.npk(), shared_secret)],
vec![private_keys.nsk],
vec![None],
&claimer_program.into(),
)
.unwrap();
@ -4356,16 +4358,174 @@ pub mod tests {
// Step 3: Try to execute noop program with authentication but without initialization
let res = execute_and_prove(
std::slice::from_ref(&account_metadata),
&Program::serialize_instruction(()).unwrap(),
&[1],
&[nonce2],
&[(private_keys.npk(), shared_secret2)],
&[private_keys.nsk],
&[None],
vec![account_metadata],
Program::serialize_instruction(()).unwrap(),
vec![1],
vec![nonce2],
vec![(private_keys.npk(), shared_secret2)],
vec![private_keys.nsk],
vec![None],
&noop_program.into(),
);
assert!(matches!(res, Err(NssaError::CircuitProvingError(_))));
}
#[test]
fn test_public_changer_claimer_no_data_change_no_claim_succeeds() {
let initial_data = [];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let account_id = AccountId::new([1; 32]);
let program_id = Program::changer_claimer().id();
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
// Account should remain default/unclaimed
assert_eq!(state.get_account_by_id(&account_id), Account::default());
}
#[test]
fn test_public_changer_claimer_data_change_no_claim_fails() {
let initial_data = [];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let account_id = AccountId::new([1; 32]);
let program_id = Program::changer_claimer().id();
// 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);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
#[test]
fn test_private_changer_claimer_no_data_change_no_claim_succeeds() {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
let result = execute_and_prove(
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1],
vec![2],
vec![(
sender_keys.npk(),
SharedSecretKey::new(&[3; 32], &sender_keys.ivk()),
)],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
}
#[test]
fn test_private_changer_claimer_data_change_no_claim_fails() {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
// 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);
let result = execute_and_prove(
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1],
vec![2],
vec![(
sender_keys.npk(),
SharedSecretKey::new(&[3; 32], &sender_keys.ivk()),
)],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
#[test]
fn test_malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
// Arrange
let malicious_program = Program::malicious_authorization_changer();
let auth_transfers = Program::authenticated_transfer_program();
let sender_keys = test_public_account_keys_1();
let recipient_keys = test_private_account_keys_1();
let sender_account = AccountWithMetadata::new(
Account {
program_owner: auth_transfers.id(),
balance: 100,
..Default::default()
},
false,
sender_keys.account_id(),
);
let recipient_account =
AccountWithMetadata::new(Account::default(), true, &recipient_keys.npk());
let recipient_commitment =
Commitment::new(&recipient_keys.npk(), &recipient_account.account);
let state = V02State::new_with_genesis_accounts(
&[(sender_account.account_id, sender_account.account.balance)],
std::slice::from_ref(&recipient_commitment),
)
.with_test_programs();
let balance_to_transfer = 10u128;
let instruction = (balance_to_transfer, auth_transfers.id());
let recipient_esk = [3; 32];
let recipient = SharedSecretKey::new(&recipient_esk, &recipient_keys.ivk());
let mut dependencies = HashMap::new();
dependencies.insert(auth_transfers.id(), auth_transfers);
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
let recipient_new_nonce = 0xdeadbeef1;
// Act - execute the malicious program - this should fail during proving
let result = execute_and_prove(
vec![sender_account, recipient_account],
Program::serialize_instruction(instruction).unwrap(),
vec![0, 1],
vec![recipient_new_nonce],
vec![(recipient_keys.npk(), recipient)],
vec![recipient_keys.nsk],
vec![state.get_proof_for_commitment(&recipient_commitment)],
&program_with_deps,
);
// Assert - should fail because the malicious program tries to manipulate is_authorized
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
}

View File

@ -77,7 +77,7 @@ fn main() {
instruction_words,
vec![pinata, winner],
vec![
AccountPostState::new(pinata_post),
AccountPostState::new_claimed_if_default(pinata_post),
AccountPostState::new(winner_post),
],
);

View File

@ -1,12 +1,18 @@
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
convert::Infallible,
};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
account::{Account, AccountId, AccountWithMetadata},
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof,
Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
encryption::Ciphertext,
program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution},
program::{
AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId,
ProgramOutput, validate_execution,
},
};
use risc0_zkvm::{guest::env, serde::to_vec};
@ -18,118 +24,224 @@ fn main() {
private_account_keys,
private_account_nsks,
private_account_membership_proofs,
mut program_id,
program_id,
} = env::read();
let mut pre_states: Vec<AccountWithMetadata> = Vec::new();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs);
let num_calls = program_outputs.len();
if num_calls > MAX_NUMBER_CHAINED_CALLS {
panic!("Max chained calls depth is exceeded");
}
let output = compute_circuit_output(
execution_state,
&visibility_mask,
&private_account_nonces,
&private_account_keys,
&private_account_nsks,
&private_account_membership_proofs,
);
let Some(last_program_call) = program_outputs.last() else {
panic!("Program outputs is empty")
};
env::commit(&output);
}
if !last_program_call.chained_calls.is_empty() {
panic!("Call stack is incomplete");
}
/// State of the involved accounts before and after program execution.
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
post_states: HashMap<AccountId, Account>,
}
for window in program_outputs.windows(2) {
let caller = &window[0];
let callee = &window[1];
if caller.chained_calls.len() > 1 {
panic!("Privacy Multi-chained calls are not supported yet");
}
// TODO: Modify when multi-chain calls are supported in the circuit
let Some(caller_chained_call) = &caller.chained_calls.first() else {
panic!("Expected chained call");
impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec<ProgramOutput>) -> Self {
let Some(first_output) = program_outputs.first() else {
panic!("No program outputs provided");
};
// Check that instruction data in caller is the instruction data in callee
if caller_chained_call.instruction_data != callee.instruction_data {
panic!("Invalid instruction data");
}
// Check that account pre_states in caller are the ones in calle
if caller_chained_call.pre_states != callee.pre_states {
panic!("Invalid pre states");
}
}
for (i, program_output) in program_outputs.iter().enumerate() {
let mut program_output = program_output.clone();
// Check that `program_output` is consistent with the execution of the corresponding
// program.
let program_output_words =
&to_vec(&program_output).expect("program_output must be serializable");
env::verify(program_id, program_output_words)
.expect("program output must match the program's execution");
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
if !validate_execution(
&program_output.pre_states,
&program_output.post_states,
let initial_call = ChainedCall {
program_id,
) {
panic!("Bad behaved program");
}
instruction_data: first_output.instruction_data.clone(),
pre_states: first_output.pre_states.clone(),
pda_seeds: Vec::new(),
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
// The invoked program claims the accounts with default program id.
for post in program_output
.post_states
.iter_mut()
.filter(|post| post.requires_claim())
{
// The invoked program can only claim accounts with default program id.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = program_id;
} else {
panic!("Cannot claim an initialized account")
let mut execution_state = ExecutionState {
pre_states: Vec::new(),
post_states: HashMap::new(),
};
let mut program_outputs_iter = program_outputs.into_iter();
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
assert!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
"Max chained calls depth is exceeded"
);
let Some(program_output) = program_outputs_iter.next() else {
panic!("Insufficient program outputs for chained calls");
};
// Check that instruction data in chained call is the instruction data in program output
assert_eq!(
chained_call.instruction_data, program_output.instruction_data,
"Mismatched instruction data between chained call and program output"
);
// Check that `program_output` is consistent with the execution of the corresponding
// program.
let program_output_words =
&to_vec(&program_output).expect("program_output must be serializable");
env::verify(chained_call.program_id, program_output_words).unwrap_or_else(
|_: Infallible| unreachable!("Infallible error is never constructed"),
);
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
let execution_valid = validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
);
assert!(execution_valid, "Bad behaved program");
for next_call in program_output.chained_calls.iter().rev() {
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
}
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
execution_state.validate_and_sync_states(
chained_call.program_id,
authorized_pdas,
program_output.pre_states,
program_output.post_states,
);
chain_calls_counter += 1;
}
for (pre, post) in program_output
assert!(
program_outputs_iter.next().is_none(),
"Inner call without a chained call found",
);
// Check that all modified uninitialized accounts were claimed
for (account_id, post) in execution_state
.pre_states
.iter()
.zip(&program_output.post_states)
.filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID)
.map(|a| {
let post = execution_state
.post_states
.get(&a.account_id)
.expect("Post state must exist for pre state");
(a, post)
})
.filter(|(pre_default, post)| pre_default.account != **post)
.map(|(pre, post)| (pre.account_id, post))
{
if let Some(account_pre) = state_diff.get(&pre.account_id) {
if account_pre != &pre.account {
panic!("Invalid input");
}
} else {
pre_states.push(pre.clone());
}
state_diff.insert(pre.account_id, post.account().clone());
assert_ne!(
post.program_owner, DEFAULT_PROGRAM_ID,
"Account {account_id:?} was modified but not claimed"
);
}
// TODO: Modify when multi-chain calls are supported in the circuit
if let Some(next_chained_call) = &program_output.chained_calls.first() {
program_id = next_chained_call.program_id;
} else if i != program_outputs.len() - 1 {
panic!("Inner call without a chained call found")
};
execution_state
}
let n_accounts = pre_states.len();
if visibility_mask.len() != n_accounts {
panic!("Invalid visibility mask length");
/// Validate program pre and post states and populate the execution state.
fn validate_and_sync_states(
&mut self,
program_id: ProgramId,
authorized_pdas: HashSet<AccountId>,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
for (pre, mut post) in pre_states.into_iter().zip(post_states) {
let pre_account_id = pre.account_id;
let post_states_entry = self.post_states.entry(pre.account_id);
match &post_states_entry {
Entry::Occupied(occupied) => {
// Ensure that new pre state is the same as known post state
assert_eq!(
occupied.get(),
&pre.account,
"Inconsistent pre state for account {pre_account_id:?}",
);
let previous_is_authorized = self
.pre_states
.iter()
.find(|acc| acc.account_id == pre_account_id)
.map(|acc| acc.is_authorized)
.unwrap_or_else(|| {
panic!(
"Pre state must exist in execution state for account {pre_account_id:?}",
)
});
let is_authorized =
previous_is_authorized || authorized_pdas.contains(&pre_account_id);
assert_eq!(
pre.is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id:?}",
);
}
Entry::Vacant(_) => {
self.pre_states.push(pre);
}
}
if post.requires_claim() {
// The invoked program can only claim accounts with default program id.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = program_id;
} else {
panic!("Cannot claim an initialized account {pre_account_id:?}");
}
}
post_states_entry.insert_entry(post.into_account());
}
}
// These lists will be the public outputs of this circuit
// and will be populated next.
let mut public_pre_states: Vec<AccountWithMetadata> = Vec::new();
let mut public_post_states: Vec<Account> = Vec::new();
let mut ciphertexts: Vec<Ciphertext> = Vec::new();
let mut new_commitments: Vec<Commitment> = Vec::new();
let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new();
/// Get an iterator over pre and post states of each account involved in the execution.
pub fn into_states_iter(
mut self,
) -> impl ExactSizeIterator<Item = (AccountWithMetadata, Account)> {
self.pre_states.into_iter().map(move |pre| {
let post = self
.post_states
.remove(&pre.account_id)
.expect("Account from pre states should exist in state diff");
(pre, post)
})
}
}
fn compute_circuit_output(
execution_state: ExecutionState,
visibility_mask: &[u8],
private_account_nonces: &[Nonce],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
) -> PrivacyPreservingCircuitOutput {
let mut output = PrivacyPreservingCircuitOutput {
public_pre_states: Vec::new(),
public_post_states: Vec::new(),
ciphertexts: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
};
let states_iter = execution_state.into_states_iter();
assert_eq!(
visibility_mask.len(),
states_iter.len(),
"Invalid visibility mask length"
);
let mut private_nonces_iter = private_account_nonces.iter();
let mut private_keys_iter = private_account_keys.iter();
@ -137,141 +249,156 @@ fn main() {
let mut private_membership_proofs_iter = private_account_membership_proofs.iter();
let mut output_index = 0;
for i in 0..n_accounts {
match visibility_mask[i] {
for (visibility_mask, (pre_state, post_state)) in
visibility_mask.iter().copied().zip(states_iter)
{
match visibility_mask {
0 => {
// Public account
public_pre_states.push(pre_states[i].clone());
let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone();
if post.program_owner == DEFAULT_PROGRAM_ID {
// Claim account
post.program_owner = program_id;
}
public_post_states.push(post);
output.public_pre_states.push(pre_state);
output.public_post_states.push(post_state);
}
1 | 2 => {
let new_nonce = private_nonces_iter.next().expect("Missing private nonce");
let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys");
let Some((npk, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
if AccountId::from(npk) != pre_states[i].account_id {
panic!("AccountId mismatch");
}
assert_eq!(
AccountId::from(npk),
pre_state.account_id,
"AccountId mismatch"
);
if visibility_mask[i] == 1 {
let new_nullifier = if visibility_mask == 1 {
// Private account with authentication
let nsk = private_nsks_iter.next().expect("Missing nsk");
let Some(nsk) = private_nsks_iter.next() else {
panic!("Missing private account nullifier secret key");
};
// Verify the nullifier public key
let expected_npk = NullifierPublicKey::from(nsk);
if &expected_npk != npk {
panic!("Nullifier public key mismatch");
}
assert_eq!(
npk,
&NullifierPublicKey::from(nsk),
"Nullifier public key mismatch"
);
// Check pre_state authorization
if !pre_states[i].is_authorized {
panic!("Pre-state not authorized");
}
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
let membership_proof_opt = private_membership_proofs_iter
.next()
.expect("Missing membership proof");
let (nullifier, set_digest) = membership_proof_opt
.as_ref()
.map(|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, &pre_states[i].account);
let set_digest =
compute_digest_for_path(&commitment_pre, membership_proof);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
// Compute update nullifier
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
})
.unwrap_or_else(|| {
if pre_states[i].account != Account::default() {
panic!("Found new private account with non default values.");
}
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
(nullifier, DUMMY_COMMITMENT_HASH)
});
new_nullifiers.push((nullifier, set_digest));
compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
nsk,
)
} else {
// Private account without authentication
if pre_states[i].account != Account::default() {
panic!("Found new private account with non default values.");
}
if pre_states[i].is_authorized {
panic!("Found new private account marked as authorized.");
}
assert_eq!(
pre_state.account,
Account::default(),
"Found new private account with non default values",
);
assert!(
!pre_state.is_authorized,
"Found new private account marked as authorized."
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
let membership_proof_opt = private_membership_proofs_iter
.next()
.expect("Missing membership proof");
assert!(
membership_proof_opt.is_none(),
"Membership proof must be None for unauthorized accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
new_nullifiers.push((nullifier, DUMMY_COMMITMENT_HASH));
}
(nullifier, DUMMY_COMMITMENT_HASH)
};
output.new_nullifiers.push(new_nullifier);
// Update post-state with new nonce
let mut post_with_updated_values =
state_diff.get(&pre_states[i].account_id).unwrap().clone();
post_with_updated_values.nonce = *new_nonce;
if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID {
// Claim account
post_with_updated_values.program_owner = program_id;
}
let mut post_with_updated_nonce = post_state;
let Some(new_nonce) = private_nonces_iter.next() else {
panic!("Missing private account nonce");
};
post_with_updated_nonce.nonce = *new_nonce;
// Compute commitment
let commitment_post = Commitment::new(npk, &post_with_updated_values);
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
// Encrypt and push post state
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_values,
&post_with_updated_nonce,
shared_secret,
&commitment_post,
output_index,
);
new_commitments.push(commitment_post);
ciphertexts.push(encrypted_account);
output.new_commitments.push(commitment_post);
output.ciphertexts.push(encrypted_account);
output_index += 1;
}
_ => panic!("Invalid visibility mask value"),
}
}
if private_nonces_iter.next().is_some() {
panic!("Too many nonces");
}
assert!(private_nonces_iter.next().is_none(), "Too many nonces");
if private_keys_iter.next().is_some() {
panic!("Too many private account keys");
}
assert!(
private_keys_iter.next().is_none(),
"Too many private account keys"
);
if private_nsks_iter.next().is_some() {
panic!("Too many private account authentication keys");
}
assert!(
private_nsks_iter.next().is_none(),
"Too many private account nullifier secret keys"
);
if private_membership_proofs_iter.next().is_some() {
panic!("Too many private account membership proofs");
}
assert!(
private_membership_proofs_iter.next().is_none(),
"Too many private account membership proofs"
);
let output = PrivacyPreservingCircuitOutput {
public_pre_states,
public_post_states,
ciphertexts,
new_commitments,
new_nullifiers,
};
env::commit(&output);
output
}
fn compute_nullifier_and_set_digest(
membership_proof_opt: Option<&MembershipProof>,
pre_account: &Account,
npk: &NullifierPublicKey,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
membership_proof_opt
.as_ref()
.map(|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
})
.unwrap_or_else(|| {
assert_eq!(
*pre_account,
Account::default(),
"Found new private account with non default values"
);
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
(nullifier, DUMMY_COMMITMENT_HASH)
})
}

View File

@ -18,8 +18,8 @@ tempfile.workspace = true
chrono.workspace = true
log.workspace = true
bedrock_client.workspace = true
key-management-system-service.workspace = true
nomos-core.workspace=true
logos-blockchain-key-management-system-service.workspace = true
logos-blockchain-core.workspace = true
rand.workspace = true
reqwest.workspace = true
borsh.workspace = true

View File

@ -3,11 +3,13 @@ use std::{fs, path::Path};
use anyhow::{Result, anyhow};
use bedrock_client::BedrockClient;
use common::block::HashableBlockData;
use key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key, Ed25519PublicKey};
use nomos_core::mantle::{
use logos_blockchain_core::mantle::{
MantleTx, Op, OpProof, SignedMantleTx, Transaction, TxHash, ledger,
ops::channel::{ChannelId, MsgId, inscribe::InscriptionOp},
};
use logos_blockchain_key_management_system_service::keys::{
ED25519_SECRET_KEY_SIZE, Ed25519Key, Ed25519PublicKey,
};
use crate::config::BedrockConfig;
@ -63,7 +65,9 @@ impl BlockSettlementClient {
.sign_payload(tx_hash.as_signing_bytes().as_ref())
.to_bytes();
let signature =
key_management_system_service::keys::Ed25519Signature::from_bytes(&signature_bytes);
logos_blockchain_key_management_system_service::keys::Ed25519Signature::from_bytes(
&signature_bytes,
);
let signed_mantle_tx = SignedMantleTx {
ops_proofs: vec![OpProof::Ed25519Sig(signature)],
@ -103,7 +107,9 @@ fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
}
}
fn empty_ledger_signature(tx_hash: &TxHash) -> key_management_system_service::keys::ZkSignature {
key_management_system_service::keys::ZkKey::multi_sign(&[], tx_hash.as_ref())
fn empty_ledger_signature(
tx_hash: &TxHash,
) -> logos_blockchain_key_management_system_service::keys::ZkSignature {
logos_blockchain_key_management_system_service::keys::ZkKey::multi_sign(&[], tx_hash.as_ref())
.expect("multi-sign with empty key set works")
}

View File

@ -0,0 +1,38 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = (Option<Vec<u8>>, bool);
/// A program that optionally modifies the account data and optionally claims it.
fn main() {
let (
ProgramInput {
pre_states,
instruction: (data_opt, should_claim),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [pre] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let account_pre = &pre.account;
let mut account_post = account_pre.clone();
// Update data if provided
if let Some(data) = data_opt {
account_post.data = data
.try_into()
.expect("provided data should fit into data limit");
}
// Claim or not based on the boolean flag
let post_state = if should_claim {
AccountPostState::new_claimed(account_post)
} else {
AccountPostState::new(account_post)
};
write_nssa_outputs(instruction_words, vec![pre], vec![post_state]);
}

View File

@ -0,0 +1,53 @@
use nssa_core::{
account::AccountWithMetadata,
program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
},
};
use risc0_zkvm::serde::to_vec;
type Instruction = (u128, ProgramId);
/// A malicious test program that attempts to change authorization status.
/// It accepts two accounts and executes a native token transfer program via chain call,
/// but sets the `is_authorized` field of the first account to true.
fn main() {
let (
ProgramInput {
pre_states,
instruction: (balance, transfer_program_id),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [sender, receiver] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
// Maliciously set is_authorized to true for the first account
let authorised_sender = AccountWithMetadata {
is_authorized: true,
..sender.clone()
};
let instruction_data = to_vec(&balance).unwrap();
let chained_call = ChainedCall {
program_id: transfer_program_id,
instruction_data,
pre_states: vec![authorised_sender.clone(), receiver.clone()],
pda_seeds: vec![],
};
write_nssa_outputs_with_chained_call(
instruction_words,
vec![sender.clone(), receiver.clone()],
vec![
AccountPostState::new(sender.account),
AccountPostState::new(receiver.account),
],
vec![chained_call],
);
}

View File

@ -3,7 +3,7 @@ use base58::ToBase58;
use clap::Subcommand;
use itertools::Itertools as _;
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use nssa::{Account, program::Program};
use nssa::{Account, PublicKey, program::Program};
use serde::Serialize;
use crate::{
@ -20,6 +20,9 @@ pub enum AccountSubcommand {
/// Flag to get raw account data
#[arg(short, long)]
raw: bool,
/// Display keys (pk for public accounts, npk/ipk for private accounts)
#[arg(short, long)]
keys: bool,
/// Valid 32 byte base58 string with privacy prefix
#[arg(short, long)]
account_id: String,
@ -64,9 +67,18 @@ impl WalletSubcommand for NewSubcommand {
NewSubcommand::Public { cci } => {
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
let private_key = wallet_core
.storage
.user_data
.get_pub_account_signing_key(&account_id)
.unwrap();
let public_key = PublicKey::new_from_private_key(private_key);
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
println!("With pk {}", hex::encode(public_key.value()));
wallet_core.store_persistent_data().await?;
@ -201,7 +213,11 @@ impl WalletSubcommand for AccountSubcommand {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
AccountSubcommand::Get { raw, account_id } => {
AccountSubcommand::Get {
raw,
keys,
account_id,
} => {
let (account_id, addr_kind) = parse_addr_with_privacy_prefix(&account_id)?;
let account_id = account_id.parse()?;
@ -215,9 +231,43 @@ impl WalletSubcommand for AccountSubcommand {
.ok_or(anyhow::anyhow!("Private account not found in storage"))?,
};
// Helper closure to display keys for the account
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
match addr_kind {
AccountPrivacyKind::Public => {
let private_key = wallet_core
.storage
.user_data
.get_pub_account_signing_key(&account_id)
.ok_or(anyhow::anyhow!("Public account not found in storage"))?;
let public_key = PublicKey::new_from_private_key(private_key);
println!("pk {}", hex::encode(public_key.value()));
}
AccountPrivacyKind::Private => {
let (key, _) = wallet_core
.storage
.user_data
.get_private_account(&account_id)
.ok_or(anyhow::anyhow!("Private account not found in storage"))?;
println!("npk {}", hex::encode(key.nullifer_public_key.0));
println!(
"ipk {}",
hex::encode(key.incoming_viewing_public_key.to_bytes())
);
}
}
Ok(())
};
if account == Account::default() {
println!("Account is Uninitialized");
if keys {
display_keys(wallet_core)?;
}
return Ok(SubcommandReturnValue::Empty);
}
@ -232,6 +282,10 @@ impl WalletSubcommand for AccountSubcommand {
println!("{description}");
println!("{json_view}");
if keys {
display_keys(wallet_core)?;
}
Ok(SubcommandReturnValue::Empty)
}
AccountSubcommand::New(new_subcommand) => {
@ -288,7 +342,7 @@ impl WalletSubcommand for AccountSubcommand {
.iter()
.map(|(id, chain_index)| format!("{chain_index} Private/{id}")),
)
.format(",\n");
.format("\n");
println!("{accounts}");
return Ok(SubcommandReturnValue::Empty);

View File

@ -19,7 +19,7 @@ pub enum ChainSubcommand {
/// Get transaction at hash from sequencer
Transaction {
/// hash - valid 32 byte hex string
#[arg(short, long)]
#[arg(short = 't', long)]
hash: String,
},
}

View File

@ -375,7 +375,7 @@ impl WalletCore {
pub async fn send_privacy_preserving_tx(
&self,
accounts: Vec<PrivacyPreservingAccount>,
instruction_data: &InstructionData,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
@ -387,7 +387,7 @@ impl WalletCore {
pub async fn send_privacy_preserving_tx_with_pre_check(
&self,
accounts: Vec<PrivacyPreservingAccount>,
instruction_data: &InstructionData,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
@ -403,16 +403,16 @@ impl WalletCore {
let private_account_keys = acc_manager.private_account_keys();
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
&pre_states,
pre_states,
instruction_data,
acc_manager.visibility_mask(),
&produce_random_nonces(private_account_keys.len()),
&private_account_keys
acc_manager.visibility_mask().to_vec(),
produce_random_nonces(private_account_keys.len()),
private_account_keys
.iter()
.map(|keys| (keys.npk.clone(), keys.ssk))
.collect::<Vec<_>>(),
&acc_manager.private_account_auth(),
&acc_manager.private_account_membership_proofs(),
acc_manager.private_account_auth(),
acc_manager.private_account_membership_proofs(),
&program.to_owned(),
)
.unwrap();

View File

@ -19,7 +19,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateOwned(from),
PrivacyPreservingAccount::Public(to),
],
&instruction_data,
instruction_data,
&program.into(),
tx_pre_check,
)

View File

@ -17,7 +17,7 @@ impl NativeTokenTransfer<'_> {
self.0
.send_privacy_preserving_tx(
vec![PrivacyPreservingAccount::PrivateOwned(from)],
&Program::serialize_instruction(instruction).unwrap(),
Program::serialize_instruction(instruction).unwrap(),
&Program::authenticated_transfer_program().into(),
)
.await
@ -46,7 +46,7 @@ impl NativeTokenTransfer<'_> {
ipk: to_ipk,
},
],
&instruction_data,
instruction_data,
&program.into(),
tx_pre_check,
)
@ -73,7 +73,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateOwned(from),
PrivacyPreservingAccount::PrivateOwned(to),
],
&instruction_data,
instruction_data,
&program.into(),
tx_pre_check,
)

View File

@ -20,7 +20,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::Public(from),
PrivacyPreservingAccount::PrivateOwned(to),
],
&instruction_data,
instruction_data,
&program.into(),
tx_pre_check,
)
@ -52,7 +52,7 @@ impl NativeTokenTransfer<'_> {
ipk: to_ipk,
},
],
&instruction_data,
instruction_data,
&program.into(),
tx_pre_check,
)

View File

@ -37,7 +37,7 @@ impl Pinata<'_> {
PrivacyPreservingAccount::Public(pinata_account_id),
PrivacyPreservingAccount::PrivateOwned(winner_account_id),
],
&nssa::program::Program::serialize_instruction(solution).unwrap(),
nssa::program::Program::serialize_instruction(solution).unwrap(),
&nssa::program::Program::pinata().into(),
)
.await

View File

@ -52,7 +52,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -82,7 +82,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(supply_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -112,7 +112,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -180,7 +180,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -212,7 +212,7 @@ impl Token<'_> {
ipk: recipient_ipk,
},
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -240,7 +240,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
PrivacyPreservingAccount::Public(recipient_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -269,7 +269,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(sender_account_id),
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -302,7 +302,7 @@ impl Token<'_> {
ipk: recipient_ipk,
},
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -365,7 +365,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -393,7 +393,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -422,7 +422,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -491,7 +491,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -523,7 +523,7 @@ impl Token<'_> {
ipk: holder_ipk,
},
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -551,7 +551,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -580,7 +580,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await
@ -613,7 +613,7 @@ impl Token<'_> {
ipk: holder_ipk,
},
],
&instruction_data,
instruction_data,
&Program::token().into(),
)
.await