feat: move initial accounts data into genesis

This commit is contained in:
Daniil Polyakov 2026-04-10 20:23:25 +03:00
parent b8a4d94d96
commit 89f8ddf6ce
161 changed files with 5643 additions and 6084 deletions

View File

@ -14,8 +14,8 @@ ignore = [
{ id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." },
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"},
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"},
{ id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
]
yanked = "deny"
unused-ignored-advisory = "deny"

View File

@ -158,35 +158,7 @@ jobs:
env:
RISC0_DEV_MODE: "1"
RUST_LOG: "info"
run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer
integration-tests-indexer:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
- uses: ./.github/actions/install-risc0
- uses: ./.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install active toolchain
run: rustup install
- name: Install nextest
run: cargo install --locked cargo-nextest
- name: Run tests
env:
RISC0_DEV_MODE: "1"
RUST_LOG: "info"
run: cargo nextest run -p integration_tests indexer -- --skip tps_test
run: cargo nextest run -p integration_tests -- --skip tps_test
valid-proof-test:
runs-on: ubuntu-latest

31
Cargo.lock generated
View File

@ -912,6 +912,13 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "authenticated_transfer_core"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@ -1622,6 +1629,7 @@ name = "common"
version = "0.1.0"
dependencies = [
"anyhow",
"authenticated_transfer_core",
"base64 0.22.1",
"borsh",
"clock_core",
@ -1765,6 +1773,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.11.0"
@ -2181,6 +2198,7 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
@ -3775,6 +3793,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"authenticated_transfer_core",
"borsh",
"common",
"futures",
@ -3915,6 +3934,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"ata_core",
"authenticated_transfer_core",
"bytesize",
"common",
"env_logger",
@ -6298,6 +6318,7 @@ name = "nssa"
version = "0.1.0"
dependencies = [
"anyhow",
"authenticated_transfer_core",
"borsh",
"clock_core",
"env_logger",
@ -7034,6 +7055,7 @@ dependencies = [
"amm_program",
"ata_core",
"ata_program",
"authenticated_transfer_core",
"clock_core",
"nssa_core",
"risc0-zkvm",
@ -8394,6 +8416,7 @@ name = "sequencer_core"
version = "0.1.0"
dependencies = [
"anyhow",
"authenticated_transfer_core",
"borsh",
"bytesize",
"chrono",
@ -8408,6 +8431,7 @@ dependencies = [
"nssa",
"nssa_core",
"rand 0.8.5",
"rocksdb",
"serde",
"serde_json",
"storage",
@ -9143,6 +9167,7 @@ dependencies = [
name = "test_programs"
version = "0.1.0"
dependencies = [
"authenticated_transfer_core",
"clock_core",
"nssa_core",
"risc0-zkvm",
@ -10082,10 +10107,13 @@ dependencies = [
"anyhow",
"async-stream",
"ata_core",
"authenticated_transfer_core",
"base58",
"bincode",
"bip39",
"clap",
"common",
"derive_more",
"env_logger",
"futures",
"hex",
@ -10103,6 +10131,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"tempfile",
"testnet_initial_state",
"thiserror 2.0.18",
"token_core",
@ -10115,9 +10144,11 @@ name = "wallet-ffi"
version = "0.1.0"
dependencies = [
"cbindgen",
"key_protocol",
"nssa",
"nssa_core",
"sequencer_service_rpc",
"serde_json",
"tempfile",
"tokio",
"wallet",

View File

@ -20,6 +20,7 @@ members = [
"programs/token",
"programs/associated_token_account/core",
"programs/associated_token_account",
"programs/authenticated_transfer/core",
"sequencer/core",
"sequencer/service",
"sequencer/service/protocol",
@ -65,6 +66,7 @@ amm_core = { path = "programs/amm/core" }
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" }
test_program_methods = { path = "test_program_methods" }
testnet_initial_state = { path = "testnet_initial_state" }
@ -78,6 +80,7 @@ tokio-util = "0.7.18"
risc0-zkvm = { version = "3.0.5", features = ['std'] }
risc0-build = "3.0.5"
anyhow = "1.0.98"
derive_more = "2.1.1"
num_cpus = "1.13.1"
openssl = { version = "0.10", features = ["vendored"] }
openssl-probe = { version = "0.1.2" }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,6 +10,7 @@ workspace = true
[dependencies]
nssa.workspace = true
nssa_core.workspace = true
authenticated_transfer_core.workspace = true
clock_core.workspace = true
anyhow.workspace = true

View File

@ -47,12 +47,11 @@ pub fn produce_dummy_empty_transaction() -> NSSATransaction {
let program_id = nssa::program::Program::authenticated_transfer_program().id();
let account_ids = vec![];
let nonces = vec![];
let instruction_data: u128 = 0;
let message = nssa::public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
instruction_data,
authenticated_transfer_core::Instruction::Initialize,
)
.unwrap();
let private_key = nssa::PrivateKey::try_new([1; 32]).unwrap();
@ -78,7 +77,9 @@ pub fn create_transaction_native_token_transfer(
program_id,
account_ids,
nonces,
balance_to_move,
authenticated_transfer_core::Instruction::Transfer {
amount: balance_to_move,
},
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);

View File

@ -4,153 +4,5 @@
"bedrock_config": {
"addr": "http://logos-blockchain-node-0:18080"
},
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
"initial_accounts": [
{
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000
},
{
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000
}
],
"initial_commitments": [
{
"npk":[
177,
64,
1,
11,
87,
38,
254,
159,
231,
165,
1,
94,
64,
137,
243,
76,
249,
101,
251,
129,
33,
101,
189,
30,
42,
11,
191,
34,
103,
186,
227,
230
] ,
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 10000,
"data": [],
"nonce": 0
}
},
{
"npk": [
32,
67,
72,
164,
106,
53,
66,
239,
141,
15,
52,
230,
136,
177,
2,
236,
207,
243,
134,
135,
210,
143,
87,
232,
215,
128,
194,
120,
113,
224,
4,
165
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 20000,
"data": [],
"nonce": 0
}
}
],
"signing_key": [
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37
]
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101"
}

View File

@ -16,117 +16,29 @@
"node_url": "http://logos-blockchain-node-0:18080"
},
"indexer_rpc_url": "ws://indexer_service:8779",
"initial_accounts": [
"genesis": [
{
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000
},
{
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000
}
],
"initial_commitments": [
{
"npk":[
177,
64,
1,
11,
87,
38,
254,
159,
231,
165,
1,
94,
64,
137,
243,
76,
249,
101,
251,
129,
33,
101,
189,
30,
42,
11,
191,
34,
103,
186,
227,
230
] ,
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 10000,
"data": [],
"nonce": 0
"supply_public_account": {
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000
}
},
{
"npk": [
32,
67,
72,
164,
106,
53,
66,
239,
141,
15,
52,
230,
136,
177,
2,
236,
207,
243,
134,
135,
210,
143,
87,
232,
215,
128,
194,
120,
113,
224,
4,
165
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 20000,
"data": [],
"nonce": 0
"supply_public_account": {
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000
}
},
{
"supply_private_account": {
"npk": [177,64,1,11,87,38,254,159,231,165,1,94,64,137,243,76,249,101,251,129,33,101,189,30,42,11,191,34,103,186,227,230],
"balance": 10000
}
},
{
"supply_private_account": {
"npk": [32,67,72,164,106,53,66,239,141,15,52,230,136,177,2,236,207,243,134,135,210,143,87,232,215,128,194,120,113,224,4,165],
"balance": 20000
}
}
],

View File

@ -50,8 +50,8 @@ async fn main() {
// Load signing keys to provide authorization
let signing_key = wallet_core
.storage()
.user_data
.get_pub_account_signing_key(account_id)
.key_chain()
.pub_account_signing_key(account_id)
.expect("Input account should be a self owned public account");
// Define the desired greeting in ASCII

View File

@ -86,7 +86,7 @@ pub async fn get_block_by_id(block_id: BlockId) -> Result<Block, ServerFnError>
/// Get latest block ID
#[server]
pub async fn get_latest_block_id() -> Result<BlockId, ServerFnError> {
pub async fn get_latest_block_id() -> Result<Option<BlockId>, ServerFnError> {
use indexer_service_rpc::RpcClient as _;
let client = expect_context::<IndexerRpcClient>();
client

View File

@ -29,3 +29,4 @@ tokio.workspace = true
[dev-dependencies]
tempfile.workspace = true
authenticated_transfer_core.workspace = true

View File

@ -5,9 +5,10 @@ use common::{
block::{BedrockStatus, Block},
transaction::{NSSATransaction, clock_invocation},
};
use log::info;
use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId};
use logos_blockchain_zone_sdk::Slot;
use nssa::{Account, AccountId, V03State};
use nssa::{Account, AccountId, V03State, ValidatedStateDiff};
use nssa_core::BlockId;
use storage::indexer::RocksDBIO;
use tokio::sync::RwLock;
@ -21,14 +22,10 @@ pub struct IndexerStore {
impl IndexerStore {
/// Starting database at the start of new chain.
/// Creates files if necessary.
///
/// ATTENTION: Will overwrite genesis block.
pub fn open_db_with_genesis(
location: &Path,
genesis_block: &Block,
initial_state: &V03State,
) -> Result<Self> {
let dbio = RocksDBIO::open_or_create(location, genesis_block, initial_state)?;
pub fn open_db(location: &Path) -> Result<Self> {
let initial_state = testnet_initial_state::initial_state();
let dbio = RocksDBIO::open_or_create(location, &initial_state)?;
let current_state = dbio.final_state()?;
Ok(Self {
@ -44,8 +41,8 @@ impl IndexerStore {
.map(HeaderId::from))
}
pub fn get_last_block_id(&self) -> Result<u64> {
Ok(self.dbio.get_meta_last_block_in_db()?)
pub fn get_last_block_id(&self) -> Result<Option<u64>> {
self.dbio.get_meta_last_block_id_in_db().map_err(Into::into)
}
pub fn get_block_at_id(&self, id: u64) -> Result<Option<Block>> {
@ -86,18 +83,14 @@ impl IndexerStore {
Ok(self.dbio.get_acc_transactions(acc_id, offset, limit)?)
}
#[must_use]
pub fn genesis_id(&self) -> u64 {
pub fn genesis_id(&self) -> Result<Option<u64>> {
self.dbio
.get_meta_first_block_in_db()
.expect("Must be set at the DB startup")
.get_meta_first_block_id_in_db()
.map_err(Into::into)
}
#[must_use]
pub fn last_block(&self) -> u64 {
self.dbio
.get_meta_last_block_in_db()
.expect("Must be set at the DB startup")
pub fn last_block(&self) -> Result<Option<u64>> {
self.dbio.get_meta_last_block_id_in_db().map_err(Into::into)
}
pub fn get_state_at_block(&self, block_id: u64) -> Result<V03State> {
@ -142,6 +135,7 @@ impl IndexerStore {
}
pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> {
info!("Applying block {}", block.header.block_id);
{
let mut state_guard = self.current_state.write().await;
@ -156,15 +150,32 @@ impl IndexerStore {
"Last transaction in block must be the clock invocation for the block timestamp"
);
let is_genesis = block.header.block_id == 1;
for transaction in user_txs {
transaction
.clone()
.transaction_stateless_check()?
.execute_check_on_state(
&mut state_guard,
block.header.block_id,
block.header.timestamp,
)?;
if is_genesis {
let genesis_tx = match transaction {
NSSATransaction::Public(public_tx) => public_tx,
NSSATransaction::PrivacyPreserving(_)
| NSSATransaction::ProgramDeployment(_) => {
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);
} else {
transaction
.clone()
.transaction_stateless_check()?
.execute_check_on_state(
&mut state_guard,
block.header.block_id,
block.header.timestamp,
)?;
}
}
// Apply the clock invocation directly (it is expected to modify clock accounts).
@ -183,21 +194,19 @@ impl IndexerStore {
// to represent correct block finality
block.bedrock_status = BedrockStatus::Finalized;
info!("Putting block {} into DB", block.header.block_id);
Ok(self.dbio.put_block(&block, l1_header.into())?)
}
}
#[cfg(test)]
mod tests {
use nssa::{AccountId, PublicKey};
use common::{HashType, block::HashableBlockData};
use nssa::{AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PublicKey, PublicTransaction};
use tempfile::tempdir;
use super::*;
fn genesis_block() -> Block {
common::test_utils::produce_dummy_block(1, None, vec![])
}
fn acc1_sign_key() -> nssa::PrivateKey {
nssa::PrivateKey::try_new([1; 32]).unwrap()
}
@ -214,49 +223,59 @@ mod tests {
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();
let storage = IndexerStore::open_db_with_genesis(
home.as_ref(),
&genesis_block(),
&nssa::V03State::new_with_genesis_accounts(
&[(acc1(), 10000), (acc2(), 20000)],
vec![],
0,
),
)
.unwrap();
let storage = IndexerStore::open_db(home.as_ref()).unwrap();
let block = storage.get_block_at_id(1).unwrap().unwrap();
let final_id = storage.get_last_block_id().unwrap();
assert_eq!(block.header.hash, genesis_block().header.hash);
assert_eq!(final_id, 1);
assert_eq!(final_id, None);
}
#[tokio::test]
async fn state_transition() {
let home = tempdir().unwrap();
let storage = IndexerStore::open_db_with_genesis(
home.as_ref(),
&genesis_block(),
&nssa::V03State::new_with_genesis_accounts(
&[(acc1(), 10000), (acc2(), 20000)],
vec![],
0,
),
)
.unwrap();
let mut prev_hash = genesis_block().header.hash;
let storage = IndexerStore::open_db(home.as_ref()).unwrap();
let from = acc1();
let to = acc2();
let sign_key = acc1_sign_key();
// 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],
};
let genesis_block = genesis_block_data.into_pending_block(
&common::test_utils::sequencer_sign_key_for_testing(),
[0; 32],
);
let mut prev_hash = Some(genesis_block.header.hash);
storage
.put_block(genesis_block, HeaderId::from([0_u8; 32]))
.await
.unwrap();
for i in 2..10 {
let tx = common::test_utils::create_transaction_native_token_transfer(
from,
@ -267,9 +286,8 @@ mod tests {
);
let block_id = u64::try_from(i).unwrap();
let next_block =
common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]);
prev_hash = next_block.header.hash;
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]))
@ -288,18 +306,9 @@ mod tests {
async fn account_state_at_block() {
let home = tempdir().unwrap();
let storage = IndexerStore::open_db_with_genesis(
home.as_ref(),
&genesis_block(),
&nssa::V03State::new_with_genesis_accounts(
&[(acc1(), 10000), (acc2(), 20000)],
vec![],
0,
),
)
.unwrap();
let storage = IndexerStore::open_db(home.as_ref()).unwrap();
let mut prev_hash = genesis_block().header.hash;
let mut prev_hash = None;
let from = acc1();
let to = acc2();
@ -315,9 +324,8 @@ mod tests {
);
let block_id = u64::try_from(i).unwrap();
let next_block =
common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]);
prev_hash = next_block.header.hash;
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]))

View File

@ -10,7 +10,6 @@ use common::config::BasicAuth;
use humantime_serde;
pub use logos_blockchain_core::mantle::ops::channel::ChannelId;
use serde::{Deserialize, Serialize};
use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData};
use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -22,18 +21,12 @@ pub struct ClientConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexerConfig {
/// Home dir of sequencer storage.
/// Home dir of indexer storage.
pub home: PathBuf,
/// Sequencers signing key.
pub signing_key: [u8; 32],
#[serde(with = "humantime_serde")]
pub consensus_info_polling_interval: Duration,
pub bedrock_config: ClientConfig,
pub channel_id: ChannelId,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_public_accounts: Option<Vec<PublicAccountPublicInitialData>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_private_accounts: Option<Vec<PrivateAccountPublicInitialData>>,
}
impl IndexerConfig {

View File

@ -1,17 +1,14 @@
use std::sync::Arc;
use anyhow::Result;
use common::block::{Block, HashableBlockData};
use common::block::Block;
// ToDo: Remove after testnet
use common::{HashType, PINATA_BASE58};
use futures::StreamExt as _;
use log::{error, info, warn};
use logos_blockchain_core::header::HeaderId;
use logos_blockchain_zone_sdk::{
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
};
use nssa::V03State;
use testnet_initial_state::initial_state_testnet;
use crate::{block_store::IndexerStore, config::IndexerConfig};
@ -27,69 +24,6 @@ pub struct IndexerCore {
impl IndexerCore {
pub fn new(config: IndexerConfig) -> Result<Self> {
let hashable_data = HashableBlockData {
block_id: 1,
transactions: vec![],
prev_block_hash: HashType([0; 32]),
timestamp: 0,
};
// Genesis creation is fine as it is,
// because it will be overwritten by sequencer.
// Therefore:
// ToDo: remove key from indexer config, use some default.
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
let channel_genesis_msg_id = [0; 32];
let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id);
let initial_private_accounts: Option<Vec<(nssa_core::Commitment, nssa_core::Nullifier)>> =
config.initial_private_accounts.as_ref().map(|accounts| {
accounts
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
acc.program_owner =
nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect()
});
let init_accs: Option<Vec<(nssa::AccountId, u128)>> = config
.initial_public_accounts
.as_ref()
.map(|initial_accounts| {
initial_accounts
.iter()
.map(|acc_data| (acc_data.account_id, acc_data.balance))
.collect()
});
// If initial commitments or accounts are present in config, need to construct state from
// them
let state = if initial_private_accounts.is_some() || init_accs.is_some() {
let mut state = V03State::new_with_genesis_accounts(
&init_accs.unwrap_or_default(),
initial_private_accounts.unwrap_or_default(),
genesis_block.header.timestamp,
);
// ToDo: Remove after testnet
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
state
} else {
initial_state_testnet()
};
let home = config.home.join("rocksdb");
let basic_auth = config.bedrock_config.auth.clone().map(Into::into);
@ -102,7 +36,7 @@ impl IndexerCore {
Ok(Self {
zone_indexer: Arc::new(zone_indexer),
config,
store: IndexerStore::open_db_with_genesis(&home, &genesis_block, &state)?,
store: IndexerStore::open_db(&home)?,
})
}

View File

@ -22,9 +22,10 @@ typedef enum FfiBedrockStatus {
Finalized,
} FfiBedrockStatus;
typedef struct Option_u64 Option_u64;
typedef struct IndexerServiceFFI {
void *indexer_handle;
void *runtime;
void *indexer_client;
} IndexerServiceFFI;
@ -41,16 +42,56 @@ typedef struct PointerResult_IndexerServiceFFI__OperationStatus {
typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult;
typedef enum PointerKind_Tag {
Owned,
Borrowed,
Null,
} PointerKind_Tag;
typedef struct PointerKind {
PointerKind_Tag tag;
union {
struct {
void *owned;
};
struct {
const void *borrowed;
};
};
} PointerKind;
typedef struct Pointer_Runtime {
struct PointerKind kind;
} Pointer_Runtime;
/**
* Wrapper around [`tokio::runtime::Runtime`] that can be safely passed across the FFI boundary.
*/
typedef struct Runtime {
struct Pointer_Runtime inner;
} Runtime;
/**
* Simple wrapper around a pointer to a value or an error.
*
* Pointer is not guaranteed. You should check the error field before
* dereferencing the pointer.
*/
typedef struct PointerResult_u64__OperationStatus {
uint64_t *value;
typedef struct PointerResult_Runtime__OperationStatus {
struct Runtime *value;
enum OperationStatus error;
} PointerResult_u64__OperationStatus;
} PointerResult_Runtime__OperationStatus;
/**
* Simple wrapper around a pointer to a value or an error.
*
* Pointer is not guaranteed. You should check the error field before
* dereferencing the pointer.
*/
typedef struct PointerResult_Option_u64_____OperationStatus {
struct Option_u64 *value;
enum OperationStatus error;
} PointerResult_Option_u64_____OperationStatus;
typedef uint64_t FfiBlockId;
@ -379,8 +420,20 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus {
*
* An `InitializedIndexerServiceFFIResult` containing either a pointer to the
* initialized `IndexerServiceFFI` or an error code.
*
* # Safety
* The caller must ensure that:
* - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance.
* - `config_path` is a valid pointer to a null-terminated C string.
*/
InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port);
InitializedIndexerServiceFFIResult start_indexer(const struct Runtime *runtime,
const char *config_path,
uint16_t port);
/**
* Creates a new [`tokio::runtime::Runtime`].
*/
struct PointerResult_Runtime__OperationStatus new_runtime(void);
/**
* Stops and frees the resources associated with the given indexer service.
@ -415,25 +468,27 @@ void free_cstring(char *block);
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
*
* # Returns
*
* A `PointerResult<u64, OperationStatus>` indicating success or failure.
* A `PointerResult<Option<u64>, OperationStatus>` indicating success or failure.
*
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerServiceFFI *indexer);
struct PointerResult_Option_u64_____OperationStatus query_last_block(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer);
/**
* Query the block by id from indexer.
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `block_id`: `u64` number of block id
*
* # Returns
@ -443,9 +498,11 @@ struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerS
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
FfiBlockId block_id);
/**
@ -453,7 +510,7 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Index
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `hash`: `FfiHashType` - hash of block
*
* # Returns
@ -463,9 +520,11 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Index
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
FfiHashType hash);
/**
@ -473,7 +532,7 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `account_id`: `FfiAccountId` - id of queried account
*
* # Returns
@ -483,9 +542,11 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
FfiAccountId account_id);
/**
@ -493,7 +554,7 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Inde
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `hash`: `FfiHashType` - hash of transaction
*
* # Returns
@ -503,9 +564,11 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Inde
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
FfiHashType hash);
/**
@ -513,7 +576,7 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `before`: `FfiOption<u64>` - end block of query
* - `limit`: `u64` - number of blocks to query before `before`
*
@ -524,9 +587,11 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct FfiOption_u64 before,
uint64_t limit);
@ -535,7 +600,7 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s
*
* # Arguments
*
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
* - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
* - `account_id`: `FfiAccountId` - id of queried account
* - `offset`: `u64` - first tx id of query
* - `limit`: `u64` - number of tx ids to query after `offset`
@ -547,9 +612,11 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
FfiAccountId account_id,
uint64_t offset,
uint64_t limit);

View File

@ -1,9 +1,7 @@
use std::{ffi::c_char, path::PathBuf};
use tokio::runtime::Runtime;
use crate::{
IndexerServiceFFI,
IndexerServiceFFI, Runtime,
api::{
PointerResult,
client::{UrlProtocol, addr_to_url},
@ -26,17 +24,33 @@ pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, O
///
/// An `InitializedIndexerServiceFFIResult` containing either a pointer to the
/// initialized `IndexerServiceFFI` or an error code.
///
/// # Safety
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance.
/// - `config_path` is a valid pointer to a null-terminated C string.
#[unsafe(no_mangle)]
pub extern "C" fn start_indexer(
pub unsafe extern "C" fn start_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> InitializedIndexerServiceFFIResult {
setup_indexer(config_path, port).map_or_else(
// SAFETY: The caller must ensure the validness of the `runtime` and `config_path` pointers.
unsafe { setup_indexer(runtime, config_path, port) }.map_or_else(
InitializedIndexerServiceFFIResult::from_error,
InitializedIndexerServiceFFIResult::from_value,
)
}
/// Creates a new [`tokio::runtime::Runtime`].
#[unsafe(no_mangle)]
pub extern "C" fn new_runtime() -> PointerResult<Runtime, OperationStatus> {
Runtime::new().map_or_else(
|_e| PointerResult::from_error(OperationStatus::InitializationError),
PointerResult::from_value,
)
}
/// Initializes and starts an indexer based on the provided
/// configuration file path.
///
@ -49,7 +63,13 @@ pub extern "C" fn start_indexer(
///
/// A `Result` containing either the initialized `IndexerServiceFFI` or an
/// error code.
fn setup_indexer(
///
/// # Safety
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance.
/// - `config_path` is a valid pointer to a null-terminated C string.
unsafe fn setup_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> Result<IndexerServiceFFI, OperationStatus> {
@ -66,9 +86,11 @@ fn setup_indexer(
OperationStatus::InitializationError
})?;
let rt = Runtime::new().unwrap();
// SAFETY: The caller must ensure that `runtime` is a valid pointer to a
// `tokio::runtime::Runtime` instance.
let runtime = unsafe { &*runtime };
let indexer_handle = rt
let indexer_handle = runtime
.block_on(indexer_service::run_server(config, port))
.map_err(|e| {
log::error!("Could not start indexer service: {e}");
@ -76,12 +98,14 @@ fn setup_indexer(
})?;
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?;
let indexer_client = rt.block_on(IndexerClient::new(&indexer_url)).map_err(|e| {
log::error!("Could not start indexer client: {e}");
OperationStatus::InitializationError
})?;
let indexer_client = runtime
.block_on(IndexerClient::new(&indexer_url))
.map_err(|e| {
log::error!("Could not start indexer client: {e}");
OperationStatus::InitializationError
})?;
Ok(IndexerServiceFFI::new(indexer_handle, rt, indexer_client))
Ok(IndexerServiceFFI::new(indexer_handle, indexer_client))
}
/// Stops and frees the resources associated with the given indexer service.

View File

@ -2,7 +2,7 @@ use indexer_service_protocol::{AccountId, HashType};
use indexer_service_rpc::RpcClient as _;
use crate::{
IndexerServiceFFI,
IndexerServiceFFI, Runtime,
api::{
PointerResult,
types::{
@ -19,20 +19,22 @@ use crate::{
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
///
/// # Returns
///
/// A `PointerResult<u64, OperationStatus>` indicating success or failure.
/// A `PointerResult<Option<u64>, OperationStatus>` indicating success or failure.
///
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_last_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
) -> PointerResult<u64, OperationStatus> {
) -> PointerResult<Option<u64>, OperationStatus> {
if indexer.is_null() {
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
return PointerResult::from_error(OperationStatus::NullPointer);
@ -40,8 +42,8 @@ pub unsafe extern "C" fn query_last_block(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_last_finalized_block_id())
@ -55,7 +57,7 @@ pub unsafe extern "C" fn query_last_block(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `block_id`: `u64` number of block id
///
/// # Returns
@ -65,9 +67,11 @@ pub unsafe extern "C" fn query_last_block(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
block_id: FfiBlockId,
) -> PointerResult<FfiBlockOpt, OperationStatus> {
@ -78,8 +82,8 @@ pub unsafe extern "C" fn query_block(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_block_by_id(block_id))
@ -99,7 +103,7 @@ pub unsafe extern "C" fn query_block(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `hash`: `FfiHashType` - hash of block
///
/// # Returns
@ -109,9 +113,11 @@ pub unsafe extern "C" fn query_block(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block_by_hash(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
hash: FfiHashType,
) -> PointerResult<FfiBlockOpt, OperationStatus> {
@ -122,8 +128,8 @@ pub unsafe extern "C" fn query_block_by_hash(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_block_by_hash(HashType(hash.data)))
@ -143,7 +149,7 @@ pub unsafe extern "C" fn query_block_by_hash(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `account_id`: `FfiAccountId` - id of queried account
///
/// # Returns
@ -153,9 +159,11 @@ pub unsafe extern "C" fn query_block_by_hash(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
) -> PointerResult<FfiAccount, OperationStatus> {
@ -166,8 +174,8 @@ pub unsafe extern "C" fn query_account(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_account(AccountId {
@ -187,7 +195,7 @@ pub unsafe extern "C" fn query_account(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `hash`: `FfiHashType` - hash of transaction
///
/// # Returns
@ -197,9 +205,11 @@ pub unsafe extern "C" fn query_account(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_transaction(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
hash: FfiHashType,
) -> PointerResult<FfiOption<FfiTransaction>, OperationStatus> {
@ -210,8 +220,8 @@ pub unsafe extern "C" fn query_transaction(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_transaction(HashType(hash.data)))
@ -231,7 +241,7 @@ pub unsafe extern "C" fn query_transaction(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `before`: `FfiOption<u64>` - end block of query
/// - `limit`: `u64` - number of blocks to query before `before`
///
@ -242,9 +252,11 @@ pub unsafe extern "C" fn query_transaction(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block_vec(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
before: FfiOption<u64>,
limit: u64,
@ -256,8 +268,8 @@ pub unsafe extern "C" fn query_block_vec(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
let before_std = before.is_some.then(|| unsafe { *before.value });
@ -281,7 +293,7 @@ pub unsafe extern "C" fn query_block_vec(
///
/// # Arguments
///
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried.
/// - `account_id`: `FfiAccountId` - id of queried account
/// - `offset`: `u64` - first tx id of query
/// - `limit`: `u64` - number of tx ids to query after `offset`
@ -293,9 +305,11 @@ pub unsafe extern "C" fn query_block_vec(
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_transactions_by_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
offset: u64,
@ -308,8 +322,8 @@ pub unsafe extern "C" fn query_transactions_by_account(
let indexer = unsafe { &*indexer };
let client = unsafe { indexer.client() };
let runtime = unsafe { indexer.runtime() };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_transactions_by_account(

View File

@ -1,53 +1,49 @@
use std::{ffi::c_void, net::SocketAddr};
use indexer_service::IndexerHandle;
use tokio::runtime::Runtime;
use crate::client::IndexerClient;
#[repr(C)]
pub struct IndexerServiceFFI {
indexer_handle: *mut c_void,
runtime: *mut c_void,
indexer_client: *mut c_void,
}
impl IndexerServiceFFI {
#[must_use]
pub fn new(
indexer_handle: indexer_service::IndexerHandle,
runtime: Runtime,
indexer_client: IndexerClient,
) -> Self {
Self {
// Box the complex types and convert to opaque pointers
indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::<c_void>(),
runtime: Box::into_raw(Box::new(runtime)).cast::<c_void>(),
indexer_client: Box::into_raw(Box::new(indexer_client)).cast::<c_void>(),
}
}
/// Helper to take ownership back.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub unsafe fn into_parts(self) -> (Box<IndexerHandle>, Box<Runtime>, Box<IndexerClient>) {
let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::<IndexerHandle>()) };
let runtime = unsafe { Box::from_raw(self.runtime.cast::<Runtime>()) };
let indexer_client = unsafe { Box::from_raw(self.indexer_client.cast::<IndexerClient>()) };
(indexer_handle, runtime, indexer_client)
pub fn into_parts(mut self) -> (Box<IndexerHandle>, Box<IndexerClient>) {
let Self {
indexer_handle,
indexer_client,
} = &mut self;
let indexer_handle_boxed = unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) };
let indexer_client_boxed = unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) };
// Assigning nulls to prevent double free on drop, since ownership is transferred to caller
*indexer_handle = std::ptr::null_mut();
*indexer_client = std::ptr::null_mut();
(indexer_handle_boxed, indexer_client_boxed)
}
/// Helper to get indexer handle addr.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn addr(&self) -> SocketAddr {
pub const fn addr(&self) -> SocketAddr {
let indexer_handle = unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
@ -59,13 +55,8 @@ impl IndexerServiceFFI {
}
/// Helper to get indexer handle ref.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn handle(&self) -> &IndexerHandle {
pub const fn handle(&self) -> &IndexerHandle {
unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
@ -75,13 +66,8 @@ impl IndexerServiceFFI {
}
/// Helper to get indexer client ref.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn client(&self) -> &IndexerClient {
pub const fn client(&self) -> &IndexerClient {
unsafe {
self.indexer_client
.cast::<IndexerClient>()
@ -89,22 +75,6 @@ impl IndexerServiceFFI {
.expect("Indexer Client must be non-null pointer")
}
}
/// Helper to get indexer runtime ref.
///
/// # Safety
///
/// The caller must ensure that:
/// - `self` is a valid object(contains valid pointers in all fields)
#[must_use]
pub const unsafe fn runtime(&self) -> &Runtime {
unsafe {
self.runtime
.cast::<Runtime>()
.as_ref()
.expect("Indexer Runtime must be non-null pointer")
}
}
}
// Implement Drop to prevent memory leaks
@ -112,21 +82,14 @@ impl Drop for IndexerServiceFFI {
fn drop(&mut self) {
let Self {
indexer_handle,
runtime,
indexer_client,
} = self;
if indexer_handle.is_null() {
log::error!("Attempted to drop a null indexer pointer. This is a bug");
if !indexer_handle.is_null() {
drop(unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) });
}
if runtime.is_null() {
log::error!("Attempted to drop a null tokio runtime pointer. This is a bug");
if !indexer_client.is_null() {
drop(unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) });
}
if indexer_client.is_null() {
log::error!("Attempted to drop a null client pointer. This is a bug");
}
drop(unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) });
drop(unsafe { Box::from_raw(runtime.cast::<Runtime>()) });
drop(unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) });
}
}

View File

@ -2,8 +2,10 @@
pub use errors::OperationStatus;
pub use indexer::IndexerServiceFFI;
pub use runtime::Runtime;
pub mod api;
mod client;
mod errors;
mod indexer;
mod runtime;

129
indexer/ffi/src/runtime.rs Normal file
View File

@ -0,0 +1,129 @@
use std::ffi::c_void;
/// Wrapper around [`tokio::runtime::Runtime`] that can be safely passed across the FFI boundary.
#[repr(C)]
pub struct Runtime {
inner: Pointer<tokio::runtime::Runtime>,
}
impl Runtime {
/// Creates a new owned [`Runtime`] instance.
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let inner = tokio::runtime::Runtime::new()?;
Ok(Self {
inner: Pointer::owned(inner),
})
}
/// Creates a new owned [`Runtime`] instance from an existing [`tokio::runtime::Runtime`].
pub fn from_owned(inner: tokio::runtime::Runtime) -> Self {
Self {
inner: Pointer::owned(inner),
}
}
/// Creates a new borrowed [`Runtime`] instance from a reference to an existing
/// `tokio::runtime::Runtime`.
///
/// # Safety
/// The caller must ensure that the provided reference remains valid for the lifetime of the
/// returned [`Runtime`].
pub const unsafe fn from_borrowed(inner: &tokio::runtime::Runtime) -> Self {
Self {
// SAFETY: The caller must ensure the validness of the `inner` reference.
inner: unsafe { Pointer::borrowed(inner) },
}
}
}
impl AsRef<tokio::runtime::Runtime> for Runtime {
fn as_ref(&self) -> &tokio::runtime::Runtime {
self.inner
.as_ref()
.expect("Runtime pointer should not be null")
}
}
impl std::ops::Deref for Runtime {
type Target = tokio::runtime::Runtime;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
#[repr(C)]
struct Pointer<T> {
kind: PointerKind,
_marker: std::marker::PhantomData<T>,
}
#[repr(C)]
enum PointerKind {
Owned(*mut c_void),
Borrowed(*const c_void),
Null,
}
impl<T> Pointer<T> {
/// Creates a new owned pointer from a value.
pub fn owned(value: T) -> Self {
let boxed = Box::new(value);
let kind = PointerKind::Owned(Box::into_raw(boxed).cast::<c_void>());
Self {
kind,
_marker: std::marker::PhantomData,
}
}
/// Creates a new borrowed pointer from a reference to an existing value.
///
/// # Safety
/// The caller must ensure that the provided reference remains valid for the lifetime of the
/// returned pointer.
pub const unsafe fn borrowed(value: &T) -> Self {
let kind = PointerKind::Borrowed(std::ptr::from_ref(value).cast::<c_void>());
Self {
kind,
_marker: std::marker::PhantomData,
}
}
/// Returns a reference to the value if the pointer is owned or borrowed, or [`None`] if it is
/// null.
pub const fn as_ref(&self) -> Option<&T> {
match self.kind {
PointerKind::Owned(ptr) => unsafe { (ptr.cast::<T>()).as_ref() },
PointerKind::Borrowed(ptr) => unsafe { (ptr.cast::<T>()).as_ref() },
PointerKind::Null => None,
}
}
/// Takes ownership of the pointer if it is owned, returning the raw pointer and leaving a null
/// pointer in its place.
/// If the pointer is borrowed or null, returns [`None`].
#[expect(dead_code, reason = "May be useful in future")]
pub fn take(&mut self) -> Option<T> {
match std::mem::replace(&mut self.kind, PointerKind::Null) {
PointerKind::Owned(ptr) => {
// SAFETY: We ensure that the pointer is valid and was allocated by us.
let boxed = unsafe { Box::from_raw(ptr.cast::<T>()) };
Some(*boxed)
}
PointerKind::Borrowed(_) | PointerKind::Null => None,
}
}
}
impl<T> Drop for Pointer<T> {
fn drop(&mut self) {
let Self { kind, _marker } = self;
if let PointerKind::Owned(ptr) = *kind {
// SAFETY: We ensure that the pointer is valid and was allocated by us.
unsafe {
drop(Box::from_raw(ptr.cast::<T>()));
}
}
}
}

View File

@ -4,153 +4,5 @@
"bedrock_config": {
"addr": "http://localhost:8080"
},
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
"initial_accounts": [
{
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
"balance": 10000
},
{
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
"balance": 20000
}
],
"initial_commitments": [
{
"npk": [
139,
19,
158,
11,
155,
231,
85,
206,
132,
228,
220,
114,
145,
89,
113,
156,
238,
142,
242,
74,
182,
91,
43,
100,
6,
190,
31,
15,
31,
88,
96,
204
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 10000,
"data": [],
"nonce": 0
}
},
{
"npk": [
173,
134,
33,
223,
54,
226,
10,
71,
215,
254,
143,
172,
24,
244,
243,
208,
65,
112,
118,
70,
217,
240,
69,
100,
129,
3,
121,
25,
213,
132,
42,
45
],
"account": {
"program_owner": [
0,
0,
0,
0,
0,
0,
0,
0
],
"balance": 20000,
"data": [],
"nonce": 0
}
}
],
"signing_key": [
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37,
37
]
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101"
}

View File

@ -27,7 +27,7 @@ pub trait Rpc {
async fn subscribe_to_finalized_blocks(&self) -> SubscriptionResult;
#[method(name = "getLastFinalizedBlockId")]
async fn get_last_finalized_block_id(&self) -> Result<BlockId, ErrorObjectOwned>;
async fn get_last_finalized_block_id(&self) -> Result<Option<BlockId>, ErrorObjectOwned>;
#[method(name = "getBlockById")]
async fn get_block_by_id(&self, block_id: BlockId) -> Result<Option<Block>, ErrorObjectOwned>;

View File

@ -16,6 +16,7 @@ pub struct IndexerHandle {
/// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`.
server_handle: Option<ServerHandle>,
}
impl IndexerHandle {
const fn new(addr: SocketAddr, server_handle: ServerHandle) -> Self {
Self {

View File

@ -190,18 +190,16 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
Ok(())
}
async fn get_last_finalized_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
self.state
async fn get_last_finalized_block_id(&self) -> Result<Option<BlockId>, ErrorObjectOwned> {
Ok(self
.state
.read()
.await
.blocks
.iter()
.rev()
.find(|block| block.bedrock_status == BedrockStatus::Finalized)
.map(|block| block.header.block_id)
.ok_or_else(|| {
ErrorObjectOwned::owned(-32001, "Last block not found".to_owned(), None::<()>)
})
.map(|block| block.header.block_id))
}
async fn get_block_by_id(&self, block_id: BlockId) -> Result<Option<Block>, ErrorObjectOwned> {

View File

@ -48,7 +48,7 @@ impl indexer_service_rpc::RpcServer for IndexerService {
Ok(())
}
async fn get_last_finalized_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
async fn get_last_finalized_block_id(&self) -> Result<Option<BlockId>, ErrorObjectOwned> {
self.indexer.store.get_last_block_id().map_err(db_error)
}
@ -214,43 +214,49 @@ impl SubscriptionService {
tokio::sync::mpsc::unbounded_channel::<Subscription<BlockId>>();
let handle = tokio::spawn(async move {
let mut subscribers = Vec::new();
let run_loop = async {
let mut subscribers = Vec::new();
let mut block_stream = pin!(indexer.subscribe_parse_block_stream());
let mut block_stream = pin!(indexer.subscribe_parse_block_stream());
#[expect(
clippy::integer_division_remainder_used,
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
)]
loop {
tokio::select! {
sub = sub_receiver.recv() => {
let Some(subscription) = sub else {
bail!("Subscription receiver closed unexpectedly");
};
info!("Added new subscription with ID {:?}", subscription.sink.subscription_id());
subscribers.push(subscription);
}
block_opt = block_stream.next() => {
debug!("Got new block from block stream");
let Some(block) = block_opt else {
bail!("Block stream ended unexpectedly");
};
let block = block.context("Failed to get L2 block data")?;
let block: indexer_service_protocol::Block = block.into();
#[expect(
clippy::integer_division_remainder_used,
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
)]
loop {
tokio::select! {
sub = sub_receiver.recv() => {
let Some(subscription) = sub else {
bail!("Subscription receiver closed unexpectedly");
};
info!("Added new subscription with ID {:?}", subscription.sink.subscription_id());
subscribers.push(subscription);
}
block_opt = block_stream.next() => {
debug!("Got new block from block stream");
let Some(block) = block_opt else {
bail!("Block stream ended unexpectedly");
};
let block = block.context("Failed to get L2 block data")?;
let block: indexer_service_protocol::Block = block.into();
for sub in &mut subscribers {
if let Err(err) = sub.try_send(&block.header.block_id) {
warn!(
"Failed to send block ID {:?} to subscription ID {:?} with error: {err:#?}",
block.header.block_id,
sub.sink.subscription_id(),
);
for sub in &mut subscribers {
if let Err(err) = sub.try_send(&block.header.block_id) {
warn!(
"Failed to send block ID {:?} to subscription ID {:?} with error: {err:#?}",
block.header.block_id,
sub.sink.subscription_id(),
);
}
}
}
}
}
}
};
let res: anyhow::Result<futures::never::Never> = run_loop.await;
let Err(err) = res;
error!("Subscription service loop has unexpectedly finished with error: {err:#?}");
Err(err)
});
SubscriptionLoopParts {
handle,

View File

@ -10,6 +10,7 @@ workspace = true
[dependencies]
nssa_core = { workspace = true, features = ["host"] }
nssa.workspace = true
authenticated_transfer_core.workspace = true
sequencer_core = { workspace = true, features = ["default", "testnet"] }
sequencer_service.workspace = true
wallet.workspace = true
@ -28,7 +29,6 @@ testnet_initial_state.workspace = true
indexer_service_protocol.workspace = true
url.workspace = true
anyhow.workspace = true
env_logger.workspace = true
log.workspace = true

View File

@ -3,16 +3,13 @@ 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::{Account, AccountId, PrivateKey, PublicKey};
use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID};
use sequencer_core::config::{BedrockConfig, SequencerConfig};
use testnet_initial_state::{
PrivateAccountPrivateInitialData, PrivateAccountPublicInitialData,
PublicAccountPrivateInitialData, PublicAccountPublicInitialData,
};
use nssa::{AccountId, PrivateKey, PublicKey};
use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig};
use url::Url;
use wallet::config::{InitialAccountData, WalletConfig};
use wallet::config::WalletConfig;
pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [20_000, 40_000];
pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
/// Sequencer config options available for custom changes in integration tests.
#[derive(Debug, Clone, Copy)]
@ -34,121 +31,6 @@ impl Default for SequencerPartialConfig {
}
}
pub struct InitialData {
pub public_accounts: Vec<(PrivateKey, u128)>,
pub private_accounts: Vec<(KeyChain, Account)>,
}
impl InitialData {
#[must_use]
pub fn with_two_public_and_two_private_initialized_accounts() -> Self {
let mut public_alice_private_key = PrivateKey::new_os_random();
let mut public_alice_public_key =
PublicKey::new_from_private_key(&public_alice_private_key);
let mut public_alice_account_id = AccountId::from(&public_alice_public_key);
let mut public_bob_private_key = PrivateKey::new_os_random();
let mut public_bob_public_key = PublicKey::new_from_private_key(&public_bob_private_key);
let mut public_bob_account_id = AccountId::from(&public_bob_public_key);
// Ensure consistent ordering
if public_alice_account_id > public_bob_account_id {
std::mem::swap(&mut public_alice_private_key, &mut public_bob_private_key);
std::mem::swap(&mut public_alice_public_key, &mut public_bob_public_key);
std::mem::swap(&mut public_alice_account_id, &mut public_bob_account_id);
}
let mut private_charlie_key_chain = KeyChain::new_os_random();
let mut private_charlie_account_id =
AccountId::from((&private_charlie_key_chain.nullifier_public_key, 0));
let mut private_david_key_chain = KeyChain::new_os_random();
let mut private_david_account_id =
AccountId::from((&private_david_key_chain.nullifier_public_key, 0));
// Ensure consistent ordering
if private_charlie_account_id > private_david_account_id {
std::mem::swap(&mut private_charlie_key_chain, &mut private_david_key_chain);
std::mem::swap(
&mut private_charlie_account_id,
&mut private_david_account_id,
);
}
Self {
public_accounts: vec![
(public_alice_private_key, 10_000),
(public_bob_private_key, 20_000),
],
private_accounts: vec![
(
private_charlie_key_chain,
Account {
balance: 10_000,
data: Data::default(),
program_owner: DEFAULT_PROGRAM_ID,
nonce: 0_u128.into(),
},
),
(
private_david_key_chain,
Account {
balance: 20_000,
data: Data::default(),
program_owner: DEFAULT_PROGRAM_ID,
nonce: 0_u128.into(),
},
),
],
}
}
fn sequencer_initial_public_accounts(&self) -> Vec<PublicAccountPublicInitialData> {
self.public_accounts
.iter()
.map(|(priv_key, balance)| {
let pub_key = PublicKey::new_from_private_key(priv_key);
let account_id = AccountId::from(&pub_key);
PublicAccountPublicInitialData {
account_id,
balance: *balance,
}
})
.collect()
}
fn sequencer_initial_private_accounts(&self) -> Vec<PrivateAccountPublicInitialData> {
self.private_accounts
.iter()
.map(|(key_chain, account)| PrivateAccountPublicInitialData {
npk: key_chain.nullifier_public_key,
account: account.clone(),
})
.collect()
}
fn wallet_initial_accounts(&self) -> Vec<InitialAccountData> {
self.public_accounts
.iter()
.map(|(priv_key, _)| {
let pub_key = PublicKey::new_from_private_key(priv_key);
let account_id = AccountId::from(&pub_key);
InitialAccountData::Public(PublicAccountPrivateInitialData {
account_id,
pub_sign_key: priv_key.clone(),
})
})
.chain(self.private_accounts.iter().map(|(key_chain, account)| {
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account: account.clone(),
key_chain: key_chain.clone(),
identifier: 0,
}))
}))
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub enum UrlProtocol {
Http,
@ -168,7 +50,7 @@ pub fn sequencer_config(
partial: SequencerPartialConfig,
home: PathBuf,
bedrock_addr: SocketAddr,
initial_data: &InitialData,
genesis_transactions: Vec<GenesisTransaction>,
) -> Result<SequencerConfig> {
let SequencerPartialConfig {
max_num_tx_in_block,
@ -179,15 +61,13 @@ pub fn sequencer_config(
Ok(SequencerConfig {
home,
genesis_id: 1,
is_genesis_random: true,
max_num_tx_in_block,
max_block_size,
mempool_max_size,
block_create_timeout,
retry_pending_blocks_timeout: Duration::from_secs(5),
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
genesis: genesis_transactions,
signing_key: [37; 32],
bedrock_config: BedrockConfig {
channel_id: bedrock_channel_id(),
@ -198,10 +78,46 @@ pub fn sequencer_config(
})
}
pub fn wallet_config(
sequencer_addr: SocketAddr,
initial_data: &InitialData,
) -> Result<WalletConfig> {
#[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 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]),
]
}
#[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,
}
})
.collect()
}
pub fn wallet_config(sequencer_addr: SocketAddr) -> Result<WalletConfig> {
Ok(WalletConfig {
sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr)
.context("Failed to convert sequencer addr to URL")?,
@ -209,16 +125,11 @@ pub fn wallet_config(
seq_tx_poll_max_blocks: 15,
seq_poll_max_retries: 10,
seq_block_poll_max_amount: 100,
initial_accounts: Some(initial_data.wallet_initial_accounts()),
basic_auth: None,
})
}
pub fn indexer_config(
bedrock_addr: SocketAddr,
home: PathBuf,
initial_data: &InitialData,
) -> Result<IndexerConfig> {
pub fn indexer_config(bedrock_addr: SocketAddr, home: PathBuf) -> Result<IndexerConfig> {
Ok(IndexerConfig {
home,
consensus_info_polling_interval: Duration::from_secs(1),
@ -227,9 +138,6 @@ pub fn indexer_config(
.context("Failed to convert bedrock addr to URL")?,
auth: None,
},
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
signing_key: [37; 32],
channel_id: bedrock_channel_id(),
})
}

View File

@ -1,6 +1,6 @@
//! This library contains common code for integration tests.
use std::sync::LazyLock;
use std::{net::SocketAddr, sync::LazyLock};
use anyhow::{Context as _, Result};
use common::{HashType, transaction::NSSATransaction};
@ -9,21 +9,24 @@ use indexer_service::IndexerHandle;
use log::{debug, error};
use nssa::{AccountId, PrivacyPreservingTransaction};
use nssa_core::Commitment;
use sequencer_core::config::GenesisTransaction;
use sequencer_service::SequencerHandle;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::WalletCore;
use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention};
use crate::{
indexer_client::IndexerClient,
setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet},
setup::{
setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply,
setup_sequencer, setup_wallet,
},
};
pub mod config;
pub mod indexer_client;
pub mod setup;
pub mod test_context_ffi;
// TODO: Remove this and control time from tests
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
@ -35,6 +38,26 @@ const BEDROCK_SERVICE_PORT: u16 = 18080;
static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init);
struct IndexerComponents {
indexer_handle: IndexerHandle,
indexer_client: IndexerClient,
_temp_dir: TempDir,
}
impl Drop for IndexerComponents {
fn drop(&mut self) {
let Self {
indexer_handle,
indexer_client: _,
_temp_dir: _,
} = self;
if !indexer_handle.is_healthy() {
error!("Indexer handle has unexpectedly stopped before IndexerComponents drop");
}
}
}
/// Test context which sets up a sequencer and a wallet for integration tests.
///
/// It's memory and logically safe to create multiple instances of this struct in parallel tests,
@ -42,14 +65,13 @@ static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init);
// NOTE: Order of fields is important for proper drop order.
pub struct TestContext {
sequencer_client: SequencerClient,
indexer_client: IndexerClient,
wallet: WalletCore,
wallet_password: String,
/// Optional to move out value in Drop.
sequencer_handle: Option<SequencerHandle>,
indexer_handle: IndexerHandle,
indexer_components: Option<IndexerComponents>,
bedrock_compose: DockerCompose,
_temp_indexer_dir: TempDir,
bedrock_addr: SocketAddr,
_temp_sequencer_dir: TempDir,
_temp_wallet_dir: TempDir,
}
@ -60,61 +82,12 @@ impl TestContext {
Self::builder().build().await
}
/// Get a builder for the test context to customize its configuration.
#[must_use]
pub const fn builder() -> TestContextBuilder {
TestContextBuilder::new()
}
async fn new_configured(
sequencer_partial_config: config::SequencerPartialConfig,
initial_data: config::InitialData,
) -> Result<Self> {
// Ensure logger is initialized only once
*LOGGER;
debug!("Test context setup");
let (bedrock_compose, bedrock_addr) = setup_bedrock_node().await?;
let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr, &initial_data)
.await
.context("Failed to setup Indexer")?;
let (sequencer_handle, temp_sequencer_dir) =
setup_sequencer(sequencer_partial_config, bedrock_addr, &initial_data)
.await
.context("Failed to setup Sequencer")?;
let (wallet, temp_wallet_dir, wallet_password) =
setup_wallet(sequencer_handle.addr(), &initial_data)
.await
.context("Failed to setup wallet")?;
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
.context("Failed to convert sequencer addr to URL")?;
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr())
.context("Failed to convert indexer addr to URL")?;
let sequencer_client = SequencerClientBuilder::default()
.build(sequencer_url)
.context("Failed to create sequencer client")?;
let indexer_client = IndexerClient::new(&indexer_url)
.await
.context("Failed to create indexer client")?;
Ok(Self {
sequencer_client,
indexer_client,
wallet,
wallet_password,
bedrock_compose,
sequencer_handle: Some(sequencer_handle),
indexer_handle,
_temp_indexer_dir: temp_indexer_dir,
_temp_sequencer_dir: temp_sequencer_dir,
_temp_wallet_dir: temp_wallet_dir,
})
}
/// Get reference to the wallet.
#[must_use]
pub const fn wallet(&self) -> &WalletCore {
@ -137,10 +110,38 @@ impl TestContext {
&self.sequencer_client
}
/// Get reference to the indexer client.
/// Get the Bedrock Node address.
#[must_use]
pub const fn indexer_client(&self) -> &IndexerClient {
&self.indexer_client
pub const fn bedrock_addr(&self) -> SocketAddr {
self.bedrock_addr
}
/// Get reference to the indexer.
///
/// # Panics
///
/// Panics if the indexer is not enabled in the test context. See
/// [`TestContextBuilder::disable_indexer()`].
#[must_use]
pub fn indexer(&self) -> &IndexerHandle {
self.indexer_components
.as_ref()
.map(|components| &components.indexer_handle)
.expect("Called `TestContext::indexer()` on context with disabled indexer")
}
/// Get reference to the indexer client.
///
/// # Panics
///
/// Panics if the indexer is not enabled in the test context. See
/// [`TestContextBuilder::disable_indexer()`].
#[must_use]
pub fn indexer_client(&self) -> &IndexerClient {
self.indexer_components
.as_ref()
.map(|components| &components.indexer_client)
.expect("Called `TestContext::indexer_client()` on context with disabled indexer")
}
/// Get existing public account IDs in the wallet.
@ -148,8 +149,9 @@ impl TestContext {
pub fn existing_public_accounts(&self) -> Vec<AccountId> {
self.wallet
.storage()
.user_data
.key_chain()
.public_account_ids()
.map(|(account_id, _idx)| account_id)
.collect()
}
@ -158,8 +160,9 @@ impl TestContext {
pub fn existing_private_accounts(&self) -> Vec<AccountId> {
self.wallet
.storage()
.user_data
.key_chain()
.private_account_ids()
.map(|(account_id, _idx)| account_id)
.collect()
}
}
@ -168,15 +171,14 @@ impl Drop for TestContext {
fn drop(&mut self) {
let Self {
sequencer_handle,
indexer_handle,
bedrock_compose,
_temp_indexer_dir: _,
_temp_sequencer_dir: _,
_temp_wallet_dir: _,
bedrock_addr: _,
indexer_components: _,
sequencer_client: _,
indexer_client: _,
wallet: _,
wallet_password: _,
_temp_sequencer_dir: _,
_temp_wallet_dir: _,
} = self;
let sequencer_handle = sequencer_handle
@ -192,10 +194,6 @@ impl Drop for TestContext {
);
}
if !indexer_handle.is_healthy() {
error!("Indexer handle has unexpectedly stopped before TestContext drop");
}
let container = bedrock_compose
.service(BEDROCK_SERVICE_WITH_OPEN_PORT)
.unwrap_or_else(|| {
@ -216,43 +214,24 @@ impl Drop for TestContext {
}
}
/// A test context to be used in normal #[test] tests.
pub struct BlockingTestContext {
ctx: Option<TestContext>,
runtime: tokio::runtime::Runtime,
}
impl BlockingTestContext {
pub fn new() -> Result<Self> {
let runtime = tokio::runtime::Runtime::new().unwrap();
let ctx = runtime.block_on(TestContext::new())?;
Ok(Self {
ctx: Some(ctx),
runtime,
})
}
pub const fn ctx(&self) -> &TestContext {
self.ctx.as_ref().expect("TestContext is set")
}
}
pub struct TestContextBuilder {
initial_data: Option<config::InitialData>,
genesis_transactions: Option<Vec<GenesisTransaction>>,
sequencer_partial_config: Option<config::SequencerPartialConfig>,
enable_indexer: bool,
}
impl TestContextBuilder {
const fn new() -> Self {
Self {
initial_data: None,
genesis_transactions: None,
sequencer_partial_config: None,
enable_indexer: false,
}
}
#[must_use]
pub fn with_initial_data(mut self, initial_data: config::InitialData) -> Self {
self.initial_data = Some(initial_data);
pub fn with_genesis(mut self, genesis_transactions: Vec<GenesisTransaction>) -> Self {
self.genesis_transactions = Some(genesis_transactions);
self
}
@ -265,14 +244,135 @@ impl TestContextBuilder {
self
}
/// Exclude Indexer from test context.
/// Indexer is enabled by default.
///
/// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if
/// called when indexer is disabled.
#[must_use]
pub const fn disable_indexer(mut self) -> Self {
self.enable_indexer = false;
self
}
pub async fn build(self) -> Result<TestContext> {
TestContext::new_configured(
self.sequencer_partial_config.unwrap_or_default(),
self.initial_data.unwrap_or_else(|| {
config::InitialData::with_two_public_and_two_private_initialized_accounts()
}),
let Self {
genesis_transactions,
sequencer_partial_config,
enable_indexer,
} = self;
// Ensure logger is initialized only once
*LOGGER;
debug!("Test context setup");
let (bedrock_compose, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to setup Bedrock node")?;
let indexer_components = if enable_indexer {
let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr)
.await
.context("Failed to setup Indexer")?;
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr())
.context("Failed to convert indexer addr to URL")?;
let indexer_client = IndexerClient::new(&indexer_url)
.await
.context("Failed to create indexer client")?;
Some(IndexerComponents {
indexer_handle,
indexer_client,
_temp_dir: temp_indexer_dir,
})
} else {
None
};
let initial_public_accounts = config::default_public_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)),
)
.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)
.await
.context("Failed to initialize private accounts in wallet")?;
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
.context("Failed to convert sequencer addr to URL")?;
let sequencer_client = SequencerClientBuilder::default()
.build(sequencer_url)
.context("Failed to create sequencer client")?;
Ok(TestContext {
sequencer_client,
wallet,
wallet_password,
bedrock_compose,
bedrock_addr,
sequencer_handle: Some(sequencer_handle),
indexer_components,
_temp_sequencer_dir: temp_sequencer_dir,
_temp_wallet_dir: temp_wallet_dir,
})
}
pub fn build_blocking(self) -> Result<BlockingTestContext> {
let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?;
let ctx = runtime.block_on(self.build())?;
Ok(BlockingTestContext {
ctx: Some(ctx),
runtime,
})
}
}
/// A test context to be used in normal #[test] tests.
pub struct BlockingTestContext {
ctx: Option<TestContext>,
runtime: tokio::runtime::Runtime,
}
impl BlockingTestContext {
pub fn new() -> Result<Self> {
TestContext::builder().build_blocking()
}
pub const fn ctx(&self) -> &TestContext {
self.ctx.as_ref().expect("TestContext is set")
}
pub const fn runtime(&self) -> &tokio::runtime::Runtime {
&self.runtime
}
pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output
where
F: std::future::Future + 'ctx,
{
let future = f(self.ctx());
self.runtime.block_on(future)
}
pub fn block_on_mut<'ctx, F>(
&'ctx mut self,
f: impl FnOnce(&'ctx mut TestContext) -> F,
) -> F::Output
where
F: std::future::Future + 'ctx,
{
let ctx_mut = self.ctx.as_mut().expect("TestContext is set");
let future = f(ctx_mut);
self.runtime.block_on(future)
}
}
@ -290,13 +390,13 @@ impl Drop for BlockingTestContext {
}
#[must_use]
pub fn format_public_account_id(account_id: AccountId) -> String {
format!("Public/{account_id}")
pub const fn public_mention(account_id: AccountId) -> CliAccountMention {
CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id))
}
#[must_use]
pub fn format_private_account_id(account_id: AccountId) -> String {
format!("Private/{account_id}")
pub const fn private_mention(account_id: AccountId) -> CliAccountMention {
CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id))
}
#[expect(

View File

@ -1,27 +1,30 @@
use std::{
ffi::{CString, c_char},
fs::File,
io::Write as _,
net::SocketAddr,
path::PathBuf,
};
use std::{net::SocketAddr, path::PathBuf};
use anyhow::{Context as _, Result, bail};
use indexer_ffi::{IndexerServiceFFI, api::lifecycle::InitializedIndexerServiceFFIResult};
use indexer_service::IndexerHandle;
use log::{debug, warn};
use sequencer_service::SequencerHandle;
use nssa::PrivateKey;
use sequencer_service::{GenesisTransaction, SequencerHandle};
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::{WalletCore, config::WalletConfigOverrides};
use wallet::{
WalletCore,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
execute_subcommand,
programs::native_token_transfer::AuthTransferSubcommand,
},
config::WalletConfigOverrides,
};
use crate::{BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, config};
use crate::{
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT,
config::{self, INITIAL_PRIVATE_BALANCES_FOR_WALLET},
private_mention, public_mention,
};
unsafe extern "C" {
fn start_indexer(config_path: *const c_char, port: u16) -> InitializedIndexerServiceFFIResult;
}
pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let bedrock_compose_path = PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml");
@ -91,10 +94,7 @@ pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)>
Ok((compose, addr))
}
pub(crate) async fn setup_indexer(
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(IndexerHandle, TempDir)> {
pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
@ -103,12 +103,8 @@ pub(crate) async fn setup_indexer(
temp_indexer_dir.path().display()
);
let indexer_config = config::indexer_config(
bedrock_addr,
temp_indexer_dir.path().to_owned(),
initial_data,
)
.context("Failed to create Indexer config")?;
let indexer_config = config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned())
.context("Failed to create Indexer config")?;
indexer_service::run_server(indexer_config, 0)
.await
@ -116,10 +112,10 @@ pub(crate) async fn setup_indexer(
.map(|handle| (handle, temp_indexer_dir))
}
pub(crate) async fn setup_sequencer(
pub async fn setup_sequencer(
partial: config::SequencerPartialConfig,
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
genesis_transactions: Vec<GenesisTransaction>,
) -> Result<(SequencerHandle, TempDir)> {
let temp_sequencer_dir =
tempfile::tempdir().context("Failed to create temp dir for sequencer home")?;
@ -133,7 +129,7 @@ pub(crate) async fn setup_sequencer(
partial,
temp_sequencer_dir.path().to_owned(),
bedrock_addr,
initial_data,
genesis_transactions,
)
.context("Failed to create Sequencer config")?;
@ -142,12 +138,11 @@ pub(crate) async fn setup_sequencer(
Ok((sequencer_handle, temp_sequencer_dir))
}
pub(crate) async fn setup_wallet(
pub fn setup_wallet(
sequencer_addr: SocketAddr,
initial_data: &config::InitialData,
initial_public_accounts: &[(PrivateKey, u128)],
) -> Result<(WalletCore, TempDir, String)> {
let config = config::wallet_config(sequencer_addr, initial_data)
.context("Failed to create Wallet config")?;
let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?;
let config_serialized =
serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?;
@ -162,57 +157,94 @@ pub(crate) async fn setup_wallet(
let config_overrides = WalletConfigOverrides::default();
let wallet_password = "test_pass".to_owned();
let (wallet, _mnemonic) = WalletCore::new_init_storage(
let (mut wallet, _mnemonic) = WalletCore::new_init_storage(
config_path,
storage_path,
Some(config_overrides),
&wallet_password,
)
.context("Failed to init wallet")?;
for (private_key, _balance) in initial_public_accounts {
wallet
.storage_mut()
.key_chain_mut()
.add_imported_public_account(private_key.clone());
}
wallet
.store_persistent_data()
.await
.context("Failed to store wallet persistent data")?;
Ok((wallet, temp_wallet_dir, wallet_password))
}
pub(crate) fn setup_indexer_ffi(
bedrock_addr: SocketAddr,
initial_data: &config::InitialData,
) -> Result<(IndexerServiceFFI, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
debug!(
"Using temp indexer home at {}",
temp_indexer_dir.path().display()
);
let indexer_config = config::indexer_config(
bedrock_addr,
temp_indexer_dir.path().to_owned(),
initial_data,
)
.context("Failed to create Indexer config")?;
let config_json = serde_json::to_vec(&indexer_config)?;
let config_path = temp_indexer_dir.path().join("indexer_config.json");
let mut file = File::create(config_path.as_path())?;
file.write_all(&config_json)?;
file.flush()?;
let res =
// SAFETY: lib function ensures validity of value.
unsafe { start_indexer(CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) };
if res.error.is_error() {
anyhow::bail!("Indexer FFI error {:?}", res.error);
pub async fn setup_private_accounts_with_initial_supply(wallet: &mut WalletCore) -> Result<()> {
for _ in INITIAL_PRIVATE_BALANCES_FOR_WALLET {
let result = execute_subcommand(
wallet,
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await
.context("Failed to create a private account")?;
let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else {
bail!("Expected RegisterAccount return value when creating private account");
};
}
Ok((
// SAFETY: lib function ensures validity of value.
unsafe { std::ptr::read(res.value) },
temp_indexer_dir,
))
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"
);
}
}
Ok(())
}

View File

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

View File

@ -3,16 +3,21 @@
reason = "We don't care about these in tests"
)]
use anyhow::Result;
use integration_tests::{TestContext, format_private_account_id};
use anyhow::{Context as _, Result};
use integration_tests::{TestContext, private_mention};
use key_protocol::key_management::KeyChain;
use log::info;
use nssa::program::Program;
use nssa::{Data, program::Program};
use nssa_core::account::Nonce;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::{
Command,
account::{AccountSubcommand, NewSubcommand},
execute_subcommand,
use wallet::{
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, ImportSubcommand, NewSubcommand},
execute_subcommand,
},
};
#[test]
@ -30,7 +35,7 @@ async fn get_existing_account() -> Result<()> {
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
assert_eq!(account.nonce.0, 0);
assert_eq!(account.nonce.0, 1);
info!("Successfully retrieved account with correct details");
@ -41,7 +46,7 @@ async fn get_existing_account() -> Result<()> {
async fn new_public_account_with_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
let label = "my-test-public-account".to_owned();
let label = Label::new("my-test-public-account");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(label.clone()),
@ -55,14 +60,9 @@ async fn new_public_account_with_label() -> Result<()> {
};
// Verify the label was stored
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the new account");
let resolved = ctx.wallet().storage().resolve_label(&label);
assert_eq!(stored_label.to_string(), label);
assert_eq!(resolved, Some(AccountIdWithPrivacy::Public(account_id)));
info!("Successfully created public account with label");
@ -74,23 +74,17 @@ async fn add_label_to_existing_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let account_id = ctx.existing_private_accounts()[0];
let label = "my-test-private-account".to_owned();
let label = Label::new("my-test-private-account");
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_private_account_id(account_id)),
account_label: None,
account_id: private_mention(account_id),
label: label.clone(),
});
execute_subcommand(ctx.wallet_mut(), command).await?;
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the account");
let resolved = ctx.wallet().storage().resolve_label(&label);
assert_eq!(stored_label.to_string(), label);
assert_eq!(resolved, Some(AccountIdWithPrivacy::Private(account_id)));
info!("Successfully set label on existing private account");
@ -114,12 +108,13 @@ async fn new_public_account_without_label() -> Result<()> {
panic!("Expected RegisterAccount return value")
};
// Verify no label was stored
// Verify no label was stored for the account id
assert!(
!ctx.wallet()
ctx.wallet()
.storage()
.labels
.contains_key(&account_id.to_string()),
.labels_for_account(AccountIdWithPrivacy::Public(account_id))
.next()
.is_none(),
"No label should be stored when not provided"
);
@ -127,3 +122,150 @@ async fn new_public_account_without_label() -> Result<()> {
Ok(())
}
#[test]
async fn import_public_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let private_key = nssa::PrivateKey::new_os_random();
let account_id = nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(&private_key));
let command = Command::Account(AccountSubcommand::Import(ImportSubcommand::Public {
private_key,
}));
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::Empty = sub_ret else {
anyhow::bail!("Expected Empty return value");
};
let imported_key = ctx
.wallet()
.storage()
.key_chain()
.pub_account_signing_key(account_id);
assert!(
imported_key.is_some(),
"Imported public account should be present"
);
Ok(())
}
#[test]
async fn import_private_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let key_chain = KeyChain::new_os_random();
let account_id = nssa::AccountId::from((&key_chain.nullifier_public_key, 0));
let account = nssa::Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: 777,
data: Data::default(),
nonce: Nonce::default(),
};
let key_chain_json = serde_json::to_string(&key_chain)
.context("Failed to serialize key chain for private import")?;
let account_state = HumanReadableAccount::from(account.clone());
let command = Command::Account(AccountSubcommand::Import(ImportSubcommand::Private {
key_chain_json,
account_state,
chain_index: None,
identifier: 0,
}));
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::Empty = sub_ret else {
anyhow::bail!("Expected Empty return value");
};
let imported_acc = ctx
.wallet()
.storage()
.key_chain()
.private_account(account_id)
.context("Imported private account should be present")?;
assert_eq!(
imported_acc.key_chain.secret_spending_key,
key_chain.secret_spending_key
);
assert_eq!(
imported_acc.key_chain.nullifier_public_key,
key_chain.nullifier_public_key
);
assert_eq!(
imported_acc.key_chain.viewing_public_key,
key_chain.viewing_public_key
);
assert_eq!(imported_acc.chain_index, None);
assert_eq!(imported_acc.identifier, 0);
assert_eq!(imported_acc.account, &account);
Ok(())
}
#[test]
async fn import_private_account_second_time_overrides_account_data() -> Result<()> {
let mut ctx = TestContext::new().await?;
let key_chain = KeyChain::new_os_random();
let account_id = nssa::AccountId::from((&key_chain.nullifier_public_key, 0));
let key_chain_json =
serde_json::to_string(&key_chain).context("Failed to serialize key chain")?;
let initial_account = nssa::Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
data: Data::default(),
nonce: Nonce::default(),
};
// First import
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::Import(ImportSubcommand::Private {
key_chain_json: key_chain_json.clone(),
account_state: HumanReadableAccount::from(initial_account),
chain_index: None,
identifier: 0,
})),
)
.await?;
let updated_account = nssa::Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: 999,
data: Data::default(),
nonce: Nonce::default(),
};
// Second import with different account data (same key chain)
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::Import(ImportSubcommand::Private {
key_chain_json,
account_state: HumanReadableAccount::from(updated_account.clone()),
chain_index: None,
identifier: 0,
})),
)
.await?;
let imported = ctx
.wallet()
.storage()
.key_chain()
.private_account(account_id)
.context("Imported private account should be present")?;
assert_eq!(
imported.account, &updated_account,
"Second import should override account data"
);
Ok(())
}

View File

@ -7,14 +7,17 @@
use std::time::Duration;
use anyhow::Result;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id};
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand},
use wallet::{
account::Label,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand},
},
};
#[test]
@ -113,10 +116,8 @@ async fn amm_public() -> Result<()> {
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_1)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_1)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id_1),
supply_account_id: public_mention(supply_account_id_1),
name: "A NAM1".to_owned(),
total_supply: 37,
@ -127,10 +128,8 @@ async fn amm_public() -> Result<()> {
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_1)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id_1)),
to_label: None,
from: public_mention(supply_account_id_1),
to: Some(public_mention(recipient_account_id_1)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -143,10 +142,8 @@ async fn amm_public() -> Result<()> {
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_2)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_2)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id_2),
supply_account_id: public_mention(supply_account_id_2),
name: "A NAM2".to_owned(),
total_supply: 37,
@ -157,10 +154,8 @@ async fn amm_public() -> Result<()> {
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_2)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id_2)),
to_label: None,
from: public_mention(supply_account_id_2),
to: Some(public_mention(recipient_account_id_2)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -193,12 +188,9 @@ async fn amm_public() -> Result<()> {
// Send creation tx
let subcommand = AmmProgramAgnosticSubcommand::New {
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
user_holding_a: public_mention(recipient_account_id_1),
user_holding_b: public_mention(recipient_account_id_2),
user_holding_lp: public_mention(user_holding_lp),
balance_a: 3,
balance_b: 3,
};
@ -239,13 +231,11 @@ async fn amm_public() -> Result<()> {
// Make swap
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_a: public_mention(recipient_account_id_1),
user_holding_b: public_mention(recipient_account_id_2),
amount_in: 2,
min_amount_out: 1,
token_definition: definition_account_id_1.to_string(),
token_definition: definition_account_id_1,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?;
@ -284,13 +274,11 @@ async fn amm_public() -> Result<()> {
// Make swap
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_a: public_mention(recipient_account_id_1),
user_holding_b: public_mention(recipient_account_id_2),
amount_in: 2,
min_amount_out: 1,
token_definition: definition_account_id_2.to_string(),
token_definition: definition_account_id_2,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?;
@ -329,12 +317,9 @@ async fn amm_public() -> Result<()> {
// Add liquidity
let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity {
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
user_holding_a: public_mention(recipient_account_id_1),
user_holding_b: public_mention(recipient_account_id_2),
user_holding_lp: public_mention(user_holding_lp),
min_amount_lp: 1,
max_amount_a: 2,
max_amount_b: 2,
@ -376,12 +361,9 @@ async fn amm_public() -> Result<()> {
// Remove liquidity
let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity {
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
user_holding_a: public_mention(recipient_account_id_1),
user_holding_b: public_mention(recipient_account_id_2),
user_holding_lp: public_mention(user_holding_lp),
balance_lp: 2,
min_amount_a: 1,
min_amount_b: 1,
@ -457,14 +439,14 @@ async fn amm_new_pool_using_labels() -> Result<()> {
};
// Create holding_a with a label
let holding_a_label = "amm-holding-a-label".to_owned();
let holding_a_label = Label::new("amm-holding-a-label");
let SubcommandReturnValue::RegisterAccount {
account_id: holding_a_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_a_label.clone()),
label: Some(Label::new(holding_a_label.clone())),
})),
)
.await?
@ -502,14 +484,14 @@ async fn amm_new_pool_using_labels() -> Result<()> {
};
// Create holding_b with a label
let holding_b_label = "amm-holding-b-label".to_owned();
let holding_b_label = Label::new("amm-holding-b-label");
let SubcommandReturnValue::RegisterAccount {
account_id: holding_b_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_b_label.clone()),
label: Some(Label::new(holding_b_label.clone())),
})),
)
.await?
@ -518,14 +500,14 @@ async fn amm_new_pool_using_labels() -> Result<()> {
};
// Create holding_lp with a label
let holding_lp_label = "amm-holding-lp-label".to_owned();
let holding_lp_label = Label::new("amm-holding-lp-label");
let SubcommandReturnValue::RegisterAccount {
account_id: holding_lp_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_lp_label.clone()),
label: Some(Label::new(holding_lp_label.clone())),
})),
)
.await?
@ -535,10 +517,8 @@ async fn amm_new_pool_using_labels() -> Result<()> {
// Create token 1 and distribute to holding_a
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_1)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_1)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id_1),
supply_account_id: public_mention(supply_account_id_1),
name: "TOKEN1".to_owned(),
total_supply: 10,
};
@ -546,10 +526,8 @@ async fn amm_new_pool_using_labels() -> Result<()> {
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_1)),
from_label: None,
to: Some(format_public_account_id(holding_a_id)),
to_label: None,
from: public_mention(supply_account_id_1),
to: Some(public_mention(holding_a_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -560,10 +538,8 @@ async fn amm_new_pool_using_labels() -> Result<()> {
// Create token 2 and distribute to holding_b
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_2)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_2)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id_2),
supply_account_id: public_mention(supply_account_id_2),
name: "TOKEN2".to_owned(),
total_supply: 10,
};
@ -571,10 +547,8 @@ async fn amm_new_pool_using_labels() -> Result<()> {
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_2)),
from_label: None,
to: Some(format_public_account_id(holding_b_id)),
to_label: None,
from: public_mention(supply_account_id_2),
to: Some(public_mention(holding_b_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -585,12 +559,9 @@ async fn amm_new_pool_using_labels() -> Result<()> {
// Create AMM pool using account labels instead of IDs
let subcommand = AmmProgramAgnosticSubcommand::New {
user_holding_a: None,
user_holding_a_label: Some(holding_a_label),
user_holding_b: None,
user_holding_b_label: Some(holding_b_label),
user_holding_lp: None,
user_holding_lp_label: Some(holding_lp_label),
user_holding_a: holding_a_label.into(),
user_holding_b: holding_b_label.into(),
user_holding_lp: holding_lp_label.into(),
balance_a: 3,
balance_b: 3,
};

View File

@ -9,8 +9,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use ata_core::{compute_ata_seed, get_associated_token_account_id};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
verify_commitment_is_in_state,
};
use log::info;
use nssa::program::Program;
@ -68,10 +68,8 @@ async fn create_ata_initializes_holding_account() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply,
}),
@ -85,8 +83,8 @@ async fn create_ata_initializes_holding_account() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(owner_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -132,10 +130,8 @@ async fn create_ata_is_idempotent() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply: 100,
}),
@ -149,8 +145,8 @@ async fn create_ata_is_idempotent() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(owner_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -162,8 +158,8 @@ async fn create_ata_is_idempotent() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(owner_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -212,10 +208,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply,
}),
@ -240,16 +234,16 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(sender_account_id),
token_definition: definition_account_id,
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(recipient_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(recipient_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -262,10 +256,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
from: public_mention(supply_account_id),
to: Some(public_mention(sender_ata_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -282,9 +274,9 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Send {
from: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
to: recipient_ata_id.to_string(),
from: public_mention(sender_account_id),
token_definition: definition_account_id,
to: recipient_ata_id,
amount: transfer_amount,
}),
)
@ -320,8 +312,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Burn {
holder: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
holder: public_mention(sender_account_id),
token_definition: definition_account_id,
amount: burn_amount,
}),
)
@ -371,10 +363,8 @@ async fn create_ata_with_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply: 100,
}),
@ -388,8 +378,8 @@ async fn create_ata_with_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
owner: private_mention(owner_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -445,10 +435,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply,
}),
@ -473,16 +461,16 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
owner: private_mention(sender_account_id),
token_definition: definition_account_id,
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(recipient_account_id),
token_definition: definition_account_id.to_string(),
owner: public_mention(recipient_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -495,10 +483,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
from: public_mention(supply_account_id),
to: Some(public_mention(sender_ata_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -515,9 +501,9 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Send {
from: format_private_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
to: recipient_ata_id.to_string(),
from: private_mention(sender_account_id),
token_definition: definition_account_id,
to: recipient_ata_id,
amount: transfer_amount,
}),
)
@ -572,10 +558,8 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "TEST".to_owned(),
total_supply,
}),
@ -596,8 +580,8 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(holder_account_id),
token_definition: definition_account_id.to_string(),
owner: private_mention(holder_account_id),
token_definition: definition_account_id,
}),
)
.await?;
@ -610,10 +594,8 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(holder_ata_id)),
to_label: None,
from: public_mention(supply_account_id),
to: Some(public_mention(holder_ata_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -630,8 +612,8 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Burn {
holder: format_private_account_id(holder_account_id),
token_definition: definition_account_id.to_string(),
holder: private_mention(holder_account_id),
token_definition: definition_account_id,
amount: burn_amount,
}),
)

View File

@ -2,18 +2,21 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx,
format_private_account_id, format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
public_mention, verify_commitment_is_in_state,
};
use log::info;
use nssa::{AccountId, program::Program};
use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point};
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::native_token_transfer::AuthTransferSubcommand,
use wallet::{
account::Label,
cli::{
CliAccountMention, Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::native_token_transfer::AuthTransferSubcommand,
},
};
#[test]
@ -24,10 +27,8 @@ async fn private_transfer_to_owned_account() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
from: private_mention(from),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -66,10 +67,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
let to_vpk = Secp256k1Point::from_scalar(to_npk.0);
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
from: private_mention(from),
to: None,
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
@ -117,10 +116,8 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
assert_eq!(from_acc.balance, 10000);
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to)),
to_label: None,
from: private_mention(from),
to: Some(public_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -173,22 +170,20 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _, to_identifier) = ctx
let to = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.key_chain()
.private_account(to_account_id)
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
from: private_mention(from),
to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)),
to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)),
to_identifier: Some(to.identifier),
amount: 100,
});
@ -233,10 +228,8 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
from: public_mention(from),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -278,10 +271,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
let from: AccountId = ctx.existing_public_accounts()[0];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(from)),
from_label: None,
from: public_mention(from),
to: None,
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
@ -341,22 +332,20 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
};
// Get the newly created account's keys
let (to_keys, _, to_identifier) = ctx
let to = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.key_chain()
.private_account(to_account_id)
.context("Failed to get private account")?;
// Send transfer using nullifier and viewing public keys
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
from: private_mention(from),
to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)),
to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)),
to_identifier: Some(to.identifier),
amount: 100,
});
@ -402,8 +391,7 @@ async fn initialize_private_account() -> Result<()> {
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: Some(format_private_account_id(account_id)),
account_label: None,
account_id: private_mention(account_id),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -444,20 +432,17 @@ async fn private_transfer_using_from_label() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1];
// Assign a label to the sender account
let label = "private-sender-label".to_owned();
let label = Label::new("private-sender-label");
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_private_account_id(from)),
account_label: None,
account_id: private_mention(from),
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(label),
to: Some(format_private_account_id(to)),
to_label: None,
from: CliAccountMention::Label(label),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -491,7 +476,7 @@ async fn initialize_private_account_using_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create a new private account with a label
let label = "init-private-label".to_owned();
let label = Label::new("init-private-label");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: Some(label.clone()),
@ -503,8 +488,7 @@ async fn initialize_private_account_using_label() -> Result<()> {
// Initialize using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: None,
account_label: Some(label),
account_id: label.into(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -541,15 +525,12 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
// Both transfers below will target this same node with distinct identifiers.
let chain_index = ctx.wallet_mut().create_private_accounts_key(None);
let (npk, vpk) = {
let node = ctx
let key_chain = ctx
.wallet()
.storage()
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("node was just inserted");
let key_chain = &node.value.0;
.key_chain()
.private_account_key_chain_by_index(&chain_index)
.expect("Failed to get private account key chain for chain index");
(
key_chain.nullifier_public_key,
key_chain.viewing_public_key.clone(),
@ -568,10 +549,8 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_0)),
from_label: None,
from: public_mention(sender_0),
to: None,
to_label: None,
to_npk: Some(npk_hex.clone()),
to_vpk: Some(vpk_hex.clone()),
to_identifier: Some(identifier_1),
@ -583,10 +562,8 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_1)),
from_label: None,
from: public_mention(sender_1),
to: None,
to_label: None,
to_npk: Some(npk_hex),
to_vpk: Some(vpk_hex),
to_identifier: Some(identifier_2),
@ -620,21 +597,25 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
assert_eq!(acc_2.balance, 200);
// Both account ids must resolve to the same key node.
let tree = &ctx.wallet().storage().user_data.private_key_tree;
let ci_1 = tree
.account_id_map
.get(&account_id_1)
.context("account_id_1 missing from private_key_tree.account_id_map")?;
let ci_2 = tree
.account_id_map
.get(&account_id_2)
.context("account_id_2 missing from private_key_tree.account_id_map")?;
let found_acc1 = ctx
.wallet()
.storage()
.key_chain()
.private_account(account_id_1)
.context("account_id_1 not found in key chain")?;
let found_acc2 = ctx
.wallet()
.storage()
.key_chain()
.private_account(account_id_2)
.context("account_id_2 not found in key chain")?;
assert_eq!(
ci_1, ci_2,
found_acc1.chain_index, found_acc2.chain_index,
"identifiers 1 and 2 under the same NPK must share a single chain_index"
);
assert_eq!(
ci_1, &chain_index,
found_acc1.chain_index,
Some(chain_index),
"both accounts must resolve to the key node created at the start of the test"
);

View File

@ -1,15 +1,18 @@
use std::time::Duration;
use anyhow::Result;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id};
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
use log::info;
use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::native_token_transfer::AuthTransferSubcommand,
use wallet::{
account::Label,
cli::{
CliAccountMention, Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::native_token_transfer::AuthTransferSubcommand,
},
};
#[test]
@ -17,10 +20,8 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -67,8 +68,9 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
let new_persistent_account_id = ctx
.wallet()
.storage()
.user_data
.account_ids()
.key_chain()
.public_account_ids()
.map(|(account_id, _)| account_id)
.find(|acc_id| {
*acc_id != ctx.existing_public_accounts()[0]
&& *acc_id != ctx.existing_public_accounts()[1]
@ -76,10 +78,8 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
.expect("Failed to find newly created account in the wallet storage");
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(new_persistent_account_id)),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(new_persistent_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -115,10 +115,8 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -156,10 +154,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
// First transfer
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -191,10 +187,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
// Second transfer
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -241,8 +235,7 @@ async fn initialize_public_account() -> Result<()> {
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: Some(format_public_account_id(account_id)),
account_label: None,
account_id: public_mention(account_id),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -267,20 +260,17 @@ async fn successful_transfer_using_from_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign a label to the sender account
let label = "sender-label".to_owned();
let label = Label::new("sender-label");
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
account_id: public_mention(ctx.existing_public_accounts()[0]),
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(label),
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: CliAccountMention::Label(label),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -315,20 +305,17 @@ async fn successful_transfer_using_to_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign a label to the receiver account
let label = "receiver-label".to_owned();
let label = Label::new("receiver-label");
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
account_id: public_mention(ctx.existing_public_accounts()[1]),
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label for the recipient
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: None,
to_label: Some(label),
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(CliAccountMention::Label(label)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),

View File

@ -9,54 +9,61 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use indexer_service_rpc::RpcClient as _;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
verify_commitment_is_in_state,
};
use log::info;
use nssa::AccountId;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
use wallet::{
account::Label,
cli::{CliAccountMention, Command, programs::native_token_transfer::AuthTransferSubcommand},
};
/// Maximum time to wait for the indexer to catch up to the sequencer.
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000;
/// Poll the indexer until its last finalized block id reaches the sequencer's
/// current last block id (and at least the genesis block has been advanced past),
/// or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses. Returns the last indexer block
/// id observed.
async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 {
/// current last block id or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses.
/// Returns the last indexer block id observed.
async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> Result<u64> {
let timeout = Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS);
let block_id_to_catch_up =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?;
let mut last_ind: u64 = 1;
let inner = async {
loop {
let seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client())
.await
.unwrap_or(0);
let ind = ctx
.indexer_client()
.get_last_finalized_block_id()
.await
.unwrap_or(1);
.await?
.unwrap_or(0);
last_ind = ind;
if ind >= seq && ind > 1 {
info!("Indexer caught up: seq={seq}, ind={ind}");
return ind;
if ind >= block_id_to_catch_up {
let last_seq =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client())
.await?;
info!(
"Indexer caught up. Indexer last block id: {ind}. Current sequencer last block id: {last_seq}"
);
return Ok(ind);
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
};
tokio::time::timeout(timeout, inner)
.await
.unwrap_or_else(|_| {
info!("Indexer catch-up timed out: ind={last_ind}");
last_ind
})
.with_context(|| {
format!(
"Indexer failed to catch up within {L2_TO_L1_TIMEOUT_MILLIS} milliseconds. Last indexer block id observed: {last_ind}, but needed to catch up to at least {block_id_to_catch_up}"
)
})?
}
#[tokio::test]
async fn indexer_test_run() -> Result<()> {
let ctx = TestContext::new().await?;
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await;
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await?;
let last_block_seq =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?;
@ -64,7 +71,7 @@ async fn indexer_test_run() -> Result<()> {
info!("Last block on seq now is {last_block_seq}");
info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 1);
assert!(last_block_indexer > 0);
Ok(())
}
@ -74,11 +81,11 @@ async fn indexer_block_batching() -> Result<()> {
let ctx = TestContext::new().await?;
info!("Waiting for indexer to parse blocks");
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await;
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await?;
info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 1);
assert!(last_block_indexer > 0);
// Getting wide batch to fit all blocks (from latest backwards)
let mut block_batch = ctx.indexer_client().get_blocks(None, 100).await.unwrap();
@ -105,10 +112,8 @@ async fn indexer_state_consistency() -> Result<()> {
let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.existing_public_accounts()[0]),
to: Some(public_mention(ctx.existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -142,10 +147,8 @@ async fn indexer_state_consistency() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
from: private_mention(from),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -172,7 +175,7 @@ async fn indexer_state_consistency() -> Result<()> {
info!("Successfully transferred privately to owned account");
info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await;
wait_for_indexer_to_catch_up(&ctx).await?;
let acc1_ind_state = ctx
.indexer_client()
@ -210,29 +213,25 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign labels to both accounts
let from_label = "idx-sender-label".to_owned();
let to_label_str = "idx-receiver-label".to_owned();
let from_label = Label::new("idx-sender-label");
let to_label = Label::new("idx-receiver-label");
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
account_id: public_mention(ctx.existing_public_accounts()[0]),
label: from_label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?;
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
label: to_label_str.clone(),
account_id: public_mention(ctx.existing_public_accounts()[1]),
label: to_label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?;
// Send using labels instead of account IDs
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(from_label),
to: None,
to_label: Some(to_label_str),
from: CliAccountMention::Label(from_label),
to: Some(CliAccountMention::Label(to_label)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -259,7 +258,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await;
wait_for_indexer_to_catch_up(&ctx).await?;
let acc1_ind_state = ctx
.indexer_client()

View File

@ -5,82 +5,145 @@
reason = "We don't care about these in tests"
)]
use std::{
ffi::{CString, c_char},
fs::File,
io::Write as _,
net::SocketAddr,
};
use anyhow::{Context as _, Result};
use indexer_ffi::{
IndexerServiceFFI, OperationStatus,
IndexerServiceFFI, OperationStatus, Runtime,
api::{
PointerResult,
lifecycle::InitializedIndexerServiceFFIResult,
types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock},
},
};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id,
test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state,
BlockingTestContext, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention,
public_mention, verify_commitment_is_in_state,
};
use log::info;
use log::{debug, info};
use nssa::AccountId;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
use tempfile::TempDir;
use wallet::{
account::Label,
cli::{Command, programs::native_token_transfer::AuthTransferSubcommand},
};
/// Maximum time to wait for the indexer to catch up to the sequencer.
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000;
unsafe extern "C" {
unsafe fn query_last_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
) -> PointerResult<u64, OperationStatus>;
unsafe fn query_block_vec(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
before: FfiOption<u64>,
limit: u64,
) -> PointerResult<FfiVec<FfiBlock>, OperationStatus>;
unsafe fn query_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
) -> PointerResult<FfiAccount, OperationStatus>;
unsafe fn start_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> InitializedIndexerServiceFFIResult;
}
fn setup_indexer_ffi(
runtime: &Runtime,
bedrock_addr: SocketAddr,
) -> Result<(IndexerServiceFFI, TempDir)> {
let temp_indexer_dir =
tempfile::tempdir().context("Failed to create temp dir for indexer home")?;
debug!(
"Using temp indexer home at {}",
temp_indexer_dir.path().display()
);
let indexer_config =
integration_tests::config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned())
.context("Failed to create Indexer config")?;
let config_json = serde_json::to_vec(&indexer_config)?;
let config_path = temp_indexer_dir.path().join("indexer_config.json");
let mut file = File::create(config_path.as_path())?;
file.write_all(&config_json)?;
file.flush()?;
let res =
// SAFETY: lib function ensures validity of value.
unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) };
if res.error.is_error() {
anyhow::bail!("Indexer FFI error {:?}", res.error);
}
Ok((
// SAFETY: lib function ensures validity of value.
unsafe { std::ptr::read(res.value) },
temp_indexer_dir,
))
}
/// Prepare setup for tests.
fn setup() -> Result<(BlockingTestContext, IndexerServiceFFI, TempDir)> {
let ctx = TestContext::builder().disable_indexer().build_blocking()?;
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let (indexer_ffi, indexer_dir) = setup_indexer_ffi(&runtime, ctx.ctx().bedrock_addr())?;
Ok((ctx, indexer_ffi, indexer_dir))
}
#[test]
fn indexer_test_run_ffi() -> Result<()> {
let blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime();
let (ctx, indexer_ffi, _indexer_dir) = setup()?;
// RUN OBSERVATION
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS));
let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?;
let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) };
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let last_block_indexer_ffi_res =
unsafe { query_last_block(&raw const runtime, &raw const indexer_ffi) };
assert!(last_block_indexer_ffi_res.error.is_ok());
let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value };
info!("Last block on ind now is {last_block_indexer}");
info!("Last block on ind ffi now is {last_block_indexer_ffi}");
assert!(last_block_indexer > 1);
assert!(last_block_indexer_ffi > 1);
assert_eq!(last_block_indexer, last_block_indexer_ffi);
Ok(())
}
#[test]
fn indexer_ffi_block_batching() -> Result<()> {
let blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime();
let (ctx, indexer_ffi, _indexer_dir) = setup()?;
// WAIT
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS));
let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) };
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let last_block_indexer_ffi_res =
unsafe { query_last_block(&raw const runtime, &raw const indexer_ffi) };
assert!(last_block_indexer_ffi_res.error.is_ok());
@ -93,8 +156,14 @@ fn indexer_ffi_block_batching() -> Result<()> {
let before_ffi = FfiOption::<u64>::from_none();
let limit = 100;
let block_batch_ffi_res =
unsafe { query_block_vec(blocking_ctx.indexer_ffi(), before_ffi, limit) };
let block_batch_ffi_res = unsafe {
query_block_vec(
&raw const runtime,
&raw const indexer_ffi,
before_ffi,
limit,
)
};
assert!(block_batch_ffi_res.error.is_ok());
@ -117,43 +186,37 @@ fn indexer_ffi_block_batching() -> Result<()> {
#[test]
fn indexer_ffi_state_consistency() -> Result<()> {
let mut blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime_clone();
let indexer_ffi = blocking_ctx.indexer_ffi();
let ctx = blocking_ctx.ctx_mut();
let (mut ctx, indexer_ffi, _indexer_dir) = setup()?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
from: public_mention(ctx.ctx().existing_public_accounts()[0]),
to: Some(public_mention(ctx.ctx().existing_public_accounts()[1])),
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
std::thread::sleep(std::time::Duration::from_secs(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
));
info!("Checking correct balance move");
let acc_1_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
let acc_1_balance = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc_2_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
)
})?;
let acc_2_balance = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
)
})?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
@ -161,68 +224,71 @@ fn indexer_ffi_state_consistency() -> Result<()> {
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
let from: AccountId = ctx.existing_private_accounts()[0];
let to: AccountId = ctx.existing_private_accounts()[1];
let from: AccountId = ctx.ctx().existing_private_accounts()[0];
let to: AccountId = ctx.ctx().existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
from: private_mention(from),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
std::thread::sleep(std::time::Duration::from_secs(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
));
let new_commitment1 = ctx
.ctx()
.wallet()
.get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?;
let commitment_check1 = runtime_wrapped.block_on(verify_commitment_is_in_state(
new_commitment1,
ctx.sequencer_client(),
));
let commitment_check1 =
ctx.block_on(|ctx| verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()));
assert!(commitment_check1);
let new_commitment2 = ctx
.ctx()
.wallet()
.get_private_account_commitment(to)
.context("Failed to get private account commitment for receiver")?;
let commitment_check2 = runtime_wrapped.block_on(verify_commitment_is_in_state(
new_commitment2,
ctx.sequencer_client(),
));
let commitment_check2 =
ctx.block_on(|ctx| verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()));
assert!(commitment_check2);
info!("Successfully transferred privately to owned account");
// WAIT
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS));
let acc1_ind_state_ffi =
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) };
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let acc1_ind_state_ffi = unsafe {
query_account(
&raw const runtime,
&raw const indexer_ffi,
(&ctx.ctx().existing_public_accounts()[0]).into(),
)
};
assert!(acc1_ind_state_ffi.error.is_ok());
let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value };
let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into();
let acc2_ind_state_ffi =
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[1]).into()) };
let acc2_ind_state_ffi = unsafe {
query_account(
&raw const runtime,
&raw const indexer_ffi,
(&ctx.ctx().existing_public_accounts()[1]).into(),
)
};
assert!(acc2_ind_state_ffi.error.is_ok());
@ -230,16 +296,18 @@ fn indexer_ffi_state_consistency() -> Result<()> {
let acc2_ind_state: indexer_service_protocol::Account = acc2_ind_state_pre.into();
info!("Checking correct state transition");
let acc1_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
let acc1_seq_state = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc2_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
)
})?;
let acc2_seq_state = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
)
})?;
assert_eq!(acc1_ind_state, acc1_seq_state.into());
assert_eq!(acc2_ind_state, acc2_seq_state.into());
@ -251,83 +319,81 @@ fn indexer_ffi_state_consistency() -> Result<()> {
#[test]
fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
let mut blocking_ctx = BlockingTestContextFFI::new()?;
let runtime_wrapped = blocking_ctx.runtime_clone();
let indexer_ffi = blocking_ctx.indexer_ffi();
let ctx = blocking_ctx.ctx_mut();
let (mut ctx, indexer_ffi, _indexer_dir) = setup()?;
// Assign labels to both accounts
let from_label = "idx-sender-label".to_owned();
let to_label_str = "idx-receiver-label".to_owned();
let from_label = Label::new("idx-sender-label");
let to_label = Label::new("idx-receiver-label");
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
account_id: public_mention(ctx.ctx().existing_public_accounts()[0]),
label: from_label.clone(),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
label: to_label_str.clone(),
account_id: public_mention(ctx.ctx().existing_public_accounts()[1]),
label: to_label.clone(),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?;
// Send using labels instead of account IDs
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(from_label),
to: None,
to_label: Some(to_label_str),
from: from_label.into(),
to: Some(to_label.into()),
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
))
.await;
});
std::thread::sleep(std::time::Duration::from_secs(
TIME_TO_WAIT_FOR_BLOCK_SECONDS,
));
let acc_1_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
let acc_1_balance = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
let acc_2_balance =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance(
)
})?;
let acc_2_balance = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
))?;
)
})?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
runtime_wrapped.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
});
std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS));
let acc1_ind_state_ffi =
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) };
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let acc1_ind_state_ffi = unsafe {
query_account(
&raw const runtime,
&raw const indexer_ffi,
(&ctx.ctx().existing_public_accounts()[0]).into(),
)
};
assert!(acc1_ind_state_ffi.error.is_ok());
let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value };
let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into();
let acc1_seq_state =
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
let acc1_seq_state = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
))?;
)
})?;
assert_eq!(acc1_ind_state, acc1_seq_state.into());

View File

@ -8,8 +8,8 @@ use std::{str::FromStr as _, time::Duration};
use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx,
format_private_account_id, format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
public_mention, verify_commitment_is_in_state,
};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use log::info;
@ -59,22 +59,20 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _, to_identifier) = ctx
let to_account = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.key_chain()
.private_account(to_account_id)
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
from: private_mention(from),
to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)),
to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)),
to_identifier: Some(to_account.identifier),
amount: 100,
});
@ -145,10 +143,8 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to first private account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to_account_id1)),
to_label: None,
from: private_mention(from),
to: Some(private_mention(to_account_id1)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -158,10 +154,8 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to second private account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to_account_id2)),
to_label: None,
from: private_mention(from),
to: Some(private_mention(to_account_id2)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -199,10 +193,8 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to first public account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to_account_id3)),
to_label: None,
from: public_mention(from),
to: Some(public_mention(to_account_id3)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -212,10 +204,8 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to second public account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to_account_id4)),
to_label: None,
from: public_mention(from),
to: Some(public_mention(to_account_id4)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -232,56 +222,50 @@ async fn restore_keys_from_seed() -> Result<()> {
let acc1 = ctx
.wallet()
.storage()
.user_data
.private_key_tree
.get_node(to_account_id1)
.key_chain()
.private_account(to_account_id1)
.expect("Acc 1 should be restored");
let acc2 = ctx
.wallet()
.storage()
.user_data
.private_key_tree
.get_node(to_account_id2)
.key_chain()
.private_account(to_account_id2)
.expect("Acc 2 should be restored");
// Verify restored public accounts
let _acc3 = ctx
.wallet()
.storage()
.user_data
.public_key_tree
.get_node(to_account_id3)
.key_chain()
.pub_account_signing_key(to_account_id3)
.expect("Acc 3 should be restored");
let _acc4 = ctx
.wallet()
.storage()
.user_data
.public_key_tree
.get_node(to_account_id4)
.key_chain()
.pub_account_signing_key(to_account_id4)
.expect("Acc 4 should be restored");
assert_eq!(
acc1.value.1[0].1.program_owner,
acc1.account.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(
acc2.value.1[0].1.program_owner,
acc2.account.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(acc1.value.1[0].1.balance, 100);
assert_eq!(acc2.value.1[0].1.balance, 101);
assert_eq!(acc1.account.balance, 100);
assert_eq!(acc2.account.balance, 101);
info!("Tree checks passed, testing restored accounts can transact");
// Test that restored accounts can send transactions
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(to_account_id1)),
from_label: None,
to: Some(format_private_account_id(to_account_id2)),
to_label: None,
from: private_mention(to_account_id1),
to: Some(private_mention(to_account_id2)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -290,10 +274,8 @@ async fn restore_keys_from_seed() -> Result<()> {
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(to_account_id3)),
from_label: None,
to: Some(format_public_account_id(to_account_id4)),
to_label: None,
from: public_mention(to_account_id3),
to: Some(public_mention(to_account_id4)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),

View File

@ -9,8 +9,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use common::PINATA_BASE58;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
verify_commitment_is_in_state,
};
use log::info;
use sequencer_service_rpc::RpcClient as _;
@ -42,8 +42,6 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
anyhow::bail!("Expected RegisterAccount return value");
};
let winner_account_id_formatted = format_public_account_id(winner_account_id);
let pinata_balance_pre = ctx
.sequencer_client()
.get_account_balance(PINATA_BASE58.parse().unwrap())
@ -52,8 +50,7 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
let claim_result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: Some(winner_account_id_formatted),
to_label: None,
to: public_mention(winner_account_id),
}),
)
.await;
@ -97,8 +94,6 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()
anyhow::bail!("Expected RegisterAccount return value");
};
let winner_account_id_formatted = format_private_account_id(winner_account_id);
let pinata_balance_pre = ctx
.sequencer_client()
.get_account_balance(PINATA_BASE58.parse().unwrap())
@ -107,8 +102,7 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()
let claim_result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: Some(winner_account_id_formatted),
to_label: None,
to: private_mention(winner_account_id),
}),
)
.await;
@ -139,8 +133,7 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
let pinata_prize = 150;
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
to_label: None,
to: public_mention(ctx.existing_public_accounts()[0]),
});
let pinata_balance_pre = ctx
@ -178,10 +171,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
let pinata_prize = 150;
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: Some(format_private_account_id(
ctx.existing_private_accounts()[0],
)),
to_label: None,
to: private_mention(ctx.existing_private_accounts()[0]),
});
let pinata_balance_pre = ctx
@ -241,12 +231,9 @@ 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);
// Initialize account under auth transfer program
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: Some(winner_account_id_formatted.clone()),
account_label: None,
account_id: private_mention(winner_account_id),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -261,8 +248,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
// Claim pinata to the new private account
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: Some(winner_account_id_formatted),
to_label: None,
to: private_mention(winner_account_id),
});
let pinata_balance_pre = ctx

View File

@ -18,14 +18,19 @@
use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
};
use log::info;
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
group::GroupSubcommand,
programs::native_token_transfer::AuthTransferSubcommand,
use wallet::{
account::Label,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
group::GroupSubcommand,
programs::native_token_transfer::AuthTransferSubcommand,
},
};
/// Create a group, create a shared account from it, and verify registration.
@ -43,8 +48,8 @@ async fn group_create_and_shared_account_registration() -> Result<()> {
assert!(
ctx.wallet()
.storage()
.user_data
.group_key_holder("test-group")
.key_chain()
.group_key_holder(&Label::new("test-group"))
.is_some()
);
@ -69,10 +74,10 @@ async fn group_create_and_shared_account_registration() -> Result<()> {
let entry = ctx
.wallet()
.storage()
.user_data
.key_chain()
.shared_private_account(&shared_account_id)
.context("Shared account not found in storage")?;
assert_eq!(entry.group_label, "test-group");
assert_eq!(entry.group_label, Label::new("test-group"));
assert!(entry.pda_seed.is_none());
info!("Shared account registered: {shared_account_id}");
@ -98,8 +103,8 @@ async fn group_invite_join_key_agreement() -> Result<()> {
let sealing_sk = ctx
.wallet()
.storage()
.user_data
.sealing_secret_key
.key_chain()
.sealing_secret_key()
.context("Sealing key not found")?;
let sealing_pk =
key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk);
@ -107,8 +112,8 @@ async fn group_invite_join_key_agreement() -> Result<()> {
let holder = ctx
.wallet()
.storage()
.user_data
.group_key_holder("alice-group")
.key_chain()
.group_key_holder(&Label::new("alice-group"))
.context("Group not found")?;
let sealed = holder.seal_for(&sealing_pk);
let sealed_hex = hex::encode(&sealed);
@ -124,14 +129,14 @@ async fn group_invite_join_key_agreement() -> Result<()> {
let alice_holder = ctx
.wallet()
.storage()
.user_data
.group_key_holder("alice-group")
.key_chain()
.group_key_holder(&Label::new("alice-group"))
.unwrap();
let bob_holder = ctx
.wallet()
.storage()
.user_data
.group_key_holder("bob-copy")
.key_chain()
.group_key_holder(&Label::new("bob-copy"))
.unwrap();
let seed = [42_u8; 32];
@ -181,8 +186,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
// Initialize the shared account under auth-transfer
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: Some(format!("Private/{shared_id}")),
account_label: None,
account_id: private_mention(shared_id),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -191,10 +195,8 @@ async fn fund_shared_account_from_public() -> Result<()> {
// Fund from a public account
let from_public = ctx.existing_public_accounts()[0];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(from_public)),
from_label: None,
to: Some(format!("Private/{shared_id}")),
to_label: None,
from: public_mention(from_public),
to: Some(private_mention(shared_id)),
to_npk: None,
to_vpk: None,
to_identifier: None,
@ -212,7 +214,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
let entry = ctx
.wallet()
.storage()
.user_data
.key_chain()
.shared_private_account(&shared_id)
.context("Shared account not found after sync")?;

View File

@ -8,8 +8,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
verify_commitment_is_in_state,
};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use log::info;
@ -17,10 +17,13 @@ use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
use token_core::{TokenDefinition, TokenHolding};
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::token::TokenProgramAgnosticSubcommand,
use wallet::{
account::Label,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::token::TokenProgramAgnosticSubcommand,
},
};
#[test]
@ -79,10 +82,8 @@ async fn create_and_transfer_public_token() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: name.clone(),
total_supply,
};
@ -128,10 +129,8 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Transfer 7 tokens from supply_acc to recipient_account_id
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
from: public_mention(supply_account_id),
to: Some(public_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -176,10 +175,8 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Burn 3 tokens from recipient_acc
let burn_amount = 3;
let subcommand = TokenProgramAgnosticSubcommand::Burn {
definition: Some(format_public_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_public_account_id(recipient_account_id)),
holder_label: None,
definition: public_mention(definition_account_id),
holder: public_mention(recipient_account_id),
amount: burn_amount,
};
@ -222,10 +219,8 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Mint 10 tokens at recipient_acc
let mint_amount = 10;
let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: Some(format_public_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_public_account_id(recipient_account_id)),
holder_label: None,
definition: public_mention(definition_account_id),
holder: Some(public_mention(recipient_account_id)),
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
@ -329,10 +324,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: private_mention(supply_account_id),
name: name.clone(),
total_supply,
};
@ -368,10 +361,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Transfer 7 tokens from supply_acc to recipient_account_id
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
from: private_mention(supply_account_id),
to: Some(private_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -398,10 +389,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Burn 3 tokens from recipient_acc
let burn_amount = 3;
let subcommand = TokenProgramAgnosticSubcommand::Burn {
definition: Some(format_public_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_private_account_id(recipient_account_id)),
holder_label: None,
definition: public_mention(definition_account_id),
holder: private_mention(recipient_account_id),
amount: burn_amount,
};
@ -492,10 +481,8 @@ async fn create_token_with_private_definition() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_private_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: private_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: name.clone(),
total_supply,
};
@ -563,10 +550,8 @@ async fn create_token_with_private_definition() -> Result<()> {
// Mint to public account
let mint_amount_public = 10;
let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_public_account_id(recipient_account_id_public)),
holder_label: None,
definition: private_mention(definition_account_id),
holder: Some(public_mention(recipient_account_id_public)),
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
@ -612,10 +597,8 @@ async fn create_token_with_private_definition() -> Result<()> {
// Mint to private account
let mint_amount_private = 5;
let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_private_account_id(recipient_account_id_private)),
holder_label: None,
definition: private_mention(definition_account_id),
holder: Some(private_mention(recipient_account_id_private)),
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
@ -694,10 +677,8 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_private_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: private_mention(definition_account_id),
supply_account_id: private_mention(supply_account_id),
name,
total_supply,
};
@ -755,10 +736,8 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
// Transfer tokens
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
from: private_mention(supply_account_id),
to: Some(private_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -871,10 +850,8 @@ async fn shielded_token_transfer() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name,
total_supply,
};
@ -887,10 +864,8 @@ async fn shielded_token_transfer() -> Result<()> {
// Perform shielded transfer: public supply -> private recipient
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
from: public_mention(supply_account_id),
to: Some(private_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -998,10 +973,8 @@ async fn deshielded_token_transfer() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: private_mention(supply_account_id),
name,
total_supply,
};
@ -1014,10 +987,8 @@ async fn deshielded_token_transfer() -> Result<()> {
// Perform deshielded transfer: private supply -> public recipient
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
from: private_mention(supply_account_id),
to: Some(public_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -1109,10 +1080,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
let name = "A NAME".to_owned();
let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_private_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: private_mention(definition_account_id),
supply_account_id: private_mention(supply_account_id),
name,
total_supply,
};
@ -1139,22 +1108,23 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
};
// Get keys for foreign mint (claiming path)
let (holder_keys, _, holder_identifier) = ctx
let holder = ctx
.wallet()
.storage()
.user_data
.get_private_account(recipient_account_id)
.key_chain()
.private_account(recipient_account_id)
.context("Failed to get private account keys")?;
let holder_keys = holder.key_chain;
let holder_identifier = holder.identifier;
// Mint using claiming path (foreign account)
let mint_amount = 9;
let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
definition: private_mention(definition_account_id),
holder: None,
holder_label: None,
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)),
holder_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)),
holder_identifier: Some(holder_identifier),
amount: mint_amount,
};
@ -1199,8 +1169,8 @@ async fn create_token_using_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create definition and supply accounts with labels
let def_label = "token-definition-label".to_owned();
let supply_label = "token-supply-label".to_owned();
let def_label = Label::new("token-definition-label");
let supply_label = Label::new("token-supply-label");
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
@ -1221,7 +1191,7 @@ async fn create_token_using_labels() -> Result<()> {
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(supply_label.clone()),
label: Some(Label::new(supply_label.clone())),
})),
)
.await?;
@ -1236,10 +1206,8 @@ async fn create_token_using_labels() -> Result<()> {
let name = "LABELED TOKEN".to_owned();
let total_supply = 100;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: None,
definition_account_label: Some(def_label),
supply_account_id: None,
supply_account_label: Some(supply_label),
definition_account_id: def_label.into(),
supply_account_id: supply_label.into(),
name: name.clone(),
total_supply,
};
@ -1303,7 +1271,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
};
// Create supply account with a label
let supply_label = "token-supply-sender".to_owned();
let supply_label = Label::new("token-supply-sender");
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
@ -1338,10 +1306,8 @@ async fn transfer_token_using_from_label() -> Result<()> {
// Create token
let total_supply = 50;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
definition_account_id: public_mention(definition_account_id),
supply_account_id: public_mention(supply_account_id),
name: "LABEL TEST TOKEN".to_owned(),
total_supply,
};
@ -1353,10 +1319,8 @@ async fn transfer_token_using_from_label() -> Result<()> {
// Transfer token using from_label instead of from
let transfer_amount = 20;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: None,
from_label: Some(supply_label),
to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
from: supply_label.into(),
to: Some(public_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),

View File

@ -14,11 +14,8 @@ use std::time::{Duration, Instant};
use anyhow::Result;
use bytesize::ByteSize;
use common::transaction::NSSATransaction;
use integration_tests::{
TestContext,
config::{InitialData, SequencerPartialConfig},
};
use key_protocol::key_management::{KeyChain, ephemeral_key_holder::EphemeralKeyHolder};
use integration_tests::{TestContext, config::SequencerPartialConfig};
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use log::info;
use nssa::{
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
@ -31,6 +28,7 @@ use nssa_core::{
account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
};
use sequencer_core::config::GenesisTransaction;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
@ -81,7 +79,7 @@ impl TpsTestManager {
program.id(),
[pair[0].1, pair[1].1].to_vec(),
[Nonce(0_u128)].to_vec(),
amount,
authenticated_transfer_core::Instruction::Transfer { amount },
)
.unwrap();
let witness_set =
@ -96,28 +94,14 @@ impl TpsTestManager {
/// Generates a sequencer configuration with initial balance in a number of public accounts.
/// The transactions generated with the function `build_public_txs` will be valid in a node
/// started with the config from this method.
fn generate_initial_data(&self) -> InitialData {
// Create public public keypairs
let public_accounts = self
.public_keypairs
fn generate_genesis(&self) -> Vec<GenesisTransaction> {
self.public_keypairs
.iter()
.map(|(key, _)| (key.clone(), 10))
.collect();
// Generate an initial commitment to be used with the privacy preserving transaction
// created with the `build_privacy_transaction` function.
let key_chain = KeyChain::new_os_random();
let account = Account {
balance: 100,
nonce: Nonce(0xdead_beef),
program_owner: Program::authenticated_transfer_program().id(),
data: Data::default(),
};
InitialData {
public_accounts,
private_accounts: vec![(key_chain, account)],
}
.map(|(_, account_id)| GenesisTransaction::SupplyPublicAccount {
account_id: *account_id,
balance: 10,
})
.collect()
}
const fn generate_sequencer_partial_config() -> SequencerPartialConfig {
@ -139,7 +123,7 @@ pub async fn tps_test() -> Result<()> {
let tps_test = TpsTestManager::new(target_tps, num_transactions);
let ctx = TestContext::builder()
.with_sequencer_partial_config(TpsTestManager::generate_sequencer_partial_config())
.with_initial_data(tps_test.generate_initial_data())
.with_genesis(tps_test.generate_genesis())
.build()
.await?;
@ -166,7 +150,7 @@ pub async fn tps_test() -> Result<()> {
loop {
assert!(
now.elapsed().as_millis() <= target_time.as_millis(),
"TPS test failed by timeout"
"TPS test failed by timeout, transactions processed {i}/{num_transactions}"
);
let tx_obj = ctx
@ -250,7 +234,10 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
);
let (output, proof) = circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: balance_to_move,
})
.unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: sender_ss,

View File

@ -24,6 +24,7 @@ use log::info;
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir;
use wallet::account::HumanReadableAccount;
use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, FfiU128, WalletHandle, error,
@ -53,11 +54,24 @@ unsafe extern "C" {
out_account_id: *mut FfiBytes32,
) -> error::WalletFfiError;
fn wallet_ffi_import_public_account(
handle: *mut WalletHandle,
private_key_hex: *const c_char,
) -> error::WalletFfiError;
fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle,
out_keys: *mut FfiPrivateAccountKeys,
) -> error::WalletFfiError;
fn wallet_ffi_import_private_account(
handle: *mut WalletHandle,
key_chain_json: *const c_char,
chain_index: *const c_char,
identifier: *const FfiU128,
account_state_json: *const c_char,
) -> error::WalletFfiError;
fn wallet_ffi_list_accounts(
handle: *mut WalletHandle,
out_list: *mut FfiAccountList,
@ -191,13 +205,59 @@ fn new_wallet_ffi_with_test_context_config(
let storage_path = CString::new(storage_path.to_str().unwrap())?;
let password = CString::new(ctx.ctx().wallet_password())?;
Ok(unsafe {
let wallet_ffi_handle = unsafe {
wallet_ffi_create_new(
config_path.as_ptr(),
storage_path.as_ptr(),
password.as_ptr(),
)
})
};
// Import accounts from source wallet
let source_wallet = ctx.ctx().wallet();
let source_key_chain = source_wallet.storage().key_chain();
for (account_id, _chain_index) in source_key_chain.public_account_ids() {
let private_key_hex = source_wallet
.get_account_public_signing_key(account_id)
.unwrap()
.to_string();
let private_key_hex = CString::new(private_key_hex)?;
unsafe { wallet_ffi_import_public_account(wallet_ffi_handle, private_key_hex.as_ptr()) }
.unwrap();
}
for (account_id, _chain_index) in source_key_chain.private_account_ids() {
let account = source_key_chain.private_account(account_id).unwrap();
let key_chain_json = CString::new(serde_json::to_string(account.key_chain)?)?;
let account_state_json = CString::new(serde_json::to_string(
&HumanReadableAccount::from(account.account.clone()),
)?)?;
let chain_index = account
.chain_index
.map(|chain_index| CString::new(chain_index.to_string()))
.transpose()?;
let chain_index_ptr = chain_index
.as_ref()
.map_or(std::ptr::null(), |value| value.as_ptr());
let identifier = FfiU128 {
data: account.identifier.to_le_bytes(),
};
unsafe {
wallet_ffi_import_private_account(
wallet_ffi_handle,
key_chain_json.as_ptr(),
chain_index_ptr,
&raw const identifier,
account_state_json.as_ptr(),
)
}
.unwrap();
}
Ok(wallet_ffi_handle)
}
fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandle> {
@ -405,7 +465,7 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> {
let balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let ffi_account_id = FfiBytes32::from(&account_id);
let ffi_account_id = FfiBytes32::from(account_id);
wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const ffi_account_id,
@ -435,7 +495,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> {
let mut out_account = FfiAccount::default();
let account: Account = unsafe {
let ffi_account_id = FfiBytes32::from(&account_id);
let ffi_account_id = FfiBytes32::from(account_id);
wallet_ffi_get_account_public(
wallet_ffi_handle,
&raw const ffi_account_id,
@ -451,7 +511,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> {
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
assert_eq!(account.nonce.0, 0);
assert_eq!(account.nonce.0, 1);
unsafe {
wallet_ffi_free_account_data(&raw mut out_account);
@ -472,7 +532,7 @@ fn test_wallet_ffi_get_account_private() -> Result<()> {
let mut out_account = FfiAccount::default();
let account: Account = unsafe {
let ffi_account_id = FfiBytes32::from(&account_id);
let ffi_account_id = FfiBytes32::from(account_id);
wallet_ffi_get_account_private(
wallet_ffi_handle,
&raw const ffi_account_id,
@ -488,7 +548,6 @@ fn test_wallet_ffi_get_account_private() -> Result<()> {
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
assert_eq!(account.nonce, 0_u128.into());
unsafe {
wallet_ffi_free_account_data(&raw mut out_account);
@ -509,7 +568,7 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> {
let mut out_key = FfiPublicAccountKey::default();
let key: PublicKey = unsafe {
let ffi_account_id = FfiBytes32::from(&account_id);
let ffi_account_id = FfiBytes32::from(account_id);
wallet_ffi_get_public_account_key(
wallet_ffi_handle,
&raw const ffi_account_id,
@ -548,7 +607,7 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> {
let mut keys = FfiPrivateAccountKeys::default();
unsafe {
let ffi_account_id = FfiBytes32::from(&account_id);
let ffi_account_id = FfiBytes32::from(account_id);
wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const ffi_account_id,
@ -557,15 +616,15 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> {
.unwrap();
};
let key_chain = &ctx
let account = &ctx
.ctx()
.wallet()
.storage()
.user_data
.get_private_account(account_id)
.unwrap()
.0;
.key_chain()
.private_account(account_id)
.unwrap();
let key_chain = account.key_chain;
let expected_npk = &key_chain.nullifier_public_key;
let expected_vpk = &key_chain.viewing_public_key;
@ -587,7 +646,7 @@ fn test_wallet_ffi_account_id_to_base58() -> Result<()> {
let private_key = PrivateKey::new_os_random();
let public_key = PublicKey::new_from_private_key(&private_key);
let account_id = AccountId::from(&public_key);
let ffi_bytes: FfiBytes32 = (&account_id).into();
let ffi_bytes: FfiBytes32 = account_id.into();
let ptr = unsafe { wallet_ffi_account_id_to_base58(&raw const ffi_bytes) };
let ffi_result = unsafe { CStr::from_ptr(ptr).to_str()? };
@ -744,8 +803,8 @@ fn test_wallet_ffi_transfer_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[1]).into();
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into();
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
@ -797,12 +856,12 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let (to, to_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
let amount: [u8; 16] = 100_u128.to_le_bytes();
@ -871,8 +930,8 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
@ -883,8 +942,9 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
&raw const to,
&raw const amount,
&raw mut transfer_result,
);
)
}
.unwrap();
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
@ -892,9 +952,9 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height);
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height);
};
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
}
let from_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
@ -931,12 +991,12 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let (to, to_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};

View File

@ -7,6 +7,10 @@ license = { workspace = true }
[lints]
workspace = true
[features]
default = []
test_utils = []
[dependencies]
nssa.workspace = true
nssa_core.workspace = true

View File

@ -36,7 +36,7 @@ impl EphemeralKeyHolder {
&self,
receiver_viewing_public_key: &ViewingPublicKey,
) -> SharedSecretKey {
SharedSecretKey::new(&self.ephemeral_secret_key, receiver_viewing_public_key)
SharedSecretKey::new(self.ephemeral_secret_key, receiver_viewing_public_key)
}
}
@ -47,7 +47,7 @@ pub fn produce_one_sided_shared_secret_receiver(
let mut esk = [0; 32];
OsRng.fill_bytes(&mut esk);
(
SharedSecretKey::new(&esk, vpk),
SharedSecretKey::new(esk, vpk),
EphemeralPublicKey::from_scalar(esk),
)
}

View File

@ -59,7 +59,7 @@ pub type SealingSecretKey = Scalar;
/// `Debug` is implemented manually to redact the GMS; formatting this value with `{:?}`
/// will not leak the secret. Code that formats through `{:#?}` on containing types is
/// safe for the same reason.
#[derive(Serialize, Deserialize, Clone)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroupKeyHolder {
gms: [u8; 32],
}
@ -164,7 +164,7 @@ impl GroupKeyHolder {
let mut ephemeral_scalar: Scalar = [0_u8; 32];
OsRng.fill_bytes(&mut ephemeral_scalar);
let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar);
let shared = SharedSecretKey::new(&ephemeral_scalar, &recipient_key.0);
let shared = SharedSecretKey::new(ephemeral_scalar, &recipient_key.0);
let aes_key = Self::seal_kdf(&shared);
let cipher = Aes256Gcm::new(&aes_key.into());
@ -191,7 +191,7 @@ impl GroupKeyHolder {
///
/// Returns `Err` if the ciphertext is too short, the ECDH point is invalid, or the
/// AES-GCM authentication tag doesn't verify (wrong key or tampered data).
pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result<Self, SealError> {
pub fn unseal(sealed: &[u8], own_key: SealingSecretKey) -> Result<Self, SealError> {
const HEADER_LEN: usize = 33 + 12;
const MIN_LEN: usize = HEADER_LEN + 16;
if sealed.len() < MIN_LEN {
@ -407,7 +407,7 @@ mod tests {
let recipient_vsk = recipient_keys.viewing_secret_key;
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal");
let restored = GroupKeyHolder::unseal(&sealed, recipient_vsk).expect("unseal");
assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms());
@ -438,7 +438,7 @@ mod tests {
.viewing_secret_key;
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk);
let result = GroupKeyHolder::unseal(&sealed, wrong_vsk);
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
}
@ -457,7 +457,7 @@ mod tests {
let last = sealed.len() - 1;
sealed[last] ^= 0xFF;
let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk);
let result = GroupKeyHolder::unseal(&sealed, recipient_vsk);
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
}
@ -481,7 +481,7 @@ mod tests {
#[test]
fn unseal_too_short_fails() {
let vsk: SealingSecretKey = [7_u8; 32];
let result = GroupKeyHolder::unseal(&[0_u8; 10], &vsk);
let result = GroupKeyHolder::unseal(&[0_u8; 10], vsk);
assert!(matches!(result, Err(super::SealError::TooShort)));
}
@ -537,7 +537,7 @@ mod tests {
let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0));
let bob_holder =
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
GroupKeyHolder::unseal(&sealed, bob_vsk).expect("Bob should unseal the GMS");
// Key agreement: both derive identical NPK and AccountId
let bob_npk = bob_holder

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use k256::{Scalar, elliptic_curve::PrimeField as _};
use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey};
use serde::{Deserialize, Serialize};
@ -9,8 +11,9 @@ use crate::key_management::{
};
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
pub struct ChildKeysPrivate {
pub value: (KeyChain, Vec<(Identifier, nssa::Account)>),
pub value: (KeyChain, BTreeMap<Identifier, nssa::Account>),
pub ccc: [u8; 32],
/// Can be [`None`] if root.
pub cci: Option<u32>,
@ -47,7 +50,7 @@ impl ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
vec![],
BTreeMap::from_iter([(0, nssa::Account::default())]),
),
ccc,
cci: None,
@ -97,7 +100,7 @@ impl ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
vec![],
BTreeMap::from_iter([(0, nssa::Account::default())]),
),
ccc,
cci: Some(cci),
@ -115,7 +118,7 @@ impl KeyTreeNode for ChildKeysPrivate {
}
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.value.1.iter().map(|(identifier, _)| {
self.value.1.keys().map(|identifier| {
nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier))
})
}

View File

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyTreeNode;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
pub struct ChildKeysPublic {
pub csk: nssa::PrivateKey,
pub cpk: nssa::PublicKey,

View File

@ -21,6 +21,7 @@ pub mod traits;
pub const DEPTH_SOFT_CAP: u32 = 20;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
pub struct KeyTree<N: KeyTreeNode> {
pub key_map: BTreeMap<ChainIndex, N>,
pub account_id_map: BTreeMap<nssa::AccountId, ChainIndex>,
@ -297,7 +298,13 @@ impl KeyTree<ChildKeysPrivate> {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id).cloned() {
if node.value.1.is_empty() {
if node.value.1.is_empty()
|| node
.value
.1
.iter()
.all(|(_, acc)| acc == &nssa::Account::default())
{
let account_ids = node.account_ids();
self.key_map.remove(&id);
for addr in account_ids {
@ -531,49 +538,49 @@ mod tests {
.key_map
.get_mut(&ChainIndex::from_str("/1").unwrap())
.unwrap();
acc.value.1.push((
acc.value.1.insert(
0,
nssa::Account {
balance: 2,
..nssa::Account::default()
},
));
);
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/2").unwrap())
.unwrap();
acc.value.1.push((
acc.value.1.insert(
0,
nssa::Account {
balance: 3,
..nssa::Account::default()
},
));
);
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
acc.value.1.push((
acc.value.1.insert(
0,
nssa::Account {
balance: 5,
..nssa::Account::default()
},
));
);
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
acc.value.1.push((
acc.value.1.insert(
0,
nssa::Account {
balance: 6,
..nssa::Account::default()
},
));
);
// Update account_id_map for nodes that now have entries
for chain_index_str in ["/1", "/2", "/0/1", "/1/0"] {
@ -605,15 +612,15 @@ mod tests {
assert_eq!(key_set, key_set_res);
let acc = &tree.key_map[&ChainIndex::from_str("/1").unwrap()];
assert_eq!(acc.value.1[0].1.balance, 2);
assert_eq!(acc.value.1[&0].balance, 2);
let acc = &tree.key_map[&ChainIndex::from_str("/2").unwrap()];
assert_eq!(acc.value.1[0].1.balance, 3);
assert_eq!(acc.value.1[&0].balance, 3);
let acc = &tree.key_map[&ChainIndex::from_str("/0/1").unwrap()];
assert_eq!(acc.value.1[0].1.balance, 5);
assert_eq!(acc.value.1[&0].balance, 5);
let acc = &tree.key_map[&ChainIndex::from_str("/1/0").unwrap()];
assert_eq!(acc.value.1[0].1.balance, 6);
assert_eq!(acc.value.1[&0].balance, 6);
}
}

View File

@ -12,8 +12,8 @@ pub mod secret_holders;
pub type PublicAccountSigningKey = [u8; 32];
#[derive(Serialize, Deserialize, Clone, Debug)]
/// Entrypoint to key management.
/// Private account keychain.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct KeyChain {
pub secret_spending_key: SecretSpendingKey,
pub private_key_holder: PrivateKeyHolder,
@ -72,7 +72,7 @@ impl KeyChain {
index: Option<u32>,
) -> SharedSecretKey {
SharedSecretKey::new(
&self.secret_spending_key.generate_viewing_secret_key(index),
self.secret_spending_key.generate_viewing_secret_key(index),
ephemeral_public_key_sender,
)
}

View File

@ -17,14 +17,14 @@ pub struct SeedHolder {
}
/// Secret spending key object. Can produce `PrivateKeyHolder` objects.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SecretSpendingKey(pub [u8; 32]);
pub type ViewingSecretKey = Scalar;
#[derive(Serialize, Deserialize, Debug, Clone)]
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
/// for recepient.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PrivateKeyHolder {
pub nullifier_secret_key: NullifierSecretKey,
pub viewing_secret_key: ViewingSecretKey,

View File

@ -1,417 +0,0 @@
use std::collections::BTreeMap;
use anyhow::Result;
use k256::AffinePoint;
use nssa::{Account, AccountId};
use nssa_core::Identifier;
use serde::{Deserialize, Serialize};
use crate::key_management::{
KeyChain,
group_key_holder::GroupKeyHolder,
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
secret_holders::SeedHolder,
};
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserPrivateAccountData {
pub key_chain: KeyChain,
pub accounts: Vec<(Identifier, Account)>,
}
/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state.
/// The group label and identifier (or PDA seed) are needed to re-derive keys during sync.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SharedAccountEntry {
pub group_label: String,
pub identifier: Identifier,
/// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`.
/// `None` for regular shared accounts (keys derived from identifier via derivation seed).
#[serde(default)]
pub pda_seed: Option<nssa_core::program::PdaSeed>,
#[serde(default)]
pub pda_program_id: Option<nssa_core::program::ProgramId>,
pub account: Account,
}
#[derive(Clone, Debug)]
pub struct NSSAUserData {
/// Default public accounts.
pub default_pub_account_signing_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
/// Default private accounts.
pub default_user_private_accounts: BTreeMap<AccountId, UserPrivateAccountData>,
/// Tree of public keys.
pub public_key_tree: KeyTreePublic,
/// Tree of private keys.
pub private_key_tree: KeyTreePrivate,
/// Group key holders for shared account management, keyed by a human-readable label.
pub group_key_holders: BTreeMap<String, GroupKeyHolder>,
/// Cached plaintext state of shared private accounts (PDAs and regular shared accounts),
/// keyed by `AccountId`. Each entry stores the group label and identifier needed
/// to re-derive keys during sync.
pub shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
/// Dedicated sealing secret key for GMS distribution. Generated once via
/// `wallet group new-sealing-key`. The corresponding public key is shared with
/// group members so they can seal GMS for this wallet.
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
}
impl NSSAUserData {
fn valid_public_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<nssa::AccountId, nssa::PrivateKey>,
) -> bool {
let mut check_res = true;
for (account_id, key) in accounts_keys_map {
let expected_account_id =
nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(key));
if &expected_account_id != account_id {
println!("{expected_account_id}, {account_id}");
check_res = false;
}
}
check_res
}
fn valid_private_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<AccountId, UserPrivateAccountData>,
) -> bool {
let mut check_res = true;
for (account_id, entry) in accounts_keys_map {
let any_match = entry.accounts.iter().any(|(identifier, _)| {
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier))
== *account_id
});
if !any_match {
println!("No matching entry found for account_id {account_id}");
check_res = false;
}
}
check_res
}
pub fn new_with_accounts(
default_accounts_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
default_accounts_key_chains: BTreeMap<AccountId, UserPrivateAccountData>,
public_key_tree: KeyTreePublic,
private_key_tree: KeyTreePrivate,
) -> Result<Self> {
if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) {
anyhow::bail!(
"Key transaction pairing check not satisfied, there are public account_ids, which are not derived from keys"
);
}
if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) {
anyhow::bail!(
"Key transaction pairing check not satisfied, there are private account_ids, which are not derived from keys"
);
}
Ok(Self {
default_pub_account_signing_keys: default_accounts_keys,
default_user_private_accounts: default_accounts_key_chains,
public_key_tree,
private_key_tree,
group_key_holders: BTreeMap::new(),
shared_private_accounts: BTreeMap::new(),
sealing_secret_key: None,
})
}
/// Generated new private key for public transaction signatures.
///
/// Returns the `account_id` of new account.
pub fn generate_new_public_transaction_private_key(
&mut self,
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_public_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_public_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures.
#[must_use]
pub fn get_pub_account_signing_key(
&self,
account_id: nssa::AccountId,
) -> Option<&nssa::PrivateKey> {
self.default_pub_account_signing_keys
.get(&account_id)
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
}
/// Creates a new receiving key node and returns its `ChainIndex`.
pub fn create_private_accounts_key(&mut self, parent_cci: Option<ChainIndex>) -> ChainIndex {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.create_private_accounts_key_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.create_private_accounts_key_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Registers an additional identifier on an existing private key node, deriving and recording
/// the corresponding `AccountId`. Returns `None` if the node does not exist or the identifier
/// is already registered.
pub fn register_identifier_on_private_key_chain(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
self.private_key_tree
.register_identifier_on_node(cci, identifier)
}
/// Returns the key chain and account data for the given private account ID.
#[must_use]
pub fn get_private_account(
&self,
account_id: nssa::AccountId,
) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> {
// Check default accounts
if let Some(entry) = self.default_user_private_accounts.get(&account_id) {
for (identifier, account) in &entry.accounts {
let expected_id =
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((entry.key_chain.clone(), account.clone(), *identifier));
}
}
return None;
}
// Check tree
if let Some(node) = self.private_key_tree.get_node(account_id) {
let key_chain = &node.value.0;
for (identifier, account) in &node.value.1 {
let expected_id =
nssa::AccountId::from((&key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((key_chain.clone(), account.clone(), *identifier));
}
}
}
None
}
pub fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.public_account_ids().chain(self.private_account_ids())
}
pub fn public_account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.default_pub_account_signing_keys
.keys()
.copied()
.chain(self.public_key_tree.account_id_map.keys().copied())
}
pub fn private_account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.default_user_private_accounts
.keys()
.copied()
.chain(self.private_key_tree.account_id_map.keys().copied())
}
/// Returns the `GroupKeyHolder` for the given label, if it exists.
#[must_use]
pub fn group_key_holder(&self, label: &str) -> Option<&GroupKeyHolder> {
self.group_key_holders.get(label)
}
/// Inserts or replaces a `GroupKeyHolder` under the given label.
///
/// If a holder already exists under this label, it is silently replaced and the old
/// GMS is lost. Callers must ensure label uniqueness across groups.
pub fn insert_group_key_holder(&mut self, label: String, holder: GroupKeyHolder) {
self.group_key_holders.insert(label, holder);
}
/// Returns the cached account for a shared private account, if it exists.
#[must_use]
pub fn shared_private_account(
&self,
account_id: &nssa::AccountId,
) -> Option<&SharedAccountEntry> {
self.shared_private_accounts.get(account_id)
}
/// Inserts or replaces a shared private account entry.
pub fn insert_shared_private_account(
&mut self,
account_id: nssa::AccountId,
entry: SharedAccountEntry,
) {
self.shared_private_accounts.insert(account_id, entry);
}
/// Updates the cached account state for a shared private account.
pub fn update_shared_private_account_state(
&mut self,
account_id: &nssa::AccountId,
account: nssa_core::account::Account,
) {
if let Some(entry) = self.shared_private_accounts.get_mut(account_id) {
entry.account = account;
}
}
/// Iterates over all shared private accounts.
pub fn shared_private_accounts_iter(
&self,
) -> impl Iterator<Item = (&nssa::AccountId, &SharedAccountEntry)> {
self.shared_private_accounts.iter()
}
}
impl Default for NSSAUserData {
fn default() -> Self {
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
Self::new_with_accounts(
BTreeMap::new(),
BTreeMap::new(),
KeyTreePublic::new(&seed_holder),
KeyTreePrivate::new(&seed_holder),
)
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_key_holder_storage_round_trip() {
let mut user_data = NSSAUserData::default();
assert!(user_data.group_key_holder("test-group").is_none());
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
user_data.insert_group_key_holder(String::from("test-group"), holder.clone());
let retrieved = user_data
.group_key_holder("test-group")
.expect("should exist");
assert_eq!(retrieved.dangerous_raw_gms(), holder.dangerous_raw_gms());
}
#[test]
fn group_key_holders_default_empty() {
let user_data = NSSAUserData::default();
assert!(user_data.group_key_holders.is_empty());
assert!(user_data.shared_private_accounts.is_empty());
}
#[test]
fn shared_account_entry_serde_round_trip() {
use nssa_core::program::PdaSeed;
let entry = SharedAccountEntry {
group_label: String::from("test-group"),
identifier: 42,
pda_seed: None,
pda_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
assert_eq!(decoded.group_label, "test-group");
assert_eq!(decoded.identifier, 42);
assert!(decoded.pda_seed.is_none());
let pda_entry = SharedAccountEntry {
group_label: String::from("pda-group"),
identifier: u128::MAX,
pda_seed: Some(PdaSeed::new([7_u8; 32])),
pda_program_id: Some([9; 8]),
account: nssa_core::account::Account::default(),
};
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
let pda_decoded: SharedAccountEntry =
bincode::deserialize(&pda_encoded).expect("deserialize pda");
assert_eq!(pda_decoded.group_label, "pda-group");
assert_eq!(pda_decoded.identifier, u128::MAX);
assert_eq!(pda_decoded.pda_seed.unwrap(), PdaSeed::new([7_u8; 32]));
}
#[test]
fn shared_account_entry_none_pda_seed_round_trips() {
// Verify that an entry with pda_seed=None serializes and deserializes correctly,
// confirming the #[serde(default)] attribute works for backward compatibility.
let entry = SharedAccountEntry {
group_label: String::from("old"),
identifier: 1,
pda_seed: None,
pda_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
assert_eq!(decoded.group_label, "old");
assert_eq!(decoded.identifier, 1);
assert!(decoded.pda_seed.is_none());
}
#[test]
fn shared_account_derives_consistent_keys_from_group() {
use nssa_core::program::PdaSeed;
let mut user_data = NSSAUserData::default();
let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]);
user_data.insert_group_key_holder(String::from("my-group"), gms_holder);
let holder = user_data.group_key_holder("my-group").unwrap();
// Regular shared account: derive via tag
let tag = [1_u8; 32];
let keys_a = holder.derive_keys_for_shared_account(&tag);
let keys_b = holder.derive_keys_for_shared_account(&tag);
assert_eq!(
keys_a.generate_nullifier_public_key(),
keys_b.generate_nullifier_public_key(),
);
// PDA shared account: derive via seed
let seed = PdaSeed::new([2_u8; 32]);
let pda_keys_a = holder.derive_keys_for_pda(&[9; 8], &seed);
let pda_keys_b = holder.derive_keys_for_pda(&[9; 8], &seed);
assert_eq!(
pda_keys_a.generate_nullifier_public_key(),
pda_keys_b.generate_nullifier_public_key(),
);
// PDA and shared derivations don't collide
assert_ne!(
keys_a.generate_nullifier_public_key(),
pda_keys_a.generate_nullifier_public_key(),
);
}
#[test]
fn new_account() {
let mut user_data = NSSAUserData::default();
let chain_index = user_data.create_private_accounts_key(Some(ChainIndex::root()));
let is_key_chain_generated = user_data
.private_key_tree
.key_map
.contains_key(&chain_index);
assert!(is_key_chain_generated);
let key_chain = &user_data.private_key_tree.key_map[&chain_index].value.0;
println!("{key_chain:#?}");
}
}

View File

@ -1,4 +1,3 @@
#![expect(clippy::print_stdout, reason = "TODO: fix later")]
pub mod key_management;
pub mod key_protocol_core;

View File

@ -30,6 +30,7 @@ risc0-binfmt = "3.0.2"
[dev-dependencies]
token_core.workspace = true
authenticated_transfer_core.workspace = true
test_program_methods.workspace = true
env_logger.workspace = true

View File

@ -17,7 +17,9 @@ use serde::{Deserialize, Serialize};
use crate::{SharedSecretKey, encryption::Scalar};
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(
Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize,
)]
pub struct Secp256k1Point(pub Vec<u8>);
impl std::fmt::Debug for Secp256k1Point {
@ -56,8 +58,8 @@ impl From<&EphemeralSecretKey> for EphemeralPublicKey {
impl SharedSecretKey {
/// Creates a new shared secret key from a scalar and a point.
#[must_use]
pub fn new(scalar: &Scalar, point: &Secp256k1Point) -> Self {
let scalar = k256::Scalar::from_repr((*scalar).into()).unwrap();
pub fn new(scalar: Scalar, point: &Secp256k1Point) -> Self {
let scalar = k256::Scalar::from_repr(scalar.into()).unwrap();
let point: [u8; 33] = point.0.clone().try_into().unwrap();
let encoded = EncodedPoint::from_bytes(point).unwrap();

View File

@ -24,6 +24,8 @@ pub mod program;
#[cfg(feature = "host")]
pub mod error;
pub const GENESIS_BLOCK_ID: BlockId = 1;
pub type BlockId = u64;
/// Unix timestamp in milliseconds.
pub type Timestamp = u64;

View File

@ -8,7 +8,7 @@ const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\
pub type Identifier = u128;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Hash))]
pub struct NullifierPublicKey(pub [u8; 32]);

View File

@ -636,7 +636,6 @@ pub fn validate_execution(
}
// 8. Total balance is preserved
let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else {

View File

@ -4,7 +4,7 @@
)]
pub use nssa_core::{
SharedSecretKey,
GENESIS_BLOCK_ID, SharedSecretKey,
account::{Account, AccountId, Data},
encryption::EphemeralPublicKey,
program::ProgramId,

View File

@ -17,6 +17,26 @@ pub struct MerkleTree {
}
impl MerkleTree {
pub fn with_capacity(capacity: usize) -> Self {
// Adjust capacity to ensure power of two
let capacity = capacity.next_power_of_two();
let total_depth = usize::try_from(capacity.trailing_zeros()).expect("u32 fits in usize");
let nodes = default_values::DEFAULT_VALUES[..=total_depth]
.iter()
.rev()
.enumerate()
.flat_map(|(level, default_value)| std::iter::repeat_n(default_value, 1 << level))
.copied()
.collect();
Self {
nodes,
capacity,
length: 0,
}
}
pub fn root(&self) -> Node {
let root_index = self.root_index();
*self.get_node(root_index)
@ -49,26 +69,6 @@ impl MerkleTree {
self.nodes[index] = node;
}
pub fn with_capacity(capacity: usize) -> Self {
// Adjust capacity to ensure power of two
let capacity = capacity.next_power_of_two();
let total_depth = usize::try_from(capacity.trailing_zeros()).expect("u32 fits in usize");
let nodes = default_values::DEFAULT_VALUES[..=total_depth]
.iter()
.rev()
.enumerate()
.flat_map(|(level, default_value)| std::iter::repeat_n(default_value, 1 << level))
.copied()
.collect();
Self {
nodes,
capacity,
length: 0,
}
}
/// Reallocates storage of Merkle tree for double capacity.
/// The current tree is embedded into the new tree as a subtree.
fn reallocate_to_double_capacity(&mut self) {

View File

@ -228,11 +228,14 @@ mod tests {
let expected_sender_pre = sender.clone();
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk());
let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk());
let (output, proof) = execute_and_prove(
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: balance_to_move,
})
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
@ -322,14 +325,17 @@ mod tests {
];
let esk_1 = [3; 32];
let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk());
let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk());
let esk_2 = [5; 32];
let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.vpk());
let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk());
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: balance_to_move,
})
.unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: shared_secret_1,
@ -397,7 +403,7 @@ mod tests {
.unwrap();
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk());
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
@ -428,7 +434,7 @@ mod tests {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk());
// PDA (new, private PDA) — AccountId derived from auth_transfer_proxy's program ID
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
@ -465,7 +471,7 @@ mod tests {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk());
// PDA (new, private PDA)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
@ -518,7 +524,7 @@ mod tests {
let shared_keys = test_private_account_keys_1();
let shared_npk = shared_keys.npk();
let shared_identifier: u128 = 42;
let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk());
let shared_secret = SharedSecretKey::new([55; 32], &shared_keys.vpk());
// Sender: public account with balance, owned by auth-transfer
let sender_id = AccountId::new([99; 32]);

View File

@ -250,7 +250,7 @@ pub mod tests {
let account_id = nssa_core::account::AccountId::from((&npk, 0));
let commitment = Commitment::new(&account_id, &account);
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &vpk);
let shared_secret = SharedSecretKey::new(esk, &vpk);
let epk = EphemeralPublicKey::from_scalar(esk);
let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2);
let encrypted_account_data =

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