refactor: use faucet program to manage faucet account

This commit is contained in:
Daniil Polyakov 2026-05-14 02:00:39 +03:00
parent fe047e169c
commit 28904fe611
26 changed files with 248 additions and 60 deletions

12
Cargo.lock generated
View File

@ -2666,6 +2666,14 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "faucet_core"
version = "0.1.0"
dependencies = [
"nssa_core",
"serde",
]
[[package]]
name = "ferroid"
version = "2.0.0"
@ -3938,6 +3946,7 @@ dependencies = [
"bytesize",
"common",
"env_logger",
"faucet_core",
"futures",
"hex",
"indexer_ffi",
@ -6322,6 +6331,7 @@ dependencies = [
"borsh",
"clock_core",
"env_logger",
"faucet_core",
"hex",
"hex-literal 1.1.0",
"k256",
@ -7057,6 +7067,7 @@ dependencies = [
"ata_program",
"authenticated_transfer_core",
"clock_core",
"faucet_core",
"nssa_core",
"risc0-zkvm",
"serde",
@ -8421,6 +8432,7 @@ dependencies = [
"bytesize",
"chrono",
"common",
"faucet_core",
"futures",
"humantime-serde",
"log",

View File

@ -21,6 +21,7 @@ members = [
"programs/associated_token_account/core",
"programs/associated_token_account",
"programs/authenticated_transfer/core",
"programs/faucet/core",
"programs/vault/core",
"sequencer/core",
"sequencer/service",
@ -68,6 +69,7 @@ amm_program = { path = "programs/amm" }
ata_core = { path = "programs/associated_token_account/core" }
ata_program = { path = "programs/associated_token_account" }
authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
faucet_core = { path = "programs/faucet/core" }
vault_core = { path = "programs/vault/core" }
test_program_methods = { path = "test_program_methods" }
testnet_initial_state = { path = "testnet_initial_state" }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -68,7 +68,8 @@ impl NSSATransaction {
/// Validates the transaction against the current state and returns the resulting diff
/// without applying it. Rejects transactions that modify clock system accounts and
/// rejects unsafe modifications of the system faucet account.
/// rejects unsafe modifications of the system faucet account. Also rejects direct
/// invocation of the faucet program for user-submitted transactions.
///
/// This check is required for all user transactions. Only sequencer transaction may bypass this
/// check.
@ -78,6 +79,14 @@ impl NSSATransaction {
block_id: BlockId,
timestamp: Timestamp,
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
if let Self::Public(tx) = self
&& tx.message().program_id == nssa::program::Program::faucet().id()
{
return Err(nssa::error::NssaError::InvalidInput(
"Transaction invokes restricted faucet program".into(),
));
}
let diff = match self {
Self::Public(tx) => {
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
@ -102,36 +111,6 @@ impl NSSATransaction {
));
}
let faucet_account_id = nssa::SYSTEM_FAUCET_ACCOUNT_ID;
if let Some(post_faucet) = public_diff.get(&faucet_account_id) {
let pre_faucet = state.get_account_by_id(faucet_account_id);
let nssa::Account {
program_owner: post_program_owner,
data: post_data,
nonce: post_nonce,
balance: post_balance,
} = post_faucet;
let nssa::Account {
program_owner: pre_program_owner,
data: pre_data,
nonce: pre_nonce,
balance: pre_balance,
} = pre_faucet;
let faucet_change_is_allowed = *post_program_owner == pre_program_owner
&& *post_data == pre_data
&& *post_nonce == pre_nonce
&& *post_balance >= pre_balance;
if !faucet_change_is_allowed {
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system faucet account".into(),
));
}
}
Ok(diff)
}

View File

@ -21,6 +21,7 @@ serde_json.workspace = true
token_core.workspace = true
ata_core.workspace = true
vault_core.workspace = true
faucet_core.workspace = true
indexer_service_rpc = { workspace = true, features = ["client"] }
sequencer_service_rpc = { workspace = true, features = ["client"] }
jsonrpsee = { workspace = true, features = ["ws-client"] }

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use common::transaction::NSSATransaction;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
use log::info;
use nssa::{SYSTEM_FAUCET_ACCOUNT_ID, program::Program, public_transaction};
use nssa::{program::Program, public_transaction, system_faucet_account_id};
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
@ -349,6 +349,7 @@ async fn successful_transfer_using_to_label() -> Result<()> {
#[test]
async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
let ctx = TestContext::new().await?;
let faucet_account_id = system_faucet_account_id();
let recipient = ctx.existing_public_accounts()[0];
let recipient_balance_before = ctx
@ -357,13 +358,13 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
.await?;
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.get_account_balance(faucet_account_id)
.await?;
let amount = 1_u128;
let message = public_transaction::Message::try_new(
Program::authenticated_transfer_program().id(),
vec![SYSTEM_FAUCET_ACCOUNT_ID, recipient],
vec![faucet_account_id, recipient],
vec![],
authenticated_transfer_core::Instruction::Transfer { amount },
)?;
@ -385,7 +386,7 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
.await?;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.get_account_balance(faucet_account_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
@ -399,18 +400,19 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
#[test]
async fn can_transfer_funds_to_system_faucet_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let faucet_account_id = system_faucet_account_id();
let sender = ctx.existing_public_accounts()[0];
let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?;
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.get_account_balance(faucet_account_id)
.await?;
let amount = 100_u128;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: public_mention(sender),
to: Some(public_mention(SYSTEM_FAUCET_ACCOUNT_ID)),
to: Some(public_mention(faucet_account_id)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
@ -424,7 +426,7 @@ async fn can_transfer_funds_to_system_faucet_account() -> Result<()> {
let sender_balance_after = ctx.sequencer_client().get_account_balance(sender).await?;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.get_account_balance(faucet_account_id)
.await?;
assert_eq!(sender_balance_after, sender_balance_before - amount);
@ -432,3 +434,61 @@ async fn can_transfer_funds_to_system_faucet_account() -> Result<()> {
Ok(())
}
#[test]
async fn cannot_execute_faucet_program() -> Result<()> {
let ctx = TestContext::new().await?;
let faucet_account_id = system_faucet_account_id();
let recipient = ctx.existing_public_accounts()[0];
let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient);
let recipient_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient)
.await?;
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let amount = 1_u128;
let message = public_transaction::Message::try_new(
Program::faucet().id(),
vec![faucet_account_id, recipient_vault_id],
vec![],
faucet_core::Instruction::Transfer {
vault_program_id,
recipient_id: recipient,
amount,
},
)?;
let tx = nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
);
let tx_hash = ctx
.sequencer_client()
.send_transaction(NSSATransaction::Public(tx))
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let recipient_balance_after = ctx
.sequencer_client()
.get_account_balance(recipient)
.await?;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(faucet_account_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(recipient_balance_after, recipient_balance_before);
assert_eq!(faucet_balance_after, faucet_balance_before);
assert!(tx_on_chain.is_none());
Ok(())
}

View File

@ -10,6 +10,7 @@ workspace = true
[dependencies]
nssa_core = { workspace = true, features = ["host"] }
clock_core.workspace = true
faucet_core.workspace = true
anyhow.workspace = true
thiserror.workspace = true

View File

@ -4,7 +4,7 @@
)]
pub use nssa_core::{
GENESIS_BLOCK_ID, SYSTEM_FAUCET_ACCOUNT_ID, SharedSecretKey,
GENESIS_BLOCK_ID, SharedSecretKey,
account::{Account, AccountId, Data},
encryption::EphemeralPublicKey,
program::ProgramId,
@ -18,7 +18,7 @@ pub use public_transaction::PublicTransaction;
pub use signature::{PrivateKey, PublicKey, Signature};
pub use state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State, system_faucet_account_id,
};
pub use validated_state_diff::ValidatedStateDiff;

View File

@ -10,8 +10,8 @@ use crate::{
error::NssaError,
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, FAUCET_ELF,
FAUCET_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
},
};
@ -156,6 +156,14 @@ impl Program {
elf: VAULT_ELF.to_vec(),
}
}
#[must_use]
pub fn faucet() -> Self {
Self {
id: FAUCET_ID,
elf: FAUCET_ELF.to_vec(),
}
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
@ -186,8 +194,9 @@ mod tests {
program::Program,
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, FAUCET_ELF,
FAUCET_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF,
TOKEN_ID, VAULT_ELF, VAULT_ID,
},
};
@ -501,6 +510,7 @@ mod tests {
let auth_transfer_program = Program::authenticated_transfer_program();
let token_program = Program::token();
let vault_program = Program::vault();
let faucet_program = Program::faucet();
let pinata_program = Program::pinata();
assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID);
@ -509,6 +519,8 @@ mod tests {
assert_eq!(token_program.elf, TOKEN_ELF);
assert_eq!(vault_program.id, VAULT_ID);
assert_eq!(vault_program.elf, VAULT_ELF);
assert_eq!(faucet_program.id, FAUCET_ID);
assert_eq!(faucet_program.elf, FAUCET_ELF);
assert_eq!(pinata_program.id, PINATA_ID);
assert_eq!(pinata_program.elf, PINATA_ELF);
}
@ -520,6 +532,7 @@ mod tests {
(AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID),
(ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID),
(CLOCK_ELF, CLOCK_ID),
(FAUCET_ELF, FAUCET_ID),
(PINATA_ELF, PINATA_ID),
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
(TOKEN_ELF, TOKEN_ID),

View File

@ -8,7 +8,7 @@ pub use clock_core::{
};
use nssa_core::{
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
SYSTEM_FAUCET_ACCOUNT_ID, Timestamp,
Timestamp,
account::{Account, AccountId, Nonce},
program::ProgramId,
};
@ -124,9 +124,10 @@ pub struct V03State {
impl Default for V03State {
fn default() -> Self {
let faucet_account_id = system_faucet_account_id();
let faucet_account = system_faucet_account();
let mut public_state = HashMap::new();
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
public_state.insert(faucet_account_id, faucet_account);
Self {
public_state,
@ -148,6 +149,7 @@ impl V03State {
initial_private_accounts: Vec<(Commitment, Nullifier)>,
genesis_timestamp: nssa_core::Timestamp,
) -> Self {
let faucet_account_id = system_faucet_account_id();
let authenticated_transfer_program = Program::authenticated_transfer_program();
let mut public_state: HashMap<_, _> = initial_data
.iter()
@ -162,7 +164,7 @@ impl V03State {
})
.collect();
let faucet_account = system_faucet_account();
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
public_state.insert(faucet_account_id, faucet_account);
let mut commitment_set = CommitmentSet::with_capacity(32);
commitment_set.extend(&[DUMMY_COMMITMENT]);
@ -187,6 +189,7 @@ impl V03State {
this.insert_program(Program::amm());
this.insert_program(Program::ata());
this.insert_program(Program::vault());
this.insert_program(Program::faucet());
this
}
@ -381,6 +384,11 @@ fn system_faucet_account() -> Account {
}
}
#[must_use]
pub fn system_faucet_account_id() -> AccountId {
faucet_core::compute_faucet_account_id(Program::faucet().id())
}
#[cfg(test)]
pub mod tests {
#![expect(
@ -404,7 +412,7 @@ pub mod tests {
};
use crate::{
PublicKey, PublicTransaction, SYSTEM_FAUCET_ACCOUNT_ID, V03State,
PublicKey, PublicTransaction, V03State,
error::{InvalidProgramBehaviorError, NssaError},
execute_and_prove,
privacy_preserving_transaction::{
@ -420,6 +428,7 @@ pub mod tests {
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_faucet_account,
},
system_faucet_account_id,
};
impl V03State {
@ -612,7 +621,7 @@ pub mod tests {
..Account::default()
},
);
this.insert(SYSTEM_FAUCET_ACCOUNT_ID, system_faucet_account());
this.insert(system_faucet_account_id(), system_faucet_account());
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
this.insert(
account_id,
@ -636,6 +645,7 @@ pub mod tests {
this.insert(Program::amm().id(), Program::amm());
this.insert(Program::ata().id(), Program::ata());
this.insert(Program::vault().id(), Program::vault());
this.insert(Program::faucet().id(), Program::faucet());
this
};

View File

@ -17,6 +17,7 @@ amm_core.workspace = true
amm_program.workspace = true
ata_core.workspace = true
ata_program.workspace = true
faucet_core.workspace = true
vault_core.workspace = true
risc0-zkvm.workspace = true
serde = { workspace = true, default-features = false }

View File

@ -1,6 +1,5 @@
use authenticated_transfer_core::Instruction;
use nssa_core::{
SYSTEM_FAUCET_ACCOUNT_ID,
account::{Account, AccountWithMetadata},
program::{
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
@ -26,13 +25,8 @@ fn transfer(
recipient: AccountWithMetadata,
balance_to_move: u128,
) -> Vec<AccountPostState> {
// Continue only if the sender has authorized this operation
// or it's the system faucet account which is allowed without authorization as it may be used
// only by sequencer.
assert!(
sender.is_authorized || sender.account_id == SYSTEM_FAUCET_ACCOUNT_ID,
"Sender must be authorized"
);
// Continue only if the sender has authorized this operation.
assert!(sender.is_authorized, "Sender must be authorized");
// Create accounts post states, with updated balances
let sender_post = {

View File

@ -0,0 +1,71 @@
use faucet_core::Instruction;
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs,
};
fn unchanged_post_states(
pre_states: &[nssa_core::account::AccountWithMetadata],
) -> Vec<AccountPostState> {
pre_states
.iter()
.map(|pre_state| AccountPostState::new(pre_state.account.clone()))
.collect()
}
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let pre_states_clone = pre_states.clone();
let post_states = unchanged_post_states(&pre_states_clone);
let chained_calls = match instruction {
Instruction::Transfer {
vault_program_id,
recipient_id,
amount,
} => {
let [faucet, recipient_vault] = pre_states
.try_into()
.expect("Transfer requires exactly 2 accounts");
assert_eq!(
faucet.account_id,
faucet_core::compute_faucet_account_id(self_program_id),
"First account must be faucet PDA"
);
let mut faucet_for_vault = faucet;
faucet_for_vault.is_authorized = true;
vec![
ChainedCall::new(
vault_program_id,
vec![faucet_for_vault, recipient_vault],
&vault_core::Instruction::Transfer {
recipient_id,
amount,
},
)
.with_pda_seeds(vec![faucet_core::compute_faucet_seed()]),
]
}
};
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -0,0 +1,12 @@
[package]
name = "faucet_core"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[lints]
workspace = true
[dependencies]
nssa_core.workspace = true
serde = { workspace = true, default-features = false }

View File

@ -0,0 +1,29 @@
pub use nssa_core::program::PdaSeed;
use nssa_core::{account::AccountId, program::ProgramId};
use serde::{Deserialize, Serialize};
const FAUCET_SEED_DOMAIN_SEPARATOR: [u8; 32] = *b"/LEZ/v0.3/FaucetSeed/0000000000/";
#[derive(Serialize, Deserialize)]
pub enum Instruction {
/// Transfers native tokens from system faucet to recipient's vault.
///
/// Required accounts (2):
/// - Faucet PDA account
/// - Recipient vault PDA account
Transfer {
vault_program_id: ProgramId,
recipient_id: AccountId,
amount: u128,
},
}
#[must_use]
pub const fn compute_faucet_seed() -> PdaSeed {
PdaSeed::new(FAUCET_SEED_DOMAIN_SEPARATOR)
}
#[must_use]
pub fn compute_faucet_account_id(faucet_program_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&faucet_program_id, &compute_faucet_seed())
}

View File

@ -15,6 +15,7 @@ storage.workspace = true
mempool.workspace = true
logos-blockchain-zone-sdk.workspace = true
testnet_initial_state.workspace = true
faucet_core.workspace = true
vault_core.workspace = true
thiserror.workspace = true

View File

@ -383,14 +383,16 @@ fn build_supply_account_genesis_transaction(
account_id: &AccountId,
balance: u128,
) -> PublicTransaction {
let faucet_program_id = Program::faucet().id();
let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, *account_id);
let message = Message::try_new(
vault_program_id,
vec![nssa::SYSTEM_FAUCET_ACCOUNT_ID, recipient_vault_id],
faucet_program_id,
vec![nssa::system_faucet_account_id(), recipient_vault_id],
vec![],
vault_core::Instruction::Transfer {
faucet_core::Instruction::Transfer {
vault_program_id,
recipient_id: *account_id,
amount: balance,
},