mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-13 19:49:29 +00:00
refactor: use system faucet and vaults to supply accounts from genesis
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
7f6fffe6cb
commit
f3e807de3f
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -3959,6 +3959,7 @@ dependencies = [
|
||||
"token_core",
|
||||
"tokio",
|
||||
"url",
|
||||
"vault_core",
|
||||
"wallet",
|
||||
"wallet-ffi",
|
||||
]
|
||||
@ -7062,6 +7063,7 @@ dependencies = [
|
||||
"serde",
|
||||
"token_core",
|
||||
"token_program",
|
||||
"vault_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8416,7 +8418,6 @@ name = "sequencer_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
@ -8440,6 +8441,7 @@ dependencies = [
|
||||
"testnet_initial_state",
|
||||
"tokio",
|
||||
"url",
|
||||
"vault_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -10077,6 +10079,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vault_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
||||
@ -21,6 +21,7 @@ members = [
|
||||
"programs/associated_token_account/core",
|
||||
"programs/associated_token_account",
|
||||
"programs/authenticated_transfer/core",
|
||||
"programs/vault/core",
|
||||
"sequencer/core",
|
||||
"sequencer/service",
|
||||
"sequencer/service/protocol",
|
||||
@ -67,6 +68,7 @@ amm_program = { path = "programs/amm" }
|
||||
ata_core = { path = "programs/associated_token_account/core" }
|
||||
ata_program = { path = "programs/associated_token_account" }
|
||||
authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
|
||||
vault_core = { path = "programs/vault/core" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
testnet_initial_state = { path = "testnet_initial_state" }
|
||||
|
||||
|
||||
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.
BIN
artifacts/program_methods/vault.bin
Normal file
BIN
artifacts/program_methods/vault.bin
Normal file
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.
@ -67,7 +67,8 @@ impl NSSATransaction {
|
||||
}
|
||||
|
||||
/// Validates the transaction against the current state and returns the resulting diff
|
||||
/// without applying it. Rejects transactions that modify clock system accounts.
|
||||
/// without applying it. Rejects transactions that modify clock system accounts and
|
||||
/// rejects unsafe modifications of the system faucet account.
|
||||
pub fn validate_on_state(
|
||||
&self,
|
||||
state: &V03State,
|
||||
@ -98,6 +99,36 @@ impl NSSATransaction {
|
||||
));
|
||||
}
|
||||
|
||||
let faucet_account_id = nssa::SYSTEM_FAUCET_ACCOUNT_ID;
|
||||
if let Some(post_faucet) = public_diff.get(&faucet_account_id) {
|
||||
let pre_faucet = state.get_account_by_id(faucet_account_id);
|
||||
|
||||
let nssa::Account {
|
||||
program_owner: post_program_owner,
|
||||
data: post_data,
|
||||
nonce: post_nonce,
|
||||
balance: post_balance,
|
||||
} = post_faucet;
|
||||
|
||||
let nssa::Account {
|
||||
program_owner: pre_program_owner,
|
||||
data: pre_data,
|
||||
nonce: pre_nonce,
|
||||
balance: pre_balance,
|
||||
} = pre_faucet;
|
||||
|
||||
let faucet_change_is_allowed = *post_program_owner == pre_program_owner
|
||||
&& *post_data == pre_data
|
||||
&& *post_nonce == pre_nonce
|
||||
&& *post_balance >= pre_balance;
|
||||
|
||||
if !faucet_change_is_allowed {
|
||||
return Err(nssa::error::NssaError::InvalidInput(
|
||||
"Transaction modifies system faucet account".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
|
||||
@ -18,13 +18,13 @@
|
||||
"indexer_rpc_url": "ws://indexer_service:8779",
|
||||
"genesis": [
|
||||
{
|
||||
"supply_public_account": {
|
||||
"supply_account": {
|
||||
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||
"balance": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"supply_public_account": {
|
||||
"supply_account": {
|
||||
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||
"balance": 20000
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ use common::{
|
||||
use log::info;
|
||||
use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId};
|
||||
use logos_blockchain_zone_sdk::Slot;
|
||||
use nssa::{Account, AccountId, V03State, ValidatedStateDiff};
|
||||
use nssa::{Account, AccountId, V03State};
|
||||
use nssa_core::BlockId;
|
||||
use storage::indexer::RocksDBIO;
|
||||
use tokio::sync::RwLock;
|
||||
@ -160,12 +160,13 @@ impl IndexerStore {
|
||||
anyhow::bail!("Genesis block should contain only public transactions")
|
||||
}
|
||||
};
|
||||
let state_diff = ValidatedStateDiff::from_public_genesis_transaction(
|
||||
genesis_tx,
|
||||
&state_guard,
|
||||
)
|
||||
.context("Failed to create state diff from genesis transaction")?;
|
||||
state_guard.apply_state_diff(state_diff);
|
||||
state_guard
|
||||
.transition_from_public_transaction(
|
||||
genesis_tx,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
)
|
||||
.context("Failed to execute genesis public transaction")?;
|
||||
} else {
|
||||
transaction
|
||||
.clone()
|
||||
@ -202,39 +203,11 @@ impl IndexerStore {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::{HashType, block::HashableBlockData};
|
||||
use nssa::{AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PublicKey, PublicTransaction};
|
||||
use tempfile::tempdir;
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn acc1_sign_key() -> nssa::PrivateKey {
|
||||
nssa::PrivateKey::try_new([1; 32]).unwrap()
|
||||
}
|
||||
|
||||
fn acc2_sign_key() -> nssa::PrivateKey {
|
||||
nssa::PrivateKey::try_new([2; 32]).unwrap()
|
||||
}
|
||||
|
||||
fn acc1() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key()))
|
||||
}
|
||||
|
||||
fn acc2() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key()))
|
||||
}
|
||||
|
||||
fn genesis_mint_tx(account: AccountId, balance: u128) -> NSSATransaction {
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
nssa::program::Program::authenticated_transfer_program().id(),
|
||||
vec![account, CLOCK_01_PROGRAM_ACCOUNT_ID],
|
||||
vec![],
|
||||
authenticated_transfer_core::Instruction::Mint { amount: balance },
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set).into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correct_startup() {
|
||||
let home = tempdir().unwrap();
|
||||
@ -252,19 +225,18 @@ mod tests {
|
||||
|
||||
let storage = IndexerStore::open_db(home.as_ref()).unwrap();
|
||||
|
||||
let from = acc1();
|
||||
let to = acc2();
|
||||
let sign_key = acc1_sign_key();
|
||||
let initial_accounts = initial_pub_accounts_private_keys();
|
||||
let from = initial_accounts[0].account_id;
|
||||
let to = initial_accounts[1].account_id;
|
||||
let sign_key = initial_accounts[0].pub_sign_key.clone();
|
||||
|
||||
// Submit genesis block
|
||||
let clock_tx = NSSATransaction::Public(clock_invocation(0));
|
||||
let supply_from_tx = genesis_mint_tx(from, 10000);
|
||||
let supply_to_tx = genesis_mint_tx(to, 20000);
|
||||
let genesis_block_data = HashableBlockData {
|
||||
block_id: 1,
|
||||
prev_block_hash: HashType::default(),
|
||||
timestamp: 0,
|
||||
transactions: vec![supply_from_tx, supply_to_tx, clock_tx],
|
||||
transactions: vec![clock_tx],
|
||||
};
|
||||
let genesis_block = genesis_block_data.into_pending_block(
|
||||
&common::test_utils::sequencer_sign_key_for_testing(),
|
||||
@ -276,30 +248,29 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for i in 2..10 {
|
||||
for i in 0..10 {
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
from,
|
||||
i - 2,
|
||||
to,
|
||||
10,
|
||||
&sign_key,
|
||||
from, i, to, 10, &sign_key,
|
||||
);
|
||||
let block_id = u64::try_from(i).unwrap();
|
||||
let block_id = u64::try_from(i + 1).unwrap();
|
||||
|
||||
let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]);
|
||||
prev_hash = Some(next_block.header.hash);
|
||||
|
||||
storage
|
||||
.put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32]))
|
||||
.put_block(
|
||||
next_block,
|
||||
HeaderId::from([u8::try_from(i + 1).unwrap(); 32]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let acc1_val = storage.account_current_state(&acc1()).await.unwrap();
|
||||
let acc2_val = storage.account_current_state(&acc2()).await.unwrap();
|
||||
let acc1_val = storage.account_current_state(&from).await.unwrap();
|
||||
let acc2_val = storage.account_current_state(&to).await.unwrap();
|
||||
|
||||
assert_eq!(acc1_val.balance, 9920);
|
||||
assert_eq!(acc2_val.balance, 20080);
|
||||
assert_eq!(acc1_val.balance, 9900);
|
||||
assert_eq!(acc2_val.balance, 20100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -310,45 +281,45 @@ mod tests {
|
||||
|
||||
let mut prev_hash = None;
|
||||
|
||||
let from = acc1();
|
||||
let to = acc2();
|
||||
let sign_key = acc1_sign_key();
|
||||
let initial_accounts = initial_pub_accounts_private_keys();
|
||||
let from = initial_accounts[0].account_id;
|
||||
let to = initial_accounts[1].account_id;
|
||||
let sign_key = initial_accounts[0].pub_sign_key.clone();
|
||||
|
||||
for i in 2..10 {
|
||||
for i in 0..10 {
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
from,
|
||||
i - 2,
|
||||
to,
|
||||
10,
|
||||
&sign_key,
|
||||
from, i, to, 10, &sign_key,
|
||||
);
|
||||
let block_id = u64::try_from(i).unwrap();
|
||||
let block_id = u64::try_from(i + 1).unwrap();
|
||||
|
||||
let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]);
|
||||
prev_hash = Some(next_block.header.hash);
|
||||
|
||||
storage
|
||||
.put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32]))
|
||||
.put_block(
|
||||
next_block,
|
||||
HeaderId::from([u8::try_from(i + 1).unwrap(); 32]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Genesis block: no transfers applied yet.
|
||||
let acc1_at_1 = storage.account_state_at_block(&acc1(), 1).unwrap();
|
||||
let acc2_at_1 = storage.account_state_at_block(&acc2(), 1).unwrap();
|
||||
assert_eq!(acc1_at_1.balance, 10000);
|
||||
assert_eq!(acc2_at_1.balance, 20000);
|
||||
let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap();
|
||||
let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap();
|
||||
assert_eq!(acc1_at_1.balance, 9990);
|
||||
assert_eq!(acc2_at_1.balance, 20010);
|
||||
|
||||
// After block 5: 4 transfers of 10 applied (one each in blocks 2..=5).
|
||||
let acc1_at_5 = storage.account_state_at_block(&acc1(), 5).unwrap();
|
||||
let acc2_at_5 = storage.account_state_at_block(&acc2(), 5).unwrap();
|
||||
assert_eq!(acc1_at_5.balance, 9960);
|
||||
assert_eq!(acc2_at_5.balance, 20040);
|
||||
let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap();
|
||||
let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap();
|
||||
assert_eq!(acc1_at_5.balance, 9950);
|
||||
assert_eq!(acc2_at_5.balance, 20050);
|
||||
|
||||
// After final block 9: 8 transfers applied; should match current state.
|
||||
let acc1_at_9 = storage.account_state_at_block(&acc1(), 9).unwrap();
|
||||
let acc2_at_9 = storage.account_state_at_block(&acc2(), 9).unwrap();
|
||||
assert_eq!(acc1_at_9.balance, 9920);
|
||||
assert_eq!(acc2_at_9.balance, 20080);
|
||||
let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap();
|
||||
let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap();
|
||||
assert_eq!(acc1_at_9.balance, 9910);
|
||||
assert_eq!(acc2_at_9.balance, 20090);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ indexer_service.workspace = true
|
||||
serde_json.workspace = true
|
||||
token_core.workspace = true
|
||||
ata_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
indexer_service_rpc = { workspace = true, features = ["client"] }
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
jsonrpsee = { workspace = true, features = ["ws-client"] }
|
||||
|
||||
@ -3,14 +3,30 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
|
||||
use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use nssa::{AccountId, PrivateKey, PublicKey};
|
||||
use nssa_core::Identifier;
|
||||
use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig};
|
||||
use url::Url;
|
||||
use wallet::config::WalletConfig;
|
||||
|
||||
pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [20_000, 40_000];
|
||||
pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
|
||||
pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InitialPrivateAccountForWallet {
|
||||
pub key_chain: KeyChain,
|
||||
pub identifier: Identifier,
|
||||
pub balance: u128,
|
||||
}
|
||||
|
||||
impl InitialPrivateAccountForWallet {
|
||||
#[must_use]
|
||||
pub fn account_id(&self) -> AccountId {
|
||||
AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequencer config options available for custom changes in integration tests.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SequencerPartialConfig {
|
||||
@ -80,43 +96,58 @@ pub fn sequencer_config(
|
||||
|
||||
#[must_use]
|
||||
pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> {
|
||||
let mut first_private_key = PrivateKey::new_os_random();
|
||||
let first_public_key = PublicKey::new_from_private_key(&first_private_key);
|
||||
let mut first_account_id = AccountId::from(&first_public_key);
|
||||
let mut private_keys = vec![PrivateKey::new_os_random(), PrivateKey::new_os_random()];
|
||||
private_keys.sort_unstable_by_key(|private_key| {
|
||||
AccountId::from(&PublicKey::new_from_private_key(private_key))
|
||||
});
|
||||
|
||||
let mut second_private_key = PrivateKey::new_os_random();
|
||||
let second_public_key = PublicKey::new_from_private_key(&second_private_key);
|
||||
let mut second_account_id = AccountId::from(&second_public_key);
|
||||
|
||||
// Keep account ordering deterministic for tests that index into account lists.
|
||||
if first_account_id > second_account_id {
|
||||
std::mem::swap(&mut first_private_key, &mut second_private_key);
|
||||
std::mem::swap(&mut first_account_id, &mut second_account_id);
|
||||
}
|
||||
|
||||
vec![
|
||||
(first_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[0]),
|
||||
(second_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[1]),
|
||||
]
|
||||
private_keys
|
||||
.into_iter()
|
||||
.zip(INITIAL_PUBLIC_BALANCES_FOR_WALLET)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn genesis_from_public_accounts(
|
||||
public_accounts: &[(PrivateKey, u128)],
|
||||
) -> Vec<GenesisTransaction> {
|
||||
public_accounts
|
||||
.iter()
|
||||
.map(|(private_key, balance)| {
|
||||
let public_key = PublicKey::new_from_private_key(private_key);
|
||||
let account_id = AccountId::from(&public_key);
|
||||
GenesisTransaction::SupplyPublicAccount {
|
||||
account_id,
|
||||
balance: *balance,
|
||||
}
|
||||
pub fn default_private_accounts_for_wallet() -> Vec<InitialPrivateAccountForWallet> {
|
||||
let mut key_chains = vec![KeyChain::new_os_random(), KeyChain::new_os_random()];
|
||||
key_chains.sort_unstable();
|
||||
|
||||
key_chains
|
||||
.into_iter()
|
||||
.zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET)
|
||||
.map(|(key_chain, balance)| InitialPrivateAccountForWallet {
|
||||
key_chain,
|
||||
identifier: 0,
|
||||
balance,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn genesis_from_accounts(
|
||||
public_accounts: &[(PrivateKey, u128)],
|
||||
private_accounts: &[InitialPrivateAccountForWallet],
|
||||
) -> Vec<GenesisTransaction> {
|
||||
let public_genesis = public_accounts.iter().map(|(private_key, balance)| {
|
||||
let public_key = PublicKey::new_from_private_key(private_key);
|
||||
let account_id = AccountId::from(&public_key);
|
||||
GenesisTransaction::SupplyAccount {
|
||||
account_id,
|
||||
balance: *balance,
|
||||
}
|
||||
});
|
||||
|
||||
let private_genesis =
|
||||
private_accounts
|
||||
.iter()
|
||||
.map(|account| GenesisTransaction::SupplyAccount {
|
||||
account_id: account.account_id(),
|
||||
balance: account.balance,
|
||||
});
|
||||
|
||||
public_genesis.chain(private_genesis).collect()
|
||||
}
|
||||
|
||||
pub fn wallet_config(sequencer_addr: SocketAddr) -> Result<WalletConfig> {
|
||||
Ok(WalletConfig {
|
||||
sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr)
|
||||
|
||||
@ -20,7 +20,7 @@ use crate::{
|
||||
indexer_client::IndexerClient,
|
||||
setup::{
|
||||
setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply,
|
||||
setup_sequencer, setup_wallet,
|
||||
setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet,
|
||||
},
|
||||
};
|
||||
|
||||
@ -225,7 +225,7 @@ impl TestContextBuilder {
|
||||
Self {
|
||||
genesis_transactions: None,
|
||||
sequencer_partial_config: None,
|
||||
enable_indexer: false,
|
||||
enable_indexer: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,19 +290,29 @@ impl TestContextBuilder {
|
||||
};
|
||||
|
||||
let initial_public_accounts = config::default_public_accounts_for_wallet();
|
||||
let initial_private_accounts = config::default_private_accounts_for_wallet();
|
||||
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
|
||||
sequencer_partial_config.unwrap_or_default(),
|
||||
bedrock_addr,
|
||||
genesis_transactions
|
||||
.unwrap_or_else(|| config::genesis_from_public_accounts(&initial_public_accounts)),
|
||||
genesis_transactions.unwrap_or_else(|| {
|
||||
config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup Sequencer")?;
|
||||
|
||||
let (mut wallet, temp_wallet_dir, wallet_password) =
|
||||
setup_wallet(sequencer_handle.addr(), &initial_public_accounts)
|
||||
.context("Failed to setup wallet")?;
|
||||
setup_private_accounts_with_initial_supply(&mut wallet)
|
||||
let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet(
|
||||
sequencer_handle.addr(),
|
||||
&initial_public_accounts,
|
||||
&initial_private_accounts,
|
||||
)
|
||||
.context("Failed to setup wallet")?;
|
||||
|
||||
setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts)
|
||||
.await
|
||||
.context("Failed to initialize public accounts in wallet")?;
|
||||
|
||||
setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts)
|
||||
.await
|
||||
.context("Failed to initialize private accounts in wallet")?;
|
||||
|
||||
|
||||
@ -1,27 +1,21 @@
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use common::transaction::NSSATransaction;
|
||||
use indexer_service::IndexerHandle;
|
||||
use log::{debug, warn};
|
||||
use nssa::PrivateKey;
|
||||
use nssa::{AccountId, PrivateKey, PublicKey, PublicTransaction, program::Program};
|
||||
use sequencer_service::{GenesisTransaction, SequencerHandle};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tempfile::TempDir;
|
||||
use testcontainers::compose::DockerCompose;
|
||||
use wallet::{
|
||||
WalletCore,
|
||||
cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
execute_subcommand,
|
||||
programs::native_token_transfer::AuthTransferSubcommand,
|
||||
},
|
||||
config::WalletConfigOverrides,
|
||||
AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT,
|
||||
config::{self, INITIAL_PRIVATE_BALANCES_FOR_WALLET},
|
||||
private_mention, public_mention,
|
||||
config::{self, InitialPrivateAccountForWallet},
|
||||
};
|
||||
|
||||
pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
|
||||
@ -141,6 +135,7 @@ pub async fn setup_sequencer(
|
||||
pub fn setup_wallet(
|
||||
sequencer_addr: SocketAddr,
|
||||
initial_public_accounts: &[(PrivateKey, u128)],
|
||||
initial_private_accounts: &[InitialPrivateAccountForWallet],
|
||||
) -> Result<(WalletCore, TempDir, String)> {
|
||||
let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?;
|
||||
let config_serialized =
|
||||
@ -172,6 +167,18 @@ pub fn setup_wallet(
|
||||
.add_imported_public_account(private_key.clone());
|
||||
}
|
||||
|
||||
for private_account in initial_private_accounts {
|
||||
wallet
|
||||
.storage_mut()
|
||||
.key_chain_mut()
|
||||
.add_imported_private_account(
|
||||
private_account.key_chain.clone(),
|
||||
None,
|
||||
private_account.identifier,
|
||||
nssa::Account::default(),
|
||||
);
|
||||
}
|
||||
|
||||
wallet
|
||||
.store_persistent_data()
|
||||
.context("Failed to store wallet persistent data")?;
|
||||
@ -179,72 +186,142 @@ pub fn setup_wallet(
|
||||
Ok((wallet, temp_wallet_dir, wallet_password))
|
||||
}
|
||||
|
||||
pub async fn setup_private_accounts_with_initial_supply(wallet: &mut WalletCore) -> Result<()> {
|
||||
for _ in INITIAL_PRIVATE_BALANCES_FOR_WALLET {
|
||||
let result = execute_subcommand(
|
||||
pub async fn setup_public_accounts_with_initial_supply(
|
||||
wallet: &WalletCore,
|
||||
initial_public_accounts: &[(PrivateKey, u128)],
|
||||
) -> Result<()> {
|
||||
for (private_key, amount) in initial_public_accounts {
|
||||
claim_funds_from_vault(
|
||||
wallet,
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
AccountId::from(&PublicKey::new_from_private_key(private_key)),
|
||||
*amount,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create a private account")?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else {
|
||||
bail!("Expected RegisterAccount return value when creating private account");
|
||||
};
|
||||
}
|
||||
|
||||
let public_account_ids: Vec<_> = wallet
|
||||
.storage()
|
||||
.key_chain()
|
||||
.public_account_ids()
|
||||
.map(|(account_id, _idx)| account_id)
|
||||
.collect();
|
||||
|
||||
if public_account_ids.len() < INITIAL_PRIVATE_BALANCES_FOR_WALLET.len() {
|
||||
bail!(
|
||||
"Expected at least {} public accounts in wallet storage, found {}",
|
||||
INITIAL_PRIVATE_BALANCES_FOR_WALLET.len(),
|
||||
public_account_ids.len()
|
||||
);
|
||||
}
|
||||
|
||||
let private_account_ids: Vec<_> = wallet
|
||||
.storage()
|
||||
.key_chain()
|
||||
.private_account_ids()
|
||||
.map(|(account_id, _idx)| account_id)
|
||||
.collect();
|
||||
|
||||
for ((from, to), amount) in public_account_ids
|
||||
.into_iter()
|
||||
.zip(private_account_ids.into_iter())
|
||||
.zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET)
|
||||
{
|
||||
let result = execute_subcommand(
|
||||
wallet,
|
||||
Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(from),
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_identifier: None,
|
||||
amount,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to perform initial shielded transfer to private account")?;
|
||||
|
||||
if !matches!(
|
||||
result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
) {
|
||||
bail!(
|
||||
"Expected PrivacyPreservingTransfer return value when shielding initial private funds"
|
||||
);
|
||||
}
|
||||
.context("Failed to claim funds from vault into public account")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_private_accounts_with_initial_supply(
|
||||
wallet: &mut WalletCore,
|
||||
initial_private_accounts: &[InitialPrivateAccountForWallet],
|
||||
) -> Result<()> {
|
||||
for private_account in initial_private_accounts {
|
||||
claim_funds_from_vault_to_private(
|
||||
wallet,
|
||||
private_account.account_id(),
|
||||
private_account.balance,
|
||||
)
|
||||
.await
|
||||
.context("Failed to claim funds from vault into private account")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn claim_funds_from_vault(
|
||||
wallet: &WalletCore,
|
||||
owner_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
let vault_program_id = Program::vault().id();
|
||||
let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id);
|
||||
|
||||
let nonces = wallet
|
||||
.get_accounts_nonces(vec![owner_id])
|
||||
.await
|
||||
.context("Failed to fetch owner nonce")?;
|
||||
|
||||
let signing_key = wallet
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(owner_id)
|
||||
.with_context(|| format!("Missing signing key for public account {owner_id}"))?;
|
||||
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
vault_program_id,
|
||||
vec![owner_id, owner_vault_id],
|
||||
nonces,
|
||||
vault_core::Instruction::Claim { amount },
|
||||
)
|
||||
.context("Failed to build vault claim message")?;
|
||||
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let tx_hash = wallet
|
||||
.sequencer_client
|
||||
.send_transaction(NSSATransaction::Public(tx))
|
||||
.await
|
||||
.context("Failed to submit vault claim transaction")?;
|
||||
|
||||
wallet
|
||||
.poll_native_token_transfer(tx_hash)
|
||||
.await
|
||||
.context("Failed to confirm vault claim transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn claim_funds_from_vault_to_private(
|
||||
wallet: &mut WalletCore,
|
||||
owner_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
let Some(_) = wallet.storage().key_chain().private_account(owner_id) else {
|
||||
bail!("Missing private account in wallet key chain for account {owner_id}");
|
||||
};
|
||||
|
||||
let vault_program = Program::vault();
|
||||
let vault_program_id = vault_program.id();
|
||||
let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id);
|
||||
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(vault_core::Instruction::Claim { amount })
|
||||
.context("Failed to serialize vault private claim instruction")?;
|
||||
|
||||
let program_with_dependencies =
|
||||
nssa::privacy_preserving_transaction::circuit::ProgramWithDependencies::new(
|
||||
vault_program,
|
||||
HashMap::from([(
|
||||
Program::authenticated_transfer_program().id(),
|
||||
Program::authenticated_transfer_program(),
|
||||
)]),
|
||||
);
|
||||
|
||||
let (tx_hash, mut secrets) = wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(owner_id),
|
||||
PrivacyPreservingAccount::Public(owner_vault_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program_with_dependencies,
|
||||
)
|
||||
.await
|
||||
.context("Failed to submit private vault claim transaction")?;
|
||||
|
||||
let secret = secrets
|
||||
.pop()
|
||||
.context("Expected one private output secret for vault claim")?;
|
||||
|
||||
let transfer_tx = wallet
|
||||
.poll_native_token_transfer(tx_hash)
|
||||
.await
|
||||
.context("Failed to confirm private vault claim transaction")?;
|
||||
|
||||
let NSSATransaction::PrivacyPreserving(tx) = transfer_tx else {
|
||||
bail!("Expected privacy preserving transaction result for private vault claim");
|
||||
};
|
||||
|
||||
wallet
|
||||
.decode_insert_privacy_preserving_transaction_results(&tx, &[Decode(secret, owner_id)])
|
||||
.context("Failed to decode private vault claim transaction")?;
|
||||
|
||||
wallet
|
||||
.store_persistent_data()
|
||||
.context("Failed to store wallet data after private vault claim")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use common::transaction::NSSATransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
|
||||
use log::info;
|
||||
use nssa::program::Program;
|
||||
use nssa::{SYSTEM_FAUCET_ACCOUNT_ID, program::Program, public_transaction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -344,3 +345,90 @@ async fn successful_transfer_using_to_label() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let recipient = ctx.existing_public_accounts()[0];
|
||||
let recipient_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
|
||||
.await?;
|
||||
|
||||
let amount = 1_u128;
|
||||
let message = public_transaction::Message::try_new(
|
||||
Program::authenticated_transfer_program().id(),
|
||||
vec![SYSTEM_FAUCET_ACCOUNT_ID, recipient],
|
||||
vec![],
|
||||
authenticated_transfer_core::Instruction::Transfer { amount },
|
||||
)?;
|
||||
let tx = nssa::PublicTransaction::new(
|
||||
message,
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
);
|
||||
let tx_hash = ctx
|
||||
.sequencer_client()
|
||||
.send_transaction(NSSATransaction::Public(tx))
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let recipient_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
|
||||
.await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(recipient_balance_after, recipient_balance_before);
|
||||
assert_eq!(faucet_balance_after, faucet_balance_before);
|
||||
assert!(tx_on_chain.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn can_transfer_funds_to_system_faucet_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
|
||||
.await?;
|
||||
|
||||
let amount = 100_u128;
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(sender),
|
||||
to: Some(public_mention(SYSTEM_FAUCET_ACCOUNT_ID)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_identifier: Some(0),
|
||||
amount,
|
||||
});
|
||||
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 sender_balance_after = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
|
||||
.await?;
|
||||
|
||||
assert_eq!(sender_balance_after, sender_balance_before - amount);
|
||||
assert_eq!(faucet_balance_after, faucet_balance_before + amount);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -125,9 +125,9 @@ fn indexer_test_run_ffi() -> Result<()> {
|
||||
|
||||
let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value };
|
||||
|
||||
info!("Last block on ind ffi now is {last_block_indexer_ffi}");
|
||||
info!("Last block on indexer FFI now is {last_block_indexer_ffi}");
|
||||
|
||||
assert!(last_block_indexer_ffi > 1);
|
||||
assert!(last_block_indexer_ffi > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -149,9 +149,9 @@ fn indexer_ffi_block_batching() -> Result<()> {
|
||||
|
||||
let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value };
|
||||
|
||||
info!("Last block on ind now is {last_block_indexer}");
|
||||
info!("Last block on indexer FFI now is {last_block_indexer}");
|
||||
|
||||
assert!(last_block_indexer > 1);
|
||||
assert!(last_block_indexer > 0);
|
||||
|
||||
let before_ffi = FfiOption::<u64>::from_none();
|
||||
let limit = 100;
|
||||
|
||||
@ -97,7 +97,7 @@ impl TpsTestManager {
|
||||
fn generate_genesis(&self) -> Vec<GenesisTransaction> {
|
||||
self.public_keypairs
|
||||
.iter()
|
||||
.map(|(_, account_id)| GenesisTransaction::SupplyPublicAccount {
|
||||
.map(|(_, account_id)| GenesisTransaction::SupplyAccount {
|
||||
account_id: *account_id,
|
||||
balance: 10,
|
||||
})
|
||||
|
||||
@ -25,6 +25,8 @@ pub mod program;
|
||||
pub mod error;
|
||||
|
||||
pub const GENESIS_BLOCK_ID: BlockId = 1;
|
||||
pub const SYSTEM_FAUCET_ACCOUNT_ID: account::AccountId =
|
||||
account::AccountId::new(*b"/LEZ/SystemFaucetAccount/0000000");
|
||||
|
||||
pub type BlockId = u64;
|
||||
/// Unix timestamp in milliseconds.
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
)]
|
||||
|
||||
pub use nssa_core::{
|
||||
GENESIS_BLOCK_ID, SharedSecretKey,
|
||||
GENESIS_BLOCK_ID, SYSTEM_FAUCET_ACCOUNT_ID, SharedSecretKey,
|
||||
account::{Account, AccountId, Data},
|
||||
encryption::EphemeralPublicKey,
|
||||
program::ProgramId,
|
||||
|
||||
@ -543,7 +543,11 @@ mod tests {
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let instruction =
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
|
||||
amount: balance_to_move,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::{
|
||||
program_methods::{
|
||||
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
|
||||
PINATA_ID, TOKEN_ELF, TOKEN_ID,
|
||||
PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
|
||||
},
|
||||
};
|
||||
|
||||
@ -148,6 +148,14 @@ impl Program {
|
||||
elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn vault() -> Self {
|
||||
Self {
|
||||
id: VAULT_ID,
|
||||
elf: VAULT_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
|
||||
@ -179,7 +187,7 @@ mod tests {
|
||||
program_methods::{
|
||||
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
|
||||
PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID,
|
||||
PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
|
||||
},
|
||||
};
|
||||
|
||||
@ -482,12 +490,15 @@ mod tests {
|
||||
fn builtin_programs() {
|
||||
let auth_transfer_program = Program::authenticated_transfer_program();
|
||||
let token_program = Program::token();
|
||||
let vault_program = Program::vault();
|
||||
let pinata_program = Program::pinata();
|
||||
|
||||
assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID);
|
||||
assert_eq!(auth_transfer_program.elf, AUTHENTICATED_TRANSFER_ELF);
|
||||
assert_eq!(token_program.id, TOKEN_ID);
|
||||
assert_eq!(token_program.elf, TOKEN_ELF);
|
||||
assert_eq!(vault_program.id, VAULT_ID);
|
||||
assert_eq!(vault_program.elf, VAULT_ELF);
|
||||
assert_eq!(pinata_program.id, PINATA_ID);
|
||||
assert_eq!(pinata_program.elf, PINATA_ELF);
|
||||
}
|
||||
@ -502,6 +513,7 @@ mod tests {
|
||||
(PINATA_ELF, PINATA_ID),
|
||||
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
|
||||
(TOKEN_ELF, TOKEN_ID),
|
||||
(VAULT_ELF, VAULT_ID),
|
||||
];
|
||||
for (elf, expected_id) in cases {
|
||||
let program = Program::new(elf.to_vec()).unwrap();
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use log::debug;
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{ChainedCall, ProgramId, ProgramOutput},
|
||||
};
|
||||
|
||||
use crate::{PublicTransaction, V03State, error::NssaError};
|
||||
|
||||
pub trait Validator {
|
||||
fn validate_pre_execution(&mut self) -> Result<(), NssaError>;
|
||||
|
||||
fn on_chained_call(&mut self) -> Result<(), NssaError>;
|
||||
|
||||
fn validate_output(
|
||||
&mut self,
|
||||
state_diff: &HashMap<AccountId, Account>,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
chained_call: &ChainedCall,
|
||||
program_output: &ProgramOutput,
|
||||
) -> Result<(), NssaError>;
|
||||
|
||||
fn validate_post_execution(
|
||||
&mut self,
|
||||
state_diff: &HashMap<AccountId, Account>,
|
||||
) -> Result<(), NssaError>;
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
mut validator: impl Validator,
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<HashMap<AccountId, Account>, NssaError> {
|
||||
validator.validate_pre_execution()?;
|
||||
|
||||
let message = tx.message();
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
|
||||
// Build pre_states for execution
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
|
||||
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
validator.on_chained_call()?;
|
||||
|
||||
// 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()));
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = program.execute(
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_id, program_output
|
||||
);
|
||||
|
||||
validator.validate_output(
|
||||
&state_diff,
|
||||
caller_program_id,
|
||||
&chained_call,
|
||||
&program_output,
|
||||
)?;
|
||||
|
||||
for post in program_output
|
||||
.post_states
|
||||
.iter_mut()
|
||||
.filter(|post| post.required_claim().is_some())
|
||||
{
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
}
|
||||
|
||||
// Update the state diff
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
}
|
||||
|
||||
validator.validate_post_execution(&state_diff)?;
|
||||
|
||||
Ok(state_diff)
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
pub use execution::{Validator, execute};
|
||||
pub use message::Message;
|
||||
pub use transaction::PublicTransaction;
|
||||
pub use witness_set::WitnessSet;
|
||||
|
||||
mod execution;
|
||||
mod message;
|
||||
mod transaction;
|
||||
mod witness_set;
|
||||
|
||||
@ -8,7 +8,7 @@ pub use clock_core::{
|
||||
};
|
||||
use nssa_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
|
||||
Timestamp,
|
||||
SYSTEM_FAUCET_ACCOUNT_ID, Timestamp,
|
||||
account::{Account, AccountId, Nonce},
|
||||
program::ProgramId,
|
||||
};
|
||||
@ -124,8 +124,12 @@ pub struct V03State {
|
||||
|
||||
impl Default for V03State {
|
||||
fn default() -> Self {
|
||||
let faucet_account = system_faucet_account();
|
||||
let mut public_state = HashMap::new();
|
||||
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
|
||||
|
||||
Self {
|
||||
public_state: HashMap::new(),
|
||||
public_state,
|
||||
private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()),
|
||||
programs: HashMap::new(),
|
||||
}
|
||||
@ -145,7 +149,7 @@ impl V03State {
|
||||
genesis_timestamp: nssa_core::Timestamp,
|
||||
) -> Self {
|
||||
let authenticated_transfer_program = Program::authenticated_transfer_program();
|
||||
let public_state = initial_data
|
||||
let mut public_state: HashMap<_, _> = initial_data
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(account_id, balance)| {
|
||||
@ -157,6 +161,8 @@ impl V03State {
|
||||
(account_id, account)
|
||||
})
|
||||
.collect();
|
||||
let faucet_account = system_faucet_account();
|
||||
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
|
||||
|
||||
let mut commitment_set = CommitmentSet::with_capacity(32);
|
||||
commitment_set.extend(&[DUMMY_COMMITMENT]);
|
||||
@ -180,6 +186,7 @@ impl V03State {
|
||||
this.insert_program(Program::token());
|
||||
this.insert_program(Program::amm());
|
||||
this.insert_program(Program::ata());
|
||||
this.insert_program(Program::vault());
|
||||
|
||||
this
|
||||
}
|
||||
@ -366,6 +373,14 @@ impl V03State {
|
||||
}
|
||||
}
|
||||
|
||||
fn system_faucet_account() -> Account {
|
||||
Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: u128::MAX,
|
||||
..Account::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
#![expect(
|
||||
@ -389,7 +404,7 @@ pub mod tests {
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PublicKey, PublicTransaction, V03State,
|
||||
PublicKey, PublicTransaction, SYSTEM_FAUCET_ACCOUNT_ID, V03State,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
@ -403,7 +418,7 @@ pub mod tests {
|
||||
signature::PrivateKey,
|
||||
state::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_faucet_account,
|
||||
},
|
||||
};
|
||||
|
||||
@ -597,6 +612,7 @@ pub mod tests {
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this.insert(SYSTEM_FAUCET_ACCOUNT_ID, system_faucet_account());
|
||||
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
|
||||
this.insert(
|
||||
account_id,
|
||||
@ -619,6 +635,7 @@ pub mod tests {
|
||||
this.insert(Program::token().id(), Program::token());
|
||||
this.insert(Program::amm().id(), Program::amm());
|
||||
this.insert(Program::ata().id(), Program::ata());
|
||||
this.insert(Program::vault().id(), Program::vault());
|
||||
this
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use log::debug;
|
||||
use nssa_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, ProgramOutput,
|
||||
compute_public_authorized_pdas, validate_execution,
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
|
||||
@ -45,11 +45,237 @@ impl ValidatedStateDiff {
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, NssaError> {
|
||||
let validator = PublicTransactionValidator::new(tx, state, block_id, timestamp);
|
||||
let state_diff = crate::public_transaction::execute(validator, tx, state)?;
|
||||
let message = tx.message();
|
||||
let witness_set = tx.witness_set();
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
|
||||
);
|
||||
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
NssaError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
NssaError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
// Build pre_states for execution
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
ensure!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
NssaError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// 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()));
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = program.execute(
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_id, program_output
|
||||
);
|
||||
|
||||
let authorized_pdas =
|
||||
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
|
||||
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
|
||||
};
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// 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)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
InvalidProgramBehaviorError::InconsistentAccountPreState {
|
||||
account_id,
|
||||
expected: Box::new(expected_pre),
|
||||
actual: Box::new(pre.account.clone())
|
||||
}
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
let expected_is_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
pre.is_authorized == expected_is_authorized,
|
||||
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
|
||||
account_id,
|
||||
expected_authorization: expected_is_authorized,
|
||||
actual_authorization: pre.is_authorized
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
InvalidProgramBehaviorError::MismatchedProgramId {
|
||||
expected: chained_call.program_id,
|
||||
actual: program_output.self_program_id
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_program_id,
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_program_id,
|
||||
actual: program_output.caller_program_id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
program_output.block_validity_window.is_valid_for(block_id)
|
||||
&& program_output
|
||||
.timestamp_validity_window
|
||||
.is_valid_for(timestamp),
|
||||
NssaError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter_mut().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim. The public-execution path only sees public
|
||||
// accounts, so the public-PDA derivation is the correct formula here.
|
||||
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
}
|
||||
|
||||
// Update the state diff
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for (account_id, 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((*account_id, post))
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids: tx.signer_account_ids(),
|
||||
signer_account_ids,
|
||||
public_diff: state_diff,
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
@ -190,22 +416,6 @@ impl ValidatedStateDiff {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_public_genesis_transaction(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Self, NssaError> {
|
||||
let validator = GenesisPublicTransactionValidator;
|
||||
let state_diff = crate::public_transaction::execute(validator, tx, state)?;
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids: tx.signer_account_ids(),
|
||||
public_diff: state_diff,
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
program: None,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the public account changes produced by this transaction.
|
||||
///
|
||||
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
|
||||
@ -220,256 +430,6 @@ impl ValidatedStateDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PublicTransactionValidator<'tx, 'state> {
|
||||
tx: &'tx PublicTransaction,
|
||||
state: &'state V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
chain_calls_counter: usize,
|
||||
}
|
||||
|
||||
impl<'tx, 'state> PublicTransactionValidator<'tx, 'state> {
|
||||
pub const fn new(
|
||||
tx: &'tx PublicTransaction,
|
||||
state: &'state V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Self {
|
||||
Self {
|
||||
tx,
|
||||
state,
|
||||
block_id,
|
||||
timestamp,
|
||||
chain_calls_counter: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::public_transaction::Validator for PublicTransactionValidator<'_, '_> {
|
||||
fn validate_pre_execution(&mut self) -> Result<(), NssaError> {
|
||||
let message = self.tx.message();
|
||||
let witness_set = self.tx.witness_set();
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
|
||||
);
|
||||
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
NssaError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = self.tx.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = self.state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
NssaError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_chained_call(&mut self) -> Result<(), NssaError> {
|
||||
self.chain_calls_counter = self
|
||||
.chain_calls_counter
|
||||
.checked_add(1)
|
||||
.ok_or(NssaError::MaxChainedCallsDepthExceeded)?;
|
||||
ensure!(
|
||||
self.chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
NssaError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_output(
|
||||
&mut self,
|
||||
state_diff: &HashMap<AccountId, Account>,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
chained_call: &ChainedCall,
|
||||
program_output: &ProgramOutput,
|
||||
) -> Result<(), NssaError> {
|
||||
let authorized_pdas =
|
||||
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
|
||||
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
self.tx.signer_account_ids().contains(account_id)
|
||||
|| authorized_pdas.contains(account_id)
|
||||
};
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// 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)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
InvalidProgramBehaviorError::InconsistentAccountPreState {
|
||||
account_id,
|
||||
expected: Box::new(expected_pre),
|
||||
actual: Box::new(pre.account.clone())
|
||||
}
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
let expected_is_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
pre.is_authorized == expected_is_authorized,
|
||||
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
|
||||
account_id,
|
||||
expected_authorization: expected_is_authorized,
|
||||
actual_authorization: pre.is_authorized
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
InvalidProgramBehaviorError::MismatchedProgramId {
|
||||
expected: chained_call.program_id,
|
||||
actual: program_output.self_program_id
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_program_id,
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_program_id,
|
||||
actual: program_output.caller_program_id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
program_output
|
||||
.block_validity_window
|
||||
.is_valid_for(self.block_id)
|
||||
&& program_output
|
||||
.timestamp_validity_window
|
||||
.is_valid_for(self.timestamp),
|
||||
NssaError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim.
|
||||
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_post_execution(
|
||||
&mut self,
|
||||
state_diff: &HashMap<AccountId, Account>,
|
||||
) -> Result<(), NssaError> {
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = self.state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
}
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some((*account_id, post))
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GenesisPublicTransactionValidator;
|
||||
|
||||
impl crate::public_transaction::Validator for GenesisPublicTransactionValidator {
|
||||
fn validate_pre_execution(&mut self) -> Result<(), NssaError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_chained_call(&mut self) -> Result<(), NssaError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_output(
|
||||
&mut self,
|
||||
_state_diff: &HashMap<AccountId, Account>,
|
||||
_caller_program_id: Option<ProgramId>,
|
||||
_chained_call: &ChainedCall,
|
||||
_program_output: &ProgramOutput,
|
||||
) -> Result<(), NssaError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_post_execution(
|
||||
&mut self,
|
||||
_state_diff: &HashMap<AccountId, Account>,
|
||||
) -> Result<(), NssaError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
|
||||
@ -17,5 +17,6 @@ amm_core.workspace = true
|
||||
amm_program.workspace = true
|
||||
ata_core.workspace = true
|
||||
ata_program.workspace = true
|
||||
vault_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use authenticated_transfer_core::Instruction;
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use nssa_core::{
|
||||
SYSTEM_FAUCET_ACCOUNT_ID,
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{
|
||||
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
@ -27,7 +27,12 @@ fn transfer(
|
||||
balance_to_move: u128,
|
||||
) -> Vec<AccountPostState> {
|
||||
// Continue only if the sender has authorized this operation
|
||||
assert!(sender.is_authorized, "Sender must be authorized");
|
||||
// or it's the system faucet account which is allowed without authorization as it may be used
|
||||
// only by sequencer.
|
||||
assert!(
|
||||
sender.is_authorized || sender.account_id == SYSTEM_FAUCET_ACCOUNT_ID,
|
||||
"Sender must be authorized"
|
||||
);
|
||||
|
||||
// Create accounts post states, with updated balances
|
||||
let sender_post = {
|
||||
@ -59,39 +64,6 @@ fn transfer(
|
||||
vec![sender_post, recipient_post]
|
||||
}
|
||||
|
||||
/// Mints `balance` into a new account at genesis (`block_id` == 0).
|
||||
///
|
||||
/// Claims the target account and sets its balance in a single operation.
|
||||
fn mint(
|
||||
target: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
balance: u128,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Second account must be the clock account"
|
||||
);
|
||||
|
||||
let clock_data = ClockAccountData::from_bytes(&clock.account.data.clone().into_inner());
|
||||
assert_eq!(
|
||||
clock_data.block_id, 0,
|
||||
"Mint can only execute at genesis (block_id must be 0)"
|
||||
);
|
||||
|
||||
assert!(
|
||||
target.account == Account::default(),
|
||||
"Target account must be uninitialized"
|
||||
);
|
||||
|
||||
let mut target_post_account = target.account;
|
||||
target_post_account.balance = balance;
|
||||
let target_post = AccountPostState::new_claimed(target_post_account, Claim::Authorized);
|
||||
|
||||
let clock_post = AccountPostState::new(clock.account);
|
||||
|
||||
vec![target_post, clock_post]
|
||||
}
|
||||
|
||||
/// A transfer of balance program.
|
||||
/// To be used both in public and private contexts.
|
||||
fn main() {
|
||||
@ -119,11 +91,6 @@ fn main() {
|
||||
.expect("Transfer requires exactly 2 accounts");
|
||||
transfer(sender, recipient, balance_to_move)
|
||||
}
|
||||
Instruction::Mint { amount: balance } => {
|
||||
let [target, clock] = <[_; 2]>::try_from(pre_states.clone())
|
||||
.expect("Mint requires exactly 2 accounts: target, clock");
|
||||
mint(target, clock, balance)
|
||||
}
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
|
||||
103
program_methods/guest/src/bin/vault.rs
Normal file
103
program_methods/guest/src/bin/vault.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
};
|
||||
use vault_core::Instruction;
|
||||
|
||||
fn unchanged_post_states(
|
||||
pre_states: &[nssa_core::account::AccountWithMetadata],
|
||||
) -> Vec<AccountPostState> {
|
||||
pre_states
|
||||
.iter()
|
||||
.map(|pre_state| AccountPostState::new(pre_state.account.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let pre_states_clone = pre_states.clone();
|
||||
let post_states = unchanged_post_states(&pre_states_clone);
|
||||
|
||||
let chained_calls = match instruction {
|
||||
Instruction::Initialize => {
|
||||
let [owner, owner_vault] = pre_states
|
||||
.try_into()
|
||||
.expect("Initialize requires exactly 2 accounts");
|
||||
|
||||
let seed = vault_core::compute_vault_seed(owner.account_id);
|
||||
assert!(
|
||||
owner.account.program_owner != nssa_core::program::DEFAULT_PROGRAM_ID,
|
||||
"Owner account must be initialized"
|
||||
);
|
||||
|
||||
let mut owner_vault_for_callee = owner_vault;
|
||||
owner_vault_for_callee.is_authorized = true;
|
||||
|
||||
vec![
|
||||
ChainedCall::new(
|
||||
owner.account.program_owner,
|
||||
vec![owner_vault_for_callee],
|
||||
&AuthTransferInstruction::Initialize,
|
||||
)
|
||||
.with_pda_seeds(vec![seed]),
|
||||
]
|
||||
}
|
||||
Instruction::Transfer { amount } => {
|
||||
let [sender, recipient, recipient_vault] = pre_states
|
||||
.try_into()
|
||||
.expect("Transfer requires exactly 3 accounts");
|
||||
|
||||
let seed = vault_core::compute_vault_seed(recipient.account_id);
|
||||
|
||||
let mut recipient_vault_for_callee = recipient_vault;
|
||||
recipient_vault_for_callee.is_authorized = true;
|
||||
|
||||
vec![
|
||||
ChainedCall::new(
|
||||
sender.account.program_owner,
|
||||
vec![sender, recipient_vault_for_callee],
|
||||
&AuthTransferInstruction::Transfer { amount },
|
||||
)
|
||||
.with_pda_seeds(vec![seed]),
|
||||
]
|
||||
}
|
||||
Instruction::Claim { amount } => {
|
||||
let [owner, owner_vault] = pre_states
|
||||
.try_into()
|
||||
.expect("Claim requires exactly 2 accounts");
|
||||
|
||||
let seed = vault_core::compute_vault_seed(owner.account_id);
|
||||
|
||||
let mut owner_vault_for_callee = owner_vault;
|
||||
owner_vault_for_callee.is_authorized = true;
|
||||
|
||||
vec![
|
||||
ChainedCall::new(
|
||||
owner_vault_for_callee.account.program_owner,
|
||||
vec![owner_vault_for_callee, owner],
|
||||
&AuthTransferInstruction::Transfer { amount },
|
||||
)
|
||||
.with_pda_seeds(vec![seed]),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states_clone,
|
||||
post_states,
|
||||
)
|
||||
.with_chained_calls(chained_calls)
|
||||
.write();
|
||||
}
|
||||
@ -14,16 +14,4 @@ pub enum Instruction {
|
||||
///
|
||||
/// Required accounts: `[account_to_initialize]`.
|
||||
Initialize,
|
||||
|
||||
/// Mint `amount` into a new account at genesis (`block_id` == 0).
|
||||
///
|
||||
/// Claims the target account (sets `program_owner` to `authenticated_transfer` program id)
|
||||
/// and sets its balance in a single operation.
|
||||
///
|
||||
/// Required accounts: `[target_account, clock_account]`.
|
||||
///
|
||||
/// Panics if:
|
||||
/// - `target_account` is not in the default (uninitialized) state
|
||||
/// - clock's `block_id` is not 0
|
||||
Mint { amount: u128 },
|
||||
}
|
||||
|
||||
13
programs/vault/core/Cargo.toml
Normal file
13
programs/vault/core/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "vault_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
risc0-zkvm.workspace = true
|
||||
57
programs/vault/core/src/lib.rs
Normal file
57
programs/vault/core/src/lib.rs
Normal file
@ -0,0 +1,57 @@
|
||||
pub use nssa_core::program::PdaSeed;
|
||||
use nssa_core::{account::AccountId, program::ProgramId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const VAULT_SEED_DOMAIN_SEPARATOR: &[u8] = b"/LEZ/v0.3/VaultSeed/00000000000/";
|
||||
|
||||
const _: () = assert!(
|
||||
VAULT_SEED_DOMAIN_SEPARATOR.len() == 32,
|
||||
"Domain separator must be exactly 32 bytes long"
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Initializes a vault account for owner.
|
||||
///
|
||||
/// Required accounts (2):
|
||||
/// - Owner account
|
||||
/// - Owner vault PDA account
|
||||
Initialize,
|
||||
|
||||
/// Transfers native tokens from sender to recipient's vault.
|
||||
///
|
||||
/// Required accounts (3):
|
||||
/// - Sender account
|
||||
/// - Recipient account
|
||||
/// - Recipient vault PDA account
|
||||
Transfer { amount: u128 },
|
||||
|
||||
/// Claims native tokens from owner's vault into owner's account.
|
||||
///
|
||||
/// Required accounts (2):
|
||||
/// - Owner account
|
||||
/// - Owner vault PDA account
|
||||
Claim { amount: u128 },
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn compute_vault_seed(owner_id: AccountId) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = [0_u8; 64];
|
||||
bytes[..32].copy_from_slice(VAULT_SEED_DOMAIN_SEPARATOR);
|
||||
bytes[32..64].copy_from_slice(&owner_id.to_bytes());
|
||||
|
||||
PdaSeed::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn compute_vault_account_id(vault_program_id: ProgramId, owner_id: AccountId) -> AccountId {
|
||||
let seed = compute_vault_seed(owner_id);
|
||||
AccountId::for_public_pda(&vault_program_id, &seed)
|
||||
}
|
||||
@ -15,7 +15,7 @@ storage.workspace = true
|
||||
mempool.workspace = true
|
||||
logos-blockchain-zone-sdk.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@ -18,7 +18,7 @@ use url::Url;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenesisTransaction {
|
||||
SupplyPublicAccount {
|
||||
SupplyAccount {
|
||||
account_id: AccountId,
|
||||
balance: u128,
|
||||
},
|
||||
|
||||
@ -12,9 +12,7 @@ use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SI
|
||||
use mempool::{MemPool, MemPoolHandle};
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::SequencerCoreWithMockClients;
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction, ValidatedStateDiff, program::Program, public_transaction::Message,
|
||||
};
|
||||
use nssa::{AccountId, PublicTransaction, program::Program, public_transaction::Message};
|
||||
use nssa_core::GENESIS_BLOCK_ID;
|
||||
pub use storage::error::DbError;
|
||||
|
||||
@ -363,49 +361,48 @@ fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec<NSSATra
|
||||
#[cfg(feature = "testnet")]
|
||||
let mut state = testnet_initial_state::initial_state_testnet();
|
||||
|
||||
let mut genesis_txs = Vec::new();
|
||||
|
||||
for genesis_tx in &config.genesis {
|
||||
let (tx, diff) = match genesis_tx {
|
||||
GenesisTransaction::SupplyPublicAccount {
|
||||
let genesis_txs = config
|
||||
.genesis
|
||||
.iter()
|
||||
.map(|genesis_tx| match genesis_tx {
|
||||
GenesisTransaction::SupplyAccount {
|
||||
account_id,
|
||||
balance,
|
||||
} => build_supply_public_account_genesis_transaction(&state, account_id, *balance),
|
||||
};
|
||||
state.apply_state_diff(diff);
|
||||
genesis_txs.push(tx);
|
||||
}
|
||||
|
||||
let clock_tx = clock_invocation(0);
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&clock_tx, &state, GENESIS_BLOCK_ID, 0)
|
||||
.expect("Failed to execute clock transaction for genesis block");
|
||||
state.apply_state_diff(diff);
|
||||
genesis_txs.push(clock_tx.into());
|
||||
} => build_supply_account_genesis_transaction(account_id, *balance),
|
||||
})
|
||||
.chain(std::iter::once(clock_invocation(0)))
|
||||
.inspect(|tx| {
|
||||
state
|
||||
.transition_from_public_transaction(tx, GENESIS_BLOCK_ID, 0)
|
||||
.expect("Failed to execute genesis transaction");
|
||||
})
|
||||
.map(NSSATransaction::Public)
|
||||
.collect();
|
||||
|
||||
(state, genesis_txs)
|
||||
}
|
||||
|
||||
fn build_supply_public_account_genesis_transaction(
|
||||
state: &nssa::V03State,
|
||||
fn build_supply_account_genesis_transaction(
|
||||
account_id: &AccountId,
|
||||
balance: u128,
|
||||
) -> (NSSATransaction, ValidatedStateDiff) {
|
||||
let authenticated_transfer_id = Program::authenticated_transfer_program().id();
|
||||
) -> PublicTransaction {
|
||||
let vault_program_id = Program::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, *account_id);
|
||||
|
||||
let message = Message::try_new(
|
||||
authenticated_transfer_id,
|
||||
vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID],
|
||||
vault_program_id,
|
||||
vec![
|
||||
nssa::SYSTEM_FAUCET_ACCOUNT_ID,
|
||||
*account_id,
|
||||
recipient_vault_id,
|
||||
],
|
||||
vec![],
|
||||
authenticated_transfer_core::Instruction::Mint { amount: balance },
|
||||
vault_core::Instruction::Transfer { amount: balance },
|
||||
)
|
||||
.expect("Failed to serialize genesis mint instruction");
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
.expect("Failed to serialize genesis transfer instruction");
|
||||
let witness_set = nssa::public_transaction::WitnessSet::from_raw_parts(vec![]);
|
||||
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let diff = ValidatedStateDiff::from_public_genesis_transaction(&tx, state)
|
||||
.expect("Failed to execute genesis mint public transaction");
|
||||
|
||||
(tx.into(), diff)
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
/// Load signing key from file or generate a new one if it doesn't exist.
|
||||
|
||||
@ -4,7 +4,7 @@ use common::{
|
||||
block::Block,
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use nssa::{GENESIS_BLOCK_ID, V03State, ValidatedStateDiff};
|
||||
use nssa::{GENESIS_BLOCK_ID, V03State};
|
||||
use rocksdb::{
|
||||
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options,
|
||||
};
|
||||
@ -189,16 +189,17 @@ impl RocksDBIO {
|
||||
));
|
||||
}
|
||||
};
|
||||
let state_diff = ValidatedStateDiff::from_public_genesis_transaction(
|
||||
&genesis_tx,
|
||||
&breakpoint,
|
||||
)
|
||||
.map_err(|err| {
|
||||
DbError::db_interaction_error(format!(
|
||||
"Failed to create state diff from genesis transaction with err {err:?}"
|
||||
))
|
||||
})?;
|
||||
breakpoint.apply_state_diff(state_diff);
|
||||
breakpoint
|
||||
.transition_from_public_transaction(
|
||||
&genesis_tx,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
)
|
||||
.map_err(|err| {
|
||||
DbError::db_interaction_error(format!(
|
||||
"genesis transaction execution failed with err {err:?}"
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
transaction
|
||||
.transaction_stateless_check()
|
||||
|
||||
@ -56,9 +56,12 @@ fn main() {
|
||||
// private PDA (seed, npk) binding when pda_seeds match the private PDA derivation.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call =
|
||||
ChainedCall::new(auth_transfer_id, vec![auth_pda_pre, recipient_pre], &amount)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
let auth_call = ChainedCall::new(
|
||||
auth_transfer_id,
|
||||
vec![auth_pda_pre, recipient_pre],
|
||||
&authenticated_transfer_core::Instruction::Transfer { amount },
|
||||
)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
@ -81,8 +84,12 @@ fn main() {
|
||||
// to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call = ChainedCall::new(auth_transfer_id, vec![auth_pda_pre], &0_u128)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
let auth_call = ChainedCall::new(
|
||||
auth_transfer_id,
|
||||
vec![auth_pda_pre],
|
||||
&authenticated_transfer_core::Instruction::Initialize,
|
||||
)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user