Skip to content

Commit 3279806

Browse files
committed
perf: cache verifiers to eliminate duplicate Idemix proofs
Signed-off-by: AkramBitar <akram@il.ibm.com>
1 parent 57b15b2 commit 3279806

2 files changed

Lines changed: 189 additions & 5 deletions

File tree

token/core/zkatdlog/nogh/v1/validator/validator_extra_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

token/core/zkatdlog/nogh/v1/validator/validator_transfer.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ func TransferSignatureValidate(c context.Context, ctx *Context) error {
3636
return ErrInvalidInputs
3737
}
3838

39+
// Cache verifiers within this transaction to avoid redundant verifyProof() calls
40+
// for inputs with the same owner identity
41+
verifierCache := make(map[string]driver.Verifier)
42+
3943
var isRedeem bool
4044
var inputToken []*token.Token
4145
for i, in := range ctx.TransferAction.Inputs {
@@ -44,10 +48,19 @@ func TransferSignatureValidate(c context.Context, ctx *Context) error {
4448

4549
// check sender signature
4650
ctx.Logger.Debugf("check sender [%d][%s]", i, driver.Identity(tok.Owner).UniqueID())
47-
verifier, err := ctx.Deserializer.GetOwnerVerifier(c, tok.Owner)
48-
if err != nil {
49-
return errors.Wrapf(err, "failed deserializing owner [%d][%v][%s]", i, in, driver.Identity(tok.Owner))
51+
52+
// Check cache first to avoid redundant expensive verifyProof() calls
53+
ownerKey := driver.Identity(tok.Owner).UniqueID()
54+
verifier, cached := verifierCache[ownerKey]
55+
if !cached {
56+
var err error
57+
verifier, err = ctx.Deserializer.GetOwnerVerifier(c, tok.Owner)
58+
if err != nil {
59+
return errors.Wrapf(err, "failed deserializing owner [%d][%v][%s]", i, in, driver.Identity(tok.Owner))
60+
}
61+
verifierCache[ownerKey] = verifier
5062
}
63+
5164
ctx.Logger.Debugf("signature verification [%d][%v][%s]", i, in, driver.Identity(tok.Owner).UniqueID())
5265
sigma, err := ctx.SignatureProvider.HasBeenSignedBy(c, tok.Owner, verifier)
5366
if err != nil {
@@ -132,10 +145,11 @@ func TransferZKProofValidate(c context.Context, ctx *Context) error {
132145
in[i] = tok.Data
133146
}
134147

135-
if err := transfer.NewVerifier(
148+
err := transfer.NewVerifier(
136149
in,
137150
ctx.TransferAction.GetOutputCommitments(),
138-
ctx.PP).Verify(ctx.TransferAction.GetProof()); err != nil {
151+
ctx.PP).Verify(ctx.TransferAction.GetProof())
152+
if err != nil {
139153
return errors.Join(err, ErrInvalidZKP)
140154
}
141155

0 commit comments

Comments
 (0)