Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9999afc
feat: aggregate eth_call batches with multicall3
0x666c6f Jan 14, 2026
6dd3515
fix: log multicall3 fallbacks and refine helpers
0x666c6f Jan 14, 2026
af53f95
feat: add config flag to disable multicall3 aggregation
0x666c6f Jan 14, 2026
f4619fa
fix: add NaN/Inf guards with logging to prevent score propagation issues
0x666c6f Jan 8, 2026
0af72d4
fix: add gock mocks for DynamoDB cache test
0x666c6f Jan 15, 2026
79b0fc1
fix: add gock mocks for Redis cache test
0x666c6f Jan 15, 2026
c409bbe
fix: add safe integer conversion to prevent overflow in multicall3
0x666c6f Jan 15, 2026
2dece38
fix: address PR review feedback for multicall3 aggregation
0x666c6f Jan 15, 2026
151c0ca
fix: prevent integer overflow in multicall3 result count multiplication
0x666c6f Jan 15, 2026
765788e
fix: record MetricNetworkRequestsReceived for aggregated batch requests
0x666c6f Jan 15, 2026
2a9a588
fix: widen fallback error pattern matching for multicall3
0x666c6f Jan 15, 2026
6d570d4
feat: add per-call caching for multicall3 aggregated batches
0x666c6f Jan 15, 2026
7bb01d2
test: add tests for multicall3 per-call caching
0x666c6f Jan 15, 2026
baa23f4
docs: refine multicall3 batching design
0x666c6f Jan 15, 2026
b6d432c
feat: add Multicall3AggregationConfig for network-level batching
0x666c6f Jan 15, 2026
6520dba
feat: add CompositeTypeMulticall3 constant
0x666c6f Jan 15, 2026
697bac4
feat: add Multicall3 batching key and entry types
0x666c6f Jan 15, 2026
400f800
feat: add Multicall3 eligibility checking
0x666c6f Jan 15, 2026
71ba2dd
feat: add allowedBlockTags map for block ref eligibility checking
0x666c6f Jan 15, 2026
2ff459a
fix: add defensive type assertions and tests for ExtractCallInfo
0x666c6f Jan 15, 2026
cd3981d
feat: implement Multicall3 Batcher with window and caps
0x666c6f Jan 15, 2026
af5261f
feat: implement Multicall3 batch forwarding and result mapping
0x666c6f Jan 15, 2026
ebbc1de
feat: add BatcherManager for per-network batcher instances
0x666c6f Jan 15, 2026
50c6ff3
feat: integrate Multicall3 batching into eth_call pre-forward hook
0x666c6f Jan 15, 2026
965bdbd
feat: add Multicall3 batching observability metrics
0x666c6f Jan 15, 2026
7f8252f
fix: improve multicall3 batch handling and caching
0x666c6f Jan 15, 2026
4e811ec
fix: address PR review comments
0x666c6f Jan 15, 2026
cd9e0ce
fix: improve observability and error handling in multicall3 batching
0x666c6f Jan 15, 2026
6a8f923
test: add critical tests for shutdown and double-flush scenarios
0x666c6f Jan 15, 2026
6648084
fix: validate call fields and normalize block params for multicall3
0x666c6f Jan 16, 2026
d895793
fix: improve panic handling, logging, and key safety in multicall3 ba…
0x666c6f Jan 16, 2026
3224c5d
fix: address P1/P2 issues in multicall3 batching
0x666c6f Jan 16, 2026
07ec286
fix: address PR review comments for multicall3 batching
0x666c6f Jan 16, 2026
c7cf320
fix: address HIGH priority issues in multicall3 batching
0x666c6f Jan 16, 2026
437dae1
fix: address P2/P3 review issues in multicall3 batching
0x666c6f Jan 16, 2026
5674232
fix: address critical and high priority review issues
0x666c6f Jan 16, 2026
0ec52d0
fix: address medium and low priority review issues
0x666c6f Jan 16, 2026
53cbc58
fix: explicitly disable batching in eth_call tests
0x666c6f Jan 16, 2026
ce344ac
docs: add Multicall3 aggregation configuration documentation
0x666c6f Jan 16, 2026
7b8da22
chore: remove Claude CI workflows from feature branch
0x666c6f Jan 16, 2026
355623c
chore: remove implementation plan (keep design doc only)
0x666c6f Jan 16, 2026
57d49a7
fix: correct multicall3 config defaults in user documentation
0x666c6f Jan 16, 2026
3e944bc
test: add comprehensive tests for multicall3 batching
0x666c6f Jan 16, 2026
24d1fbd
fix: use atomic counter in panic recovery test to fix race condition
0x666c6f Jan 16, 2026
be26f0a
ci: increase test workflow timeout to 30 minutes
0x666c6f Jan 16, 2026
89f5a80
fix: address PR review issues for multicall3 batching
0x666c6f Jan 19, 2026
2e665e9
fix: address remaining PR review items
0x666c6f Jan 19, 2026
8bcdd1f
fix: UseUpstream directive now overrides matchUpstreamGroup filter
0x666c6f Jan 19, 2026
876a6eb
test: add comprehensive integration tests for multicall3 batching
0x666c6f Jan 19, 2026
d76ea71
chore: remove multicall3 production test script
0x666c6f Jan 19, 2026
9bb9e7e
fix: merge Multicall3Aggregation config from networkDefaults
0x666c6f Jan 19, 2026
19bec07
fix: avoid panicking and fail-open when rate limiter redis is not ava…
aramalipoor Jan 20, 2026
49184c3
feat: add auto-detect bypass for multicall3 batching
0x666c6f Jan 21, 2026
4dd5649
Merge remote-tracking branch 'origin/main' into feature/pla-471-multi…
0x666c6f Jan 21, 2026
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
44 changes: 0 additions & 44 deletions .github/workflows/claude-code-review.yml

This file was deleted.

50 changes: 0 additions & 50 deletions .github/workflows/claude.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:
jobs:
units:
runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}"
timeout-minutes: 20
timeout-minutes: 30
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
Expand Down
195 changes: 184 additions & 11 deletions architecture/evm/eth_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,204 @@ package evm

import (
"context"
"fmt"
"sync"

"github.com/erpc/erpc/common"
)

// Global batcher manager for network-level Multicall3 batching
var (
globalBatcherManager *BatcherManager
batcherManagerOnce sync.Once
)

// defaultMulticall3AggregationConfig is the default config when Multicall3Aggregation
// is not explicitly configured. Enabled by default to match documented behavior.
var defaultMulticall3AggregationConfig = func() *common.Multicall3AggregationConfig {
cfg := &common.Multicall3AggregationConfig{Enabled: true}
cfg.SetDefaults()
return cfg
}()

// GetBatcherManager returns the global batcher manager.
func GetBatcherManager() *BatcherManager {
batcherManagerOnce.Do(func() {
globalBatcherManager = NewBatcherManager()
})
return globalBatcherManager
}

// ShutdownBatcherManager shuts down the global batcher manager.
// Should be called during application shutdown.
func ShutdownBatcherManager() {
if globalBatcherManager != nil {
globalBatcherManager.Shutdown()
}
}

// networkForwarder wraps a Network to implement Forwarder interface.
type networkForwarder struct {
network common.Network
}

func (f *networkForwarder) Forward(ctx context.Context, req *common.NormalizedRequest) (*common.NormalizedResponse, error) {
return f.network.Forward(ctx, req)
}

func (f *networkForwarder) SetCache(ctx context.Context, req *common.NormalizedRequest, resp *common.NormalizedResponse) error {
cache := f.network.Cache()
if cache == nil || cache.IsObjectNull() {
return nil
}
return cache.Set(ctx, req, resp)
}

// projectPreForward_eth_call is the pre-forward hook for eth_call requests.
// It handles Multicall3 batching when enabled, aggregating multiple eth_call requests
// into a single Multicall3 call for improved throughput.
//
// Returns:
// - handled: true if the request was handled (either batched or forwarded directly)
// - response: the response if handled, nil otherwise
// - error: any error that occurred
//
// The function will forward the request directly (bypassing batching) when:
// - Multicall3 aggregation is disabled in config
// - The request is not eligible for batching (has gas/value/from fields, etc.)
// - The batcher queue is full or at capacity
// - The request's deadline is too tight for batching
func projectPreForward_eth_call(ctx context.Context, network common.Network, nq *common.NormalizedRequest) (bool, *common.NormalizedResponse, error) {
jrq, err := nq.JsonRpcRequest()
if err != nil {
return false, nil, nil
}

// Normalize params: ensure block param is present
jrq.RLock()
if len(jrq.Params) != 1 {
jrq.RUnlock()
paramsLen := len(jrq.Params)
jrq.RUnlock()

if paramsLen == 0 {
return false, nil, nil
}
jrq.RUnlock()

// Some upstreams require the block number to be specified as a parameter.
jrq.Lock()
jrq.Params = []interface{}{
jrq.Params[0],
"latest",
// Add "latest" block param if missing (only 1 param)
if paramsLen == 1 {
jrq.Lock()
jrq.Params = append(jrq.Params, "latest")
jrq.Unlock()
}

// Get Multicall3 aggregation config, using defaults if not explicitly configured
cfg := network.Config()
var aggCfg *common.Multicall3AggregationConfig
if cfg != nil && cfg.Evm != nil && cfg.Evm.Multicall3Aggregation != nil {
aggCfg = cfg.Evm.Multicall3Aggregation
} else {
// Use default config (enabled by default)
aggCfg = defaultMulticall3AggregationConfig
}
jrq.Unlock()

resp, err := network.Forward(ctx, nq)
return true, resp, err
// Check if Multicall3 aggregation is explicitly disabled
if !aggCfg.Enabled {
// Batching disabled, use normal forward
resp, err := network.Forward(ctx, nq)
return true, resp, err
}

// Check eligibility for batching
eligible, reason := IsEligibleForBatching(nq, aggCfg)
if !eligible {
// Not eligible, forward normally
if logger := network.Logger(); logger != nil {
logger.Debug().
Str("reason", reason).
Str("method", "eth_call").
Msg("request not eligible for multicall3 batching")
}
resp, err := network.Forward(ctx, nq)
return true, resp, err
}

// Extract call info for batching key
_, _, blockRef, err := ExtractCallInfo(nq)
if err != nil {
resp, err := network.Forward(ctx, nq)
return true, resp, err
}

// Build batching key
projectId := network.ProjectId()
if projectId == "" {
projectId = fmt.Sprintf("network:%s", network.Id())
}

userId := ""
if aggCfg.AllowCrossUserBatching == nil || !*aggCfg.AllowCrossUserBatching {
userId = nq.UserId()
}

key := BatchingKey{
ProjectId: projectId,
NetworkId: network.Id(),
BlockRef: blockRef,
DirectivesKey: DeriveDirectivesKey(nq.Directives()),
UserId: userId,
}

// Check cache before batching (unless skip-cache-read is set)
if !nq.SkipCacheRead() {
cache := network.Cache()
if cache != nil && !cache.IsObjectNull() {
cachedResp, cacheErr := cache.Get(ctx, nq)
if cacheErr != nil {
// Log cache errors but continue to batching
if logger := network.Logger(); logger != nil {
logger.Warn().
Err(cacheErr).
Str("networkId", network.Id()).
Msg("multicall3 pre-batch cache get failed, continuing to batch")
}
} else if cachedResp != nil && !cachedResp.IsObjectNull(ctx) {
// Cache hit - return cached response directly
cachedResp.SetFromCache(true)
return true, cachedResp, nil
}
}
}

// Get or create batcher for this project+network
mgr := GetBatcherManager()
forwarder := &networkForwarder{network: network}
batcher := mgr.GetOrCreate(projectId, network.Id(), aggCfg, forwarder, network.Logger())
if batcher == nil {
// Batching disabled, forward normally
resp, err := network.Forward(ctx, nq)
return true, resp, err
}

// Enqueue request
entry, bypass, err := batcher.Enqueue(ctx, key, nq)
if err != nil || bypass {
// Log enqueue errors for debugging (bypass without error is normal)
if err != nil && network.Logger() != nil {
network.Logger().Debug().
Err(err).
Str("projectId", projectId).
Str("networkId", network.Id()).
Msg("multicall3 enqueue failed, forwarding normally")
}
// Bypass batching, forward normally
resp, err := network.Forward(ctx, nq)
return true, resp, err
}

// Wait for batch result
select {
case result := <-entry.ResultCh:
return true, result.Response, result.Error
case <-ctx.Done():
return true, nil, ctx.Err()
}
}
Loading
Loading