refactor: use faucet program to manage faucet account

This commit is contained in:
Daniil Polyakov
2026-05-15 01:34:07 +03:00
parent e359c1abe2
commit ee5a98fc48
47 changed files with 248 additions and 62 deletions
Generated
+12
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",
+2
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10 -31
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)
}
+1
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"] }
@@ -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(())
}
+1
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
-2
View File
@@ -26,8 +26,6 @@ pub mod program;
pub mod error;
pub const GENESIS_BLOCK_ID: BlockId = 1;
pub const SYSTEM_FAUCET_ACCOUNT_ID: account::AccountId =
account::AccountId::new(*b"/LEZ/SystemFaucetAccount/0000000");
pub type BlockId = u64;
/// Unix timestamp in milliseconds.
+2 -2
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;
+17 -4
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),
+15 -5
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
};
+1
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 }
@@ -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 = {
+71
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();
}
+12
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 }
+29
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())
}
+1
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
anyhow.workspace = true
+5 -3
View File
@@ -380,14 +380,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,
},