@@ -444,3 +444,172 @@ 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+ // TestTransferVerifierCacheError tests that cache doesn't interfere with error handling
521+ func TestTransferVerifierCacheError (t * testing.T ) {
522+ ctx := context .Background ()
523+
524+ sameOwner := []byte ("same-owner-identity" )
525+
526+ transferAction := & transfer.Action {
527+ Inputs : []* transfer.ActionInput {
528+ {Token : & token.Token {Owner : sameOwner }},
529+ {Token : & token.Token {Owner : sameOwner }}, // Should use cached verifier
530+ },
531+ Outputs : []* token.Token {
532+ {Owner : []byte ("output-owner" )},
533+ },
534+ }
535+
536+ // Mock deserializer that returns error on first call
537+ callCount := 0
538+ mockDes := & mock3.Deserializer {}
539+ mockDes .GetOwnerVerifierCalls (func (ctx context.Context , id driver.Identity ) (driver.Verifier , error ) {
540+ callCount ++
541+ if callCount == 1 {
542+ return nil , errors .New ("deserializer error" )
543+ }
544+
545+ return & mock3.Verifier {}, nil
546+ })
547+
548+ mockSigProv := & mockSignatureProvider {
549+ HasBeenSignedByFunc : func (ctx context.Context , id driver.Identity , verifier driver.Verifier ) ([]byte , error ) {
550+ return []byte ("signature" ), nil
551+ },
552+ }
553+
554+ validatorCtx := & validator.Context {
555+ Logger : & logging.MockLogger {},
556+ TransferAction : transferAction ,
557+ Deserializer : mockDes ,
558+ SignatureProvider : mockSigProv ,
559+ PP : & v1.PublicParams {},
560+ }
561+
562+ // Execute validation - should fail on first input
563+ err := validator .TransferSignatureValidate (ctx , validatorCtx )
564+ require .Error (t , err )
565+ require .Contains (t , err .Error (), "failed deserializing owner" )
566+ require .Equal (t , 1 , callCount , "Should stop on first error" )
567+ }
568+
569+ // TestTransferVerifierCacheSignatureError tests signature verification error with cache
570+ func TestTransferVerifierCacheSignatureError (t * testing.T ) {
571+ ctx := context .Background ()
572+
573+ sameOwner := []byte ("same-owner-identity" )
574+
575+ transferAction := & transfer.Action {
576+ Inputs : []* transfer.ActionInput {
577+ {Token : & token.Token {Owner : sameOwner }},
578+ {Token : & token.Token {Owner : sameOwner }}, // Should use cached verifier
579+ },
580+ Outputs : []* token.Token {
581+ {Owner : []byte ("output-owner" )},
582+ },
583+ }
584+
585+ mockDes := & mock3.Deserializer {}
586+ mockDes .GetOwnerVerifierCalls (func (ctx context.Context , id driver.Identity ) (driver.Verifier , error ) {
587+ return & mock3.Verifier {}, nil
588+ })
589+
590+ // Mock signature provider that fails on second call
591+ sigCallCount := 0
592+ mockSigProv := & mockSignatureProvider {
593+ HasBeenSignedByFunc : func (ctx context.Context , id driver.Identity , verifier driver.Verifier ) ([]byte , error ) {
594+ sigCallCount ++
595+ if sigCallCount == 2 {
596+ return nil , errors .New ("signature verification failed" )
597+ }
598+
599+ return []byte ("signature" ), nil
600+ },
601+ }
602+
603+ validatorCtx := & validator.Context {
604+ Logger : & logging.MockLogger {},
605+ TransferAction : transferAction ,
606+ Deserializer : mockDes ,
607+ SignatureProvider : mockSigProv ,
608+ PP : & v1.PublicParams {},
609+ }
610+
611+ // Execute validation - should fail on second input signature verification
612+ err := validator .TransferSignatureValidate (ctx , validatorCtx )
613+ require .Error (t , err )
614+ require .Contains (t , err .Error (), "failed signature verification" )
615+ }
0 commit comments