use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use super::{InputNote, ToInputNoteCommitments};
use crate::account::Account;
use crate::account::delta::AccountUpdateDetails;
use crate::asset::FungibleAsset;
use crate::block::BlockNumber;
use crate::note::NoteHeader;
use crate::transaction::{
AccountId,
InputNotes,
Nullifier,
OutputNote,
OutputNotes,
TransactionId,
};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::vm::ExecutionProof;
use crate::{ACCOUNT_UPDATE_MAX_SIZE, ProvenTransactionError, Word};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProvenTransaction {
id: TransactionId,
account_update: TxAccountUpdate,
input_notes: InputNotes<InputNoteCommitment>,
output_notes: OutputNotes,
ref_block_num: BlockNumber,
ref_block_commitment: Word,
fee: FungibleAsset,
expiration_block_num: BlockNumber,
proof: ExecutionProof,
}
impl ProvenTransaction {
pub fn id(&self) -> TransactionId {
self.id
}
pub fn account_id(&self) -> AccountId {
self.account_update.account_id()
}
pub fn account_update(&self) -> &TxAccountUpdate {
&self.account_update
}
pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
&self.input_notes
}
pub fn output_notes(&self) -> &OutputNotes {
&self.output_notes
}
pub fn proof(&self) -> &ExecutionProof {
&self.proof
}
pub fn ref_block_num(&self) -> BlockNumber {
self.ref_block_num
}
pub fn ref_block_commitment(&self) -> Word {
self.ref_block_commitment
}
pub fn fee(&self) -> FungibleAsset {
self.fee
}
pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
self.input_notes.iter().filter_map(|note| note.header())
}
pub fn expiration_block_num(&self) -> BlockNumber {
self.expiration_block_num
}
pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
self.input_notes.iter().map(InputNoteCommitment::nullifier)
}
fn validate(mut self) -> Result<Self, ProvenTransactionError> {
if self.account_update.initial_state_commitment()
== self.account_update.final_state_commitment()
&& self.input_notes.commitment().is_empty()
{
return Err(ProvenTransactionError::EmptyTransaction);
}
match &mut self.account_update.details {
AccountUpdateDetails::Private => (),
AccountUpdateDetails::Delta(post_fee_account_delta) => {
post_fee_account_delta.vault_mut().add_asset(self.fee.into()).map_err(|err| {
ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err))
})?;
let expected_commitment = self.account_update.account_delta_commitment;
let actual_commitment = post_fee_account_delta.to_commitment();
if expected_commitment != actual_commitment {
return Err(ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(
format!(
"expected account delta commitment {expected_commitment} but found {actual_commitment}"
),
)));
}
post_fee_account_delta.vault_mut().remove_asset(self.fee.into()).map_err(
|err| ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)),
)?;
},
}
Ok(self)
}
}
impl Serializable for ProvenTransaction {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.account_update.write_into(target);
self.input_notes.write_into(target);
self.output_notes.write_into(target);
self.ref_block_num.write_into(target);
self.ref_block_commitment.write_into(target);
self.fee.write_into(target);
self.expiration_block_num.write_into(target);
self.proof.write_into(target);
}
}
impl Deserializable for ProvenTransaction {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let account_update = TxAccountUpdate::read_from(source)?;
let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
let output_notes = OutputNotes::read_from(source)?;
let ref_block_num = BlockNumber::read_from(source)?;
let ref_block_commitment = Word::read_from(source)?;
let fee = FungibleAsset::read_from(source)?;
let expiration_block_num = BlockNumber::read_from(source)?;
let proof = ExecutionProof::read_from(source)?;
let id = TransactionId::new(
account_update.initial_state_commitment(),
account_update.final_state_commitment(),
input_notes.commitment(),
output_notes.commitment(),
);
let proven_transaction = Self {
id,
account_update,
input_notes,
output_notes,
ref_block_num,
ref_block_commitment,
fee,
expiration_block_num,
proof,
};
proven_transaction
.validate()
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[derive(Clone, Debug)]
pub struct ProvenTransactionBuilder {
account_id: AccountId,
initial_account_commitment: Word,
final_account_commitment: Word,
account_delta_commitment: Word,
account_update_details: AccountUpdateDetails,
input_notes: Vec<InputNoteCommitment>,
output_notes: Vec<OutputNote>,
ref_block_num: BlockNumber,
ref_block_commitment: Word,
fee: FungibleAsset,
expiration_block_num: BlockNumber,
proof: ExecutionProof,
}
impl ProvenTransactionBuilder {
#[allow(clippy::too_many_arguments)]
pub fn new(
account_id: AccountId,
initial_account_commitment: Word,
final_account_commitment: Word,
account_delta_commitment: Word,
ref_block_num: BlockNumber,
ref_block_commitment: Word,
fee: FungibleAsset,
expiration_block_num: BlockNumber,
proof: ExecutionProof,
) -> Self {
Self {
account_id,
initial_account_commitment,
final_account_commitment,
account_delta_commitment,
account_update_details: AccountUpdateDetails::Private,
input_notes: Vec::new(),
output_notes: Vec::new(),
ref_block_num,
ref_block_commitment,
fee,
expiration_block_num,
proof,
}
}
pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
self.account_update_details = details;
self
}
pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<InputNoteCommitment>,
{
self.input_notes.extend(notes.into_iter().map(|note| note.into()));
self
}
pub fn add_output_notes<T>(mut self, notes: T) -> Self
where
T: IntoIterator<Item = OutputNote>,
{
self.output_notes.extend(notes);
self
}
pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
let input_notes =
InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
let output_notes = OutputNotes::new(self.output_notes)
.map_err(ProvenTransactionError::OutputNotesError)?;
let id = TransactionId::new(
self.initial_account_commitment,
self.final_account_commitment,
input_notes.commitment(),
output_notes.commitment(),
);
let account_update = TxAccountUpdate::new(
self.account_id,
self.initial_account_commitment,
self.final_account_commitment,
self.account_delta_commitment,
self.account_update_details,
)?;
let proven_transaction = ProvenTransaction {
id,
account_update,
input_notes,
output_notes,
ref_block_num: self.ref_block_num,
ref_block_commitment: self.ref_block_commitment,
fee: self.fee,
expiration_block_num: self.expiration_block_num,
proof: self.proof,
};
proven_transaction.validate()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxAccountUpdate {
account_id: AccountId,
init_state_commitment: Word,
final_state_commitment: Word,
account_delta_commitment: Word,
details: AccountUpdateDetails,
}
impl TxAccountUpdate {
pub fn new(
account_id: AccountId,
init_state_commitment: Word,
final_state_commitment: Word,
account_delta_commitment: Word,
details: AccountUpdateDetails,
) -> Result<Self, ProvenTransactionError> {
let account_update = Self {
account_id,
init_state_commitment,
final_state_commitment,
account_delta_commitment,
details,
};
let account_update_size = account_update.details.get_size_hint();
if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
account_id,
update_size: account_update_size,
});
}
if account_id.is_private() {
if account_update.details.is_private() {
return Ok(account_update);
} else {
return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id));
}
}
match account_update.details() {
AccountUpdateDetails::Private => {
return Err(ProvenTransactionError::PublicStateAccountMissingDetails(
account_update.account_id(),
));
},
AccountUpdateDetails::Delta(delta) => {
let is_new_account = account_update.initial_state_commitment().is_empty();
if is_new_account {
let account = Account::try_from(delta).map_err(|err| {
ProvenTransactionError::NewPublicStateAccountRequiresFullStateDelta {
id: delta.id(),
source: err,
}
})?;
if account.id() != account_id {
return Err(ProvenTransactionError::AccountIdMismatch {
tx_account_id: account_id,
details_account_id: account.id(),
});
}
if account.commitment() != account_update.final_state_commitment {
return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
tx_final_commitment: account_update.final_state_commitment,
details_commitment: account.commitment(),
});
}
}
},
}
Ok(account_update)
}
pub fn account_id(&self) -> AccountId {
self.account_id
}
pub fn initial_state_commitment(&self) -> Word {
self.init_state_commitment
}
pub fn final_state_commitment(&self) -> Word {
self.final_state_commitment
}
pub fn account_delta_commitment(&self) -> Word {
self.account_delta_commitment
}
pub fn details(&self) -> &AccountUpdateDetails {
&self.details
}
pub fn is_private(&self) -> bool {
self.details.is_private()
}
}
impl Serializable for TxAccountUpdate {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.account_id.write_into(target);
self.init_state_commitment.write_into(target);
self.final_state_commitment.write_into(target);
self.account_delta_commitment.write_into(target);
self.details.write_into(target);
}
}
impl Deserializable for TxAccountUpdate {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let account_id = AccountId::read_from(source)?;
let init_state_commitment = Word::read_from(source)?;
let final_state_commitment = Word::read_from(source)?;
let account_delta_commitment = Word::read_from(source)?;
let details = AccountUpdateDetails::read_from(source)?;
Self::new(
account_id,
init_state_commitment,
final_state_commitment,
account_delta_commitment,
details,
)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputNoteCommitment {
nullifier: Nullifier,
header: Option<NoteHeader>,
}
impl InputNoteCommitment {
pub fn nullifier(&self) -> Nullifier {
self.nullifier
}
pub fn header(&self) -> Option<&NoteHeader> {
self.header.as_ref()
}
pub fn is_authenticated(&self) -> bool {
self.header.is_none()
}
}
impl From<InputNote> for InputNoteCommitment {
fn from(note: InputNote) -> Self {
Self::from(¬e)
}
}
impl From<&InputNote> for InputNoteCommitment {
fn from(note: &InputNote) -> Self {
match note {
InputNote::Authenticated { note, .. } => Self {
nullifier: note.nullifier(),
header: None,
},
InputNote::Unauthenticated { note } => Self {
nullifier: note.nullifier(),
header: Some(*note.header()),
},
}
}
}
impl From<Nullifier> for InputNoteCommitment {
fn from(nullifier: Nullifier) -> Self {
Self { nullifier, header: None }
}
}
impl ToInputNoteCommitments for InputNoteCommitment {
fn nullifier(&self) -> Nullifier {
self.nullifier
}
fn note_commitment(&self) -> Option<Word> {
self.header.map(|header| header.commitment())
}
}
impl Serializable for InputNoteCommitment {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.nullifier.write_into(target);
self.header.write_into(target);
}
}
impl Deserializable for InputNoteCommitment {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let nullifier = Nullifier::read_from(source)?;
let header = <Option<NoteHeader>>::read_from(source)?;
Ok(Self { nullifier, header })
}
}
#[cfg(test)]
mod tests {
use alloc::collections::BTreeMap;
use anyhow::Context;
use miden_core::utils::Deserializable;
use miden_verifier::ExecutionProof;
use winter_rand_utils::rand_value;
use super::ProvenTransaction;
use crate::account::delta::AccountUpdateDetails;
use crate::account::{
Account,
AccountDelta,
AccountId,
AccountIdVersion,
AccountStorageDelta,
AccountStorageMode,
AccountType,
AccountVaultDelta,
StorageMapDelta,
};
use crate::asset::FungibleAsset;
use crate::block::BlockNumber;
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
};
use crate::testing::add_component::AddComponent;
use crate::testing::noop_auth_component::NoopAuthComponent;
use crate::transaction::{ProvenTransactionBuilder, TxAccountUpdate};
use crate::utils::Serializable;
use crate::{
ACCOUNT_UPDATE_MAX_SIZE,
EMPTY_WORD,
LexicographicWord,
ONE,
ProvenTransactionError,
Word,
};
fn check_if_sync<T: Sync>() {}
fn check_if_send<T: Send>() {}
#[test]
fn test_proven_transaction_is_sync() {
check_if_sync::<ProvenTransaction>();
}
#[test]
fn test_proven_transaction_is_send() {
check_if_send::<ProvenTransaction>();
}
#[test]
fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> {
let account = Account::builder([9; 32])
.account_type(AccountType::RegularAccountUpdatableCode)
.storage_mode(AccountStorageMode::Public)
.with_auth_component(NoopAuthComponent)
.with_component(AddComponent)
.build_existing()?;
let delta = AccountDelta::try_from(account.clone())?;
let details = AccountUpdateDetails::Delta(delta);
TxAccountUpdate::new(
account.id(),
account.commitment(),
account.commitment(),
Word::empty(),
details,
)?;
Ok(())
}
#[test]
fn account_update_size_limit_exceeded() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let mut map = BTreeMap::new();
let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
for _ in 0..required_entries {
map.insert(LexicographicWord::new(rand_value::<Word>()), rand_value::<Word>());
}
let storage_delta = StorageMapDelta::new(map);
let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE)
.unwrap();
let details = AccountUpdateDetails::Delta(delta);
let details_size = details.get_size_hint();
let err = TxAccountUpdate::new(
AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
EMPTY_WORD,
EMPTY_WORD,
EMPTY_WORD,
details,
)
.unwrap_err();
assert!(
matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
);
}
#[test]
fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
let account_id = AccountId::dummy(
[1; 15],
AccountIdVersion::Version0,
AccountType::FungibleFaucet,
AccountStorageMode::Private,
);
let initial_account_commitment =
[2; 32].try_into().expect("failed to create initial account commitment");
let final_account_commitment =
[3; 32].try_into().expect("failed to create final account commitment");
let account_delta_commitment =
[4; 32].try_into().expect("failed to create account delta commitment");
let ref_block_num = BlockNumber::from(1);
let ref_block_commitment = Word::empty();
let expiration_block_num = BlockNumber::from(2);
let proof = ExecutionProof::new_dummy();
let tx = ProvenTransactionBuilder::new(
account_id,
initial_account_commitment,
final_account_commitment,
account_delta_commitment,
ref_block_num,
ref_block_commitment,
FungibleAsset::mock(42).unwrap_fungible(),
expiration_block_num,
proof,
)
.build()
.context("failed to build proven transaction")?;
let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
assert_eq!(tx, deserialized);
Ok(())
}
}