@@ -444,3 +444,173 @@ func TestDeserializeActionsErrors(t *testing.T) {
444444 _ , _ , err = ad .DeserializeActions (tr )
445445 require .Error (t , err )
446446}
447+
448+ // TestTransferVerifierCache tests that when multiple inputs have the same owner,
449+ // the verifier is only deserialized once (cache hit)
450+ func TestTransferVerifierCache (t * testing.T ) {
451+ ctx := context .Background ()
452+
453+ // Create a transfer action with 3 inputs, where 2 have the same owner
454+ sameOwner := []byte ("same-owner-identity" )
455+ differentOwner := []byte ("different-owner-identity" )
456+
457+ transferAction := & transfer.Action {
458+ Inputs : []* transfer.ActionInput {
459+ {Token : & token.Token {Owner : sameOwner }}, // Input 0: same owner
460+ {Token : & token.Token {Owner : sameOwner }}, // Input 1: same owner (should hit cache)
461+ {Token : & token.Token {Owner : differentOwner }}, // Input 2: different owner
462+ },
463+ Outputs : []* token.Token {
464+ {Owner : []byte ("output-owner" )},
465+ },
466+ }
467+
468+ // Track deserializer calls
469+ deserializerCallCount := 0
470+ ownerCallsByIdentity := make (map [string ]int )
471+
472+ mockDes := & mock3.Deserializer {}
473+ mockDes .GetOwnerVerifierCalls (func (ctx context.Context , id driver.Identity ) (driver.Verifier , error ) {
474+ deserializerCallCount ++
475+ key := string (id )
476+ ownerCallsByIdentity [key ]++
477+
478+ return & mock3.Verifier {}, nil
479+ })
480+
481+ mockSigProv := & mockSignatureProvider {
482+ HasBeenSignedByFunc : func (ctx context.Context , id driver.Identity , verifier driver.Verifier ) ([]byte , error ) {
483+ return []byte ("signature" ), nil
484+ },
485+ }
486+
487+ validatorCtx := & validator.Context {
488+ Logger : & logging.MockLogger {},
489+ TransferAction : transferAction ,
490+ Deserializer : mockDes ,
491+ SignatureProvider : mockSigProv ,
492+ PP : & v1.PublicParams {},
493+ }
494+
495+ // Execute validation
496+ err := validator .TransferSignatureValidate (ctx , validatorCtx )
497+ require .NoError (t , err )
498+
499+ // Verify results - WITH cache implementation
500+ t .Logf ("Total GetOwnerVerifier calls: %d" , deserializerCallCount )
501+ t .Logf ("Calls for same owner: %d" , ownerCallsByIdentity [string (sameOwner )])
502+ t .Logf ("Calls for different owner: %d" , ownerCallsByIdentity [string (differentOwner )])
503+
504+ // With verifier cache: deserializer is called only once per unique owner
505+ // Expected: 2 calls total (once for sameOwner, once for differentOwner)
506+ require .Equal (t , 2 , deserializerCallCount , "With cache: should call deserializer only for unique owners" )
507+ require .Equal (t , 1 , ownerCallsByIdentity [string (sameOwner )], "With cache: same owner called only once" )
508+ require .Equal (t , 1 , ownerCallsByIdentity [string (differentOwner )], "Different owner called once" )
509+
510+ // Calculate cache effectiveness
511+ uniqueOwners := 2
512+ totalInputs := 3
513+ cacheHits := totalInputs - uniqueOwners
514+ cacheHitRate := float64 (cacheHits ) / float64 (totalInputs ) * 100
515+
516+ t .Logf ("Cache hit rate: %.1f%% (%d cache hits out of %d inputs)" ,
517+ cacheHitRate , cacheHits , totalInputs )
518+ }
519+
520+
521+ // TestTransferVerifierCacheError tests that cache doesn't interfere with error handling
522+ func TestTransferVerifierCacheError (t * testing.T ) {
523+ ctx := context .Background ()
524+
525+ sameOwner := []byte ("same-owner-identity" )
526+
527+ transferAction := & transfer.Action {
528+ Inputs : []* transfer.ActionInput {
529+ {Token : & token.Token {Owner : sameOwner }},
530+ {Token : & token.Token {Owner : sameOwner }}, // Should use cached verifier
531+ },
532+ Outputs : []* token.Token {
533+ {Owner : []byte ("output-owner" )},
534+ },
535+ }
536+
537+ // Mock deserializer that returns error on first call
538+ callCount := 0
539+ mockDes := & mock3.Deserializer {}
540+ mockDes .GetOwnerVerifierCalls (func (ctx context.Context , id driver.Identity ) (driver.Verifier , error ) {
541+ callCount ++
542+ if callCount == 1 {
543+ return nil , errors .New ("deserializer error" )
544+ }
545+
546+ return & mock3.Verifier {}, nil
547+ })
548+
549+ mockSigProv := & mockSignatureProvider {
550+ HasBeenSignedByFunc : func (ctx context.Context , id driver.Identity , verifier driver.Verifier ) ([]byte , error ) {
551+ return []byte ("signature" ), nil
552+ },
553+ }
554+
555+ validatorCtx := & validator.Context {
556+ Logger : & logging.MockLogger {},
557+ TransferAction : transferAction ,
558+ Deserializer : mockDes ,
559+ SignatureProvider : mockSigProv ,
560+ PP : & v1.PublicParams {},
561+ }
562+
563+ // Execute validation - should fail on first input
564+ err := validator .TransferSignatureValidate (ctx , validatorCtx )
565+ require .Error (t , err )
566+ require .Contains (t , err .Error (), "failed deserializing owner" )
567+ require .Equal (t , 1 , callCount , "Should stop on first error" )
568+ }
569+
570+ // TestTransferVerifierCacheSignatureError tests signature verification error with cache
571+ func TestTransferVerifierCacheSignatureError (t * testing.T ) {
572+ ctx := context .Background ()
573+
574+ sameOwner := []byte ("same-owner-identity" )
575+
576+ transferAction := & transfer.Action {
577+ Inputs : []* transfer.ActionInput {
578+ {Token : & token.Token {Owner : sameOwner }},
579+ {Token : & token.Token {Owner : sameOwner }}, // Should use cached verifier
580+ },
581+ Outputs : []* token.Token {
582+ {Owner : []byte ("output-owner" )},
583+ },
584+ }
585+
586+ mockDes := & mock3.Deserializer {}
587+ mockDes .GetOwnerVerifierCalls (func (ctx context.Context , id driver.Identity ) (driver.Verifier , error ) {
588+ return & mock3.Verifier {}, nil
589+ })
590+
591+ // Mock signature provider that fails on second call
592+ sigCallCount := 0
593+ mockSigProv := & mockSignatureProvider {
594+ HasBeenSignedByFunc : func (ctx context.Context , id driver.Identity , verifier driver.Verifier ) ([]byte , error ) {
595+ sigCallCount ++
596+ if sigCallCount == 2 {
597+ return nil , errors .New ("signature verification failed" )
598+ }
599+
600+ return []byte ("signature" ), nil
601+ },
602+ }
603+
604+ validatorCtx := & validator.Context {
605+ Logger : & logging.MockLogger {},
606+ TransferAction : transferAction ,
607+ Deserializer : mockDes ,
608+ SignatureProvider : mockSigProv ,
609+ PP : & v1.PublicParams {},
610+ }
611+
612+ // Execute validation - should fail on second input signature verification
613+ err := validator .TransferSignatureValidate (ctx , validatorCtx )
614+ require .Error (t , err )
615+ require .Contains (t , err .Error (), "failed signature verification" )
616+ }
0 commit comments