use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; use crate::{Commitment, account::AccountId}; const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00"; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Hash))] pub struct NullifierPublicKey(pub [u8; 32]); impl AccountId { /// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public /// key. The address is stable per key; multiple notes live under it, diversified by nonce. #[must_use] pub fn for_regular_private_account(npk: &NullifierPublicKey) -> Self { // 32 bytes prefix || 32 bytes npk let mut bytes = [0; 64]; bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX); bytes[32..64].copy_from_slice(&npk.0); Self::new( Impl::hash_bytes(&bytes) .as_bytes() .try_into() .expect("Conversion should not fail"), ) } } impl From<&NullifierPublicKey> for AccountId { fn from(npk: &NullifierPublicKey) -> Self { Self::for_regular_private_account(npk) } } impl AsRef<[u8]> for NullifierPublicKey { fn as_ref(&self) -> &[u8] { self.0.as_slice() } } impl From<&NullifierSecretKey> for NullifierPublicKey { fn from(value: &NullifierSecretKey) -> Self { const PREFIX: &[u8; 8] = b"LEE/keys"; const SUFFIX_1: &[u8; 1] = &[7]; const SUFFIX_2: &[u8; 23] = &[0; 23]; let mut bytes = Vec::new(); bytes.extend_from_slice(PREFIX); bytes.extend_from_slice(value); bytes.extend_from_slice(SUFFIX_1); bytes.extend_from_slice(SUFFIX_2); Self( Impl::hash_bytes(&bytes) .as_bytes() .try_into() .expect("hash should be exactly 32 bytes long"), ) } } pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) )] pub struct Nullifier(pub(super) [u8; 32]); #[cfg(any(feature = "host", test))] impl std::fmt::Debug for Nullifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use std::fmt::Write as _; let hex: String = self.0.iter().fold(String::new(), |mut acc, b| { write!(acc, "{b:02x}").expect("writing to string should not fail"); acc }); write!(f, "Nullifier({hex})") } } impl Nullifier { /// Computes a nullifier for an account update. #[must_use] pub fn for_account_update(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { const UPDATE_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Update/\x00\x00\x00\x00\x00"; let mut bytes = UPDATE_PREFIX.to_vec(); bytes.extend_from_slice(&commitment.to_byte_array()); bytes.extend_from_slice(nsk); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } /// Computes a nullifier for an account initialization. Binds the account's *pre-state* /// commitment; since the pre-state is the default account except for the nonce, this reduces to /// a tag over `(account_id, nonce)` — re-initializable once per nonce (enabling multi-note) /// while still forbidding a duplicate `(account_id, nonce)`. #[must_use] pub fn for_account_initialization(pre_commitment: &Commitment) -> Self { const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00"; let mut bytes = INIT_PREFIX.to_vec(); bytes.extend_from_slice(&pre_commitment.to_byte_array()); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } } #[cfg(test)] mod tests { use super::*; use crate::account::{Account, Nonce}; #[test] fn constructor_for_account_update() { let commitment = Commitment((0..32_u8).collect::>().try_into().unwrap()); let nsk = [0x42; 32]; let expected_nullifier = Nullifier([ 70, 162, 122, 15, 33, 237, 244, 216, 89, 223, 90, 50, 94, 184, 210, 144, 174, 64, 189, 254, 62, 255, 5, 1, 139, 227, 194, 185, 16, 30, 55, 48, ]); let nullifier = Nullifier::for_account_update(&commitment, &nsk); assert_eq!(nullifier, expected_nullifier); } #[test] fn init_nullifier_binds_account_id_and_nonce() { let account_id = AccountId::new([7; 32]); let pre = |nonce| Account { nonce: Nonce(nonce), ..Account::default() }; let tag = |nonce| Nullifier::for_account_initialization(&Commitment::new(&account_id, &pre(nonce))); assert_eq!(tag(1), tag(1), "deterministic per (account_id, nonce)"); assert_ne!(tag(1), tag(2), "different nonce -> different tag (multi-note)"); } #[test] fn from_secret_key() { let nsk = [ 57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30, 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let expected_npk = NullifierPublicKey([ 78, 20, 20, 5, 177, 198, 233, 100, 175, 134, 174, 200, 24, 205, 68, 215, 130, 74, 35, 54, 154, 184, 219, 42, 168, 106, 126, 147, 133, 244, 18, 218, ]); let npk = NullifierPublicKey::from(&nsk); assert_eq!(npk, expected_npk); } #[test] fn account_id_is_deterministic_per_npk() { let nsk = [ 57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30, 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let npk = NullifierPublicKey::from(&nsk); assert_eq!( AccountId::for_regular_private_account(&npk), AccountId::for_regular_private_account(&npk), ); } }