Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions token/core/zkatdlog/nogh/v1/issue/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ func prepareZKIssue(t *testing.T, bits uint64, curveID math.CurveID, numOutputs
// commitments that serve as inputs to the prover/verifier in tests.
func prepareInputsForZKIssue(pp *v1.PublicParams, numOutputs int) ([]*token.Metadata, []*math.G1) {
values := make([]uint64, numOutputs)
for i := range numOutputs {
values[i] = uint64(i*10 + 10)
for i := range values {
values[i] = uint64(i)*10 + 10
}
curve := math.Curves[pp.Curve]
rand, _ := curve.Rand()
Expand Down
8 changes: 4 additions & 4 deletions token/core/zkatdlog/nogh/v1/transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,10 @@ func newTransferEnv(benchmarkCase *benchmark2.Case, configurations *benchmark.Se
return nil, err
}
outputs := make([]*token.Token, benchmarkCase.NumOutputs)
for i := range benchmarkCase.NumOutputs {
for i := range outputs {
outputs[i] = &token.Token{
Owner: ownerID,
Quantity: token.NewQuantityFromUInt64(uint64(i*10 + 10)).Hex(),
Quantity: token.NewQuantityFromUInt64(uint64(i)*10 + 10).Hex(),
Type: "ABC",
}
}
Expand All @@ -178,8 +178,8 @@ func newTransferEnv(benchmarkCase *benchmark2.Case, configurations *benchmark.Se
numInputs := benchmarkCase.NumInputs
ids := make([]*token.ID, numInputs)
values := make([]uint64, numInputs)
for i := range numInputs {
values[i] = uint64(i*10 + 10)
for i := range values {
values[i] = uint64(i)*10 + 10
}
baseTokens, metadata, err := v1token.GetTokensWithWitness(values, "ABC", pp.PedersenGenerators, math.Curves[pp.Curve])
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion token/core/zkatdlog/nogh/v1/validator/testutils/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ func prepareTransfer(
// prepare inputs
inValues := make([]*math.Zr, benchCase.NumInputs)
sumInputs := uint64(0)
for i := range benchCase.NumInputs {
for i := range inValues {
v := uint64(i*10 + 500)
sumInputs += v
inValues[i] = c.NewZrFromUint64(v)
Expand Down
21 changes: 16 additions & 5 deletions token/services/identity/idemix/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@ import (

var logger = logging.MustGetLogger()

// IdentityCacheBackendFunc generates an identity descriptor for given audit info.
type IdentityCacheBackendFunc func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error)

// IdentityCache provides a pre-provisioned cache of Idemix identities.
type IdentityCache struct {
once sync.Once
backed IdentityCacheBackendFunc
// Ensures provisioning starts once
once sync.Once
// Backend identity generator
backed IdentityCacheBackendFunc
// Audit info for cache provisioning
auditInfo []byte

cache chan *idriver.IdentityDescriptor
// Buffered channel of identities
cache chan *idriver.IdentityDescriptor
// Max wait time for cached identity
cacheTimeout time.Duration

// Cache performance metrics
metrics *Metrics
}

// NewIdentityCache creates a new identity cache with specified size and backend.
func NewIdentityCache(backed IdentityCacheBackendFunc, size int, auditInfo []byte, metrics *Metrics) *IdentityCache {
logger.Debugf("new identity cache with size [%d]", size)
ci := &IdentityCache{
Expand All @@ -45,6 +52,7 @@ func NewIdentityCache(backed IdentityCacheBackendFunc, size int, auditInfo []byt
return ci
}

// Identity retrieves an identity from cache or generates on-demand.
func (c *IdentityCache) Identity(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
// Is the auditInfo equal to that used to fill the cache? If yes, use the cache
if !bytes.Equal(auditInfo, c.auditInfo) {
Expand All @@ -63,6 +71,7 @@ func (c *IdentityCache) Identity(ctx context.Context, auditInfo []byte) (*idrive
return c.fetchIdentityFromCache(ctx)
}

// fetchIdentityFromCache retrieves identity from cache with timeout.
func (c *IdentityCache) fetchIdentityFromCache(ctx context.Context) (*idriver.IdentityDescriptor, error) {
var identityDescriptor *idriver.IdentityDescriptor

Expand Down Expand Up @@ -105,6 +114,7 @@ func (c *IdentityCache) fetchIdentityFromCache(ctx context.Context) (*idriver.Id
return identityDescriptor, nil
}

// fetchIdentityFromBackend generates identity directly from backend, bypassing cache.
func (c *IdentityCache) fetchIdentityFromBackend(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
logger.DebugfContext(ctx, "fetching identity from backend")
identityDescriptor, err := c.backed(ctx, auditInfo)
Expand All @@ -116,6 +126,7 @@ func (c *IdentityCache) fetchIdentityFromBackend(ctx context.Context, auditInfo
return identityDescriptor, nil
}

// provisionIdentities continuously fills cache with pre-generated identities.
func (c *IdentityCache) provisionIdentities() {
count := 0
ctx := context.Background()
Expand Down
133 changes: 133 additions & 0 deletions token/services/identity/idemix/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ package cache

import (
"context"
"errors"
"sync"
"testing"
"time"

"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/metrics/disabled"
"github.com/hyperledger-labs/fabric-token-sdk/token/driver"
Expand All @@ -18,6 +20,7 @@ import (
"github.com/test-go/testify/require"
)

// TestIdentityCache verifies basic cache functionality and identity retrieval.
func TestIdentityCache(t *testing.T) {
c := NewIdentityCache(func(context.Context, []byte) (*idriver.IdentityDescriptor, error) {
return &idriver.IdentityDescriptor{
Expand All @@ -36,6 +39,7 @@ func TestIdentityCache(t *testing.T) {
assert.Equal(t, []byte("audit"), identityDescriptor.AuditInfo)
}

// TestIdentityCacheForRace tests concurrent cache access for thread-safety.
func TestIdentityCacheForRace(t *testing.T) {
c := NewIdentityCache(func(context.Context, []byte) (*idriver.IdentityDescriptor, error) {
return &idriver.IdentityDescriptor{
Expand All @@ -61,3 +65,132 @@ func TestIdentityCacheForRace(t *testing.T) {
}
wg.Wait()
}

// TestFetchIdentityFromBackend verifies backend fetch when audit info doesn't match.
func TestFetchIdentityFromBackend(t *testing.T) {
expectedIdentity := &idriver.IdentityDescriptor{
Identity: []byte("backend identity"),
AuditInfo: []byte("backend audit"),
}

c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
return expectedIdentity, nil
}, 10, []byte("cache audit"), NewMetrics(&disabled.Provider{}))

// Call with different audit info to trigger backend fetch
identityDescriptor, err := c.Identity(context.Background(), []byte("different audit"))
require.NoError(t, err)
assert.Equal(t, expectedIdentity.Identity, identityDescriptor.Identity)
assert.Equal(t, expectedIdentity.AuditInfo, identityDescriptor.AuditInfo)
}

// TestFetchIdentityFromBackendError verifies error propagation from backend failures.
func TestFetchIdentityFromBackendError(t *testing.T) {
expectedErr := errors.New("backend error")

c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
return nil, expectedErr
}, 10, []byte("cache audit"), NewMetrics(&disabled.Provider{}))

// Call with different audit info to trigger backend fetch
_, err := c.Identity(context.Background(), []byte("different audit"))
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}

// TestFetchIdentityFromCacheTimeout verifies on-demand generation after cache timeout.
func TestFetchIdentityFromCacheTimeout(t *testing.T) {
callCount := make(chan struct{}, 10)
c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
callCount <- struct{}{}
// Simulate slow backend - not strictly needed for the test
// time.Sleep(10 * time.Millisecond)
return &idriver.IdentityDescriptor{
Identity: []byte("timeout identity"),
AuditInfo: []byte("timeout audit"),
}, nil
}, 0, nil, NewMetrics(&disabled.Provider{})) // cache size 0 to force timeout

// Set short timeout to trigger timeout path
c.cacheTimeout = 1 * time.Millisecond

identityDescriptor, err := c.Identity(context.Background(), nil)
require.NoError(t, err)
assert.Equal(t, driver.Identity([]byte("timeout identity")), identityDescriptor.Identity)
assert.Equal(t, []byte("timeout audit"), identityDescriptor.AuditInfo)
assert.Len(t, callCount, 1)
}

// TestFetchIdentityFromCacheTimeoutError verifies error handling after cache timeout.
func TestFetchIdentityFromCacheTimeoutError(t *testing.T) {
expectedErr := errors.New("timeout backend error")

c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
return nil, expectedErr
}, 0, nil, NewMetrics(&disabled.Provider{}))

// Set short timeout to trigger timeout path
c.cacheTimeout = 1 * time.Millisecond

_, err := c.Identity(context.Background(), nil)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}

// TestProvisionIdentitiesError verifies provisioning retries after errors.
func TestProvisionIdentitiesError(t *testing.T) {
callCount := make(chan struct{}, 100)
maxCalls := 3

c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
// Fail 3 times then succeed
callCount <- struct{}{} // send once per call
if len(callCount) <= maxCalls {
return nil, errors.New("provision error")
}

return &idriver.IdentityDescriptor{
Identity: []byte("success identity"),
AuditInfo: []byte("success audit"),
}, nil
}, 10, nil, NewMetrics(&disabled.Provider{}))

// Trigger provisioning
_, err := c.Identity(context.Background(), nil)
require.NoError(t, err)

// Wait a bit for provisioning to attempt multiple times
time.Sleep(50 * time.Millisecond)

// Verify that provisioning continued after errors
assert.Greater(t, len(callCount), maxCalls)
}

// TestFetchIdentityFromCacheNilEntry verifies backend fallback for nil cache entries.
func TestFetchIdentityFromCacheNilEntry(t *testing.T) {
backendCalled := make(chan struct{}, 1)

c := NewIdentityCache(func(ctx context.Context, auditInfo []byte) (*idriver.IdentityDescriptor, error) {
backendCalled <- struct{}{}

return &idriver.IdentityDescriptor{
Identity: []byte("backend fallback"),
AuditInfo: []byte("backend audit"),
}, nil
}, 10, nil, NewMetrics(&disabled.Provider{}))

// Send nil to cache to test nil handling
c.cache <- nil

identityDescriptor, err := c.Identity(context.Background(), nil)
require.NoError(t, err)
assert.Eventually(t, func() bool {
select {
case <-backendCalled:
return true
default:
return false
}
}, time.Second, 10*time.Millisecond)
assert.Equal(t, driver.Identity([]byte("backend fallback")), identityDescriptor.Identity)
}
6 changes: 4 additions & 2 deletions token/services/identity/idemix/cache/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ import (
)

var (
// LevelOpts defines gauge options for tracking cache level.
LevelOpts = metrics.GaugeOpts{
Name: "cache_level",
Help: "Level of the idemix cache",
LabelNames: []string{"network", "channel", "namespace"},
}
)

// Metrics contains the metrics for this package
// Metrics contains metrics for monitoring identity cache performance.
type Metrics struct {
// Current number of cached identities
CacheLevelGauge metrics.Gauge
}

// NewMetrics instantiate the metrics for this package
// NewMetrics creates a new Metrics instance.
func NewMetrics(p metrics.Provider) *Metrics {
return &Metrics{
CacheLevelGauge: p.NewGauge(LevelOpts),
Expand Down
40 changes: 23 additions & 17 deletions token/services/identity/idemix/crypto/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,52 @@ import (
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/proto"
"github.com/hyperledger-labs/fabric-token-sdk/token/core/common/encoding/json"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/idemix/schema"
)

// Schema represents the version identifier for the credential schema.
type Schema = string

// SchemaManager handles the various credential schemas. A credential schema
// contains information about the number of attributes, which attributes
// must be disclosed when creating proofs, the format of the attributes etc.
type SchemaManager interface {
// EidNymAuditOpts returns the options that `sid` must use to audit an EIDNym
EidNymAuditOpts(schema string, attrs [][]byte) (*csp.EidNymAuditOpts, error)
// RhNymAuditOpts returns the options that `sid` must use to audit an RhNym
RhNymAuditOpts(schema string, attrs [][]byte) (*csp.RhNymAuditOpts, error)
}

// AuditInfo contains cryptographic audit data for an Idemix identity.
type AuditInfo struct {
// Enrollment ID pseudonym audit data
EidNymAuditData *csp.AttrNymAuditData
RhNymAuditData *csp.AttrNymAuditData
Attributes [][]byte

Csp csp.BCCSP `json:"-"`
IssuerPublicKey csp.Key `json:"-"`
SchemaManager SchemaManager `json:"-"`
Schema string
// Revocation handle pseudonym audit data
RhNymAuditData *csp.AttrNymAuditData
// Credential attributes (e.g. EnrollmentID, RevocationHandle strings)
Attributes [][]byte

// Cryptographic service provider
Csp csp.BCCSP `json:"-"`
// Credential issuer's public key
IssuerPublicKey csp.Key `json:"-"`
// Schema-specific operations manager
SchemaManager schema.Manager `json:"-"`
// Credential schema version
Schema string
}

// Bytes serializes the AuditInfo to JSON format.
func (a *AuditInfo) Bytes() ([]byte, error) {
return json.Marshal(a)
}

// FromBytes deserializes the AuditInfo from JSON format.
func (a *AuditInfo) FromBytes(raw []byte) error {
return json.Unmarshal(raw, a)
}

// EnrollmentID returns the enrollment ID from Attributes[2].
func (a *AuditInfo) EnrollmentID() string {
return string(a.Attributes[2])
}

// RevocationHandle returns the revocation handle from Attributes[3].
func (a *AuditInfo) RevocationHandle() string {
return string(a.Attributes[3])
}

// Match verifies the identity matches this audit info by checking EID and RH pseudonyms.
func (a *AuditInfo) Match(_ context.Context, id []byte) error {
serialized := new(SerializedIdemixIdentity)
err := proto.Unmarshal(id, serialized)
Expand Down Expand Up @@ -104,6 +109,7 @@ func (a *AuditInfo) Match(_ context.Context, id []byte) error {
return nil
}

// DeserializeAuditInfo deserializes the audit information from JSON.
func DeserializeAuditInfo(raw []byte) (*AuditInfo, error) {
auditInfo := &AuditInfo{}
err := auditInfo.FromBytes(raw)
Expand Down
Loading
Loading