diff --git a/miden-tx/Cargo.toml b/miden-tx/Cargo.toml index 1a4bbec52..ded21cc26 100644 --- a/miden-tx/Cargo.toml +++ b/miden-tx/Cargo.toml @@ -24,6 +24,7 @@ miden-core = { package = "miden-core", git = "https://github.com/0xPolygonMiden/ miden-objects = { package = "miden-objects", path = "../objects", default-features = false } miden-lib = { package = "miden-lib", path = "../miden-lib" } miden-stdlib = { package = "miden-stdlib", git = "https://github.com/0xPolygonMiden/miden-vm.git", branch = "next", default-features = false } +miden-verifier = { package = "miden-verifier", git = "https://github.com/0xPolygonMiden/miden-vm.git", branch = "next", default-features = false } [dev-dependencies] miden-objects = { package = "miden-objects", path = "../objects", default-features = false } diff --git a/miden-tx/src/compiler/mod.rs b/miden-tx/src/compiler/mod.rs index 32ad1bd0b..74e88bd58 100644 --- a/miden-tx/src/compiler/mod.rs +++ b/miden-tx/src/compiler/mod.rs @@ -3,6 +3,7 @@ use super::{ Digest, MidenLib, ModuleAst, Note, NoteScript, Operation, Program, ProgramAst, SatKernel, StdLibrary, TransactionCompilerError, }; +use miden_core::ProgramInfo; #[cfg(test)] mod tests; @@ -169,24 +170,22 @@ impl TransactionComplier { let mut assembly_context = AssemblyContext::new(AssemblyContextType::Program); // Create note tree and note [CodeBlock]s - let (note_tree_root, note_roots) = - self.create_note_program_tree(&target_account_interface, notes, &mut assembly_context)?; + let (note_tree_root, note_roots) = self.compile_and_build_note_program_tree( + &target_account_interface, + notes, + &mut assembly_context, + )?; // Create the transaction program - let (tx_script_code_block, tx_script_hash) = - self.create_tx_program(tx_script, &mut assembly_context, target_account_interface)?; + let (tx_script_code_block, tx_script_hash) = self.create_tx_script_program( + tx_script, + &mut assembly_context, + target_account_interface, + )?; - // Merge transaction script code block and epilogue code block - let tx_script_and_epilogue = CodeBlock::new_join([ - CodeBlock::new_call(tx_script_code_block.hash()), - self.epilogue.clone(), - ]); - - // Merge prologue and note script tree - let prologue_and_notes = CodeBlock::new_join([self.prologue.clone(), note_tree_root]); - - // Merge prologue, note tree, tx script and epilogue - let program_root = CodeBlock::new_join([prologue_and_notes, tx_script_and_epilogue]); + // build transaction program + let program_root = + self.build_transaction_program(note_tree_root, tx_script_code_block.hash()); // Create [CodeBlockTable] from [AssemblyContext] let mut cb_table = self @@ -202,27 +201,41 @@ impl TransactionComplier { // insert transaction script into [CodeBlockTable] cb_table.insert(tx_script_code_block); - // Create transaction program + // Create transaction program with kernel let program = Program::with_kernel(program_root, self.assembler.kernel().clone(), cb_table); // Create compiled transaction Ok((program, tx_script_hash)) } + /// Returns a [ProgramInfo] which contains the hash of the transaction program associated with + /// the provided consumed note script hashes and transaction script hash. + pub fn build_program_info( + &self, + note_script_hashes: Vec, + tx_script_hash: Option, + ) -> ProgramInfo { + let tx_script_hash = + tx_script_hash.unwrap_or(CodeBlock::new_span(vec![Operation::Noop]).hash()); + let note_tree_root = self.build_note_program_tree(note_script_hashes); + let transaction_program = self.build_transaction_program(note_tree_root, tx_script_hash); + ProgramInfo::new(transaction_program.hash(), self.assembler.kernel().clone()) + } + // HELPER METHODS // -------------------------------------------------------------------------------------------- /// Returns a [CodeBlock] which contains the note program tree root and a [Vec] which /// contains the [CodeBlock]s associated with the notes. - fn create_note_program_tree( + fn compile_and_build_note_program_tree( &mut self, target_account_interface: &[Digest], notes: &[Note], assembly_context: &mut AssemblyContext, ) -> Result<(CodeBlock, Vec), TransactionCompilerError> { // Create vectors to store note programs and note roots + let mut note_script_hashes = Vec::new(); let mut note_programs = Vec::new(); - let mut note_roots = Vec::new(); // Create and verify note programs. Note programs are verified against the target account. for note in notes.iter() { @@ -235,52 +248,21 @@ impl TransactionComplier { TransactionCompilerError::NoteIncompatibleWithAccountInterface(note_root.hash()) }, )?; - note_programs.push(CodeBlock::new_join([ - self.note_setup.clone(), - CodeBlock::new_call(note_root.hash()), - ])); - note_roots.push(note_root); + note_script_hashes.push(note_root.hash()); + note_programs.push(note_root); } - // Push note processing teardown onto the note programs vector - note_programs.push(self.note_processing_teardown.clone()); - - // Merge the note programs into a tree using join blocks - while note_programs.len() != 1 { - // TODO: We should optimize this in the future - however maybe not required as this - // part will be handled by a pcall-like operation in the future. - // Pad note programs to an even number using a [Operation::Noop] span block - if note_programs.len() % 2 != 0 { - note_programs.push(CodeBlock::new_span(vec![Operation::Noop])); - } + // build the note program tree + let note_tree_root = self.build_note_program_tree(note_script_hashes); - // convert vector into an iterator - let mut note_programs_iter = note_programs.into_iter(); - - // create a temporary vector to hold the merged CodeBlocks - let mut note_programs_temp = Vec::new(); - - // Consume two code blocks at a time and merge them into a single code block - while let (Some(left_code_block), Some(right_code_block)) = - (note_programs_iter.next(), note_programs_iter.next()) - { - note_programs_temp.push(CodeBlock::new_join([left_code_block, right_code_block])); - } - - note_programs = note_programs_temp; - } - - Ok(( - note_programs.into_iter().next().expect("a single root code block exists"), - note_roots, - )) + Ok((note_tree_root, note_programs)) } /// Returns a ([CodeBlock], Option) tuple where the first element is the compiled /// transaction script program and the second element is the hash of the transaction script /// program. If no transaction script is provided, the first element is a [CodeBlock] containing /// a single [Operation::Noop] and the second element is `None`. - fn create_tx_program( + fn create_tx_script_program( &mut self, tx_script: Option, assembly_context: &mut AssemblyContext, @@ -322,6 +304,63 @@ impl TransactionComplier { NoteTarget::Procedures(procs) => Ok(procs), } } + + /// Returns a [CodeBlock] which represents the transaction program. + fn build_transaction_program( + &self, + note_program_tree: CodeBlock, + tx_script_hash: Digest, + ) -> CodeBlock { + // Merge transaction script code block and epilogue code block + let tx_script_and_epilogue = + CodeBlock::new_join([CodeBlock::new_call(tx_script_hash), self.epilogue.clone()]); + + // Merge prologue and note script tree + let prologue_and_notes = CodeBlock::new_join([self.prologue.clone(), note_program_tree]); + + // Merge prologue, note tree, tx script and epilogue + CodeBlock::new_join([prologue_and_notes, tx_script_and_epilogue]) + } + + /// Returns a [CodeBlock] which represents the note program tree. + fn build_note_program_tree(&self, note_script_hashes: Vec) -> CodeBlock { + let mut note_programs = note_script_hashes + .into_iter() + .map(|note_hash| { + CodeBlock::new_join([self.note_setup.clone(), CodeBlock::new_call(note_hash)]) + }) + .collect::>(); + + // Push note processing teardown onto the note programs vector + note_programs.push(self.note_processing_teardown.clone()); + + // Merge the note programs into a tree using join blocks + while note_programs.len() != 1 { + // TODO: We should optimize this in the future - however maybe not required as this + // part will be handled by a pcall-like operation in the future. + // Pad note programs to an even number using a [Operation::Noop] span block + if note_programs.len() % 2 != 0 { + note_programs.push(CodeBlock::new_span(vec![Operation::Noop])); + } + + // convert vector into an iterator + let mut note_programs_iter = note_programs.into_iter(); + + // create a temporary vector to hold the merged CodeBlocks + let mut note_programs_temp = Vec::new(); + + // Consume two code blocks at a time and merge them into a single code block + while let (Some(left_code_block), Some(right_code_block)) = + (note_programs_iter.next(), note_programs_iter.next()) + { + note_programs_temp.push(CodeBlock::new_join([left_code_block, right_code_block])); + } + + note_programs = note_programs_temp; + } + + note_programs.into_iter().next().expect("a single root code block exists") + } } impl Default for TransactionComplier { diff --git a/miden-tx/src/error.rs b/miden-tx/src/error.rs index c00642f84..fc680ad15 100644 --- a/miden-tx/src/error.rs +++ b/miden-tx/src/error.rs @@ -3,6 +3,7 @@ use super::{ TransactionResultError, }; use miden_objects::TransactionWitnessError; +use miden_verifier::VerificationError; #[derive(Debug)] pub enum TransactionError { @@ -42,6 +43,12 @@ pub enum TransactionProverError { CorruptTransactionWitnessConsumedNoteData(TransactionWitnessError), } +#[derive(Debug)] +pub enum TransactionVerifierError { + TransactionVerificationFailed(VerificationError), + InsufficientProofSecurityLevel(u32, u32), +} + #[derive(Debug)] pub enum DataStoreError { AccountNotFound(AccountId), diff --git a/miden-tx/src/lib.rs b/miden-tx/src/lib.rs index c02459b47..036eb0ff1 100644 --- a/miden-tx/src/lib.rs +++ b/miden-tx/src/lib.rs @@ -2,7 +2,7 @@ use assembly::{ ast::{ModuleAst, ProgramAst}, Assembler, AssemblyContext, AssemblyContextType, AssemblyError, }; -use crypto::{hash::rpo::RpoDigest as Digest, merkle::NodeIndex}; +use crypto::{hash::rpo::Rpo256 as Hasher, hash::rpo::RpoDigest as Digest, merkle::NodeIndex}; use miden_core::{code_blocks::CodeBlock, utils::collections::BTreeMap, Operation, Program}; use miden_lib::{MidenLib, SatKernel}; use miden_objects::{ @@ -22,10 +22,13 @@ mod executor; pub use error::TransactionError; use error::{ DataStoreError, TransactionCompilerError, TransactionExecutorError, TransactionProverError, + TransactionVerifierError, }; pub use executor::TransactionExecutor; mod prover; pub use prover::TransactionProver; +mod verifier; +pub use verifier::TransactionVerifier; #[cfg(test)] mod tests; diff --git a/miden-tx/src/prover/mod.rs b/miden-tx/src/prover/mod.rs index 9a078e37f..78770c05c 100644 --- a/miden-tx/src/prover/mod.rs +++ b/miden-tx/src/prover/mod.rs @@ -51,7 +51,7 @@ impl TransactionProver { 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) = + let (account, block_header, _chain, consumed_notes, _tx_program, tx_script_root) = transaction.into_parts(); Ok(ProvenTransaction::new( @@ -61,7 +61,6 @@ impl TransactionProver { consumed_notes.into(), created_notes.into(), tx_script_root, - tx_program.hash(), block_header.hash(), proof, )) @@ -112,7 +111,6 @@ impl TransactionProver { 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 ea614f876..a264d7693 100644 --- a/miden-tx/src/tests.rs +++ b/miden-tx/src/tests.rs @@ -1,13 +1,12 @@ use super::{ Account, AccountId, BlockHeader, ChainMmr, DataStore, DataStoreError, Note, NoteOrigin, - TransactionExecutor, TransactionProver, + TransactionExecutor, TransactionProver, TransactionVerifier, }; 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, @@ -249,7 +248,7 @@ fn test_transaction_result_account_delta() { } #[test] -fn test_prove_witness() { +fn test_prove_witness_and_verify() { let data_store = MockDataStore::new(); let mut executor = TransactionExecutor::new(data_store.clone()); @@ -272,9 +271,10 @@ fn test_prove_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); + let proven_transaction = prover.prove_transaction_witness(witness).unwrap(); - assert!(proven_transaction.is_ok()); + let verifier = TransactionVerifier::new(96); + assert!(verifier.verify(proven_transaction).is_ok()); } #[test] @@ -297,23 +297,11 @@ fn test_prove_and_verify_with_tx_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()); + let verifier = TransactionVerifier::new(96); + assert!(verifier.verify(proven_transaction).is_ok()); } diff --git a/miden-tx/src/verifier/mod.rs b/miden-tx/src/verifier/mod.rs new file mode 100644 index 000000000..8ea7167fe --- /dev/null +++ b/miden-tx/src/verifier/mod.rs @@ -0,0 +1,103 @@ +use super::{Digest, Hasher, TransactionComplier, TransactionVerifierError}; +use crypto::{WORD_SIZE, ZERO}; +use miden_core::{stack::STACK_TOP_SIZE, Felt, StackInputs, StackOutputs, Word}; +use miden_objects::{ + notes::NoteEnvelope, + transaction::{ConsumedNoteInfo, ProvenTransaction}, +}; +use miden_verifier::verify; + +/// The [TransactionVerifier] is used to verify a [ProvenTransaction]. +/// +/// The [TransactionVerifier] contains a [TransactionComplier] object which we use to construct +/// the transaction program associated with a transaction. The `proof_security_level` specifies +/// the minimum security level that the transaction proof must have in order to be considered +/// valid. +pub struct TransactionVerifier { + compiler: TransactionComplier, + proof_security_level: u32, +} + +impl TransactionVerifier { + /// Creates a new [TransactionVerifier] object. + pub fn new(proof_security_level: u32) -> Self { + let compiler = TransactionComplier::new(); + Self { + compiler, + proof_security_level, + } + } + + /// Verifies the provided [ProvenTransaction] against the kernel. + pub fn verify(&self, transaction: ProvenTransaction) -> Result<(), TransactionVerifierError> { + let consumed_notes_hashes = + transaction.consumed_notes().iter().map(|x| x.script_root()).collect(); + let program_info = self + .compiler + .build_program_info(consumed_notes_hashes, transaction.tx_script_root()); + + let proof_security_level = verify( + program_info, + Self::build_stack_inputs(&transaction), + Self::build_stack_outputs(&transaction), + transaction.proof().clone(), + ) + .map_err(TransactionVerifierError::TransactionVerificationFailed)?; + + if proof_security_level < self.proof_security_level { + return Err(TransactionVerifierError::InsufficientProofSecurityLevel( + proof_security_level, + self.proof_security_level, + )); + } + + Ok(()) + } + + // HELPERS + // -------------------------------------------------------------------------------------------- + /// Returns the consumed notes commitment. + fn compute_consumed_notes_hash(consumed_notes: &[ConsumedNoteInfo]) -> Digest { + let mut elements: Vec = Vec::with_capacity(consumed_notes.len() * 8); + for note in consumed_notes.iter() { + elements.extend_from_slice(note.nullifier().as_elements()); + elements.extend_from_slice(note.script_root().as_elements()); + } + Hasher::hash_elements(&elements) + } + + /// Returns the created notes commitment. + fn compute_created_notes_commitment(created_notes: &[NoteEnvelope]) -> Digest { + let mut elements: Vec = Vec::with_capacity(created_notes.len() * 8); + for note in created_notes.iter() { + elements.extend_from_slice(note.note_hash().as_elements()); + elements.extend_from_slice(&Word::from(note.metadata())); + } + Hasher::hash_elements(&elements) + } + + /// Returns the stack inputs for the transaction. + fn build_stack_inputs(transaction: &ProvenTransaction) -> StackInputs { + let mut stack_inputs: Vec = Vec::with_capacity(13); + stack_inputs.extend_from_slice( + Self::compute_consumed_notes_hash(transaction.consumed_notes()).as_elements(), + ); + stack_inputs.extend_from_slice(transaction.initial_account_hash().as_elements()); + stack_inputs.push(*transaction.account_id()); + stack_inputs.extend_from_slice(transaction.block_ref().as_elements()); + StackInputs::new(stack_inputs) + } + + /// Returns the stack outputs for the transaction. + fn build_stack_outputs(transaction: &ProvenTransaction) -> 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(transaction.created_notes()).as_elements(), + ); + stack_outputs[STACK_TOP_SIZE - (2 * WORD_SIZE)..STACK_TOP_SIZE - WORD_SIZE] + .copy_from_slice(transaction.final_account_hash().as_elements()); + stack_outputs.reverse(); + StackOutputs::from_elements(stack_outputs, Default::default()) + .expect("StackOutputs are valid") + } +} diff --git a/objects/src/transaction/proven_tx.rs b/objects/src/transaction/proven_tx.rs index 3e13da1e5..52f3ab43d 100644 --- a/objects/src/transaction/proven_tx.rs +++ b/objects/src/transaction/proven_tx.rs @@ -1,9 +1,5 @@ -use super::{ - AccountId, ConsumedNoteInfo, Digest, Felt, Hasher, NoteEnvelope, StackInputs, StackOutputs, - Vec, Word, -}; -use crypto::{WORD_SIZE, ZERO}; -use miden_core::stack::STACK_TOP_SIZE; +use super::{AccountId, ConsumedNoteInfo, Digest, NoteEnvelope, Vec}; + use miden_verifier::ExecutionProof; /// Resultant object of executing and proving a transaction. It contains the minimal @@ -24,7 +20,6 @@ pub struct ProvenTransaction { consumed_notes: Vec, created_notes: Vec, tx_script_root: Option, - program_hash: Digest, block_ref: Digest, proof: ExecutionProof, } @@ -39,7 +34,6 @@ impl ProvenTransaction { consumed_notes: Vec, created_notes: Vec, tx_script_root: Option, - program_hash: Digest, block_ref: Digest, proof: ExecutionProof, ) -> Self { @@ -50,7 +44,6 @@ impl ProvenTransaction { consumed_notes, created_notes, tx_script_root, - program_hash, block_ref, proof, } @@ -87,11 +80,6 @@ impl ProvenTransaction { 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 @@ -101,46 +89,4 @@ impl ProvenTransaction { pub fn block_ref(&self) -> Digest { self.block_ref } - - /// Returns the consumed notes commitment. - 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()); - elements.extend_from_slice(note.script_root().as_elements()); - } - Hasher::hash_elements(&elements) - } - - /// Returns the created notes commitment. - 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()); - elements.extend_from_slice(&Word::from(note.metadata())); - } - Hasher::hash_elements(&elements) - } - - /// Returns the stack inputs for the transaction. - pub fn build_stack_inputs(&self) -> StackInputs { - let mut stack_inputs: Vec = Vec::with_capacity(13); - 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()); - StackInputs::new(stack_inputs) - } - - /// Returns the stack outputs for the transaction. - 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") - } }