mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-05-12 12:49:36 +00:00
feat(wallet)!: add derive_keys_for_shared_account and PrivateShared variant
BREAKING CHANGE: `pda_accounts` field in NSSAUserData renamed to `shared_accounts`. `PrivacyPreservingAccount` enum has a new `PrivateShared` variant, exhaustive matches must handle it.
This commit is contained in:
parent
cf6eab2538
commit
7be0ed926c
@ -105,6 +105,21 @@ impl GroupKeyHolder {
|
||||
.produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Derive keys for a shared regular (non-PDA) private account.
|
||||
///
|
||||
/// Uses a distinct domain separator from `derive_keys_for_pda` to prevent cross-domain
|
||||
/// key collisions. The `tag` should be a stable, unique 32-byte value (e.g. derived from
|
||||
/// a random identifier at account creation time).
|
||||
#[must_use]
|
||||
pub fn derive_keys_for_shared_account(&self, tag: &[u8; 32]) -> PrivateKeyHolder {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeyDerivation/SHA";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(self.gms);
|
||||
hasher.update(tag);
|
||||
SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`].
|
||||
///
|
||||
/// Uses an ephemeral ECDH key exchange to derive a shared secret, then AES-256-GCM
|
||||
@ -501,4 +516,52 @@ mod tests {
|
||||
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk);
|
||||
assert_eq!(alice_account_id, bob_account_id);
|
||||
}
|
||||
|
||||
/// Same GMS + same tag produces same keys for shared accounts.
|
||||
#[test]
|
||||
fn shared_account_same_gms_same_tag_produces_same_keys() {
|
||||
let gms = [42_u8; 32];
|
||||
let tag = [1_u8; 32];
|
||||
let holder_a = GroupKeyHolder::from_gms(gms);
|
||||
let holder_b = GroupKeyHolder::from_gms(gms);
|
||||
|
||||
let npk_a = holder_a
|
||||
.derive_keys_for_shared_account(&tag)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder_b
|
||||
.derive_keys_for_shared_account(&tag)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(npk_a, npk_b);
|
||||
}
|
||||
|
||||
/// Different tags produce different keys for shared accounts.
|
||||
#[test]
|
||||
fn shared_account_different_tags_produce_different_keys() {
|
||||
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
let npk_a = holder
|
||||
.derive_keys_for_shared_account(&[1_u8; 32])
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder
|
||||
.derive_keys_for_shared_account(&[2_u8; 32])
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a, npk_b);
|
||||
}
|
||||
|
||||
/// PDA and shared account derivations from the same GMS + same bytes never collide.
|
||||
#[test]
|
||||
fn pda_and_shared_derivations_do_not_collide() {
|
||||
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
let bytes = [1_u8; 32];
|
||||
|
||||
let pda_npk = holder
|
||||
.derive_keys_for_pda(&PdaSeed::new(bytes))
|
||||
.generate_nullifier_public_key();
|
||||
let shared_npk = holder
|
||||
.derive_keys_for_shared_account(&bytes)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(pda_npk, shared_npk);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +36,12 @@ pub struct NSSAUserData {
|
||||
/// An older wallet binary that re-serializes this struct will drop the field.
|
||||
#[serde(default)]
|
||||
pub group_key_holders: BTreeMap<String, GroupKeyHolder>,
|
||||
/// Cached plaintext state of private PDA accounts, keyed by `AccountId`.
|
||||
/// Updated after each private PDA transaction by decrypting the circuit output.
|
||||
/// Cached plaintext state of shared accounts (PDAs and regular shared accounts),
|
||||
/// keyed by `AccountId`. Updated after each transaction by decrypting the circuit output.
|
||||
/// The sequencer only stores encrypted commitments, so this local cache is the
|
||||
/// only source of plaintext state for private PDAs.
|
||||
#[serde(default, alias = "group_pda_accounts")]
|
||||
pub pda_accounts: BTreeMap<nssa::AccountId, nssa_core::account::Account>,
|
||||
/// only source of plaintext state for these accounts.
|
||||
#[serde(default, alias = "group_pda_accounts", alias = "pda_accounts")]
|
||||
pub shared_accounts: BTreeMap<nssa::AccountId, nssa_core::account::Account>,
|
||||
}
|
||||
|
||||
impl NSSAUserData {
|
||||
@ -101,7 +101,7 @@ impl NSSAUserData {
|
||||
public_key_tree,
|
||||
private_key_tree,
|
||||
group_key_holders: BTreeMap::new(),
|
||||
pda_accounts: BTreeMap::new(),
|
||||
shared_accounts: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,15 @@ pub enum PrivacyPreservingAccount {
|
||||
program_id: ProgramId,
|
||||
seed: PdaSeed,
|
||||
},
|
||||
/// A shared regular private account with externally-provided keys (e.g. from GMS).
|
||||
/// Uses standard `AccountId = from((&npk, identifier))` and mask 1/2.
|
||||
/// Works with `authenticated_transfer` and all existing programs out of the box.
|
||||
PrivateShared {
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl PrivacyPreservingAccount {
|
||||
@ -49,6 +58,7 @@ impl PrivacyPreservingAccount {
|
||||
identifier: _,
|
||||
}
|
||||
| Self::PrivatePda { .. }
|
||||
| Self::PrivateShared { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -111,6 +121,7 @@ impl AccountManager {
|
||||
nsk: None,
|
||||
npk,
|
||||
identifier,
|
||||
is_pda: false,
|
||||
vpk,
|
||||
pre_state: auth_acc,
|
||||
proof: None,
|
||||
@ -130,6 +141,16 @@ impl AccountManager {
|
||||
let pre =
|
||||
private_pda_preparation(wallet, nsk, npk, vpk, &program_id, &seed).await?;
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateShared {
|
||||
nsk,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
} => {
|
||||
let pre = private_shared_preparation(wallet, nsk, npk, vpk, identifier).await?;
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
};
|
||||
@ -184,22 +205,17 @@ impl AccountManager {
|
||||
.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::Private(pre) if pre.is_pda => 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::Private(pre) => match (pre.nsk, pre.proof.clone()) {
|
||||
(Some(nsk), Some(membership_proof)) => {
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
@ -249,6 +265,7 @@ struct AccountPreparedData {
|
||||
nsk: Option<NullifierSecretKey>,
|
||||
npk: NullifierPublicKey,
|
||||
identifier: Identifier,
|
||||
is_pda: bool,
|
||||
vpk: ViewingPublicKey,
|
||||
pre_state: AccountWithMetadata,
|
||||
proof: Option<MembershipProof>,
|
||||
@ -294,6 +311,7 @@ async fn private_acc_preparation(
|
||||
nsk: Some(nsk),
|
||||
npk: from_npk,
|
||||
identifier: from_identifier,
|
||||
is_pda: false,
|
||||
vpk: from_vpk,
|
||||
pre_state: sender_pre,
|
||||
proof,
|
||||
@ -317,7 +335,7 @@ async fn private_pda_preparation(
|
||||
let acc = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.pda_accounts
|
||||
.shared_accounts
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
@ -347,6 +365,7 @@ async fn private_pda_preparation(
|
||||
nsk: exists.then_some(nsk),
|
||||
npk,
|
||||
identifier: u128::MAX,
|
||||
is_pda: true,
|
||||
vpk,
|
||||
pre_state,
|
||||
proof,
|
||||
@ -354,3 +373,66 @@ async fn private_pda_preparation(
|
||||
epk,
|
||||
})
|
||||
}
|
||||
|
||||
async fn private_shared_preparation(
|
||||
wallet: &WalletCore,
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let account_id = nssa::AccountId::from((&npk, identifier));
|
||||
|
||||
let acc = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.shared_accounts
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let exists = acc != nssa_core::account::Account::default();
|
||||
let pre_state = AccountWithMetadata::new(acc, exists, (&npk, identifier));
|
||||
|
||||
let proof = if exists {
|
||||
wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
} else {
|
||||
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,
|
||||
identifier,
|
||||
is_pda: false,
|
||||
vpk,
|
||||
pre_state,
|
||||
proof,
|
||||
ssk,
|
||||
epk,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn private_shared_is_private() {
|
||||
let acc = PrivacyPreservingAccount::PrivateShared {
|
||||
nsk: [0; 32],
|
||||
npk: NullifierPublicKey([1; 32]),
|
||||
vpk: ViewingPublicKey::from_scalar([2; 32]),
|
||||
identifier: 42,
|
||||
};
|
||||
assert!(acc.is_private());
|
||||
assert!(!acc.is_public());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user