@@ -4,8 +4,8 @@ use acvm::FieldElement;
44use borsh:: { BorshDeserialize , BorshSerialize } ;
55use hex;
66use sdk:: {
7- merkle_utils:: BorshableMerkleProof , utils:: parse_raw_calldata , Calldata , ContractName ,
8- RunResult , StateCommitment ,
7+ caller :: ExecutionContext , merkle_utils:: BorshableMerkleProof , utils:: parse_calldata , Calldata ,
8+ ContractName , RunResult , StateCommitment , StructuredBlobData ,
99} ;
1010use sparse_merkle_tree:: H256 ;
1111
@@ -20,6 +20,7 @@ const MAX_ROOTS: usize = 1000;
2020pub struct ContractConfig {
2121 pub utxo_contract_name : ContractName ,
2222 pub smt_incl_proof_contract_name : ContractName ,
23+ pub smt_contract_name : ContractName ,
2324}
2425
2526#[ derive( Debug , Default , BorshSerialize , BorshDeserialize ) ]
@@ -273,7 +274,19 @@ impl HyliUtxoZkVmState {
273274 }
274275 }
275276
276- fn check_noir_blobs ( & self , calldata : & Calldata ) -> Result < ( ) , String > {
277+ fn parse_smt_incl_blob_payload ( blob : & sdk:: Blob ) -> Result < Vec < u8 > , String > {
278+ let structured: StructuredBlobData < Vec < u8 > > =
279+ blob. data . clone ( ) . try_into ( ) . map_err ( |_| {
280+ "failed to parse hyli_smt_incl_proof blob as structured blob" . to_string ( )
281+ } ) ?;
282+ Ok ( structured. parameters )
283+ }
284+
285+ fn check_noir_blobs (
286+ & self ,
287+ calldata : & Calldata ,
288+ ctx : & mut ExecutionContext ,
289+ ) -> Result < ( ) , String > {
277290 let Some ( ( _, hyli_utxo_blob) ) = calldata
278291 . blobs
279292 . iter ( )
@@ -282,23 +295,27 @@ impl HyliUtxoZkVmState {
282295 return Err ( "hyli_utxo_noir blob not provided in calldata" . to_string ( ) ) ;
283296 } ;
284297
285- let Some ( ( _ , smt_blob ) ) = calldata
286- . blobs
298+ let Some ( smt_incl_blob_index ) = ctx
299+ . callees_blobs
287300 . iter ( )
288- . find ( | ( _ , blob) | blob. contract_name == self . config . smt_incl_proof_contract_name )
301+ . position ( | blob| blob. contract_name == self . config . smt_incl_proof_contract_name )
289302 else {
290- return Err ( "hyli_smt_incl_proof_noir blob not provided in calldata" . to_string ( ) ) ;
303+ return Err (
304+ "hyli_smt_incl_proof_noir callee blob not provided in calldata" . to_string ( ) ,
305+ ) ;
291306 } ;
307+ let smt_incl_blob = ctx. callees_blobs . remove ( smt_incl_blob_index) ;
292308
293- // Step 1: Check that the smt_blob's notes root matches the computed notes root from the witness.
309+ // Step 1: Check that the smt_incl_blob's notes root matches the computed notes root from the witness.
310+ let smt_blob_payload = Self :: parse_smt_incl_blob_payload ( & smt_incl_blob) ?;
294311 let ( smt_nullifier0, smt_nullifier1, smt_blob_notes_root) =
295- parse_hyli_smt_incl_blob ( & smt_blob . data . 0 ) ?;
312+ parse_hyli_smt_incl_blob ( & smt_blob_payload ) ?;
296313
297314 if !self . roots . contains ( smt_blob_notes_root) {
298315 return Err ( "smt inclusion proof blob does not match notes root" . to_string ( ) ) ;
299316 }
300317
301- // Step 2: Check that the nullifiers in the smt blob match those in the utxo blob.
318+ // Step 2: Check that the nullifiers in the smt_incl_blob match those in the utxo blob.
302319 let ( _, utxo_nullifiers) = parse_hyli_utxo_blob ( & hyli_utxo_blob. data . 0 ) ?;
303320
304321 if utxo_nullifiers[ 0 ] != smt_nullifier0 {
@@ -314,13 +331,35 @@ impl HyliUtxoZkVmState {
314331 ) ;
315332 }
316333
334+ // Optional step 3: Check that the blob callee topology matches the expected topology for a withdraw transaction:
335+ let withdraw_callees = ctx
336+ . callees_blobs
337+ . iter ( )
338+ . enumerate ( )
339+ . filter ( |( _, blob) | blob. contract_name == self . config . smt_contract_name )
340+ . collect :: < Vec < _ > > ( ) ;
341+
342+ if withdraw_callees. len ( ) > 1 {
343+ return Err ( "multiple withdraw callees found for hyli-utxo-state blob" . to_string ( ) ) ;
344+ }
345+ if let Some ( ( token_blob_index, _) ) = withdraw_callees. first ( ) {
346+ ctx. callees_blobs . remove ( * token_blob_index) ;
347+ }
348+ if !ctx. callees_blobs . is_empty ( ) {
349+ return Err ( format ! (
350+ "hyli-utxo-state callee set mismatch: unexpected remaining callees {:?}" ,
351+ ctx. callees_blobs
352+ ) ) ;
353+ }
354+
317355 Ok ( ( ) )
318356 }
319357
320358 fn apply_action ( & mut self , calldata : & Calldata ) -> Result < ( ) , String > {
321- let hyli_utxo_blob = calldata
359+ let ( _ , hyli_utxo_blob) = calldata
322360 . blobs
323- . get ( & sdk:: BlobIndex ( 1 ) )
361+ . iter ( )
362+ . find ( |( _, blob) | blob. contract_name == self . config . utxo_contract_name )
324363 . ok_or_else ( || "hyli_utxo blob not found in calldata" . to_string ( ) ) ?;
325364
326365 let ( created, nullified) = parse_hyli_utxo_blob ( & hyli_utxo_blob. data . 0 )
@@ -368,12 +407,12 @@ impl HyliUtxoZkVmState {
368407
369408impl sdk:: ZkContract for HyliUtxoZkVmState {
370409 fn execute ( & mut self , calldata : & Calldata ) -> RunResult {
371- let ( _, ctx) = parse_raw_calldata :: < HyliUtxoStateAction > ( calldata) ?;
410+ let ( _, mut ctx) = parse_calldata :: < HyliUtxoStateAction > ( calldata) ?;
372411
373412 self . created_notes . ensure_all_zero ( ) ?;
374413 self . nullified_notes . ensure_all_zero ( ) ?;
375414
376- self . check_noir_blobs ( calldata) ?;
415+ self . check_noir_blobs ( calldata, & mut ctx ) ?;
377416
378417 self . apply_action ( calldata) ?;
379418
@@ -484,11 +523,13 @@ pub fn parse_hyli_smt_incl_blob(
484523#[ cfg( test) ]
485524mod tests {
486525 use super :: * ;
526+ use sdk:: { Blob , BlobData , BlobIndex , ContractName , StructuredBlobData , TxHash } ;
487527
488528 fn state_with_root ( byte : u8 ) -> HyliUtxoZkVmState {
489529 let mut state = HyliUtxoZkVmState :: new ( ContractConfig {
490530 utxo_contract_name : "dummy_utxo" . into ( ) ,
491531 smt_incl_proof_contract_name : "dummy_smt_incl" . into ( ) ,
532+ smt_contract_name : "oranj" . into ( ) ,
492533 } ) ;
493534 let root = BorshableH256 :: from ( [ byte; 32 ] ) ;
494535 state. created_notes . proof = Proof :: CurrentRootHash ( root) ;
@@ -528,4 +569,104 @@ mod tests {
528569 assert_root ( & batch, 3 ) ;
529570 assert_eq ! ( batch. remaining. len( ) , 0 ) ;
530571 }
572+
573+ fn make_state_blob ( callees : Vec < BlobIndex > ) -> Blob {
574+ Blob {
575+ contract_name : ContractName ( "hyli-utxo-state" . into ( ) ) ,
576+ data : BlobData :: from ( StructuredBlobData {
577+ caller : None ,
578+ callees : Some ( callees) ,
579+ parameters : HYLI_UTXO_STATE_ACTION ,
580+ } ) ,
581+ }
582+ }
583+
584+ fn make_utxo_blob ( nullifier_byte : u8 ) -> Blob {
585+ let mut bytes = vec ! [ 0u8 ; 128 ] ;
586+ bytes[ 64 ..96 ] . copy_from_slice ( & [ nullifier_byte; 32 ] ) ;
587+ bytes[ 96 ..128 ] . copy_from_slice ( & [ nullifier_byte. wrapping_add ( 1 ) ; 32 ] ) ;
588+ Blob {
589+ contract_name : ContractName ( "dummy_utxo" . into ( ) ) ,
590+ data : BlobData ( bytes) ,
591+ }
592+ }
593+
594+ fn make_smt_blob ( root_byte : u8 , nullifier_byte : u8 ) -> Blob {
595+ let mut bytes = vec ! [ 0u8 ; 96 ] ;
596+ bytes[ 0 ..32 ] . copy_from_slice ( & [ nullifier_byte; 32 ] ) ;
597+ bytes[ 32 ..64 ] . copy_from_slice ( & [ nullifier_byte. wrapping_add ( 1 ) ; 32 ] ) ;
598+ bytes[ 64 ..96 ] . copy_from_slice ( & [ root_byte; 32 ] ) ;
599+ Blob {
600+ contract_name : ContractName ( "dummy_smt_incl" . into ( ) ) ,
601+ data : BlobData :: from ( StructuredBlobData {
602+ caller : Some ( BlobIndex ( 0 ) ) ,
603+ callees : None ,
604+ parameters : bytes,
605+ } ) ,
606+ }
607+ }
608+
609+ fn make_token_blob ( caller : Option < BlobIndex > ) -> Blob {
610+ Blob {
611+ contract_name : ContractName ( "oranj" . into ( ) ) ,
612+ data : BlobData :: from ( StructuredBlobData {
613+ caller,
614+ callees : None ,
615+ parameters : vec ! [ 1u8 , 2 , 3 ] ,
616+ } ) ,
617+ }
618+ }
619+
620+ #[ test]
621+ fn check_noir_blobs_accepts_withdraw_topology ( ) {
622+ let mut state = state_with_root ( 7 ) ;
623+ state. roots [ 0 ] = [ 7u8 ; 8 ] ;
624+
625+ let calldata = sdk:: Calldata {
626+ tx_hash : TxHash ( vec ! [ 0u8 ; 32 ] ) ,
627+ identity : "alice" . into ( ) ,
628+ blobs : vec ! [
629+ make_state_blob( vec![ BlobIndex ( 2 ) , BlobIndex ( 3 ) ] ) ,
630+ make_utxo_blob( 9 ) ,
631+ make_smt_blob( 7 , 9 ) ,
632+ make_token_blob( Some ( BlobIndex ( 0 ) ) ) ,
633+ ]
634+ . into ( ) ,
635+ tx_blob_count : 4 ,
636+ index : BlobIndex ( 0 ) ,
637+ tx_ctx : None ,
638+ private_input : Vec :: new ( ) ,
639+ } ;
640+ let ( _, mut ctx) =
641+ parse_calldata :: < HyliUtxoStateAction > ( & calldata) . expect ( "parse state calldata" ) ;
642+
643+ state
644+ . check_noir_blobs ( & calldata, & mut ctx)
645+ . expect ( "withdraw topology should be accepted" ) ;
646+ }
647+
648+ #[ test]
649+ fn check_noir_blobs_rejects_missing_withdraw_token_callee ( ) {
650+ let mut state = state_with_root ( 7 ) ;
651+ state. roots [ 0 ] = [ 7u8 ; 8 ] ;
652+
653+ let calldata = sdk:: Calldata {
654+ tx_hash : TxHash ( vec ! [ 0u8 ; 32 ] ) ,
655+ identity : "alice" . into ( ) ,
656+ blobs : vec ! [
657+ make_state_blob( vec![ BlobIndex ( 2 ) ] ) ,
658+ make_utxo_blob( 9 ) ,
659+ make_smt_blob( 7 , 9 ) ,
660+ make_token_blob( Some ( BlobIndex ( 0 ) ) ) ,
661+ ]
662+ . into ( ) ,
663+ tx_blob_count : 4 ,
664+ index : BlobIndex ( 0 ) ,
665+ tx_ctx : None ,
666+ private_input : Vec :: new ( ) ,
667+ } ;
668+ let err = parse_calldata :: < HyliUtxoStateAction > ( & calldata)
669+ . expect_err ( "withdraw topology should fail during calldata parsing" ) ;
670+ assert ! ( err. contains( "Blob callees do not match actual callees" ) ) ;
671+ }
531672}
0 commit comments