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
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
15 changes: 13 additions & 2 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
processFactory "github.com/multiversx/mx-chain-proxy-go/process/factory"
"github.com/multiversx/mx-chain-proxy-go/testing"
versionsFactory "github.com/multiversx/mx-chain-proxy-go/versions/factory"
"github.com/multiversx/mx-chain-storage-go/timecache"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -336,6 +337,7 @@ func createVersionsRegistryTestOrProduction(
HeartbeatCacheValidityDurationSec: 60,
ValStatsCacheValidityDurationSec: 60,
EconomicsMetricsCacheValidityDurationSec: 6,
BlockCacheDurationSec: 30,
FaucetValue: "10000000000",
},
ApiLogging: config.ApiLoggingConfig{
Expand Down Expand Up @@ -512,13 +514,22 @@ func createVersionsRegistry(
return nil, err
}

closableComponents.Add(nodeGroupProc, valStatsProc, nodeStatusProc, bp)
cacheDuration := time.Duration(cfg.GeneralSettings.BlockCacheDurationSec) * time.Second
timedCache, err := timecache.NewTimeCacher(timecache.ArgTimeCacher{
DefaultSpan: cacheDuration,
CacheExpiry: cacheDuration,
})
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{}, _ int) bool {
mock.Cache[string(key)] = value
return false
}

// 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{}, _ int) bool {
return false
}

// 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
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/multiversx/mx-chain-crypto-go v1.3.0
github.com/multiversx/mx-chain-es-indexer-go v1.8.0
github.com/multiversx/mx-chain-logger-go v1.1.0
github.com/multiversx/mx-chain-storage-go v1.1.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0
github.com/urfave/cli v1.22.16
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ github.com/multiversx/mx-chain-es-indexer-go v1.8.0 h1:VLN9V3yNxchyGub25yOIUoa3K
github.com/multiversx/mx-chain-es-indexer-go v1.8.0/go.mod h1:OLsBLRme3wL59qPRSJ2UG3xRU8QIR/JnbXHPEncR4dg=
github.com/multiversx/mx-chain-logger-go v1.1.0 h1:97x84A6L4RfCa6YOx1HpAFxZp1cf/WI0Qh112whgZNM=
github.com/multiversx/mx-chain-logger-go v1.1.0/go.mod h1:K9XgiohLwOsNACETMNL0LItJMREuEvTH6NsoXWXWg7g=
github.com/multiversx/mx-chain-storage-go v1.1.0 h1:M1Y9DqMrJ62s7Zw31+cyuqsnPIvlG4jLBJl5WzeZLe8=
github.com/multiversx/mx-chain-storage-go v1.1.0/go.mod h1:o6Jm7cjfPmcc6XpyihYWrd6sx3sgqwurrunw3ZrfyxI=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand Down
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
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
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
Loading
Loading