2025-11-19 09:00:32 +02:00
|
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
2026-03-04 18:42:33 +03:00
|
|
|
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
2025-08-18 07:39:41 -03:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2025-09-12 09:18:40 -03:00
|
|
|
use crate::{Commitment, account::AccountId};
|
2025-08-18 07:39:41 -03:00
|
|
|
|
2026-04-20 11:27:15 -03:00
|
|
|
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
|
|
|
|
|
|
2026-04-10 20:23:25 +03:00
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
2026-04-16 16:53:54 +02:00
|
|
|
#[cfg_attr(any(feature = "host", test), derive(Hash))]
|
2025-09-12 16:00:57 +03:00
|
|
|
pub struct NullifierPublicKey(pub [u8; 32]);
|
2025-08-18 09:21:07 -03:00
|
|
|
|
2026-05-08 21:41:48 -03:00
|
|
|
impl AccountId {
|
|
|
|
|
/// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public
|
2026-06-12 22:06:10 +04:00
|
|
|
/// key. The address is stable per key; multiple notes live under it, diversified by nonce.
|
2026-05-08 21:41:48 -03:00
|
|
|
#[must_use]
|
2026-06-12 22:06:10 +04:00
|
|
|
pub fn for_regular_private_account(npk: &NullifierPublicKey) -> Self {
|
|
|
|
|
// 32 bytes prefix || 32 bytes npk
|
|
|
|
|
let mut bytes = [0; 64];
|
2025-09-16 07:51:40 -03:00
|
|
|
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
|
2026-04-14 18:02:38 -03:00
|
|
|
bytes[32..64].copy_from_slice(&npk.0);
|
|
|
|
|
|
2026-03-09 18:27:56 +03:00
|
|
|
Self::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Conversion should not fail"),
|
|
|
|
|
)
|
2025-09-10 18:56:34 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 22:06:10 +04:00
|
|
|
impl From<&NullifierPublicKey> for AccountId {
|
|
|
|
|
fn from(npk: &NullifierPublicKey) -> Self {
|
|
|
|
|
Self::for_regular_private_account(npk)
|
2026-05-08 21:41:48 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 14:04:49 +03:00
|
|
|
impl AsRef<[u8]> for NullifierPublicKey {
|
|
|
|
|
fn as_ref(&self) -> &[u8] {
|
|
|
|
|
self.0.as_slice()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 07:39:41 -03:00
|
|
|
impl From<&NullifierSecretKey> for NullifierPublicKey {
|
2025-08-18 11:53:43 -03:00
|
|
|
fn from(value: &NullifierSecretKey) -> Self {
|
2026-01-27 16:00:42 -05:00
|
|
|
const PREFIX: &[u8; 8] = b"LEE/keys";
|
2025-08-18 11:53:43 -03:00
|
|
|
const SUFFIX_1: &[u8; 1] = &[7];
|
2026-01-27 16:00:42 -05:00
|
|
|
const SUFFIX_2: &[u8; 23] = &[0; 23];
|
2026-03-03 23:21:08 +03:00
|
|
|
let mut bytes = Vec::new();
|
2025-08-18 11:53:43 -03:00
|
|
|
bytes.extend_from_slice(PREFIX);
|
|
|
|
|
bytes.extend_from_slice(value);
|
|
|
|
|
bytes.extend_from_slice(SUFFIX_1);
|
|
|
|
|
bytes.extend_from_slice(SUFFIX_2);
|
2026-03-09 18:27:56 +03:00
|
|
|
Self(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("hash should be exactly 32 bytes long"),
|
|
|
|
|
)
|
2025-08-18 07:39:41 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type NullifierSecretKey = [u8; 32];
|
|
|
|
|
|
2025-11-19 09:00:32 +02:00
|
|
|
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
2026-01-27 10:09:34 -03:00
|
|
|
#[cfg_attr(
|
|
|
|
|
any(feature = "host", test),
|
2026-04-02 01:09:58 -03:00
|
|
|
derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
|
2026-01-27 10:09:34 -03:00
|
|
|
)]
|
2025-08-18 14:28:26 -03:00
|
|
|
pub struct Nullifier(pub(super) [u8; 32]);
|
2025-08-18 07:39:41 -03:00
|
|
|
|
2026-02-27 16:37:28 +11:00
|
|
|
#[cfg(any(feature = "host", test))]
|
|
|
|
|
impl std::fmt::Debug for Nullifier {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
2026-03-03 23:21:08 +03:00
|
|
|
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
|
|
|
|
|
});
|
2026-02-27 16:37:28 +11:00
|
|
|
write!(f, "Nullifier({hex})")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 07:39:41 -03:00
|
|
|
impl Nullifier {
|
2026-03-03 23:21:08 +03:00
|
|
|
/// Computes a nullifier for an account update.
|
|
|
|
|
#[must_use]
|
2025-10-03 20:44:29 -03:00
|
|
|
pub fn for_account_update(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self {
|
2026-03-20 10:39:04 -04:00
|
|
|
const UPDATE_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Update/\x00\x00\x00\x00\x00";
|
2025-10-03 20:44:29 -03:00
|
|
|
let mut bytes = UPDATE_PREFIX.to_vec();
|
2025-08-18 11:53:43 -03:00
|
|
|
bytes.extend_from_slice(&commitment.to_byte_array());
|
2025-08-18 09:50:11 -03:00
|
|
|
bytes.extend_from_slice(nsk);
|
|
|
|
|
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
|
|
|
|
}
|
2025-10-03 20:44:29 -03:00
|
|
|
|
2026-06-12 22:06:10 +04:00
|
|
|
/// 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)`.
|
2026-03-03 23:21:08 +03:00
|
|
|
#[must_use]
|
2026-06-12 22:06:10 +04:00
|
|
|
pub fn for_account_initialization(pre_commitment: &Commitment) -> Self {
|
2026-03-20 10:39:04 -04:00
|
|
|
const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00";
|
2025-10-03 20:44:29 -03:00
|
|
|
let mut bytes = INIT_PREFIX.to_vec();
|
2026-06-12 22:06:10 +04:00
|
|
|
bytes.extend_from_slice(&pre_commitment.to_byte_array());
|
2025-10-03 20:44:29 -03:00
|
|
|
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
|
|
|
|
}
|
2025-08-18 09:50:11 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2026-06-12 22:06:10 +04:00
|
|
|
use crate::account::{Account, Nonce};
|
2025-08-18 09:50:11 -03:00
|
|
|
|
|
|
|
|
#[test]
|
2026-03-04 18:42:33 +03:00
|
|
|
fn constructor_for_account_update() {
|
|
|
|
|
let commitment = Commitment((0..32_u8).collect::<Vec<_>>().try_into().unwrap());
|
2025-08-18 09:50:11 -03:00
|
|
|
let nsk = [0x42; 32];
|
|
|
|
|
let expected_nullifier = Nullifier([
|
2026-03-20 10:39:04 -04:00
|
|
|
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,
|
2025-10-03 20:44:29 -03:00
|
|
|
]);
|
|
|
|
|
let nullifier = Nullifier::for_account_update(&commitment, &nsk);
|
|
|
|
|
assert_eq!(nullifier, expected_nullifier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-12 22:06:10 +04:00
|
|
|
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)");
|
2025-08-18 07:39:41 -03:00
|
|
|
}
|
2025-08-18 11:53:43 -03:00
|
|
|
|
|
|
|
|
#[test]
|
2026-03-04 18:42:33 +03:00
|
|
|
fn from_secret_key() {
|
2025-08-18 11:53:43 -03:00
|
|
|
let nsk = [
|
2025-08-18 14:28:26 -03:00
|
|
|
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,
|
2025-08-18 11:53:43 -03:00
|
|
|
];
|
2025-08-26 14:53:02 -03:00
|
|
|
let expected_npk = NullifierPublicKey([
|
2026-02-11 11:43:23 -05:00
|
|
|
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,
|
2025-08-18 11:53:43 -03:00
|
|
|
]);
|
2025-08-27 16:24:20 -03:00
|
|
|
let npk = NullifierPublicKey::from(&nsk);
|
|
|
|
|
assert_eq!(npk, expected_npk);
|
2025-08-18 11:53:43 -03:00
|
|
|
}
|
2025-09-12 09:36:26 -03:00
|
|
|
|
|
|
|
|
#[test]
|
2026-06-12 22:06:10 +04:00
|
|
|
fn account_id_is_deterministic_per_npk() {
|
2026-04-28 00:04:42 -03:00
|
|
|
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);
|
2026-06-12 22:06:10 +04:00
|
|
|
assert_eq!(
|
|
|
|
|
AccountId::for_regular_private_account(&npk),
|
|
|
|
|
AccountId::for_regular_private_account(&npk),
|
|
|
|
|
);
|
2026-04-28 00:09:28 -03:00
|
|
|
}
|
2025-08-18 07:39:41 -03:00
|
|
|
}
|