Skip to content

Commit a430068

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

2 files changed

Lines changed: 188 additions & 5 deletions

File tree

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

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

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)