diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 9508ee29..feb5e5e8 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -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(()) +} diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index 54713f67..736c24c5 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -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(()) +} diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index fa151d9c..c6606145 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -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); - } }