diff --git a/internal/blockchain/chain_state.go b/internal/blockchain/chain_state.go index a98ebe82..04f74d69 100644 --- a/internal/blockchain/chain_state.go +++ b/internal/blockchain/chain_state.go @@ -892,10 +892,6 @@ func (cs *ChainState) restoreWithState( // Keep only ancestry up to the restored headerHash (fallback point) cs.KeepAncestryUpTo(blockHeaderHash) - // Clear verifier cache when restoring to a different state point - // as the epoch may have changed - ClearVerifierCache() - return nil } diff --git a/internal/blockchain/ring_verifier.go b/internal/blockchain/ring_verifier.go index 04368bbd..c4797b16 100644 --- a/internal/blockchain/ring_verifier.go +++ b/internal/blockchain/ring_verifier.go @@ -5,13 +5,19 @@ import ( "sync" "github.com/New-JAMneration/JAM-Protocol/internal/types" + "github.com/New-JAMneration/JAM-Protocol/internal/utilities/hash" vrf "github.com/New-JAMneration/JAM-Protocol/pkg/Rust-VRF/vrf-func-ffi/src" ) +type ringVerifierCacheKey struct { + epoch types.TimeSlot + gammaKHash types.OpaqueHash +} + type ringVerifierCache struct { sync.RWMutex - epoch types.TimeSlot + key ringVerifierCacheKey *vrf.Verifier } @@ -33,7 +39,7 @@ func ClearVerifierCache() { // the pointer anymore. func (c *ringVerifierCache) release() { c.Verifier = nil - c.epoch = 0 + c.key = ringVerifierCacheKey{} } func GetVerifier(epoch types.TimeSlot, gammaK types.ValidatorsData) (*vrf.Verifier, error) { @@ -41,10 +47,15 @@ func GetVerifier(epoch types.TimeSlot, gammaK types.ValidatorsData) (*vrf.Verifi return nil, fmt.Errorf("gammaK size %d is not equal to validators count %d", len(gammaK), types.ValidatorsCount) } + key, err := newRingVerifierCacheKey(epoch, gammaK) + if err != nil { + return nil, fmt.Errorf("failed to hash gammaK: %w", err) + } + // First path: read lock // Try to get both cached verifiers cache.RLock() - if cache.epoch == epoch && cache.Verifier != nil { + if cache.key == key && cache.Verifier != nil { cache.RUnlock() return cache.Verifier, nil } @@ -55,11 +66,11 @@ func GetVerifier(epoch types.TimeSlot, gammaK types.ValidatorsData) (*vrf.Verifi defer cache.Unlock() // Double check - if cache.epoch == epoch && cache.Verifier != nil { + if cache.key == key && cache.Verifier != nil { return cache.Verifier, nil } - // epoch transition or not initialized: drop the old verifier reference. + // Cache miss or key transition: drop the old verifier reference. // Do not Free() it here — a concurrent reader may still hold the pointer // from the read-lock fast path above. The finalizer frees it later. cache.release() @@ -77,7 +88,19 @@ func GetVerifier(epoch types.TimeSlot, gammaK types.ValidatorsData) (*vrf.Verifi } // update cache and return - cache.epoch = epoch + cache.key = key cache.Verifier = ringVerifier return ringVerifier, nil } + +func newRingVerifierCacheKey(epoch types.TimeSlot, gammaK types.ValidatorsData) (ringVerifierCacheKey, error) { + gammaKHash, err := hash.HashEncode(&gammaK) + if err != nil { + return ringVerifierCacheKey{}, err + } + + return ringVerifierCacheKey{ + epoch: epoch, + gammaKHash: gammaKHash, + }, nil +} diff --git a/internal/blockchain/ring_verifier_test.go b/internal/blockchain/ring_verifier_test.go new file mode 100644 index 00000000..6fed17f7 --- /dev/null +++ b/internal/blockchain/ring_verifier_test.go @@ -0,0 +1,93 @@ +package blockchain + +import ( + "testing" + + "github.com/New-JAMneration/JAM-Protocol/internal/types" + jamhash "github.com/New-JAMneration/JAM-Protocol/internal/utilities/hash" + vrf "github.com/New-JAMneration/JAM-Protocol/pkg/Rust-VRF/vrf-func-ffi/src" +) + +func testGammaK(seed byte) types.ValidatorsData { + gammaK := make(types.ValidatorsData, types.ValidatorsCount) + for i := range gammaK { + b := byte(i) + seed + gammaK[i].Bandersnatch[0] = b + gammaK[i].Ed25519[0] = b + 1 + gammaK[i].Bls[0] = b + 2 + gammaK[i].Metadata[0] = b + 3 + } + return gammaK +} + +func TestRingVerifierCacheKeyIncludesEpochAndGammaKContents(t *testing.T) { + gammaK := testGammaK(1) + + key, err := newRingVerifierCacheKey(42, gammaK) + if err != nil { + t.Fatalf("newRingVerifierCacheKey failed: %v", err) + } + + sameKey, err := newRingVerifierCacheKey(42, gammaK) + if err != nil { + t.Fatalf("newRingVerifierCacheKey failed: %v", err) + } + if key != sameKey { + t.Fatalf("same epoch and gammaK produced different keys") + } + + differentEpochKey, err := newRingVerifierCacheKey(43, gammaK) + if err != nil { + t.Fatalf("newRingVerifierCacheKey failed: %v", err) + } + if key == differentEpochKey { + t.Fatalf("different epoch produced the same key") + } + + differentGammaK := append(types.ValidatorsData(nil), gammaK...) + differentGammaK[0].Metadata[0] ^= 0xff + differentGammaKKey, err := newRingVerifierCacheKey(42, differentGammaK) + if err != nil { + t.Fatalf("newRingVerifierCacheKey failed: %v", err) + } + if key == differentGammaKKey { + t.Fatalf("different gammaK contents produced the same key") + } +} + +func TestRestoreWithStateKeepsVerifierCache(t *testing.T) { + ClearVerifierCache() + t.Cleanup(ClearVerifierCache) + + gammaK := testGammaK(2) + key, err := newRingVerifierCacheKey(7, gammaK) + if err != nil { + t.Fatalf("newRingVerifierCacheKey failed: %v", err) + } + verifier := &vrf.Verifier{} + + cache.Lock() + cache.key = key + cache.Verifier = verifier + cache.Unlock() + + cs := newChainState() + block := types.Block{Header: types.Header{Slot: 1}} + headerHash, err := jamhash.ComputeBlockHeaderHash(block.Header) + if err != nil { + t.Fatalf("ComputeBlockHeaderHash failed: %v", err) + } + + if err := cs.restoreWithState(headerHash, block, types.State{}, nil); err != nil { + t.Fatalf("restoreWithState failed: %v", err) + } + + cache.RLock() + defer cache.RUnlock() + if cache.key != key { + t.Fatalf("restoreWithState changed verifier cache key") + } + if cache.Verifier != verifier { + t.Fatalf("restoreWithState cleared verifier cache") + } +}