fix: resolve merge conflicts with main

This commit is contained in:
Moudy 2026-05-05 12:37:54 +02:00
commit 9e207450d6
53 changed files with 1416 additions and 1336 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -85,9 +85,20 @@ impl HashableBlockData {
signing_key: &nssa::PrivateKey,
bedrock_parent_id: MantleMsgId,
) -> Block {
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Block/\x00\x00\x00\x00\x00\x00\x00\x00";
let data_bytes = borsh::to_vec(&self).unwrap();
let signature = nssa::Signature::new(signing_key, &data_bytes);
let hash = OwnHasher::hash(&data_bytes);
let mut bytes = Vec::with_capacity(
PREFIX
.len()
.checked_add(data_bytes.len())
.expect("length overflow"),
);
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(&data_bytes);
let hash = OwnHasher::hash(&bytes);
let signature = nssa::Signature::new(signing_key, &hash.0);
Block {
header: BlockHeader {
block_id: self.block_id,
@ -103,11 +114,6 @@ impl HashableBlockData {
bedrock_parent_id,
}
}
#[must_use]
pub fn block_hash(&self) -> BlockHash {
OwnHasher::hash(&borsh::to_vec(&self).unwrap())
}
}
impl From<Block> for HashableBlockData {

View File

@ -6,7 +6,7 @@
clippy::integer_division_remainder_used,
reason = "Mock service uses intentional casts and format patterns for test data generation"
)]
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc, time::Duration};
use indexer_service_protocol::{
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
@ -19,15 +19,73 @@ use jsonrpsee::{
core::{SubscriptionResult, async_trait},
types::ErrorObjectOwned,
};
use tokio::sync::{RwLock, broadcast};
/// A mock implementation of the `IndexerService` RPC for testing purposes.
pub struct MockIndexerService {
const MOCK_GENESIS_TIMESTAMP_MS: u64 = 1_704_067_200_000;
const MOCK_BLOCK_INTERVAL_MS: u64 = 30_000;
struct MockState {
blocks: Vec<Block>,
accounts: HashMap<AccountId, Account>,
account_ids: Vec<AccountId>,
transactions: HashMap<HashType, (Transaction, BlockId)>,
}
/// A mock implementation of the `IndexerService` RPC for testing purposes.
pub struct MockIndexerService {
state: Arc<RwLock<MockState>>,
finalized_blocks_tx: broadcast::Sender<Block>,
}
impl MockIndexerService {
fn spawn_block_generation_task(
state: Arc<RwLock<MockState>>,
finalized_blocks_tx: broadcast::Sender<Block>,
) {
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(30)).await;
let new_block = {
let mut state = state.write().await;
let next_block_id = state
.blocks
.last()
.map_or(1, |block| block.header.block_id.saturating_add(1));
let prev_hash = state
.blocks
.last()
.map_or(HashType([0_u8; 32]), |block| block.header.hash);
let timestamp = state.blocks.last().map_or(
MOCK_GENESIS_TIMESTAMP_MS + MOCK_BLOCK_INTERVAL_MS,
|block| {
block
.header
.timestamp
.saturating_add(MOCK_BLOCK_INTERVAL_MS)
},
);
let block = build_mock_block(
next_block_id,
prev_hash,
timestamp,
&state.account_ids,
BedrockStatus::Finalized,
);
index_block_transactions(&mut state.transactions, &block);
state.blocks.push(block.clone());
block
};
let _res = finalized_blocks_tx.send(new_block);
}
});
}
#[must_use]
pub fn new_with_mock_blocks() -> Self {
let mut blocks = Vec::new();
@ -59,119 +117,38 @@ impl MockIndexerService {
let mut prev_hash = HashType([0_u8; 32]);
for block_id in 1..=100 {
let block_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = 0xff;
HashType(hash)
};
// Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and
// ProgramDeployment)
let num_txs = 2 + (block_id % 3);
let mut block_transactions = Vec::new();
for tx_idx in 0..num_txs {
let tx_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = tx_idx as u8;
HashType(hash)
};
// Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment
let tx = match (block_id + tx_idx) % 5 {
// Public transactions (most common)
0 | 1 => Transaction::Public(PublicTransaction {
hash: tx_hash,
message: PublicMessage {
program_id: ProgramId([1_u32; 8]),
account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
account_ids[(tx_idx as usize + 1) % account_ids.len()],
],
nonces: vec![block_id as u128, (block_id + 1) as u128],
instruction_data: vec![1, 2, 3, 4],
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: None,
},
}),
// PrivacyPreserving transactions
2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
hash: tx_hash,
message: PrivacyPreservingMessage {
public_account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
}],
encrypted_private_post_states: vec![EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![
0x01, 0x02, 0x03, 0x04,
]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
}],
new_commitments: vec![Commitment([block_id as u8; 32])],
new_nullifiers: vec![(
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: Some(indexer_service_protocol::Proof(vec![0; 32])),
},
}),
// ProgramDeployment transactions (rare)
_ => Transaction::ProgramDeployment(ProgramDeploymentTransaction {
hash: tx_hash,
message: ProgramDeploymentMessage {
bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic number */
},
}),
};
transactions.insert(tx_hash, (tx.clone(), block_id));
block_transactions.push(tx);
}
let block = Block {
header: BlockHeader {
block_id,
prev_block_hash: prev_hash,
hash: block_hash,
timestamp: 1_704_067_200_000 + (block_id * 12_000), // ~12 seconds per block
signature: Signature([0_u8; 64]),
},
body: BlockBody {
transactions: block_transactions,
},
bedrock_status: match block_id {
let block = build_mock_block(
block_id,
prev_hash,
MOCK_GENESIS_TIMESTAMP_MS + (block_id * MOCK_BLOCK_INTERVAL_MS),
&account_ids,
match block_id {
0..=5 => BedrockStatus::Finalized,
6..=8 => BedrockStatus::Safe,
_ => BedrockStatus::Pending,
},
bedrock_parent_id: MantleMsgId([0; 32]),
};
);
prev_hash = block_hash;
index_block_transactions(&mut transactions, &block);
prev_hash = block.header.hash;
blocks.push(block);
}
Self {
let state = Arc::new(RwLock::new(MockState {
blocks,
accounts,
account_ids,
transactions,
}));
let (finalized_blocks_tx, _) = broadcast::channel(32);
Self::spawn_block_generation_task(Arc::clone(&state), finalized_blocks_tx.clone());
Self {
state,
finalized_blocks_tx,
}
}
}
@ -183,21 +160,45 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
subscription_sink: jsonrpsee::PendingSubscriptionSink,
) -> SubscriptionResult {
let sink = subscription_sink.accept().await?;
for block in self
.blocks
.iter()
.filter(|b| b.bedrock_status == BedrockStatus::Finalized)
{
let initial_finalized_blocks: Vec<Block> = {
let state = self.state.read().await;
state
.blocks
.iter()
.filter(|b| b.bedrock_status == BedrockStatus::Finalized)
.cloned()
.collect()
};
for block in &initial_finalized_blocks {
let json = serde_json::value::to_raw_value(block).unwrap();
sink.send(json).await?;
}
let mut receiver = self.finalized_blocks_tx.subscribe();
loop {
match receiver.recv().await {
Ok(block) => {
let json = serde_json::value::to_raw_value(&block).unwrap();
sink.send(json).await?;
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => break,
}
}
Ok(())
}
async fn get_last_finalized_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
self.blocks
.last()
.map(|bl| bl.header.block_id)
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::<()>)
})
@ -205,6 +206,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
async fn get_block_by_id(&self, block_id: BlockId) -> Result<Option<Block>, ErrorObjectOwned> {
Ok(self
.state
.read()
.await
.blocks
.iter()
.find(|b| b.header.block_id == block_id)
@ -216,6 +220,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
block_hash: HashType,
) -> Result<Option<Block>, ErrorObjectOwned> {
Ok(self
.state
.read()
.await
.blocks
.iter()
.find(|b| b.header.hash == block_hash)
@ -223,7 +230,10 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
}
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> {
self.accounts
self.state
.read()
.await
.accounts
.get(&account_id)
.cloned()
.ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>))
@ -233,7 +243,13 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
&self,
tx_hash: HashType,
) -> Result<Option<Transaction>, ErrorObjectOwned> {
Ok(self.transactions.get(&tx_hash).map(|(tx, _)| tx.clone()))
Ok(self
.state
.read()
.await
.transactions
.get(&tx_hash)
.map(|(tx, _)| tx.clone()))
}
async fn get_blocks(
@ -241,15 +257,17 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
before: Option<BlockId>,
limit: u64,
) -> Result<Vec<Block>, ErrorObjectOwned> {
let state = self.state.read().await;
let start_id = before.map_or_else(
|| self.blocks.len(),
|| state.blocks.len(),
|id| usize::try_from(id.saturating_sub(1)).expect("u64 should fit in usize"),
);
let result = (1..=start_id)
.rev()
.take(limit as usize)
.map_while(|block_id| self.blocks.get(block_id - 1).cloned())
.map_while(|block_id| state.blocks.get(block_id - 1).cloned())
.collect();
Ok(result)
@ -261,20 +279,24 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
offset: u64,
limit: u64,
) -> Result<Vec<Transaction>, ErrorObjectOwned> {
let mut account_txs: Vec<_> = self
.transactions
.values()
.filter(|(tx, _)| match tx {
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
Transaction::PrivacyPreserving(priv_tx) => {
priv_tx.message.public_account_ids.contains(&account_id)
}
Transaction::ProgramDeployment(_) => false,
})
.collect();
let mut account_txs: Vec<(Transaction, BlockId)> = {
let state = self.state.read().await;
state
.transactions
.values()
.filter(|(tx, _)| match tx {
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
Transaction::PrivacyPreserving(priv_tx) => {
priv_tx.message.public_account_ids.contains(&account_id)
}
Transaction::ProgramDeployment(_) => false,
})
.cloned()
.collect()
};
// Sort by block ID descending (most recent first)
account_txs.sort_by_key(|b| std::cmp::Reverse(b.1));
account_txs.sort_by_key(|(_, block_id)| std::cmp::Reverse(*block_id));
let start = offset as usize;
if start >= account_txs.len() {
@ -293,3 +315,123 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
Ok(())
}
}
fn build_mock_block(
block_id: BlockId,
prev_hash: HashType,
timestamp: u64,
account_ids: &[AccountId],
bedrock_status: BedrockStatus,
) -> Block {
let block_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = 0xff;
HashType(hash)
};
// Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment)
let num_txs = 2 + (block_id % 3);
let mut block_transactions = Vec::new();
for tx_idx in 0..num_txs {
let tx_hash = {
let mut hash = [0_u8; 32];
hash[0] = block_id as u8;
hash[1] = tx_idx as u8;
HashType(hash)
};
// Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment
let tx = match (block_id + tx_idx) % 5 {
// Public transactions (most common)
0 | 1 => Transaction::Public(PublicTransaction {
hash: tx_hash,
message: PublicMessage {
program_id: ProgramId([1_u32; 8]),
account_ids: vec![
account_ids[tx_idx as usize % account_ids.len()],
account_ids[(tx_idx as usize + 1) % account_ids.len()],
],
nonces: vec![block_id as u128, (block_id + 1) as u128],
instruction_data: vec![1, 2, 3, 4],
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: None,
},
}),
// PrivacyPreserving transactions
2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
hash: tx_hash,
message: PrivacyPreservingMessage {
public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]],
nonces: vec![block_id as u128],
public_post_states: vec![Account {
program_owner: ProgramId([1_u32; 8]),
balance: 500,
data: Data(vec![0xdd, 0xee]),
nonce: block_id as u128,
}],
encrypted_private_post_states: vec![EncryptedAccountData {
ciphertext: indexer_service_protocol::Ciphertext(vec![
0x01, 0x02, 0x03, 0x04,
]),
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
view_tag: 42,
}],
new_commitments: vec![Commitment([block_id as u8; 32])],
new_nullifiers: vec![(
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],
proof: Some(indexer_service_protocol::Proof(vec![0; 32])),
},
}),
// ProgramDeployment transactions (rare)
_ => Transaction::ProgramDeployment(ProgramDeploymentTransaction {
hash: tx_hash,
message: ProgramDeploymentMessage {
bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic
* number */
},
}),
};
block_transactions.push(tx);
}
Block {
header: BlockHeader {
block_id,
prev_block_hash: prev_hash,
hash: block_hash,
timestamp,
signature: Signature([0_u8; 64]),
},
body: BlockBody {
transactions: block_transactions,
},
bedrock_status,
bedrock_parent_id: MantleMsgId([0; 32]),
}
}
fn index_block_transactions(
transactions: &mut HashMap<HashType, (Transaction, BlockId)>,
block: &Block,
) {
for tx in &block.body.transactions {
let tx_hash = match tx {
Transaction::Public(public_tx) => public_tx.hash,
Transaction::PrivacyPreserving(private_tx) => private_tx.hash,
Transaction::ProgramDeployment(deployment_tx) => deployment_tx.hash,
};
transactions.insert(tx_hash, (tx.clone(), block.header.block_id));
}
}

View File

@ -27,7 +27,7 @@ use nssa::{
public_transaction as putx,
};
use nssa_core::{
MembershipProof, NullifierPublicKey,
InputAccountIdentity, MembershipProof, NullifierPublicKey,
account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
};
@ -251,10 +251,19 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
let (output, proof) = circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)],
vec![sender_nsk],
vec![Some(proof)],
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: sender_ss,
nsk: sender_nsk,
membership_proof: proof,
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
npk: recipient_npk,
ssk: recipient_ss,
identifier: 0,
},
],
&program.into(),
)
.unwrap();

View File

@ -12,23 +12,92 @@ use crate::{
pub struct PrivacyPreservingCircuitInput {
/// Outputs of the program execution.
pub program_outputs: Vec<ProgramOutput>,
/// Visibility mask for accounts.
///
/// - `0` - public account
/// - `1` - private account with authentication
/// - `2` - private account without authentication
/// - `3` - private PDA account
pub visibility_mask: Vec<u8>,
/// Public keys and identifiers of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
/// Nullifier secret keys for authorized private accounts.
pub private_account_nsks: Vec<NullifierSecretKey>,
/// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts.
pub private_account_membership_proofs: Vec<Option<MembershipProof>>,
/// One entry per `pre_state`, in the same order as the program's `pre_states`.
/// Length must equal the number of `pre_states` derived from `program_outputs`.
/// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions`
/// rely on this position alignment.
pub account_identities: Vec<InputAccountIdentity>,
/// Program ID.
pub program_id: ProgramId,
}
/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields
/// the guest needs for that account's code path.
#[derive(Serialize, Deserialize, Clone)]
pub enum InputAccountIdentity {
/// Public account. The guest reads pre/post state from `program_outputs` and emits no
/// commitment, ciphertext, or nullifier.
Public,
/// Init of an authorized standalone private account: no membership proof. The `pre_state`
/// must be `Account::default()`. The `account_id` is derived as
/// `AccountId::from((&NullifierPublicKey::from(nsk), identifier))` and matched against
/// `pre_state.account_id`.
PrivateAuthorizedInit {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
identifier: Identifier,
},
/// Update of an authorized standalone private account: existing on-chain commitment, with
/// membership proof.
PrivateAuthorizedUpdate {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
membership_proof: MembershipProof,
identifier: Identifier,
},
/// Init of a standalone private account the caller does not own (e.g. a recipient who
/// doesn't yet exist on chain). No `nsk`, no membership proof.
PrivateUnauthorized {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
},
/// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream
/// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. Identifier is fixed by
/// convention to `PRIVATE_PDA_FIXED_IDENTIFIER` and not carried per-input.
PrivatePdaInit {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
},
/// Update of an existing private PDA, authorized, with membership proof. `npk` is derived
/// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a
/// previously-seen authorization in a chained call. Identifier is fixed.
PrivatePdaUpdate {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
membership_proof: MembershipProof,
},
}
impl InputAccountIdentity {
#[must_use]
pub const fn is_public(&self) -> bool {
matches!(self, Self::Public)
}
#[must_use]
pub const fn is_private_pda(&self) -> bool {
matches!(
self,
Self::PrivatePdaInit { .. } | Self::PrivatePdaUpdate { .. }
)
}
/// For private PDA variants, return the nullifier public key. `Init` carries it directly;
/// `Update` derives it from `nsk`. For non-PDA variants returns `None`.
#[must_use]
pub fn npk_if_private_pda(&self) -> Option<NullifierPublicKey> {
match self {
Self::PrivatePdaInit { npk, .. } => Some(*npk),
Self::PrivatePdaUpdate { nsk, .. } => Some(NullifierPublicKey::from(nsk)),
Self::Public
| Self::PrivateAuthorizedInit { .. }
| Self::PrivateAuthorizedUpdate { .. }
| Self::PrivateUnauthorized { .. } => None,
}
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct PrivacyPreservingCircuitOutput {

View File

@ -3,7 +3,9 @@
reason = "We prefer to group methods by functionality rather than by type for encoding"
)]
pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput};
pub use circuit_io::{
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
};
pub use commitment::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
compute_digest_for_path,

View File

@ -2,8 +2,7 @@ use std::collections::{HashMap, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
account::AccountWithMetadata,
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
};
@ -63,14 +62,10 @@ impl From<Program> for ProgramWithDependencies {
/// Generates a proof of the execution of a NSSA program inside the privacy preserving execution
/// circuit.
/// TODO: too many parameters.
pub fn execute_and_prove(
pre_states: Vec<AccountWithMetadata>,
instruction_data: InstructionData,
visibility_mask: Vec<u8>,
private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
private_account_nsks: Vec<NullifierSecretKey>,
private_account_membership_proofs: Vec<Option<MembershipProof>>,
account_identities: Vec<InputAccountIdentity>,
program_with_dependencies: &ProgramWithDependencies,
) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> {
let ProgramWithDependencies {
@ -128,10 +123,7 @@ pub fn execute_and_prove(
let circuit_input = PrivacyPreservingCircuitInput {
program_outputs,
visibility_mask,
private_account_keys,
private_account_nsks,
private_account_membership_proofs,
account_identities,
program_id: program_with_dependencies.program.id(),
};
@ -241,10 +233,14 @@ mod tests {
let (output, proof) = execute_and_prove(
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![(recipient_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
npk: recipient_keys.npk(),
ssk: shared_secret,
identifier: 0,
},
],
&Program::authenticated_transfer_program().into(),
)
.unwrap();
@ -334,13 +330,21 @@ mod tests {
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![
(sender_keys.npk(), 0, shared_secret_1),
(recipient_keys.npk(), 0, shared_secret_2),
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: shared_secret_1,
nsk: sender_keys.nsk,
membership_proof: commitment_set
.get_proof_for(&commitment_sender)
.expect("sender's commitment must be in the set"),
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
npk: recipient_keys.npk(),
ssk: shared_secret_2,
identifier: 0,
},
],
vec![sender_keys.nsk],
vec![commitment_set.get_proof_for(&commitment_sender), None],
&program.into(),
)
.unwrap();
@ -403,10 +407,11 @@ mod tests {
let result = execute_and_prove(
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
vec![InputAccountIdentity::PrivateUnauthorized {
npk: account_keys.npk(),
ssk: shared_secret,
identifier: 0,
}],
&program_with_deps,
);
@ -452,10 +457,13 @@ mod tests {
let result = execute_and_prove(
vec![pda_pre, sender_pre],
instruction,
vec![3, 0],
vec![(npk, u128::MAX, shared_secret_pda)],
vec![],
vec![None],
vec![
InputAccountIdentity::PrivatePdaInit {
npk,
ssk: shared_secret_pda,
},
InputAccountIdentity::Public,
],
&program_with_deps,
);
@ -498,10 +506,13 @@ mod tests {
let result = execute_and_prove(
vec![pda_pre, bob_pre],
instruction,
vec![3, 0],
vec![(npk, u128::MAX, shared_secret_pda)],
vec![],
vec![None],
vec![
InputAccountIdentity::PrivatePdaInit {
npk,
ssk: shared_secret_pda,
},
InputAccountIdentity::Public,
],
&program_with_deps,
);

View File

@ -9,6 +9,8 @@ use sha2::{Digest as _, Sha256};
use crate::{AccountId, error::NssaError};
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
pub type ViewTag = u8;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
@ -118,22 +120,34 @@ impl Message {
timestamp_validity_window: output.timestamp_validity_window,
})
}
#[must_use]
pub fn hash(&self) -> [u8; 32] {
let msg = self.to_bytes();
let mut bytes = Vec::with_capacity(
PREFIX
.len()
.checked_add(msg.len())
.expect("length overflow"),
);
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(&msg);
Sha256::digest(bytes).into()
}
}
#[cfg(test)]
pub mod tests {
use nssa_core::{
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
account::Account,
account::{Account, AccountId, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
use crate::{
AccountId,
privacy_preserving_transaction::message::{EncryptedAccountData, Message},
};
use super::{EncryptedAccountData, Message, PREFIX};
#[must_use]
pub fn message_for_tests() -> Message {
@ -176,6 +190,58 @@ pub mod tests {
}
}
#[test]
fn hash_privacy_pinned() {
let msg = Message {
public_account_ids: vec![AccountId::new([42_u8; 32])],
nonces: vec![Nonce(5)],
public_post_states: vec![],
encrypted_private_post_states: vec![],
new_commitments: vec![],
new_nullifiers: vec![],
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
};
let public_account_ids_bytes: &[u8] = &[42_u8; 32];
let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// all remaining vec fields are empty: u32 len=0
let empty_vec_bytes: &[u8] = &[0_u8; 4];
// validity windows: unbounded = {from: None (0u8), to: None (0u8)}
let unbounded_window_bytes: &[u8] = &[0_u8; 2];
let expected_borsh_vec: Vec<u8> = [
&[1_u8, 0, 0, 0], // public_account_ids
public_account_ids_bytes,
nonces_bytes,
empty_vec_bytes, // public_post_state
empty_vec_bytes, // encrypted_private_post_states
empty_vec_bytes, // new_commitments
empty_vec_bytes, // new_nullifiers
unbounded_window_bytes, // block_validity_window
unbounded_window_bytes, // timestamp_validity_window
]
.concat();
let expected_borsh: &[u8] = &expected_borsh_vec;
assert_eq!(
borsh::to_vec(&msg).unwrap(),
expected_borsh,
"`privacy_preserving_transaction::hash()`: expected borsh order has changed"
);
let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len());
preimage.extend_from_slice(PREFIX);
preimage.extend_from_slice(expected_borsh);
let expected_hash: [u8; 32] = Sha256::digest(&preimage).into();
assert_eq!(
msg.hash(),
expected_hash,
"`privacy_preserving_transaction::hash()`: serialization has changed"
);
}
#[test]
fn encrypted_account_data_constructor() {
let npk = NullifierPublicKey::from(&[1; 32]);

View File

@ -14,12 +14,12 @@ pub struct WitnessSet {
impl WitnessSet {
#[must_use]
pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self {
let message_bytes = message.to_bytes();
let message_hash = message.hash();
let signatures_and_public_keys = private_keys
.iter()
.map(|&key| {
(
Signature::new(key, &message_bytes),
Signature::new(key, &message_hash),
PublicKey::new_from_private_key(key),
)
})
@ -32,9 +32,9 @@ impl WitnessSet {
#[must_use]
pub fn signatures_are_valid_for(&self, message: &Message) -> bool {
let message_bytes = message.to_bytes();
let message_hash = message.hash();
for (signature, public_key) in self.signatures_and_public_keys() {
if !signature.is_valid_for(&message_bytes, public_key) {
if !signature.is_valid_for(&message_hash, public_key) {
return false;
}
}

View File

@ -4,9 +4,12 @@ use nssa_core::{
program::{InstructionData, ProgramId},
};
use serde::Serialize;
use sha2::{Digest as _, Sha256};
use crate::{AccountId, error::NssaError, program::Program};
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Public/\x00\x00\x00\x00\x00\x00\x00";
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Message {
pub program_id: ProgramId,
@ -63,4 +66,74 @@ impl Message {
instruction_data,
}
}
#[must_use]
pub fn hash(&self) -> [u8; 32] {
let mut bytes = Vec::with_capacity(
PREFIX
.len()
.checked_add(self.to_bytes().len())
.expect("length overflow"),
);
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(&self.to_bytes());
Sha256::digest(bytes).into()
}
}
#[cfg(test)]
mod tests {
use nssa_core::account::{AccountId, Nonce};
use sha2::{Digest as _, Sha256};
use super::{Message, PREFIX};
#[test]
fn hash_public_pinned() {
let msg = Message::new_preserialized(
[1_u32; 8],
vec![AccountId::new([42_u8; 32])],
vec![Nonce(5)],
vec![],
);
// program_id: [1_u32; 8], each word as LE u32
let program_id_bytes: &[u8] = &[
1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1,
0, 0, 0,
];
// account_ids: AccountId([42_u8; 32])
let account_ids_bytes: &[u8] = &[42_u8; 32];
// nonces: u32 len=1, then Nonce(5) as LE u128
let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let instruction_data_bytes: &[u8] = &[0_u8; 4];
let expected_borsh_vec: Vec<u8> = [
program_id_bytes,
&[1_u8, 0, 0, 0], // account_ids len=1
account_ids_bytes,
nonces_bytes,
instruction_data_bytes,
]
.concat();
let expected_borsh: &[u8] = &expected_borsh_vec;
assert_eq!(
borsh::to_vec(&msg).unwrap(),
expected_borsh,
"`public_transaction::hash()`: expected borsh order has changed"
);
let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len());
preimage.extend_from_slice(PREFIX);
preimage.extend_from_slice(expected_borsh);
let expected_hash: [u8; 32] = Sha256::digest(&preimage).into();
assert_eq!(
msg.hash(),
expected_hash,
"`public_transaction::hash()`: serialization has changed"
);
}
}

View File

@ -10,12 +10,12 @@ pub struct WitnessSet {
impl WitnessSet {
#[must_use]
pub fn for_message(message: &Message, private_keys: &[&PrivateKey]) -> Self {
let message_bytes = message.to_bytes();
let message_hash = message.hash();
let signatures_and_public_keys = private_keys
.iter()
.map(|&key| {
(
Signature::new(key, &message_bytes),
Signature::new(key, &message_hash),
PublicKey::new_from_private_key(key),
)
})
@ -27,9 +27,9 @@ impl WitnessSet {
#[must_use]
pub fn is_valid_for(&self, message: &Message) -> bool {
let message_bytes = message.to_bytes();
let message_hash = message.hash();
for (signature, public_key) in self.signatures_and_public_keys() {
if !signature.is_valid_for(&message_bytes, public_key) {
if !signature.is_valid_for(&message_hash, public_key) {
return false;
}
}
@ -75,7 +75,7 @@ mod tests {
assert_eq!(witness_set.signatures_and_public_keys.len(), 2);
let message_bytes = message.to_bytes();
let message_bytes = message.hash();
for ((signature, public_key), expected_public_key) in witness_set
.signatures_and_public_keys
.into_iter()

View File

@ -4,7 +4,7 @@ pub struct TestVector {
pub seckey: Option<PrivateKey>,
pub pubkey: PublicKey,
pub aux_rand: Option<[u8; 32]>,
pub message: Option<Vec<u8>>,
pub message: [u8; 32],
pub signature: Signature,
pub verification_result: bool,
}
@ -15,18 +15,21 @@ pub struct TestVector {
pub fn test_vectors() -> Vec<TestVector> {
vec![
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0000000000000000000000000000000000000000000000000000000000000003",
)).unwrap()),
seckey: Some(
PrivateKey::try_new(hex_to_bytes(
"0000000000000000000000000000000000000000000000000000000000000003",
))
.unwrap(),
),
pubkey: PublicKey::try_new(hex_to_bytes(
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
)).unwrap(),
))
.unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
)),
message: Some(
hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
.unwrap(),
message: hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
),
signature: Signature {
value: hex_to_bytes(
@ -36,18 +39,21 @@ pub fn test_vectors() -> Vec<TestVector> {
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF",
)).unwrap()),
seckey: Some(
PrivateKey::try_new(hex_to_bytes(
"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF",
))
.unwrap(),
),
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000001",
)),
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -57,18 +63,21 @@ pub fn test_vectors() -> Vec<TestVector> {
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9",
)).unwrap()),
seckey: Some(
PrivateKey::try_new(hex_to_bytes(
"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9",
))
.unwrap(),
),
pubkey: PublicKey::try_new(hex_to_bytes(
"DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
)).unwrap(),
))
.unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906",
)),
message: Some(
hex::decode("7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C")
.unwrap(),
message: hex_to_bytes::<32>(
"7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
),
signature: Signature {
value: hex_to_bytes(
@ -78,18 +87,21 @@ pub fn test_vectors() -> Vec<TestVector> {
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710",
)).unwrap()),
seckey: Some(
PrivateKey::try_new(hex_to_bytes(
"0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710",
))
.unwrap(),
),
pubkey: PublicKey::try_new(hex_to_bytes(
"25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517",
)).unwrap(),
))
.unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
)),
message: Some(
hex::decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
.unwrap(),
message: hex_to_bytes::<32>(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
),
signature: Signature {
value: hex_to_bytes(
@ -102,11 +114,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703")
.unwrap(),
message: hex_to_bytes::<32>(
"4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703",
),
signature: Signature {
value: hex_to_bytes(
@ -122,13 +134,15 @@ pub fn test_vectors() -> Vec<TestVector> {
// "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34",
// )).unwrap(),
// aux_rand: None,
// message: Some(
// hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").unwrap(),
// ),
// message:
//
// hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").
// unwrap(), ),
// signature: Signature {
// value: hex_to_bytes(
// "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B",
// ),
//
// "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
// , ),
// },
// verification_result: false,
// },
@ -136,11 +150,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -153,11 +167,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -170,11 +184,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -187,11 +201,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -204,11 +218,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -221,11 +235,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -238,11 +252,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -255,11 +269,11 @@ pub fn test_vectors() -> Vec<TestVector> {
seckey: None,
pubkey: PublicKey::try_new(hex_to_bytes(
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
)).unwrap(),
))
.unwrap(),
aux_rand: None,
message: Some(
hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89")
.unwrap(),
message: hex_to_bytes::<32>(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
),
signature: Signature {
value: hex_to_bytes(
@ -275,90 +289,96 @@ pub fn test_vectors() -> Vec<TestVector> {
// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30",
// )).unwrap(),
// aux_rand: None,
// message: Some(
// hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").unwrap(),
// ),
// message:
//
// hex::decode("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89").
// unwrap(), ),
// signature: Signature {
// value: hex_to_bytes(
// "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B",
// ),
//
// "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
// , ),
// },
// verification_result: false,
// },
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0340034003400340034003400340034003400340034003400340034003400340",
)).unwrap()),
pubkey: PublicKey::try_new(hex_to_bytes(
"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
)).unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
)),
message: None,
signature: Signature {
value: hex_to_bytes(
"71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63",
),
},
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0340034003400340034003400340034003400340034003400340034003400340",
)).unwrap()),
pubkey: PublicKey::try_new(hex_to_bytes(
"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
)).unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
)),
message: Some(hex::decode("11").unwrap()),
signature: Signature {
value: hex_to_bytes(
"08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF",
),
},
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0340034003400340034003400340034003400340034003400340034003400340",
)).unwrap()),
pubkey: PublicKey::try_new(hex_to_bytes(
"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
)).unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
)),
message: Some(hex::decode("0102030405060708090A0B0C0D0E0F1011").unwrap()),
signature: Signature {
value: hex_to_bytes(
"5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5",
),
},
verification_result: true,
},
TestVector {
seckey: Some(PrivateKey::try_new(hex_to_bytes(
"0340034003400340034003400340034003400340034003400340034003400340",
)).unwrap()),
pubkey: PublicKey::try_new(hex_to_bytes(
"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
)).unwrap(),
aux_rand: Some(hex_to_bytes::<32>(
"0000000000000000000000000000000000000000000000000000000000000000",
)),
message: Some(
hex::decode("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999").unwrap(),
),
signature: Signature {
value: hex_to_bytes(
"403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367",
),
},
verification_result: true,
},
// Test with invalid message length (0); valid test for BIP-340 post 2022.
// TestVector {
// seckey: PrivateKey::try_new(hex_to_bytes(
// "0340034003400340034003400340034003400340034003400340034003400340",
// )).unwrap()),
// pubkey: PublicKey::try_new(hex_to_bytes(
// "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
// )).unwrap(),
// aux_rand: hex_to_bytes::<32>(
// "0000000000000000000000000000000000000000000000000000000000000000",
// )),
// message: None,
// signature: Signature {
// value: hex_to_bytes(
// "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63",
// ),
// },
// verification_result: true,
// },
// Test with invalid message length (1); valid test for BIP-340 post 2022.
// TestVector {
// seckey: PrivateKey::try_new(hex_to_bytes(
// "0340034003400340034003400340034003400340034003400340034003400340",
// )).unwrap()),
// pubkey: PublicKey::try_new(hex_to_bytes(
// "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
// )).unwrap(),
// aux_rand: hex_to_bytes::<32>(
// "0000000000000000000000000000000000000000000000000000000000000000",
// )),
// message: hex::decode("11").unwrap()),
// signature: Signature {
// value: hex_to_bytes(
// "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF",
// ),
// },
// verification_result: true,
// },
// Test with invalid message length (17); valid test for BIP-340 post 2022.
// TestVector {
// seckey: PrivateKey::try_new(hex_to_bytes(
// "0340034003400340034003400340034003400340034003400340034003400340",
// )).unwrap()),
// pubkey: PublicKey::try_new(hex_to_bytes(
// "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
// )).unwrap(),
// aux_rand: hex_to_bytes::<32>(
// "0000000000000000000000000000000000000000000000000000000000000000",
// )),
// message: hex::decode("0102030405060708090A0B0C0D0E0F1011").unwrap()),
// signature: Signature {
// value: hex_to_bytes(
// "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5",
// ),
// },
// erification_result: true,
// },
// Test with invalid message length (100); valid test for BIP-340 post 2022.
// TestVector {
// seckey: PrivateKey::try_new(hex_to_bytes(
// "0340034003400340034003400340034003400340034003400340034003400340",
// )).unwrap()),
// pubkey: PublicKey::try_new(hex_to_bytes(
// "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117",
// )).unwrap(),
// aux_rand: hex_to_bytes::<32>(
// "0000000000000000000000000000000000000000000000000000000000000000",
// )),
// message:
// hex::decode("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999").unwrap(),
// ),
// signature: Signature {
// value: hex_to_bytes(
// "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367",
// ),
// },
// verification_result: true,
// },
]
}

View File

@ -36,8 +36,10 @@ impl FromStr for Signature {
}
impl Signature {
/// This function expects the incoming message to be prehashed to be pre-2022 BIP-340/Keycard
/// compatible.
#[must_use]
pub fn new(key: &PrivateKey, message: &[u8]) -> Self {
pub fn new(key: &PrivateKey, message: &[u8; 32]) -> Self {
let mut aux_random = [0_u8; 32];
OsRng.fill_bytes(&mut aux_random);
Self::new_with_aux_random(key, message, aux_random)
@ -45,14 +47,14 @@ impl Signature {
pub(crate) fn new_with_aux_random(
key: &PrivateKey,
message: &[u8],
message: &[u8; 32],
aux_random: [u8; 32],
) -> Self {
let value = {
let signing_key = k256::schnorr::SigningKey::from_bytes(key.value())
.expect("Expect valid signing key");
signing_key
.sign_raw(message, &aux_random)
.sign_prehash_with_aux_rand(message, &aux_random)
.expect("Expect to produce a valid signature")
.to_bytes()
};
@ -61,7 +63,7 @@ impl Signature {
}
#[must_use]
pub fn is_valid_for(&self, bytes: &[u8], public_key: &PublicKey) -> bool {
pub fn is_valid_for(&self, bytes: &[u8; 32], public_key: &PublicKey) -> bool {
let Ok(pk) = k256::schnorr::VerifyingKey::from_bytes(public_key.value()) else {
return false;
};
@ -97,9 +99,8 @@ mod tests {
let Some(aux_random) = test_vector.aux_rand else {
continue;
};
let Some(message) = test_vector.message else {
continue;
};
let message = test_vector.message;
if !test_vector.verification_result {
continue;
}
@ -114,7 +115,7 @@ mod tests {
#[test]
fn signature_verification_from_bip340_test_vectors() {
for (i, test_vector) in bip340_test_vectors::test_vectors().into_iter().enumerate() {
let message = test_vector.message.unwrap_or(vec![]);
let message = test_vector.message;
let expected_result = test_vector.verification_result;
let result = test_vector

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ use std::{
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier,
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
@ -17,7 +17,7 @@ use nssa_core::{
};
use risc0_zkvm::{guest::env, serde::to_vec};
const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX;
const PRIVATE_PDA_FIXED_IDENTIFIER: Identifier = u128::MAX;
/// State of the involved accounts before and after program execution.
struct ExecutionState {
@ -25,16 +25,16 @@ struct ExecutionState {
post_states: HashMap<AccountId, Account>,
block_validity_window: BlockValidityWindow,
timestamp_validity_window: TimestampValidityWindow,
/// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)`
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound
/// to their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)`
/// check.
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
/// under the private derivation. Binding is an idempotent property, not an event: the same
/// position can legitimately be bound through both paths in the same tx (e.g. a program
/// claims a private PDA and then delegates it to a callee), and the set uses `contains`,
/// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this
/// set; otherwise the npk is unbound and the circuit rejects.
/// not `assert!(insert)`. After the main loop, every private-PDA position must appear in
/// this set; otherwise the npk is unbound and the circuit rejects.
private_pda_bound_positions: HashSet<usize>,
/// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one
/// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and
@ -45,39 +45,29 @@ struct ExecutionState {
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
/// `(program, seed)` → one account per tx.
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
/// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for
/// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking
/// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and
/// caller-seeds authorization paths.
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the npk that
/// variant supplies for that position. Populated once in `derive_from_outputs` by walking
/// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and
/// caller-seeds authorization paths to verify
/// `AccountId::for_private_pda(program_id, seed, npk) == pre_state.account_id`.
private_pda_npk_by_position: HashMap<usize, NullifierPublicKey>,
}
impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
account_identities: &[InputAccountIdentity],
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
// Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in
// pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The
// downstream `compute_circuit_output` also consumes the same iterator and its trailing
// assertions catch an over-supply of keys; under-supply surfaces here.
// Build position → npk map for private-PDA pre_states, indexed by position in
// `account_identities`. The vec is documented as 1:1 with the program's pre_state order,
// so position here matches `pre_state_position` used downstream in
// `validate_and_sync_states`.
let mut private_pda_npk_by_position: HashMap<usize, NullifierPublicKey> = HashMap::new();
{
let mut keys_iter = private_account_keys.iter();
for (pos, &mask) in visibility_mask.iter().enumerate() {
if matches!(mask, 1..=3) {
let (npk, _, _) = keys_iter.next().unwrap_or_else(|| {
panic!(
"private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})"
)
});
if mask == 3 {
private_pda_npk_by_position.insert(pos, *npk);
}
}
for (pos, account_identity) in account_identities.iter().enumerate() {
if let Some(npk) = account_identity.npk_if_private_pda() {
private_pda_npk_by_position.insert(pos, npk);
}
}
@ -194,7 +184,7 @@ impl ExecutionState {
}
execution_state.validate_and_sync_states(
visibility_mask,
account_identities,
chained_call.program_id,
caller_program_id,
&chained_call.pda_seeds,
@ -211,12 +201,12 @@ impl ExecutionState {
"Inner call without a chained call found",
);
// Every mask-3 pre_state must have had its npk bound to its account_id, either via a
// `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching
// the private derivation. An unbound mask-3 pre_state has no cryptographic link between
// the supplied npk and the account_id, and must be rejected.
for (pos, &mask) in visibility_mask.iter().enumerate() {
if mask == 3 {
// Every private-PDA pre_state must have had its npk bound to its account_id, either via
// a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds`
// matching the private derivation. An unbound private-PDA pre_state has no
// cryptographic link between the supplied npk and the account_id, and must be rejected.
for (pos, account_identity) in account_identities.iter().enumerate() {
if account_identity.is_private_pda() {
assert!(
execution_state.private_pda_bound_positions.contains(&pos),
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
@ -251,7 +241,7 @@ impl ExecutionState {
/// Validate program pre and post states and populate the execution state.
fn validate_and_sync_states(
&mut self,
visibility_mask: &[u8],
account_identities: &[InputAccountIdentity],
program_id: ProgramId,
caller_program_id: Option<ProgramId>,
caller_pda_seeds: &[PdaSeed],
@ -329,9 +319,9 @@ impl ExecutionState {
.position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point");
let mask = visibility_mask[pre_state_position];
match mask {
0 => match claim {
let account_identity = &account_identities[pre_state_position];
if account_identity.is_public() {
match claim {
Claim::Authorized => {
// Note: no need to check authorized pdas because we have already
// checked consistency of authorization above.
@ -353,40 +343,40 @@ impl ExecutionState {
pre_account_id,
);
}
},
3 => {
match claim {
Claim::Authorized => {
assert!(
pre_is_authorized,
"Cannot claim unauthorized private PDA {pre_account_id}"
);
}
Claim::Pda(seed) => {
let npk = self
}
} else if account_identity.is_private_pda() {
match claim {
Claim::Authorized => {
assert!(
pre_is_authorized,
"Cannot claim unauthorized private PDA {pre_account_id}"
);
}
Claim::Pda(seed) => {
let npk = self
.private_pda_npk_by_position
.get(&pre_state_position)
.expect("private PDA pre_state must have an npk in the position map");
let pda = AccountId::for_private_pda(&program_id, &seed, npk);
assert_eq!(
pre_account_id, pda,
"Invalid private PDA claim for account {pre_account_id}"
.expect(
"private PDA pre_state must have an npk in the position map",
);
self.private_pda_bound_positions.insert(pre_state_position);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
let pda = AccountId::for_private_pda(&program_id, &seed, npk);
assert_eq!(
pre_account_id, pda,
"Invalid private PDA claim for account {pre_account_id}"
);
self.private_pda_bound_positions.insert(pre_state_position);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
}
_ => {
// Mask 1/2: standard private accounts don't enforce the claim semantics.
// Unauthorized private claiming is intentionally allowed since operating
// these accounts requires the npk/nsk keypair anyway.
}
} else {
// Standalone private accounts: don't enforce the claim semantics.
// Unauthorized private claiming is intentionally allowed since operating
// these accounts requires the npk/nsk keypair anyway.
}
post.account_mut().program_owner = program_id;
@ -488,10 +478,7 @@ fn resolve_authorization_and_record_bindings(
fn compute_circuit_output(
execution_state: ExecutionState,
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
account_identities: &[InputAccountIdentity],
) -> PrivacyPreservingCircuitOutput {
let mut output = PrivacyPreservingCircuitOutput {
public_pre_states: Vec::new(),
@ -505,290 +492,268 @@ fn compute_circuit_output(
let states_iter = execution_state.into_states_iter();
assert_eq!(
visibility_mask.len(),
account_identities.len(),
states_iter.len(),
"Invalid visibility mask length"
"Invalid account_identities length"
);
let mut private_keys_iter = private_account_keys.iter();
let mut private_nsks_iter = private_account_nsks.iter();
let mut private_membership_proofs_iter = private_account_membership_proofs.iter();
let mut output_index = 0;
for (account_visibility_mask, (pre_state, post_state)) in
visibility_mask.iter().copied().zip(states_iter)
{
match account_visibility_mask {
0 => {
// Public account
for (account_identity, (pre_state, post_state)) in account_identities.iter().zip(states_iter) {
match account_identity {
InputAccountIdentity::Public => {
output.public_pre_states.push(pre_state);
output.public_post_states.push(post_state);
}
1 | 2 => {
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
InputAccountIdentity::PrivateAuthorizedInit {
ssk,
nsk,
identifier,
} => {
assert_ne!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
);
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::from((&npk, *identifier));
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
assert_eq!(
pre_state.account,
Account::default(),
"Found new private account with non default values"
);
let new_nullifier = (
Nullifier::for_account_initialization(&account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
*identifier,
ssk,
new_nullifier,
new_nonce,
);
}
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk,
nsk,
membership_proof,
identifier,
} => {
assert_ne!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
);
let npk = NullifierPublicKey::from(nsk);
let account_id = AccountId::from((&npk, *identifier));
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&pre_state.account,
&account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
*identifier,
ssk,
new_nullifier,
new_nonce,
);
}
InputAccountIdentity::PrivateUnauthorized {
npk,
ssk,
identifier,
} => {
assert_ne!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
);
let account_id = AccountId::from((npk, *identifier));
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
let (new_nullifier, new_nonce) = if account_visibility_mask == 1 {
// Private account with authentication
let Some(nsk) = private_nsks_iter.next() else {
panic!("Missing private account nullifier secret key");
};
// Verify the nullifier public key
assert_eq!(
npk,
&NullifierPublicKey::from(nsk),
"Nullifier public key mismatch"
);
// Check pre_state authorization
assert!(
pre_state.is_authorized,
"Pre-state not authorized for authenticated private account"
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
&account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
(new_nullifier, new_nonce)
} else {
// Private account without authentication
assert_eq!(
pre_state.account,
Account::default(),
"Found new private account with non default values",
);
assert!(
!pre_state.is_authorized,
"Found new private account marked as authorized."
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
assert!(
membership_proof_opt.is_none(),
"Membership proof must be None for unauthorized accounts"
);
let nullifier = Nullifier::for_account_initialization(&account_id);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
output.new_nullifiers.push(new_nullifier);
// Update post-state with new nonce
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
// Compute commitment
let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce);
// Encrypt and push post state
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
*identifier,
shared_secret,
&commitment_post,
output_index,
);
output.new_commitments.push(commitment_post);
output.ciphertexts.push(encrypted_account);
output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
3 => {
// Private PDA account. The supplied npk has already been bound to
// `pre_state.account_id` upstream in `validate_and_sync_states`, either via a
// `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which
// assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The
// post-loop assertion in `derive_from_outputs` (see the
// `private_pda_bound_positions` check) guarantees that every mask-3
// position has been through at least one such binding, so this
// branch can safely use the wallet npk without re-verifying.
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
assert_eq!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}."
pre_state.account,
Account::default(),
"Found new private account with non default values",
);
assert!(
!pre_state.is_authorized,
"Found new private account marked as authorized."
);
let (new_nullifier, new_nonce) = if pre_state.is_authorized {
// Existing private PDA with authentication (like mask 1)
let Some(nsk) = private_nsks_iter.next() else {
panic!("Missing private account nullifier secret key");
};
assert_eq!(
npk,
&NullifierPublicKey::from(nsk),
"Nullifier public key mismatch"
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
&pre_state.account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
(new_nullifier, new_nonce)
} else {
// New private PDA (like mask 2). The default + unauthorized requirement
// here rules out use cases like a fully-private multisig, which would need
// a non-default, non-authorized private PDA input account.
// TODO(private-pdas-pr-2/3): relax this once the wallet can supply a
// `(seed, owner)` side input so the npk-to-account_id binding can be
// re-verified for an existing private PDA without a `Claim::Pda` or caller
// `pda_seeds` match.
assert_eq!(
pre_state.account,
Account::default(),
"New private PDA must be default"
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
assert!(
membership_proof_opt.is_none(),
"Membership proof must be None for new accounts"
);
let nullifier = Nullifier::for_account_initialization(&pre_state.account_id);
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
output.new_nullifiers.push(new_nullifier);
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post =
Commitment::new(&pre_state.account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
PRIVATE_PDA_FIXED_IDENTIFIER,
shared_secret,
&commitment_post,
output_index,
let new_nullifier = (
Nullifier::for_account_initialization(&account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
output.new_commitments.push(commitment_post);
output.ciphertexts.push(encrypted_account);
output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
*identifier,
ssk,
new_nullifier,
new_nonce,
);
}
InputAccountIdentity::PrivatePdaInit { npk: _, ssk } => {
// The npk-to-account_id binding is established upstream in
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
// match. Here we only enforce the init pre-conditions. The supplied npk on
// the variant has been recorded into `private_pda_npk_by_position` and used
// for the binding check; we use `pre_state.account_id` directly for nullifier
// and commitment derivation.
assert!(
!pre_state.is_authorized,
"PrivatePdaInit requires unauthorized pre_state"
);
assert_eq!(
pre_state.account,
Account::default(),
"New private PDA must be default"
);
let new_nullifier = (
Nullifier::for_account_initialization(&pre_state.account_id),
DUMMY_COMMITMENT_HASH,
);
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
let account_id = pre_state.account_id;
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
PRIVATE_PDA_FIXED_IDENTIFIER,
ssk,
new_nullifier,
new_nonce,
);
}
InputAccountIdentity::PrivatePdaUpdate {
ssk,
nsk,
membership_proof,
} => {
// The npk binding is established upstream. Authorization must already be set;
// an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an
// unbound PDA, which the upstream binding check would have rejected anyway,
// but we assert here to fail fast and document the precondition.
assert!(
pre_state.is_authorized,
"PrivatePdaUpdate requires authorized pre_state"
);
let new_nullifier = compute_update_nullifier_and_set_digest(
membership_proof,
&pre_state.account,
&pre_state.account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
let account_id = pre_state.account_id;
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
PRIVATE_PDA_FIXED_IDENTIFIER,
ssk,
new_nullifier,
new_nonce,
);
}
_ => panic!("Invalid visibility mask value"),
}
}
assert!(
private_keys_iter.next().is_none(),
"Too many private account keys"
);
assert!(
private_nsks_iter.next().is_none(),
"Too many private account nullifier secret keys"
);
assert!(
private_membership_proofs_iter.next().is_none(),
"Too many private account membership proofs"
);
output
}
fn compute_nullifier_and_set_digest(
membership_proof_opt: Option<&MembershipProof>,
#[expect(
clippy::too_many_arguments,
reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial"
)]
fn emit_private_output(
output: &mut PrivacyPreservingCircuitOutput,
output_index: &mut u32,
post_state: Account,
account_id: &AccountId,
identifier: Identifier,
shared_secret: &SharedSecretKey,
new_nullifier: (Nullifier, CommitmentSetDigest),
new_nonce: Nonce,
) {
output.new_nullifiers.push(new_nullifier);
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
identifier,
shared_secret,
&commitment_post,
*output_index,
);
output.new_commitments.push(commitment_post);
output.ciphertexts.push(encrypted_account);
*output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
fn compute_update_nullifier_and_set_digest(
membership_proof: &MembershipProof,
pre_account: &Account,
account_id: &AccountId,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
membership_proof_opt.as_ref().map_or_else(
|| {
assert_eq!(
*pre_account,
Account::default(),
"Found new private account with non default values"
);
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(account_id);
(nullifier, DUMMY_COMMITMENT_HASH)
},
|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
},
)
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
}
fn main() {
let PrivacyPreservingCircuitInput {
program_outputs,
visibility_mask,
private_account_keys,
private_account_nsks,
private_account_membership_proofs,
account_identities,
program_id,
} = env::read();
let execution_state = ExecutionState::derive_from_outputs(
&visibility_mask,
&private_account_keys,
program_id,
program_outputs,
);
let execution_state =
ExecutionState::derive_from_outputs(&account_identities, program_id, program_outputs);
let output = compute_circuit_output(
execution_state,
&visibility_mask,
&private_account_keys,
&private_account_nsks,
&private_account_membership_proofs,
);
let output = compute_circuit_output(execution_state, &account_identities);
env::commit(&output);
}

View File

@ -1076,7 +1076,7 @@ mod tests {
program::Program,
};
use nssa_core::{
SharedSecretKey,
InputAccountIdentity, SharedSecretKey,
account::AccountWithMetadata,
encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey},
};
@ -1114,10 +1114,11 @@ mod tests {
(&npk, 0),
)],
Program::serialize_instruction(0_u128).unwrap(),
vec![1],
vec![(npk, 0, shared_secret)],
vec![nsk],
vec![None],
vec![InputAccountIdentity::PrivateAuthorizedInit {
ssk: shared_secret,
nsk,
identifier: 0,
}],
&Program::authenticated_transfer_program().into(),
)
.unwrap();

View File

@ -413,13 +413,7 @@ impl WalletCore {
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
pre_states,
instruction_data,
acc_manager.visibility_mask().to_vec(),
private_account_keys
.iter()
.map(|keys| (keys.npk, keys.identifier, keys.ssk))
.collect::<Vec<_>>(),
acc_manager.private_account_auth(),
acc_manager.private_account_membership_proofs(),
acc_manager.account_identities(),
&program.to_owned(),
)
.unwrap();

View File

@ -2,7 +2,8 @@ use anyhow::Result;
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use nssa::{AccountId, PrivateKey};
use nssa_core::{
Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
SharedSecretKey,
account::{AccountWithMetadata, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{PdaSeed, ProgramId},
@ -54,7 +55,6 @@ impl PrivacyPreservingAccount {
pub struct PrivateAccountKeys {
pub npk: NullifierPublicKey,
pub identifier: Identifier,
pub ssk: SharedSecretKey,
pub vpk: ViewingPublicKey,
pub epk: EphemeralPublicKey,
@ -70,7 +70,6 @@ enum State {
pub struct AccountManager {
states: Vec<State>,
visibility_mask: Vec<u8>,
}
impl AccountManager {
@ -78,11 +77,10 @@ impl AccountManager {
wallet: &WalletCore,
accounts: Vec<PrivacyPreservingAccount>,
) -> Result<Self, ExecutionFailureKind> {
let mut pre_states = Vec::with_capacity(accounts.len());
let mut visibility_mask = Vec::with_capacity(accounts.len());
let mut states = Vec::with_capacity(accounts.len());
for account in accounts {
let (state, mask) = match account {
let state = match account {
PrivacyPreservingAccount::Public(account_id) => {
let acc = wallet
.get_account_public(account_id)
@ -92,13 +90,12 @@ impl AccountManager {
let sk = wallet.get_account_public_signing_key(account_id).cloned();
let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id);
(State::Public { account, sk }, 0)
State::Public { account, sk }
}
PrivacyPreservingAccount::PrivateOwned(account_id) => {
let pre = private_acc_preparation(wallet, account_id).await?;
let mask = if pre.pre_state.is_authorized { 1 } else { 2 };
(State::Private(pre), mask)
State::Private(pre)
}
PrivacyPreservingAccount::PrivateForeign {
npk,
@ -107,6 +104,9 @@ impl AccountManager {
} => {
let acc = nssa_core::account::Account::default();
let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier));
let eph_holder = EphemeralKeyHolder::new(&npk);
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
let epk = eph_holder.generate_ephemeral_public_key();
let pre = AccountPreparedData {
nsk: None,
npk,
@ -114,9 +114,11 @@ impl AccountManager {
vpk,
pre_state: auth_acc,
proof: None,
ssk,
epk,
};
(State::Private(pre), 2)
State::Private(pre)
}
PrivacyPreservingAccount::PrivatePda {
nsk,
@ -128,21 +130,16 @@ impl AccountManager {
let pre =
private_pda_preparation(wallet, nsk, npk, vpk, &program_id, &seed).await?;
(State::Private(pre), 3)
State::Private(pre)
}
};
pre_states.push(state);
visibility_mask.push(mask);
states.push(state);
}
Ok(Self {
states: pre_states,
visibility_mask,
})
Ok(Self { states })
}
#[must_use]
pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
self.states
.iter()
@ -153,12 +150,6 @@ impl AccountManager {
.collect()
}
#[must_use]
pub fn visibility_mask(&self) -> &[u8] {
&self.visibility_mask
}
#[must_use]
pub fn public_account_nonces(&self) -> Vec<Nonce> {
self.states
.iter()
@ -169,50 +160,70 @@ impl AccountManager {
.collect()
}
#[must_use]
pub fn private_account_keys(&self) -> Vec<PrivateAccountKeys> {
self.states
.iter()
.filter_map(|state| match state {
State::Private(pre) => {
let eph_holder = EphemeralKeyHolder::new(&pre.npk);
State::Private(pre) => Some(PrivateAccountKeys {
npk: pre.npk,
ssk: pre.ssk,
vpk: pre.vpk.clone(),
epk: pre.epk.clone(),
}),
State::Public { .. } => None,
})
.collect()
}
Some(PrivateAccountKeys {
npk: pre.npk,
identifier: pre.identifier,
ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk),
vpk: pre.vpk.clone(),
epk: eph_holder.generate_ephemeral_public_key(),
})
/// Build the per-account input vec for the privacy-preserving circuit. Each variant carries
/// exactly the fields the circuit's code path for that account needs, with the ephemeral
/// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message
/// construction also use, so all three views agree on the same ephemeral key.
pub fn account_identities(&self) -> Vec<InputAccountIdentity> {
self.states
.iter()
.map(|state| match state {
State::Public { .. } => InputAccountIdentity::Public,
State::Private(pre) if pre.identifier == u128::MAX => {
// Private PDA account
match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => {
InputAccountIdentity::PrivatePdaUpdate {
ssk: pre.ssk,
nsk,
membership_proof,
}
}
_ => InputAccountIdentity::PrivatePdaInit {
npk: pre.npk,
ssk: pre.ssk,
},
}
}
State::Public { .. } => None,
State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: pre.ssk,
nsk,
membership_proof,
identifier: pre.identifier,
}
}
(Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit {
ssk: pre.ssk,
nsk,
identifier: pre.identifier,
},
(None, _) => InputAccountIdentity::PrivateUnauthorized {
npk: pre.npk,
ssk: pre.ssk,
identifier: pre.identifier,
},
},
})
.collect()
}
#[must_use]
pub fn private_account_auth(&self) -> Vec<NullifierSecretKey> {
self.states
.iter()
.filter_map(|state| match state {
State::Private(pre) => pre.nsk,
State::Public { .. } => None,
})
.collect()
}
#[must_use]
pub fn private_account_membership_proofs(&self) -> Vec<Option<MembershipProof>> {
self.states
.iter()
.filter_map(|state| match state {
State::Private(pre) => Some(pre.proof.clone()),
State::Public { .. } => None,
})
.collect()
}
#[must_use]
pub fn public_account_ids(&self) -> Vec<AccountId> {
self.states
.iter()
@ -223,7 +234,6 @@ impl AccountManager {
.collect()
}
#[must_use]
pub fn public_account_auth(&self) -> Vec<&PrivateKey> {
self.states
.iter()
@ -242,6 +252,54 @@ struct AccountPreparedData {
vpk: ViewingPublicKey,
pre_state: AccountWithMetadata,
proof: Option<MembershipProof>,
/// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the
/// circuit input variant (`account_identities()`) and the message ephemeral-key tuples
/// (`private_account_keys()`), so all consumers see the same key. The corresponding
/// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call.
ssk: SharedSecretKey,
/// Cached ephemeral public key, paired with `ssk`.
epk: EphemeralPublicKey,
}
async fn private_acc_preparation(
wallet: &WalletCore,
account_id: AccountId,
) -> Result<AccountPreparedData, ExecutionFailureKind> {
let Some((from_keys, from_acc, from_identifier)) =
wallet.storage.user_data.get_private_account(account_id)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let nsk = from_keys.private_key_holder.nullifier_secret_key;
let from_npk = from_keys.nullifier_public_key;
let from_vpk = from_keys.viewing_public_key;
// TODO: Remove this unwrap, error types must be compatible
let proof = wallet
.check_private_account_initialized(account_id)
.await
.unwrap();
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
// support from that in the wallet.
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier));
let eph_holder = EphemeralKeyHolder::new(&from_npk);
let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk);
let epk = eph_holder.generate_ephemeral_public_key();
Ok(AccountPreparedData {
nsk: Some(nsk),
npk: from_npk,
identifier: from_identifier,
vpk: from_vpk,
pre_state: sender_pre,
proof,
ssk,
epk,
})
}
async fn private_pda_preparation(
@ -281,6 +339,10 @@ async fn private_pda_preparation(
None
};
let eph_holder = EphemeralKeyHolder::new(&npk);
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
let epk = eph_holder.generate_ephemeral_public_key();
Ok(AccountPreparedData {
nsk: exists.then_some(nsk),
npk,
@ -288,58 +350,7 @@ async fn private_pda_preparation(
vpk,
pre_state,
proof,
ssk,
epk,
})
}
async fn private_acc_preparation(
wallet: &WalletCore,
account_id: AccountId,
) -> Result<AccountPreparedData, ExecutionFailureKind> {
let Some((from_keys, from_acc, from_identifier)) =
wallet.storage.user_data.get_private_account(account_id)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let nsk = from_keys.private_key_holder.nullifier_secret_key;
let from_npk = from_keys.nullifier_public_key;
let from_vpk = from_keys.viewing_public_key;
// TODO: Remove this unwrap, error types must be compatible
let proof = wallet
.check_private_account_initialized(account_id)
.await
.unwrap();
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
// support from that in the wallet.
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier));
Ok(AccountPreparedData {
nsk: Some(nsk),
npk: from_npk,
identifier: from_identifier,
vpk: from_vpk,
pre_state: sender_pre,
proof,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_pda_is_private() {
let acc = PrivacyPreservingAccount::PrivatePda {
nsk: [0; 32],
npk: NullifierPublicKey([1; 32]),
vpk: ViewingPublicKey::from_scalar([2; 32]),
program_id: [3; 8],
seed: PdaSeed::new([4; 32]),
};
assert!(acc.is_private());
assert!(!acc.is_public());
}
}