diff --git a/miden-tx/src/error.rs b/miden-tx/src/error.rs index 1ef463601..c00642f84 100644 --- a/miden-tx/src/error.rs +++ b/miden-tx/src/error.rs @@ -2,6 +2,7 @@ use super::{ AccountError, AccountId, AssemblyError, Digest, ExecutionError, NodeIndex, TransactionResultError, }; +use miden_objects::TransactionWitnessError; #[derive(Debug)] pub enum TransactionError { @@ -34,6 +35,13 @@ pub enum TransactionExecutorError { TransactionResultError(TransactionResultError), } +#[derive(Debug)] +pub enum TransactionProverError { + ProveTransactionProgramFailed(ExecutionError), + TransactionResultError(TransactionResultError), + CorruptTransactionWitnessConsumedNoteData(TransactionWitnessError), +} + #[derive(Debug)] pub enum DataStoreError { AccountNotFound(AccountId), diff --git a/miden-tx/src/executor/mod.rs b/miden-tx/src/executor/mod.rs index 11f83aad7..07c82b931 100644 --- a/miden-tx/src/executor/mod.rs +++ b/miden-tx/src/executor/mod.rs @@ -128,9 +128,6 @@ impl TransactionExecutor { .map_err(TransactionExecutorError::TransactionResultError) } - // HELPER METHODS - // -------------------------------------------------------------------------------------------- - /// Fetches the data required to execute the transaction from the [DataStore], compiles the /// transaction into an executable program using the [TransactionComplier], and returns a /// [PreparedTransaction]. @@ -139,7 +136,7 @@ impl TransactionExecutor { /// Returns an error if: /// - If required data can not be fetched from the [DataStore]. /// - If the transaction can not be compiled. - fn prepare_transaction( + pub fn prepare_transaction( &mut self, account_id: AccountId, block_ref: u32, diff --git a/miden-tx/src/lib.rs b/miden-tx/src/lib.rs index 890b56f3e..c02459b47 100644 --- a/miden-tx/src/lib.rs +++ b/miden-tx/src/lib.rs @@ -20,8 +20,12 @@ use data::DataStore; mod error; mod executor; pub use error::TransactionError; -use error::{DataStoreError, TransactionCompilerError, TransactionExecutorError}; +use error::{ + DataStoreError, TransactionCompilerError, TransactionExecutorError, TransactionProverError, +}; pub use executor::TransactionExecutor; +mod prover; +pub use prover::TransactionProver; #[cfg(test)] mod tests; diff --git a/miden-tx/src/prover/mod.rs b/miden-tx/src/prover/mod.rs new file mode 100644 index 000000000..c9ed7126c --- /dev/null +++ b/miden-tx/src/prover/mod.rs @@ -0,0 +1,125 @@ +use super::TransactionProverError; +use miden_objects::{ + transaction::{ + CreatedNotes, FinalAccountStub, PreparedTransaction, ProvenTransaction, TransactionWitness, + }, + TryFromVmResult, +}; +use miden_prover::{prove, ProvingOptions}; +use processor::MemAdviceProvider; + +/// The [TransactionProver] is a stateless component which is responsible for proving transactions. +/// +/// The [TransactionProver] exposes the `prove_transaction` method which takes a [TransactionWitness] and +/// produces a [ProvenTransaction]. +pub struct TransactionProver { + proof_options: ProvingOptions, +} + +impl TransactionProver { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + /// Creates a new [TransactionProver] instance. + pub fn new(proof_options: ProvingOptions) -> Self { + Self { proof_options } + } + + // TODO: Do we want this method as part of the interface or is it sufficient to just have + // `prove_transaction_witness`? Without this interface we always have to execute + // the the transaction program twice - once when constructing the witness and once + // during proving. + /// Proves the provided [PreparedTransaction] and returns a [ProvenTransaction]. + /// + /// # Errors + /// - If the transaction program cannot be proven. + /// - If the transaction result is corrupt. + pub fn prove_prepared_transaction( + &self, + transaction: PreparedTransaction, + ) -> Result { + // prove transaction program + let mut advice_provider: MemAdviceProvider = transaction.advice_provider_inputs().into(); + let (outputs, proof) = prove( + transaction.tx_program(), + transaction.stack_inputs(), + &mut advice_provider, + self.proof_options.clone(), + ) + .map_err(TransactionProverError::ProveTransactionProgramFailed)?; + + // extract transaction outputs and process transaction data + let (stack, map, store) = advice_provider.into_parts(); + let final_account_stub = + FinalAccountStub::try_from_vm_result(&outputs, &stack, &map, &store) + .map_err(TransactionProverError::TransactionResultError)?; + let created_notes = CreatedNotes::try_from_vm_result(&outputs, &stack, &map, &store) + .map_err(TransactionProverError::TransactionResultError)?; + + let (account, block_header, _chain, consumed_notes, tx_program, tx_script_root) = + transaction.into_parts(); + + Ok(ProvenTransaction::new( + account.id(), + account.hash(), + final_account_stub.0.hash(), + consumed_notes.into(), + created_notes.into(), + tx_script_root, + tx_program.hash(), + block_header.hash(), + proof, + )) + } + + /// Proves the provided [TransactionWitness] and returns a [ProvenTransaction]. + /// + /// # Errors + /// - If the consumed note data in the transaction witness is corrupt. + /// - If the transaction program cannot be proven. + /// - If the transaction result is corrupt. + pub fn prove_transaction_witness( + &self, + tx_witness: TransactionWitness, + ) -> Result { + // extract required data from the transaction witness + let stack_inputs = tx_witness.get_stack_inputs(); + let consumed_notes_info = tx_witness + .consumed_notes_info() + .map_err(TransactionProverError::CorruptTransactionWitnessConsumedNoteData)?; + let ( + account_id, + initial_account_hash, + block_hash, + _consumed_notes_hash, + tx_script_root, + tx_program, + advice_witness, + ) = tx_witness.into_parts(); + + let mut advice_provider: MemAdviceProvider = advice_witness.into(); + let (outputs, proof) = + prove(&tx_program, stack_inputs, &mut advice_provider, self.proof_options.clone()) + .map_err(TransactionProverError::ProveTransactionProgramFailed)?; + + // extract transaction outputs and process transaction data + // extract transaction outputs and process transaction data + let (stack, map, store) = advice_provider.into_parts(); + let final_account_stub = + FinalAccountStub::try_from_vm_result(&outputs, &stack, &map, &store) + .map_err(TransactionProverError::TransactionResultError)?; + let created_notes = CreatedNotes::try_from_vm_result(&outputs, &stack, &map, &store) + .map_err(TransactionProverError::TransactionResultError)?; + + Ok(ProvenTransaction::new( + account_id, + initial_account_hash, + final_account_stub.0.hash(), + consumed_notes_info, + created_notes.into(), + tx_script_root, + tx_program.hash(), + block_hash, + proof, + )) + } +} diff --git a/miden-tx/src/tests.rs b/miden-tx/src/tests.rs index 3c6879b2f..ea614f876 100644 --- a/miden-tx/src/tests.rs +++ b/miden-tx/src/tests.rs @@ -1,20 +1,22 @@ use super::{ Account, AccountId, BlockHeader, ChainMmr, DataStore, DataStoreError, Note, NoteOrigin, - TransactionExecutor, + TransactionExecutor, TransactionProver, }; use assembly::{ ast::{ModuleAst, ProgramAst}, Assembler, }; use crypto::{StarkField, ONE}; +use miden_core::ProgramInfo; use miden_objects::{ mock::{ mock_inputs, prepare_word, CHILD_ROOT_PARENT_LEAF_INDEX, CHILD_SMT_DEPTH, CHILD_STORAGE_INDEX_0, }, - transaction::{testing::FinalAccountStub, CreatedNotes}, + transaction::{CreatedNotes, FinalAccountStub}, AccountCode, TryFromVmResult, }; +use miden_prover::ProvingOptions; use processor::MemAdviceProvider; #[derive(Clone)] @@ -245,3 +247,73 @@ fn test_transaction_result_account_delta() { CHILD_STORAGE_INDEX_0 ); } + +#[test] +fn test_prove_witness() { + let data_store = MockDataStore::new(); + let mut executor = TransactionExecutor::new(data_store.clone()); + + let account_id = data_store.account.id(); + executor.load_account(account_id).unwrap(); + + let block_ref = data_store.block_header.block_num().as_int() as u32; + let note_origins = data_store + .notes + .iter() + .map(|note| note.proof().as_ref().unwrap().origin().clone()) + .collect::>(); + + // execute the transaction and get the witness + let transaction_result = executor + .execute_transaction(account_id, block_ref, ¬e_origins, None) + .unwrap(); + let witness = transaction_result.clone().into_witness(); + + // prove the transaction with the witness + let proof_options = ProvingOptions::default(); + let prover = TransactionProver::new(proof_options); + let proven_transaction = prover.prove_transaction_witness(witness); + + assert!(proven_transaction.is_ok()); +} + +#[test] +fn test_prove_and_verify_with_tx_executor() { + let data_store = MockDataStore::new(); + let mut executor = TransactionExecutor::new(data_store.clone()); + + let account_id = data_store.account.id(); + executor.load_account(account_id).unwrap(); + + let block_ref = data_store.block_header.block_num().as_int() as u32; + let note_origins = data_store + .notes + .iter() + .map(|note| note.proof().as_ref().unwrap().origin().clone()) + .collect::>(); + + // prove the transaction with the executor + let prepared_transaction = executor + .prepare_transaction(account_id, block_ref, ¬e_origins, None) + .unwrap(); + + // extract transaction data for later consumption + let program_hash = prepared_transaction.tx_program().hash(); + let kernel = prepared_transaction.tx_program().kernel().clone(); + + // prove transaction + let proof_options = ProvingOptions::default(); + let prover = TransactionProver::new(proof_options); + let proven_transaction = prover.prove_prepared_transaction(prepared_transaction).unwrap(); + + let stack_inputs = proven_transaction.build_stack_inputs(); + let stack_outputs = proven_transaction.build_stack_outputs(); + let program_info = ProgramInfo::new(program_hash, kernel); + let result = miden_verifier::verify( + program_info, + stack_inputs, + stack_outputs, + proven_transaction.proof().clone(), + ); + assert!(result.is_ok()); +} diff --git a/objects/src/notes/metadata.rs b/objects/src/notes/metadata.rs index 14c83b29b..0c90839ff 100644 --- a/objects/src/notes/metadata.rs +++ b/objects/src/notes/metadata.rs @@ -4,7 +4,7 @@ use super::{AccountId, Felt, NoteError, Word}; /// - sender is the account which created the note. /// - tag is a tag which can be used to identify the target account for the note. /// - num_assets is the number of assets in the note. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct NoteMetadata { sender: AccountId, tag: Felt, diff --git a/objects/src/transaction/consumed_notes.rs b/objects/src/transaction/consumed_notes.rs index 5b1330a38..22b7b5782 100644 --- a/objects/src/transaction/consumed_notes.rs +++ b/objects/src/transaction/consumed_notes.rs @@ -91,9 +91,16 @@ impl ToAdviceInputs for ConsumedNotes { } } +impl From for Vec { + fn from(consumed_notes: ConsumedNotes) -> Self { + consumed_notes.notes.into_iter().map(|note| note.into()).collect::>() + } +} + // CONSUMED NOTE INFO // ================================================================================================ +// TODO: Should we rename this to NoteTombstone? /// Holds information about a note that was consumed by a transaction. /// Contains: /// - nullifier: nullifier of the note that was consumed @@ -150,3 +157,9 @@ impl From for [u8; 64] { elements } } + +impl From for ConsumedNoteInfo { + fn from(note: Note) -> Self { + Self::new(note.nullifier(), note.script().hash()) + } +} diff --git a/objects/src/transaction/created_notes.rs b/objects/src/transaction/created_notes.rs index 5ed3259c0..aa9a91689 100644 --- a/objects/src/transaction/created_notes.rs +++ b/objects/src/transaction/created_notes.rs @@ -1,6 +1,6 @@ use super::{ - BTreeMap, Digest, Felt, Hasher, MerkleStore, NoteStub, StackOutputs, TransactionResultError, - TryFromVmResult, Vec, Word, WORD_SIZE, + BTreeMap, Digest, Felt, Hasher, MerkleStore, NoteEnvelope, NoteStub, StackOutputs, + TransactionResultError, TryFromVmResult, Vec, Word, WORD_SIZE, }; use miden_core::utils::group_slice_elements; use miden_lib::memory::NOTE_MEM_SIZE; @@ -101,3 +101,15 @@ pub fn generate_created_notes_stub_commitment(notes: &[NoteStub]) -> Digest { Hasher::hash_elements(&elements) } + +impl From for NoteEnvelope { + fn from(note_stub: NoteStub) -> Self { + Self::new(note_stub.hash(), *note_stub.metadata()) + } +} + +impl From for Vec { + fn from(created_notes: CreatedNotes) -> Self { + created_notes.notes.into_iter().map(|note| note.into()).collect::>() + } +} diff --git a/objects/src/transaction/mod.rs b/objects/src/transaction/mod.rs index b10de3a21..32db556ae 100644 --- a/objects/src/transaction/mod.rs +++ b/objects/src/transaction/mod.rs @@ -21,10 +21,5 @@ pub use created_notes::CreatedNotes; pub use executed_tx::ExecutedTransaction; pub use prepared_tx::PreparedTransaction; pub use proven_tx::ProvenTransaction; -pub use tx_result::TransactionResult; +pub use tx_result::{FinalAccountStub, TransactionResult}; pub use tx_witness::TransactionWitness; - -#[cfg(feature = "testing")] -pub mod testing { - pub use super::tx_result::FinalAccountStub; -} diff --git a/objects/src/transaction/proven_tx.rs b/objects/src/transaction/proven_tx.rs index debb04a5f..3e13da1e5 100644 --- a/objects/src/transaction/proven_tx.rs +++ b/objects/src/transaction/proven_tx.rs @@ -2,7 +2,9 @@ use super::{ AccountId, ConsumedNoteInfo, Digest, Felt, Hasher, NoteEnvelope, StackInputs, StackOutputs, Vec, Word, }; -use miden_verifier::{verify, ExecutionProof, ProgramInfo, VerificationError}; +use crypto::{WORD_SIZE, ZERO}; +use miden_core::stack::STACK_TOP_SIZE; +use miden_verifier::ExecutionProof; /// Resultant object of executing and proving a transaction. It contains the minimal /// amount of data needed to verify that the transaction was executed correctly. @@ -22,6 +24,7 @@ pub struct ProvenTransaction { consumed_notes: Vec, created_notes: Vec, tx_script_root: Option, + program_hash: Digest, block_ref: Digest, proof: ExecutionProof, } @@ -36,6 +39,7 @@ impl ProvenTransaction { consumed_notes: Vec, created_notes: Vec, tx_script_root: Option, + program_hash: Digest, block_ref: Digest, proof: ExecutionProof, ) -> Self { @@ -46,26 +50,60 @@ impl ProvenTransaction { consumed_notes, created_notes, tx_script_root, + program_hash, block_ref, proof, } } - /// Verify the transaction using the provided data and proof. - /// Returns the security level of the proof if the specified program was executed correctly against - /// the specified inputs and outputs. - /// - /// # Errors - /// Returns an error if the provided proof does not prove a correct execution of the program. - pub fn verify(&self) -> Result { - verify(self.tx_program(), self.stack_inputs(), self.stack_outputs(), self.proof.clone()) - } - // ACCESSORS // -------------------------------------------------------------------------------------------- + /// Returns the account ID. + pub fn account_id(&self) -> AccountId { + self.account_id + } + + /// Returns the initial account hash. + pub fn initial_account_hash(&self) -> Digest { + self.initial_account_hash + } + + /// Returns the final account hash. + pub fn final_account_hash(&self) -> Digest { + self.final_account_hash + } + + /// Returns the consumed notes. + pub fn consumed_notes(&self) -> &[ConsumedNoteInfo] { + &self.consumed_notes + } + + /// Returns the created notes. + pub fn created_notes(&self) -> &[NoteEnvelope] { + &self.created_notes + } + /// Returns the script root of the transaction. + pub fn tx_script_root(&self) -> Option { + self.tx_script_root + } + + /// Returns the transaction program info. + pub fn program_hash(&self) -> Digest { + self.program_hash + } + + /// Returns the proof of the transaction. + pub fn proof(&self) -> &ExecutionProof { + &self.proof + } + + /// Returns the block reference the transaction was executed against. + pub fn block_ref(&self) -> Digest { + self.block_ref + } /// Returns the consumed notes commitment. - pub fn consumed_notes_hash(&self) -> Digest { + pub fn compute_consumed_notes_hash(&self) -> Digest { let mut elements: Vec = Vec::with_capacity(self.consumed_notes.len() * 8); for note in self.consumed_notes.iter() { elements.extend_from_slice(note.nullifier().as_elements()); @@ -75,7 +113,7 @@ impl ProvenTransaction { } /// Returns the created notes commitment. - pub fn created_notes_commitment(&self) -> Digest { + pub fn compute_created_notes_commitment(&self) -> Digest { let mut elements: Vec = Vec::with_capacity(self.created_notes.len() * 8); for note in self.created_notes.iter() { elements.extend_from_slice(note.note_hash().as_elements()); @@ -84,15 +122,10 @@ impl ProvenTransaction { Hasher::hash_elements(&elements) } - /// Returns the transaction program info. - pub fn tx_program(&self) -> ProgramInfo { - todo!() - } - /// Returns the stack inputs for the transaction. - pub fn stack_inputs(&self) -> StackInputs { + pub fn build_stack_inputs(&self) -> StackInputs { let mut stack_inputs: Vec = Vec::with_capacity(13); - stack_inputs.extend_from_slice(self.consumed_notes_hash().as_elements()); + stack_inputs.extend_from_slice(self.compute_consumed_notes_hash().as_elements()); stack_inputs.extend_from_slice(self.initial_account_hash.as_elements()); stack_inputs.push(*self.account_id); stack_inputs.extend_from_slice(self.block_ref.as_elements()); @@ -100,17 +133,14 @@ impl ProvenTransaction { } /// Returns the stack outputs for the transaction. - pub fn stack_outputs(&self) -> StackOutputs { - let mut stack_outputs: Vec = Vec::with_capacity(8); - stack_outputs.extend_from_slice(self.created_notes_commitment().as_elements()); - stack_outputs.extend_from_slice(self.final_account_hash.as_elements()); + pub fn build_stack_outputs(&self) -> StackOutputs { + let mut stack_outputs: Vec = vec![ZERO; STACK_TOP_SIZE]; + stack_outputs[STACK_TOP_SIZE - WORD_SIZE..] + .copy_from_slice(self.compute_created_notes_commitment().as_elements()); + stack_outputs[STACK_TOP_SIZE - (2 * WORD_SIZE)..STACK_TOP_SIZE - WORD_SIZE] + .copy_from_slice(self.final_account_hash.as_elements()); stack_outputs.reverse(); StackOutputs::from_elements(stack_outputs, Default::default()) .expect("StackOutputs are valid") } - - /// Returns the script root of the transaction. - pub fn tx_script_root(&self) -> Option { - self.tx_script_root - } } diff --git a/objects/src/transaction/tx_witness.rs b/objects/src/transaction/tx_witness.rs index fd8e551fd..fc7cb1b21 100644 --- a/objects/src/transaction/tx_witness.rs +++ b/objects/src/transaction/tx_witness.rs @@ -142,6 +142,23 @@ impl TransactionWitness { pub fn advice_inputs(&self) -> &AdviceInputs { &self.advice_witness } + + // CONSUMERS + // -------------------------------------------------------------------------------------------- + /// Consumes the witness and returns its parts. + pub fn into_parts( + self, + ) -> (AccountId, Digest, Digest, Digest, Option, Program, AdviceInputs) { + ( + self.account_id, + self.initial_account_hash, + self.block_hash, + self.consumed_notes_hash, + self.tx_script_root, + self.program, + self.advice_witness, + ) + } } // HELPERS