replace unit tests with integration tests

This commit is contained in:
Sergio Chouhy 2026-05-15 20:56:21 -03:00
parent 58226fd0f7
commit 0e177f1eba
3 changed files with 195 additions and 185 deletions

View File

@ -1,6 +1,7 @@
use std::time::Duration;
use anyhow::{Context as _, Result};
use common::transaction::NSSATransaction;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
public_mention, verify_commitment_is_in_state,
@ -623,3 +624,130 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
Ok(())
}
#[test]
async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
use nssa::{
EphemeralPublicKey, SharedSecretKey, execute_and_prove,
privacy_preserving_transaction::{self, circuit::ProgramWithDependencies},
};
use nssa_core::{InputAccountIdentity, account::AccountWithMetadata};
let ctx = TestContext::new().await?;
let binary = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../artifacts/test_program_methods/faucet_chain_caller.bin"),
)?;
let deploy_tx = NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new(
nssa::program_deployment_transaction::Message::new(binary.clone()),
));
ctx.sequencer_client().send_transaction(deploy_tx).await?;
info!("Waiting for deploy block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_account_id = nssa::system_faucet_account_id();
let attacker_id = ctx.existing_public_accounts()[0];
let faucet_program_id = Program::faucet().id();
let vault_program_id = Program::vault().id();
let auth_transfer_program_id = Program::authenticated_transfer_program().id();
let nsk: nssa_core::NullifierSecretKey = [3; 32];
let npk = NullifierPublicKey::from(&nsk);
let vpk = Secp256k1Point::from_scalar([4; 32]);
let ssk = SharedSecretKey::new([55; 32], &vpk);
let epk = EphemeralPublicKey::from_scalar([55; 32]);
let attacker_vault_id = {
let seed = vault_core::compute_vault_seed(attacker_id);
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
};
let amount: u128 = 1;
let faucet_pre = AccountWithMetadata::new(
ctx.sequencer_client()
.get_account(faucet_account_id)
.await?,
false,
faucet_account_id,
);
let vault_pda_pre = AccountWithMetadata::new(
ctx.sequencer_client()
.get_account(attacker_vault_id)
.await?,
false,
attacker_vault_id,
);
let faucet_chain_caller = Program::new(binary)?;
let program_with_deps = ProgramWithDependencies::new(
faucet_chain_caller,
[
(faucet_program_id, Program::faucet()),
(vault_program_id, Program::vault()),
(
auth_transfer_program_id,
Program::authenticated_transfer_program(),
),
]
.into(),
);
let instruction =
Program::serialize_instruction((faucet_program_id, vault_program_id, attacker_id, amount))?;
let (output, proof) = execute_and_prove(
vec![faucet_pre, vault_pda_pre],
instruction,
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
npk,
ssk,
identifier: 1337,
},
],
&program_with_deps,
)?;
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
vec![faucet_account_id],
vec![],
vec![(npk, vpk, epk)],
output,
)?;
let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
let attack_ppt = NSSATransaction::PrivacyPreserving(nssa::PrivacyPreservingTransaction::new(
message,
witness_set,
));
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let vault_balance_before = ctx
.sequencer_client()
.get_account_balance(attacker_vault_id)
.await?;
let tx_hash = ctx.sequencer_client().send_transaction(attack_ppt).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let vault_balance_after = ctx
.sequencer_client()
.get_account_balance(attacker_vault_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(faucet_balance_after, faucet_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
assert!(tx_on_chain.is_none());
Ok(())
}

View File

@ -1,4 +1,4 @@
use std::time::Duration;
use std::{path::PathBuf, time::Duration};
use anyhow::Result;
use common::transaction::NSSATransaction;
@ -492,3 +492,69 @@ async fn cannot_execute_faucet_program() -> Result<()> {
Ok(())
}
#[test]
async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
let ctx = TestContext::new().await?;
let binary = std::fs::read(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../artifacts/test_program_methods/faucet_chain_caller.bin"),
)?;
let faucet_chain_caller_id = Program::new(binary.clone())?.id();
let deploy_tx = NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new(
nssa::program_deployment_transaction::Message::new(binary),
));
ctx.sequencer_client().send_transaction(deploy_tx).await?;
info!("Waiting for deploy block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_account_id = system_faucet_account_id();
let attacker = ctx.existing_public_accounts()[0];
let faucet_program_id = Program::faucet().id();
let vault_program_id = Program::vault().id();
let attacker_vault_id = vault_core::compute_vault_account_id(vault_program_id, attacker);
let amount: u128 = 1;
let message = public_transaction::Message::try_new(
faucet_chain_caller_id,
vec![faucet_account_id, attacker_vault_id],
vec![],
(faucet_program_id, vault_program_id, attacker, amount),
)?;
let attack_tx = NSSATransaction::Public(nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
));
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let vault_balance_before = ctx
.sequencer_client()
.get_account_balance(attacker_vault_id)
.await?;
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let vault_balance_after = ctx
.sequencer_client()
.get_account_balance(attacker_vault_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(faucet_balance_after, faucet_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
assert!(tx_on_chain.is_none());
Ok(())
}

View File

@ -437,8 +437,6 @@ mod tests {
};
use logos_blockchain_core::mantle::ops::channel::ChannelId;
use mempool::MemPoolHandle;
use nssa::{EphemeralPublicKey, SharedSecretKey};
use nssa_core::{NullifierPublicKey, account::AccountId, encryption::ViewingPublicKey};
use tempfile::tempdir;
use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys};
@ -1062,186 +1060,4 @@ mod tests {
"Block production should abort when clock account data is corrupted"
);
}
#[tokio::test]
async fn user_tx_that_chain_calls_faucet_is_dropped() {
let (mut sequencer, mempool_handle) = common_setup().await;
// Deploy the faucet_chain_caller test program.
let deploy_tx =
NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new(
nssa::program_deployment_transaction::Message::new(
test_program_methods::FAUCET_CHAIN_CALLER_ELF.to_vec(),
),
));
mempool_handle.push(deploy_tx).await.unwrap();
sequencer.produce_new_block().await.unwrap();
// The attacker chain-calls the faucet through their own program:
// faucet_chain_caller → faucet → vault → authenticated_transfer.
// Funds from the system faucet would land in the attacker's vault PDA.
let faucet_account_id = nssa::system_faucet_account_id();
let attacker_id = initial_accounts()[0].account_id;
let faucet_program_id = nssa::program::Program::faucet().id();
let vault_program_id = nssa::program::Program::vault().id();
let attacker_vault_id = vault_core::compute_vault_account_id(vault_program_id, attacker_id);
let amount: u128 = 1;
let faucet_chain_caller_id =
nssa::program::Program::new(test_program_methods::FAUCET_CHAIN_CALLER_ELF.to_vec())
.unwrap()
.id();
let message = nssa::public_transaction::Message::try_new(
faucet_chain_caller_id,
vec![faucet_account_id, attacker_vault_id],
vec![], // no signers — faucet PDA authorization is handled internally
(faucet_program_id, vault_program_id, attacker_id, amount),
)
.unwrap();
let attack_tx = NSSATransaction::Public(nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
));
let faucet_balance_before = sequencer.state.get_account_by_id(faucet_account_id).balance;
let vault_balance_before = sequencer.state.get_account_by_id(attacker_vault_id).balance;
mempool_handle.push(attack_tx).await.unwrap();
sequencer.produce_new_block().await.unwrap();
let faucet_balance_after = sequencer.state.get_account_by_id(faucet_account_id).balance;
let vault_balance_after = sequencer.state.get_account_by_id(attacker_vault_id).balance;
assert_eq!(faucet_balance_after, faucet_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
}
#[tokio::test]
async fn ppt_that_chain_calls_faucet_is_dropped() {
use nssa::privacy_preserving_transaction::circuit::ProgramWithDependencies;
use nssa_core::{InputAccountIdentity, account::AccountWithMetadata};
let (mut sequencer, mempool_handle) = common_setup().await;
// Deploy the faucet_chain_caller test program.
let deploy_tx =
NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new(
nssa::program_deployment_transaction::Message::new(
test_program_methods::FAUCET_CHAIN_CALLER_ELF.to_vec(),
),
));
mempool_handle.push(deploy_tx).await.unwrap();
sequencer.produce_new_block().await.unwrap();
// The attacker runs faucet_chain_caller inside a PPT circuit, producing a valid proof
// that the faucet was drained into their vault PDA.
let faucet_account_id = nssa::system_faucet_account_id();
let attacker_id = initial_accounts()[0].account_id;
let faucet_program_id = nssa::program::Program::faucet().id();
let vault_program_id = nssa::program::Program::vault().id();
let auth_transfer_program_id =
nssa::program::Program::authenticated_transfer_program().id();
let nsk = [3; 32];
let npk = NullifierPublicKey::from(&nsk);
let vsk = [4; 32];
let vpk = ViewingPublicKey::from_scalar(vsk);
let ssk = SharedSecretKey::new([55; 32], &vpk);
let epk = EphemeralPublicKey::from_scalar([55; 32]);
let attacker_vault_id = {
let seed = vault_core::compute_vault_seed(attacker_id);
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
};
let amount: u128 = 1;
let faucet_pre = AccountWithMetadata::new(
sequencer.state.get_account_by_id(faucet_account_id),
false,
faucet_account_id,
);
let vault_pda_pre = AccountWithMetadata::new(
sequencer.state.get_account_by_id(attacker_vault_id),
false,
attacker_vault_id,
);
let faucet_chain_caller =
nssa::program::Program::new(test_program_methods::FAUCET_CHAIN_CALLER_ELF.to_vec())
.unwrap();
let program_with_deps = ProgramWithDependencies::new(
faucet_chain_caller,
[
(faucet_program_id, nssa::program::Program::faucet()),
(vault_program_id, nssa::program::Program::vault()),
(
auth_transfer_program_id,
nssa::program::Program::authenticated_transfer_program(),
),
]
.into(),
);
let instruction = nssa::program::Program::serialize_instruction((
faucet_program_id,
vault_program_id,
attacker_id,
amount,
))
.unwrap();
let (output, proof) = nssa::execute_and_prove(
vec![faucet_pre, vault_pda_pre],
instruction,
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
npk,
ssk,
identifier: 1337,
},
],
&program_with_deps,
)
.unwrap();
let message = nssa::privacy_preserving_transaction::Message::try_from_circuit_output(
vec![faucet_account_id],
vec![], // no public signers
vec![(npk, vpk, epk)],
output,
)
.unwrap();
let witness_set = nssa::privacy_preserving_transaction::WitnessSet::for_message(
&message,
proof,
&[], // no signatures
);
let attack_ppt = NSSATransaction::PrivacyPreserving(
nssa::PrivacyPreservingTransaction::new(message, witness_set),
);
let faucet_balance_before = sequencer.state.get_account_by_id(faucet_account_id).balance;
let vault_balance_before = sequencer.state.get_account_by_id(attacker_vault_id).balance;
mempool_handle.push(attack_ppt).await.unwrap();
sequencer.produce_new_block().await.unwrap();
let block = sequencer
.store
.get_block_at_id(sequencer.chain_height)
.unwrap()
.unwrap();
let faucet_balance_after = sequencer.state.get_account_by_id(faucet_account_id).balance;
let vault_balance_after = sequencer.state.get_account_by_id(attacker_vault_id).balance;
// The attack PPT must be dropped; only the mandatory clock invocation remains.
assert_eq!(
block.body.transactions,
vec![NSSATransaction::Public(clock_invocation(
block.header.timestamp
))]
);
assert_eq!(faucet_balance_after, faucet_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
}
}