mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-02 15:20:01 +00:00
feat: lez core base
This commit is contained in:
parent
4079b0c9c8
commit
3b479a4ad2
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -4633,6 +4633,24 @@ dependencies = [
|
||||
"tachys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lez_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"env_logger",
|
||||
"hex",
|
||||
"key_protocol",
|
||||
"log",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"sequencer_service_rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@ -10143,6 +10161,7 @@ dependencies = [
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"key_protocol",
|
||||
"lez_core",
|
||||
"log",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
|
||||
@ -40,7 +40,8 @@ members = [
|
||||
"examples/program_deployment/methods",
|
||||
"examples/program_deployment/methods/guest",
|
||||
"testnet_initial_state",
|
||||
"indexer/ffi",
|
||||
"indexer/ffi",
|
||||
"lez_core",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -73,6 +74,7 @@ faucet_core = { path = "programs/faucet/core" }
|
||||
vault_core = { path = "programs/vault/core" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
testnet_initial_state = { path = "testnet_initial_state" }
|
||||
lez_core = { path = "lez_core" }
|
||||
|
||||
tokio = { version = "1.50", features = [
|
||||
"net",
|
||||
|
||||
23
lez_core/Cargo.toml
Normal file
23
lez_core/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "lez_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
nssa.workspace = true
|
||||
common.workspace = true
|
||||
key_protocol.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
21
lez_core/src/lib.rs
Normal file
21
lez_core/src/lib.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use nssa::AccountId;
|
||||
|
||||
pub mod privacy_preserving_tx;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ExecutionFailureKind {
|
||||
#[error("Failed to get data from sequencer")]
|
||||
SequencerError(#[source] anyhow::Error),
|
||||
#[error("Inputs amounts does not match outputs")]
|
||||
AmountMismatchError,
|
||||
#[error("Accounts key not found")]
|
||||
KeyNotFoundError,
|
||||
#[error("Sequencer client error")]
|
||||
SequencerClientError(#[from] sequencer_service_rpc::ClientError),
|
||||
#[error("Can not pay for operation")]
|
||||
InsufficientFundsError,
|
||||
#[error("Account {0} data is invalid")]
|
||||
AccountDataError(AccountId),
|
||||
#[error("Failed to build transaction: {0}")]
|
||||
TransactionBuildError(#[from] nssa::error::NssaError),
|
||||
}
|
||||
315
lez_core/src/privacy_preserving_tx.rs
Normal file
315
lez_core/src/privacy_preserving_tx.rs
Normal file
@ -0,0 +1,315 @@
|
||||
use anyhow::Result;
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{AccountId, PrivateKey};
|
||||
use nssa_core::{
|
||||
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
|
||||
SharedSecretKey,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PrivacyPreservingAccount {
|
||||
Public(AccountId),
|
||||
PrivateOwned(AccountId),
|
||||
PrivateForeign {
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// An owned private PDA: wallet holds the nsk/npk; `account_id` was derived via
|
||||
/// [`AccountId::for_private_pda`].
|
||||
PrivatePdaOwned(AccountId),
|
||||
/// A foreign private PDA: wallet knows the recipient's npk/vpk but not their nsk.
|
||||
/// Uses a default (uninitialised) account.
|
||||
PrivatePdaForeign {
|
||||
account_id: AccountId,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// A shared regular private account with externally-provided keys (e.g. from GMS).
|
||||
/// Uses standard `AccountId = from((&npk, identifier))` with authorized/unauthorized private
|
||||
/// paths. Works with `authenticated_transfer` and all existing programs out of the box.
|
||||
PrivateShared {
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// A shared private PDA with externally-provided keys (e.g. from GMS).
|
||||
/// `account_id` was derived via [`AccountId::for_private_pda`].
|
||||
PrivatePdaShared {
|
||||
account_id: AccountId,
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl PrivacyPreservingAccount {
|
||||
#[must_use]
|
||||
pub const fn is_public(&self) -> bool {
|
||||
matches!(&self, Self::Public(_))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_private(&self) -> bool {
|
||||
matches!(
|
||||
&self,
|
||||
Self::PrivateOwned(_)
|
||||
| Self::PrivateForeign { .. }
|
||||
| Self::PrivatePdaOwned(_)
|
||||
| Self::PrivatePdaForeign { .. }
|
||||
| Self::PrivateShared { .. }
|
||||
| Self::PrivatePdaShared { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrivateAccountKeys {
|
||||
pub npk: NullifierPublicKey,
|
||||
pub ssk: SharedSecretKey,
|
||||
pub vpk: ViewingPublicKey,
|
||||
pub epk: EphemeralPublicKey,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Public {
|
||||
account: AccountWithMetadata,
|
||||
sk: Option<PrivateKey>,
|
||||
},
|
||||
Private(AccountPreparedData),
|
||||
}
|
||||
|
||||
pub struct AccountManager {
|
||||
states: Vec<State>,
|
||||
}
|
||||
|
||||
pub trait AccountManager<T> {
|
||||
pub async fn new(
|
||||
account_source: &T,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
) -> Result<Self, ExecutionFailureKind>;
|
||||
|
||||
pub fn states(&self) -> &[State];
|
||||
|
||||
pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
|
||||
self.states()
|
||||
.iter()
|
||||
.map(|state| match state {
|
||||
State::Public { account, .. } => account.clone(),
|
||||
State::Private(pre) => pre.pre_state.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_account_nonces(&self) -> Vec<Nonce> {
|
||||
self.states()
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce),
|
||||
State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn private_account_keys(&self) -> Vec<PrivateAccountKeys> {
|
||||
self.states()
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Private(pre) => Some(PrivateAccountKeys {
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
vpk: pre.vpk.clone(),
|
||||
epk: pre.epk.clone(),
|
||||
}),
|
||||
State::Public { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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.is_pda => match (pre.nsk, pre.proof.clone()) {
|
||||
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: pre.ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier: pre.identifier,
|
||||
},
|
||||
_ => InputAccountIdentity::PrivatePdaInit {
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
identifier: pre.identifier,
|
||||
},
|
||||
},
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn public_account_ids(&self) -> Vec<AccountId> {
|
||||
self.states()
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, .. } => Some(account.account_id),
|
||||
State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_account_auth(&self) -> Vec<&PrivateKey> {
|
||||
self.states()
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { sk, .. } => sk.as_ref(),
|
||||
State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountPreparedData {
|
||||
nsk: Option<NullifierSecretKey>,
|
||||
npk: NullifierPublicKey,
|
||||
identifier: Identifier,
|
||||
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,
|
||||
/// True when this account is a private PDA (owned or foreign). Used by `account_identities()`
|
||||
/// to select `PrivatePdaInit`/`PrivatePdaUpdate` rather than the standalone private variants.
|
||||
is_pda: bool,
|
||||
}
|
||||
|
||||
async fn private_key_tree_acc_preparation(
|
||||
wallet: &WalletCore,
|
||||
account_id: AccountId,
|
||||
is_pda: bool,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let Some(from_acc) = wallet.storage.key_chain().private_account(account_id) else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let from_identifier = from_acc.kind.identifier();
|
||||
let from_keys = &from_acc.key_chain;
|
||||
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.clone();
|
||||
|
||||
// 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.account.clone(), true, account_id);
|
||||
|
||||
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,
|
||||
is_pda,
|
||||
})
|
||||
}
|
||||
|
||||
async fn private_shared_acc_preparation(
|
||||
wallet: &WalletCore,
|
||||
account_id: AccountId,
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
is_pda: bool,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let acc = wallet
|
||||
.storage()
|
||||
.key_chain()
|
||||
.shared_private_account(account_id)
|
||||
.map(|e| e.account.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let pre_state = AccountWithMetadata::new(acc, true, account_id);
|
||||
|
||||
let proof = wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap_or(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: Some(nsk),
|
||||
npk,
|
||||
identifier,
|
||||
vpk,
|
||||
pre_state,
|
||||
proof,
|
||||
ssk,
|
||||
epk,
|
||||
is_pda,
|
||||
})
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,9 @@ token_core.workspace = true
|
||||
amm_core.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
ata_core.workspace = true
|
||||
bip39.workspace = true
|
||||
lez_core.workspace = true
|
||||
|
||||
bip39.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@ -14,6 +14,7 @@ use bip39::Mnemonic;
|
||||
use common::{HashType, transaction::NSSATransaction};
|
||||
use config::WalletConfig;
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
pub use lez_core::ExecutionFailureKind;
|
||||
use log::info;
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction,
|
||||
@ -59,24 +60,6 @@ pub struct SharedAccountInfo {
|
||||
pub vpk: nssa_core::encryption::ViewingPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ExecutionFailureKind {
|
||||
#[error("Failed to get data from sequencer")]
|
||||
SequencerError(#[source] anyhow::Error),
|
||||
#[error("Inputs amounts does not match outputs")]
|
||||
AmountMismatchError,
|
||||
#[error("Accounts key not found")]
|
||||
KeyNotFoundError,
|
||||
#[error("Sequencer client error")]
|
||||
SequencerClientError(#[from] sequencer_service_rpc::ClientError),
|
||||
#[error("Can not pay for operation")]
|
||||
InsufficientFundsError,
|
||||
#[error("Account {0} data is invalid")]
|
||||
AccountDataError(AccountId),
|
||||
#[error("Failed to build transaction: {0}")]
|
||||
TransactionBuildError(#[from] nssa::error::NssaError),
|
||||
}
|
||||
|
||||
#[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")]
|
||||
pub struct WalletCore {
|
||||
config_path: PathBuf,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user