Skip to content
Merged
3 changes: 3 additions & 0 deletions cmd/proxy/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
# before it should be updated
EconomicsMetricsCacheValidityDurationSec = 600 # 10 minutes

# BlockCacheDurationSec defines how long block/hyperblock results(queried by hash or nonce) are kept in cache, in seconds.
BlockCacheDurationSec = 30

# BalancedObservers - if this flag is set to true, then the requests will be distributed equally between observers.
# Otherwise, there are chances that only one observer from a shard will process the requests
BalancedObservers = true
Expand Down
10 changes: 8 additions & 2 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ func createVersionsRegistryTestOrProduction(
HeartbeatCacheValidityDurationSec: 60,
ValStatsCacheValidityDurationSec: 60,
EconomicsMetricsCacheValidityDurationSec: 6,
BlockCacheDurationSec: 30,
FaucetValue: "10000000000",
},
ApiLogging: config.ApiLoggingConfig{
Expand Down Expand Up @@ -512,13 +513,18 @@ func createVersionsRegistry(
return nil, err
}

closableComponents.Add(nodeGroupProc, valStatsProc, nodeStatusProc, bp)
timedCache, err := cache.NewTimeCacher(time.Duration(cfg.GeneralSettings.BlockCacheDurationSec) * time.Second)
if err != nil {
return nil, err
}

closableComponents.Add(nodeGroupProc, valStatsProc, nodeStatusProc, bp, timedCache)

nodeGroupProc.StartCacheUpdate()
valStatsProc.StartCacheUpdate()
nodeStatusProc.StartCacheUpdate()

blockProc, err := process.NewBlockProcessor(bp)
blockProc, err := process.NewBlockProcessor(bp, timedCache)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type GeneralSettingsConfig struct {
HeartbeatCacheValidityDurationSec int
ValStatsCacheValidityDurationSec int
EconomicsMetricsCacheValidityDurationSec int
BlockCacheDurationSec int
FaucetValue string
RateLimitWindowDurationSeconds int
BalancedObservers bool
Expand Down
20 changes: 20 additions & 0 deletions data/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ type BlockApiResponse struct {
Code ReturnCode `json:"code"`
}

// Hash returns internal hash
func (h *BlockApiResponse) Hash() string {
return h.Data.Block.Hash
}

// Nonce returns internal nonce
func (h *BlockApiResponse) Nonce() uint64 {
return h.Data.Block.Nonce
}

// BlockApiResponsePayload wraps a block
type BlockApiResponsePayload struct {
Block api.Block `json:"block"`
Expand All @@ -24,6 +34,16 @@ type HyperblockApiResponse struct {
Code ReturnCode `json:"code"`
}

// Hash returns internal hash
func (h *HyperblockApiResponse) Hash() string {
return h.Data.Hyperblock.Hash
}

// Nonce returns internal nonce
func (h *HyperblockApiResponse) Nonce() uint64 {
return h.Data.Hyperblock.Nonce
}

// NewHyperblockApiResponse creates a HyperblockApiResponse
func NewHyperblockApiResponse(hyperblock api.Hyperblock) *HyperblockApiResponse {
return &HyperblockApiResponse{
Expand Down
33 changes: 33 additions & 0 deletions facade/mock/timedCacheMock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mock

// TimedCacheMock -
type TimedCacheMock struct {
Cache map[string]interface{}
}

// NewTimedCacheMock -
func NewTimedCacheMock() *TimedCacheMock {
return &TimedCacheMock{Cache: make(map[string]interface{})}
}

// Put -
func (mock *TimedCacheMock) Put(key []byte, value interface{}) error {
mock.Cache[string(key)] = value
return nil
}

// Get -
func (mock *TimedCacheMock) Get(key []byte) (value interface{}, ok bool) {
val, found := mock.Cache[string(key)]
return val, found
}

// Close -
func (mock *TimedCacheMock) Close() error {
return nil
}

// IsInterfaceNil -
func (mock *TimedCacheMock) IsInterfaceNil() bool {
return mock == nil
}
25 changes: 25 additions & 0 deletions facade/mock/timedCacheStub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mock

// TimedCacheStub -
type TimedCacheStub struct {
}

// Put -
func (stub *TimedCacheStub) Put(_ []byte, _ interface{}) error {
return nil
}

// Get -
func (stub *TimedCacheStub) Get(_ []byte) (value interface{}, ok bool) {
return nil, false
}

// Close -
func (stub *TimedCacheStub) Close() error {
return nil
}

// IsInterfaceNil -
func (stub *TimedCacheStub) IsInterfaceNil() bool {
return stub == nil
}
51 changes: 42 additions & 9 deletions process/blockProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,39 @@ const (
rawPathStr = "raw"
)

const (
blockScope = "block"
hyperBlockScope = "hyperblock"
)

// BlockProcessor handles blocks retrieving
type BlockProcessor struct {
proc Processor
proc Processor
cache TimedCache
}

// NewBlockProcessor will create a new block processor
func NewBlockProcessor(proc Processor) (*BlockProcessor, error) {
func NewBlockProcessor(proc Processor, cache TimedCache) (*BlockProcessor, error) {
if check.IfNil(proc) {
return nil, ErrNilCoreProcessor
}
if check.IfNil(cache) {
return nil, ErrNilTimedCache
}

return &BlockProcessor{
proc: proc,
proc: proc,
cache: cache,
}, nil
}

// GetBlockByHash will return the block based on its hash
func (bp *BlockProcessor) GetBlockByHash(shardID uint32, hash string, options common.BlockQueryOptions) (*data.BlockApiResponse, error) {
scope := fmt.Sprintf("%s:shardID=%d", blockScope, shardID)
if cached := getObjectFromCacheWithHash[*data.BlockApiResponse](bp.cache, scope, hash, options); cached != nil {
return cached, nil
}

observers, err := bp.getObserversOrFullHistoryNodes(shardID)
if err != nil {
return nil, err
Expand All @@ -62,23 +77,28 @@ func (bp *BlockProcessor) GetBlockByHash(shardID uint32, hash string, options co

response := data.BlockApiResponse{}
for _, observer := range observers {

_, err := bp.proc.CallGetRestEndPoint(observer.Address, path, &response)
if err != nil {
log.Error("block request", "observer", observer.Address, "error", err.Error())
continue
}

log.Info("block request", "shard id", observer.ShardId, "hash", hash, "observer", observer.Address)
return &response, nil

bp.cacheObject(&response, scope, options)
return &response, nil
Comment on lines 72 to +89
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching responses with errors: The code caches the response before checking if it contains an error (the response.Error field). If an observer returns a response with an error field populated but doesn't return a connection error, that erroneous response will be cached. This could lead to caching and serving invalid data.

Consider checking the response's Error field before caching:

log.Info("block request", "shard id", observer.ShardId, "hash", hash, "observer", observer.Address)

if response.Error != "" {
    log.Error("block response contains error", "error", response.Error)
    continue
}

bp.cacheObject(&response, scope, options)
return &response, nil

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be expected

}

return nil, WrapObserversError(response.Error)
}

// GetBlockByNonce will return the block based on the nonce
func (bp *BlockProcessor) GetBlockByNonce(shardID uint32, nonce uint64, options common.BlockQueryOptions) (*data.BlockApiResponse, error) {
scope := fmt.Sprintf("%s:shardID=%d", blockScope, shardID)
if cached := getObjectFromCacheWithNonce[*data.BlockApiResponse](bp.cache, scope, nonce, options); cached != nil {
return cached, nil
}

observers, err := bp.getObserversOrFullHistoryNodes(shardID)
if err != nil {
return nil, err
Expand All @@ -88,16 +108,15 @@ func (bp *BlockProcessor) GetBlockByNonce(shardID uint32, nonce uint64, options

response := data.BlockApiResponse{}
for _, observer := range observers {

_, err := bp.proc.CallGetRestEndPoint(observer.Address, path, &response)
if err != nil {
log.Error("block request", "observer", observer.Address, "error", err.Error())
continue
}

log.Info("block request", "shard id", observer.ShardId, "nonce", nonce, "observer", observer.Address)
bp.cacheObject(&response, scope, options)
return &response, nil
Comment on lines 117 to 119
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching responses with errors: The code caches the response before checking if it contains an error (the response.Error field). If an observer returns a response with an error field populated but doesn't return a connection error, that erroneous response will be cached. This could lead to caching and serving invalid data.

Consider checking the response's Error field before caching:

log.Info("block request", "shard id", observer.ShardId, "nonce", nonce, "observer", observer.Address)

if response.Error != "" {
    log.Error("block response contains error", "error", response.Error)
    continue
}

bp.cacheObject(&response, scope, options)
return &response, nil

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be expected


}

return nil, WrapObserversError(response.Error)
Expand All @@ -114,6 +133,10 @@ func (bp *BlockProcessor) getObserversOrFullHistoryNodes(shardID uint32) ([]*dat

// GetHyperBlockByHash returns the hyperblock by hash
func (bp *BlockProcessor) GetHyperBlockByHash(hash string, options common.HyperblockQueryOptions) (*data.HyperblockApiResponse, error) {
if cached := getObjectFromCacheWithHash[*data.HyperblockApiResponse](bp.cache, hyperBlockScope, hash, options); cached != nil {
return cached, nil
}

builder := &hyperblockBuilder{}

blockQueryOptions := common.BlockQueryOptions{
Expand All @@ -136,7 +159,10 @@ func (bp *BlockProcessor) GetHyperBlockByHash(hash string, options common.Hyperb
}

hyperblock := builder.build(options.NotarizedAtSource)
return data.NewHyperblockApiResponse(hyperblock), nil
hyperBlockRsp := data.NewHyperblockApiResponse(hyperblock)
bp.cacheObject(hyperBlockRsp, hyperBlockScope, options)

return hyperBlockRsp, nil
}

func (bp *BlockProcessor) addShardBlocks(
Expand Down Expand Up @@ -181,6 +207,10 @@ func (bp *BlockProcessor) getAlteredAccountsIfNeeded(options common.HyperblockQu

// GetHyperBlockByNonce returns the hyperblock by nonce
func (bp *BlockProcessor) GetHyperBlockByNonce(nonce uint64, options common.HyperblockQueryOptions) (*data.HyperblockApiResponse, error) {
if cached := getObjectFromCacheWithNonce[*data.HyperblockApiResponse](bp.cache, hyperBlockScope, nonce, options); cached != nil {
return cached, nil
}

builder := &hyperblockBuilder{}

blockQueryOptions := common.BlockQueryOptions{
Expand All @@ -203,7 +233,10 @@ func (bp *BlockProcessor) GetHyperBlockByNonce(nonce uint64, options common.Hype
}

hyperblock := builder.build(options.NotarizedAtSource)
return data.NewHyperblockApiResponse(hyperblock), nil
hyperBlockRsp := data.NewHyperblockApiResponse(hyperblock)
bp.cacheObject(hyperBlockRsp, hyperBlockScope, options)

return hyperBlockRsp, nil
}

// GetInternalBlockByHash will return the internal block based on its hash
Expand Down
64 changes: 64 additions & 0 deletions process/blockProcessorCache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package process

import (
"encoding/json"
"fmt"
)

type cacheableBlock interface {
Hash() string
Nonce() uint64
}

// No error checks for this cache.
// These caching errors should never happen, and if they do, they should not be blocking

func (bp *BlockProcessor) cacheObject(obj cacheableBlock, scope string, opts interface{}) {
objKey := makeObjKey(scope, obj.Hash(), opts)

// Store object
_ = bp.cache.Put(objKey, obj)

// Store nonce + hash lookup keys
_ = bp.cache.Put(makeHashCacheKey(scope, obj.Hash(), opts), objKey)
_ = bp.cache.Put(makeNonceCacheKey(scope, obj.Nonce(), opts), objKey)
}

func makeObjKey(scope string, hash string, opts interface{}) []byte {
optBytes, _ := json.Marshal(opts)
return []byte(scope + ":" + hash + "|" + string(optBytes))
}

func makeHashCacheKey(scope string, hash string, opts interface{}) []byte {
optBytes, _ := json.Marshal(opts)
return []byte(fmt.Sprintf("%s:hash:%s|opts:%s", scope, hash, string(optBytes)))
}

func makeNonceCacheKey(scope string, nonce uint64, opts interface{}) []byte {
optBytes, _ := json.Marshal(opts)
return []byte(fmt.Sprintf("%s:nonce:%d|opts:%s", scope, nonce, string(optBytes)))
}

func getObjectFromCacheWithHash[T cacheableBlock](c TimedCache, scope string, hash string, opts interface{}) T {
return getObjFromCache[T](c, makeHashCacheKey(scope, hash, opts))
}

func getObjectFromCacheWithNonce[T cacheableBlock](c TimedCache, scope string, nonce uint64, opts interface{}) T {
return getObjFromCache[T](c, makeNonceCacheKey(scope, nonce, opts))
}

func getObjFromCache[T cacheableBlock](c TimedCache, lookUpKey []byte) T {
var retObj T

key, _ := c.Get(lookUpKey)
if key == nil {
return retObj
}

val, ok := c.Get(key.([]byte))
if !ok {
return retObj
}

return val.(T)
}
Loading
Loading