Skip to content

Commit 60f7260

Browse files
feat(devenv/tcapi): bound persistent-network event scans, add delivery-only Run mode (#1221)
* fix: event poller with persistent network * feat: OnchainAssertionOnly in tcapi * chore: testing * chore: extract nested setup code * fix: mirror * bootstrap: push raw pubkey, enforce one family - Push OnchainSigningPubKey for every declared chain, not just the chain's own, so JD lane resolution can derive a signer address for a family the NOP never declared (e.g. a solana-only NOP signing into a canton-destination lane). - Reject Chains configs that mix chain families: a bootstrapper is built for exactly one family and shares one signing key across every declared chain, so a mixed list would silently push a mis-formatted key for whichever family loses out. * fix compilation error * temp: bump chainlink-ccip * feat: skip offchain assertions without flag * chore: lint --------- Co-authored-by: Makram Kamaleddine <makramkd@users.noreply.github.com>
1 parent 413d7fd commit 60f7260

9 files changed

Lines changed: 294 additions & 37 deletions

File tree

build/devenv/evm/event_poller.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import (
1111
"github.com/smartcontractkit/chainlink-ccv/protocol"
1212
)
1313

14+
// fallbackLookbackBlocks bounds how far back the first eth_getLogs scan reaches on a
15+
// long-lived chain, so it neither scans from genesis nor exceeds the provider's
16+
// getLogs range limit. The bound is in blocks, not wall-clock, since the target event
17+
// is always recent. Mirrors verifier/pkg/sourcereader's DefaultMaxBlockRange.
18+
const fallbackLookbackBlocks uint64 = 1500
19+
1420
type eventKey struct {
1521
chainSelector uint64
1622
msgNum uint64
@@ -177,7 +183,19 @@ func (p *eventPoller[T]) poll() {
177183
return
178184
}
179185

180-
events, err := p.pollFn(lastScanned+1, latestBlock)
186+
start := lastScanned + 1
187+
if lastScanned == 0 {
188+
if lookbackStart := fallbackStartBlock(latestBlock); lookbackStart > start {
189+
start = lookbackStart
190+
p.logger.Debug().
191+
Uint64("fromBlock", start).
192+
Uint64("toBlock", latestBlock).
193+
Str("event", p.eventName).
194+
Msg("Using fallback start block (bounded lookback)")
195+
}
196+
}
197+
198+
events, err := p.pollFn(start, latestBlock)
181199
if err != nil {
182200
p.logger.Warn().Err(err).Str("event", p.eventName).Msg("Failed to poll events")
183201
return
@@ -220,6 +238,13 @@ func (p *eventPoller[T]) poll() {
220238
p.lastScannedBlock = latestBlock
221239
}
222240

241+
func fallbackStartBlock(latestBlock uint64) uint64 {
242+
if latestBlock > fallbackLookbackBlocks {
243+
return latestBlock - fallbackLookbackBlocks
244+
}
245+
return 0
246+
}
247+
223248
func (p *eventPoller[T]) addToCache(key eventKey, result pollerResult[T]) {
224249
seqKey := eventKey{chainSelector: key.chainSelector, msgNum: key.msgNum}
225250
if _, exists := p.cachedBySeqNum[seqKey]; !exists {

build/devenv/evm/event_poller_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,16 @@ func TestEventPollerByMessageID(t *testing.T) {
197197
}
198198
})
199199
}
200+
201+
func TestFallbackStartBlock(t *testing.T) {
202+
t.Parallel()
203+
204+
// Long-lived chain: the first scan reaches back exactly the lookback window,
205+
// independent of block time.
206+
const latest = uint64(10_000_000)
207+
require.Equal(t, latest-fallbackLookbackBlocks, fallbackStartBlock(latest))
208+
209+
// Chain younger than (or exactly at) the lookback window scans from genesis.
210+
require.Equal(t, uint64(0), fallbackStartBlock(fallbackLookbackBlocks))
211+
require.Equal(t, uint64(0), fallbackStartBlock(100))
212+
}

build/devenv/evm/impl.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,8 @@ func extractEthClientFromBackend(client any) (*ethclient.Client, error) {
136136
// NewCCIP17EVM creates new smart-contracts wrappers with utility functions for CCIP17EVM implementation.
137137
func NewCCIP17EVM(ctx context.Context, logger zerolog.Logger, e *deployment.Environment, chainSelector uint64) (*CCIP17EVM, error) {
138138
var (
139-
onRamp *onramp.OnRamp
140-
offRamp *offramp.OffRamp
141-
onRampPoller eventPoller[cciptestinterfaces.MessageSentEvent]
139+
onRamp *onramp.OnRamp
140+
offRamp *offramp.OffRamp
142141
)
143142
chainDetails, err := chainsel.GetChainDetails(chainSelector)
144143
if err != nil {
@@ -192,7 +191,6 @@ func NewCCIP17EVM(ctx context.Context, logger zerolog.Logger, e *deployment.Envi
192191
ethClient: ethClient,
193192
onRamp: onRamp,
194193
offRamp: offRamp,
195-
onRampPoller: &onRampPoller,
196194
}, nil
197195
}
198196

@@ -203,6 +201,11 @@ func (m *CCIP17EVM) ChainSelector() uint64 {
203201
func (m *CCIP17EVM) getOrCreateOnRampPoller() (*eventPoller[cciptestinterfaces.MessageSentEvent], error) {
204202
m.pollersMu.Lock()
205203
defer m.pollersMu.Unlock()
204+
205+
if m.onRampPoller != nil {
206+
return m.onRampPoller, nil
207+
}
208+
206209
onRamp := m.onRamp
207210
ethClient := m.ethClient
208211

build/devenv/lib.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ type ChainImpl struct {
3232
//
3333
// Note that not all methods may be implemented by all backends.
3434
// For example, as of writing, a CLDF environment doesn't store indexer
35-
// or aggregator endpoints, so the [Lib.Indexer] and [Lib.AllIndexers]
36-
// methods will return an error.
35+
// or aggregator endpoints.
36+
//
37+
// The plural offchain getters ([Lib.AllAggregators], [Lib.AllIndexers]) return an
38+
// empty result with a nil error when no endpoints are configured, so callers can
39+
// treat absence as a normal state via a length check and reserve a returned error
40+
// for a real construction failure.
3741
type Lib interface {
3842
// Chains returns a slice of [ChainImpl] objects in an unspecified order.
3943
Chains(ctx context.Context) ([]ChainImpl, error)
@@ -57,12 +61,13 @@ type Lib interface {
5761
// or an error if no indexer client is available.
5862
IndexerMonitor() (*IndexerMonitor, error)
5963

60-
// AllIndexers returns all indexer clients available.
61-
// or an error if no indexer clients are available.
64+
// AllIndexers returns all indexer clients available, or (nil, nil) if no
65+
// indexer endpoints are configured.
6266
AllIndexers() ([]*client.IndexerClient, error)
6367

64-
// AllAggregators returns a mapping of qualifier name to the client of the aggregator for that qualifier.
65-
// or an error if no aggregator clients are available.
68+
// AllAggregators returns a mapping of qualifier name to the client of the
69+
// aggregator for that qualifier, or an empty map (nil error) if no aggregator
70+
// endpoints are configured.
6671
AllAggregators() (map[string]*AggregatorClient, error)
6772
}
6873

@@ -139,7 +144,7 @@ func (l *libFromCCV) AllAggregators() (map[string]*AggregatorClient, error) {
139144
}
140145

141146
if len(l.cfg.AggregatorEndpoints) == 0 {
142-
return nil, fmt.Errorf("no aggregator endpoints configured")
147+
return map[string]*AggregatorClient{}, nil
143148
}
144149

145150
aggregators := make(map[string]*AggregatorClient, len(l.cfg.AggregatorEndpoints))
@@ -194,7 +199,7 @@ func (l *libFromCCV) AllIndexers() ([]*client.IndexerClient, error) {
194199
return nil, fmt.Errorf("failed to initialize indexer client: %w", err)
195200
}
196201
if len(l.cfg.IndexerEndpoints) == 0 {
197-
return nil, fmt.Errorf("no indexer endpoints configured")
202+
return nil, nil
198203
}
199204
indexers := make([]*client.IndexerClient, 0, len(l.cfg.IndexerEndpoints))
200205
httpClient := &http.Client{
@@ -229,7 +234,7 @@ type libFromCLDF struct {
229234

230235
// AllIndexers implements [Lib].
231236
func (l *libFromCLDF) AllIndexers() ([]*client.IndexerClient, error) {
232-
return nil, fmt.Errorf("no indexer clients available in CLDF environment")
237+
return nil, nil
233238
}
234239

235240
// CLDFEnvironment implements [Lib].
@@ -307,7 +312,7 @@ func (l *libFromCLDF) IndexerMonitor() (*IndexerMonitor, error) {
307312

308313
// AllAggregators implements [Lib].
309314
func (l *libFromCLDF) AllAggregators() (map[string]*AggregatorClient, error) {
310-
return nil, fmt.Errorf("no aggregator clients available in CLDF environment")
315+
return map[string]*AggregatorClient{}, nil
311316
}
312317

313318
// NewLibFromCLDFEnv creates a new [Lib] from a [deployment.Environment].

build/devenv/lib_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package ccv
2+
3+
import (
4+
"testing"
5+
6+
"github.com/rs/zerolog"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func newTestLibFromCCV(cfg *Cfg) *libFromCCV {
12+
return &libFromCCV{
13+
envOutFile: "test.toml",
14+
cfg: cfg,
15+
l: zerolog.Nop(),
16+
}
17+
}
18+
19+
// TestLib_OffchainGetters covers the absence contract for both backends: the plural
20+
// getters return an empty result with a nil error when nothing is configured, the
21+
// singular getters return an error, and a real construction failure surfaces as an
22+
// error from the plural getter too.
23+
func TestLib_OffchainGetters(t *testing.T) {
24+
ccvUnconfigured := newTestLibFromCCV(&Cfg{})
25+
ccvBadAggregatorCert := newTestLibFromCCV(&Cfg{
26+
AggregatorEndpoints: map[string]string{"default": "127.0.0.1:1"},
27+
AggregatorCACertFiles: map[string]string{"default": "/nonexistent/ca.pem"},
28+
})
29+
cldf := &libFromCLDF{l: zerolog.Nop()}
30+
31+
tests := []struct {
32+
name string
33+
call func() (any, error)
34+
wantErr bool // false: expect an empty result with a nil error
35+
}{
36+
{"ccv unconfigured: AllAggregators", func() (any, error) { return ccvUnconfigured.AllAggregators() }, false},
37+
{"ccv unconfigured: AllIndexers", func() (any, error) { return ccvUnconfigured.AllIndexers() }, false},
38+
{"ccv unconfigured: Indexer", func() (any, error) { return ccvUnconfigured.Indexer() }, true},
39+
{"ccv unconfigured: IndexerMonitor", func() (any, error) { return ccvUnconfigured.IndexerMonitor() }, true},
40+
{"ccv bad aggregator cert: AllAggregators", func() (any, error) { return ccvBadAggregatorCert.AllAggregators() }, true},
41+
{"cldf: AllAggregators", func() (any, error) { return cldf.AllAggregators() }, false},
42+
{"cldf: AllIndexers", func() (any, error) { return cldf.AllIndexers() }, false},
43+
{"cldf: Indexer", func() (any, error) { return cldf.Indexer() }, true},
44+
{"cldf: IndexerMonitor", func() (any, error) { return cldf.IndexerMonitor() }, true},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
got, err := tt.call()
50+
if tt.wantErr {
51+
require.Error(t, err)
52+
return
53+
}
54+
require.NoError(t, err)
55+
assert.Empty(t, got)
56+
})
57+
}
58+
}

build/devenv/tests/e2e/tcapi/basic/v3.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,9 @@ func (tc *v3TestCase) Run(ctx context.Context) error {
111111
}
112112
messageID := sendMessageResult.MessageID
113113

114-
aggregatorClients, err := tc.lib.AllAggregators()
114+
aggregatorClient, indexerMonitor, err := tcapi.SetupOffchainClients(tc.lib, tc.aggregatorQualifier)
115115
if err != nil {
116-
return fmt.Errorf("failed to get aggregator clients: %w", err)
117-
}
118-
aggregatorClient := aggregatorClients[common.DefaultCommitteeVerifierQualifier]
119-
if tc.aggregatorQualifier != "" && tc.aggregatorQualifier != common.DefaultCommitteeVerifierQualifier {
120-
if client, ok := aggregatorClients[tc.aggregatorQualifier]; ok {
121-
aggregatorClient = client
122-
}
123-
}
124-
indexerMonitor, err := tc.lib.IndexerMonitor()
125-
if err != nil {
126-
return fmt.Errorf("failed to get indexer monitor: %w", err)
116+
return err
127117
}
128118
testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor)
129119
defer cleanupFn()
@@ -138,10 +128,10 @@ func (tc *v3TestCase) Run(ctx context.Context) error {
138128
if err != nil {
139129
return fmt.Errorf("failed to assert message: %w", err)
140130
}
141-
if result.AggregatedResult == nil {
131+
if aggregatorClient != nil && result.AggregatedResult == nil {
142132
return fmt.Errorf("aggregated result is nil")
143133
}
144-
if len(result.IndexedVerifications.Results) != tc.numExpectedVerifications {
134+
if indexerMonitor != nil && len(result.IndexedVerifications.Results) != tc.numExpectedVerifications {
145135
return fmt.Errorf("expected %d indexed verifications, got %d", tc.numExpectedVerifications, len(result.IndexedVerifications.Results))
146136
}
147137

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package tcapi
2+
3+
import (
4+
"fmt"
5+
6+
ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv"
7+
"github.com/smartcontractkit/chainlink-ccv/build/devenv/common"
8+
)
9+
10+
// SetupOffchainClients resolves the aggregator and indexer clients from the
11+
// environment. When no offchain endpoints are configured it returns nil clients and
12+
// a nil error; NewTestingContext skips assertion stages for nil clients. A non-nil
13+
// error means a configured client failed to construct.
14+
func SetupOffchainClients(lib ccv.Lib, aggregatorQualifier string) (*ccv.AggregatorClient, *ccv.IndexerMonitor, error) {
15+
aggregatorClients, err := lib.AllAggregators()
16+
if err != nil {
17+
return nil, nil, fmt.Errorf("failed to get aggregator clients: %w", err)
18+
}
19+
var aggregatorClient *ccv.AggregatorClient
20+
if len(aggregatorClients) > 0 {
21+
aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier]
22+
if aggregatorQualifier != "" && aggregatorQualifier != common.DefaultCommitteeVerifierQualifier {
23+
if client, ok := aggregatorClients[aggregatorQualifier]; ok {
24+
aggregatorClient = client
25+
}
26+
}
27+
}
28+
29+
indexers, err := lib.AllIndexers()
30+
if err != nil {
31+
return nil, nil, fmt.Errorf("failed to get indexer clients: %w", err)
32+
}
33+
var indexerMonitor *ccv.IndexerMonitor
34+
if len(indexers) > 0 {
35+
indexerMonitor, err = lib.IndexerMonitor()
36+
if err != nil {
37+
return nil, nil, fmt.Errorf("failed to get indexer monitor: %w", err)
38+
}
39+
}
40+
return aggregatorClient, indexerMonitor, nil
41+
}

0 commit comments

Comments
 (0)