mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-17 13:20:17 +00:00
Merge 7a40979c7f5b04a46a8665a3d07cd3a300dcae63 into 390f4b48d982511883eec8628a9742976f8de8d3
This commit is contained in:
commit
9d77b6f59e
123
integration_tests/tests/auth_transfer/build.rs
Normal file
123
integration_tests/tests/auth_transfer/build.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, verify_commitment_is_in_state,
|
||||
};
|
||||
use lee::{privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{AccountIdentity, poller::TxPoller};
|
||||
|
||||
#[test]
|
||||
async fn public_build_requires_explicit_submission() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
let (account_id, _) = ctx.wallet_mut().create_new_account_public(None);
|
||||
let before_build = ctx.sequencer_client().get_account(account_id).await?;
|
||||
|
||||
let transaction = ctx
|
||||
.wallet()
|
||||
.build_pub_tx(
|
||||
vec![AccountIdentity::Public(account_id)],
|
||||
Program::serialize_instruction(AuthTransferInstruction::Initialize)?,
|
||||
programs::authenticated_transfer().id(),
|
||||
)
|
||||
.await?;
|
||||
let transaction = LeeTransaction::Public(transaction);
|
||||
let transaction_hash = transaction.hash();
|
||||
|
||||
// Let a sequencer block interval pass: accidental submission would index the transaction.
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
assert!(
|
||||
ctx.sequencer_client()
|
||||
.get_transaction(transaction_hash)
|
||||
.await?
|
||||
.is_none(),
|
||||
"build_pub_tx must not submit the transaction"
|
||||
);
|
||||
|
||||
let after_build = ctx.sequencer_client().get_account(account_id).await?;
|
||||
assert_eq!(after_build.balance, before_build.balance);
|
||||
assert_eq!(after_build.nonce, before_build.nonce);
|
||||
assert_eq!(after_build.program_owner, before_build.program_owner);
|
||||
|
||||
let hash = ctx.wallet().submit_transaction(transaction).await?;
|
||||
assert_eq!(hash, transaction_hash);
|
||||
let (submitted, _) = TxPoller::new(ctx.wallet().config(), ctx.sequencer_client().clone())
|
||||
.poll_tx(hash)
|
||||
.await?;
|
||||
assert!(matches!(submitted, LeeTransaction::Public(_)));
|
||||
|
||||
let after_submit = ctx.sequencer_client().get_account(account_id).await?;
|
||||
assert_eq!(
|
||||
after_submit.program_owner,
|
||||
programs::authenticated_transfer().id()
|
||||
);
|
||||
assert_eq!(after_submit.nonce.0, before_build.nonce.0 + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn private_build_requires_explicit_submission() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
let from = ctx.existing_private_accounts()[0];
|
||||
let to = ctx.existing_private_accounts()[1];
|
||||
let from_commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(from)
|
||||
.context("sender commitment should exist")?;
|
||||
let to_commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(to)
|
||||
.context("recipient commitment should exist")?;
|
||||
assert!(verify_commitment_is_in_state(from_commitment.clone(), ctx.sequencer_client()).await);
|
||||
assert!(verify_commitment_is_in_state(to_commitment.clone(), ctx.sequencer_client()).await);
|
||||
let program: ProgramWithDependencies = programs::authenticated_transfer().into();
|
||||
|
||||
let (transaction, shared_secrets) = ctx
|
||||
.wallet()
|
||||
.build_privacy_preserving_tx(
|
||||
vec![
|
||||
AccountIdentity::PrivateOwned(from),
|
||||
AccountIdentity::PrivateOwned(to),
|
||||
],
|
||||
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount: 100 })?,
|
||||
&program,
|
||||
)
|
||||
.await?;
|
||||
let transaction = LeeTransaction::PrivacyPreserving(transaction);
|
||||
let transaction_hash = transaction.hash();
|
||||
|
||||
assert_eq!(shared_secrets.len(), 2);
|
||||
|
||||
// Let a sequencer block interval pass: accidental submission would index the transaction.
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
assert!(
|
||||
ctx.sequencer_client()
|
||||
.get_transaction(transaction_hash)
|
||||
.await?
|
||||
.is_none(),
|
||||
"build_privacy_preserving_tx must not submit the transaction"
|
||||
);
|
||||
assert!(verify_commitment_is_in_state(from_commitment, ctx.sequencer_client()).await);
|
||||
assert!(verify_commitment_is_in_state(to_commitment, ctx.sequencer_client()).await);
|
||||
|
||||
let hash = ctx.wallet().submit_transaction(transaction).await?;
|
||||
assert_eq!(hash, transaction_hash);
|
||||
let (submitted, _) = TxPoller::new(ctx.wallet().config(), ctx.sequencer_client().clone())
|
||||
.poll_tx(hash)
|
||||
.await?;
|
||||
let LeeTransaction::PrivacyPreserving(submitted_transaction) = submitted else {
|
||||
anyhow::bail!("expected a privacy-preserving transaction");
|
||||
};
|
||||
|
||||
assert_eq!(submitted_transaction.message.new_commitments.len(), 2);
|
||||
for commitment in submitted_transaction.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -4,5 +4,6 @@
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
mod build;
|
||||
mod private;
|
||||
mod public;
|
||||
|
||||
@ -129,6 +129,31 @@ impl fmt::Debug for AccountIdentity {
|
||||
}
|
||||
|
||||
impl AccountIdentity {
|
||||
/// Returns the account ID represented by this identity.
|
||||
#[must_use]
|
||||
pub fn account_id(&self) -> AccountId {
|
||||
match self {
|
||||
Self::Public(account_id)
|
||||
| Self::PublicNoSign(account_id)
|
||||
| Self::PrivateOwned(account_id)
|
||||
| Self::PrivatePdaOwned(account_id)
|
||||
| Self::PublicKeycard { account_id, .. }
|
||||
| Self::PrivatePdaForeign { account_id, .. }
|
||||
| Self::PrivatePdaShared { account_id, .. } => *account_id,
|
||||
Self::PrivateForeign {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
}
|
||||
| Self::PrivateShared {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
..
|
||||
} => AccountId::from((npk, vpk, *identifier)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key
|
||||
/// lookup.
|
||||
@ -665,4 +690,78 @@ mod tests {
|
||||
assert!(acc.is_private());
|
||||
assert!(!acc.is_public());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_id_projects_every_identity_variant() {
|
||||
let explicit_id = AccountId::new([7; 32]);
|
||||
let npk = NullifierPublicKey([1; 32]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]);
|
||||
let identifier = 42;
|
||||
let derived_id = AccountId::from((&npk, &vpk, identifier));
|
||||
|
||||
assert_eq!(
|
||||
AccountIdentity::Public(explicit_id).account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PublicNoSign(explicit_id).account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: explicit_id,
|
||||
key_path: "m/0".to_owned(),
|
||||
}
|
||||
.account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivateOwned(explicit_id).account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk,
|
||||
vpk: vpk.clone(),
|
||||
identifier,
|
||||
}
|
||||
.account_id(),
|
||||
derived_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivatePdaOwned(explicit_id).account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivatePdaForeign {
|
||||
account_id: explicit_id,
|
||||
npk,
|
||||
vpk: vpk.clone(),
|
||||
identifier,
|
||||
}
|
||||
.account_id(),
|
||||
explicit_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivateShared {
|
||||
nsk: [4; 32],
|
||||
npk,
|
||||
vpk: vpk.clone(),
|
||||
identifier,
|
||||
}
|
||||
.account_id(),
|
||||
derived_id
|
||||
);
|
||||
assert_eq!(
|
||||
AccountIdentity::PrivatePdaShared {
|
||||
account_id: explicit_id,
|
||||
nsk: [4; 32],
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
}
|
||||
.account_id(),
|
||||
explicit_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ use common::{HashType, transaction::LeeTransaction};
|
||||
use config::WalletConfig;
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, ProgramId,
|
||||
Account, AccountId, PrivacyPreservingTransaction, ProgramId, PublicTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::ProgramWithDependencies,
|
||||
message::{EncryptedAccountData, Message},
|
||||
@ -474,6 +474,19 @@ impl WalletCore {
|
||||
Some(Commitment::new(&account_id, account))
|
||||
}
|
||||
|
||||
/// Submit a fully built transaction without polling for its result.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ExecutionFailureKind::SequencerClientError`] when the sequencer rejects the
|
||||
/// submission or cannot be reached.
|
||||
pub async fn submit_transaction(
|
||||
&self,
|
||||
transaction: LeeTransaction,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
Ok(self.sequencer_client.send_transaction(transaction).await?)
|
||||
}
|
||||
|
||||
pub async fn get_proofs_and_root(
|
||||
&self,
|
||||
commitments: Vec<Commitment>,
|
||||
@ -563,7 +576,31 @@ impl WalletCore {
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
|
||||
let (transaction, shared_secrets) = self
|
||||
.build_privacy_preserving_tx(accounts, instruction_data, program)
|
||||
.await?;
|
||||
let hash = self
|
||||
.submit_transaction(LeeTransaction::PrivacyPreserving(transaction))
|
||||
.await?;
|
||||
|
||||
Ok((hash, shared_secrets))
|
||||
}
|
||||
|
||||
/// Build a privacy-preserving transaction without submitting it.
|
||||
///
|
||||
/// Wallet may read account state, prepare private inputs, prove, and sign while building. It
|
||||
/// does not submit, poll, print, or persist Wallet state.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an [`ExecutionFailureKind`] when account preparation, proving, or signing fails.
|
||||
pub async fn build_privacy_preserving_tx(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
) -> Result<(PrivacyPreservingTransaction, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
self.build_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@ -576,6 +613,28 @@ impl WalletCore {
|
||||
program: &ProgramWithDependencies,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
let (transaction, shared_secrets) = self
|
||||
.build_privacy_preserving_tx_with_pre_check(
|
||||
accounts,
|
||||
instruction_data,
|
||||
program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await?;
|
||||
let hash = self
|
||||
.submit_transaction(LeeTransaction::PrivacyPreserving(transaction))
|
||||
.await?;
|
||||
|
||||
Ok((hash, shared_secrets))
|
||||
}
|
||||
|
||||
async fn build_privacy_preserving_tx_with_pre_check(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<(PrivacyPreservingTransaction, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
let acc_manager = account_manager::AccountManager::new(self, accounts).await?;
|
||||
|
||||
let pre_states = acc_manager.pre_states();
|
||||
@ -620,12 +679,7 @@ impl WalletCore {
|
||||
.map(|keys| keys.ssk)
|
||||
.collect();
|
||||
|
||||
Ok((
|
||||
self.sequencer_client
|
||||
.send_transaction(LeeTransaction::PrivacyPreserving(tx))
|
||||
.await?,
|
||||
shared_secrets,
|
||||
))
|
||||
Ok((tx, shared_secrets))
|
||||
}
|
||||
|
||||
pub async fn send_pub_tx(
|
||||
@ -634,7 +688,28 @@ impl WalletCore {
|
||||
instruction_data: InstructionData,
|
||||
program_id: ProgramId,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
self.send_pub_tx_with_pre_check(accounts, instruction_data, program_id, |_| Ok(()))
|
||||
let transaction = self
|
||||
.build_pub_tx(accounts, instruction_data, program_id)
|
||||
.await?;
|
||||
self.submit_transaction(LeeTransaction::Public(transaction))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Build a public transaction without submitting it.
|
||||
///
|
||||
/// Wallet may read account state and sign while building. It does not submit, poll, print,
|
||||
/// or persist Wallet state.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an [`ExecutionFailureKind`] when account preparation or signing fails.
|
||||
pub async fn build_pub_tx(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program_id: ProgramId,
|
||||
) -> Result<PublicTransaction, ExecutionFailureKind> {
|
||||
self.build_pub_tx_with_pre_check(accounts, instruction_data, program_id, |_| Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -645,6 +720,20 @@ impl WalletCore {
|
||||
program_id: ProgramId,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let transaction = self
|
||||
.build_pub_tx_with_pre_check(accounts, instruction_data, program_id, tx_pre_check)
|
||||
.await?;
|
||||
self.submit_transaction(LeeTransaction::Public(transaction))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_pub_tx_with_pre_check(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program_id: ProgramId,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<PublicTransaction, ExecutionFailureKind> {
|
||||
// Public transaction, all accounts must be public
|
||||
if accounts.iter().any(AccountIdentity::is_private) {
|
||||
return Err(ExecutionFailureKind::TransactionBuildError(
|
||||
@ -684,10 +773,7 @@ impl WalletCore {
|
||||
|
||||
let tx = lee::public_transaction::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self
|
||||
.sequencer_client
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await?)
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn sync_to_latest_block(&mut self) -> Result<u64> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user