From a701bbc9ba6e1f73998668ad361341be31546291 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 1 Jul 2026 16:11:51 +0100 Subject: [PATCH 01/10] fix: event poller with persistent network --- build/devenv/evm/event_poller.go | 27 ++++++++++++++++++++++++++- build/devenv/evm/event_poller_test.go | 13 +++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/build/devenv/evm/event_poller.go b/build/devenv/evm/event_poller.go index 2358ba200..1aac91bc8 100644 --- a/build/devenv/evm/event_poller.go +++ b/build/devenv/evm/event_poller.go @@ -11,6 +11,12 @@ import ( "github.com/smartcontractkit/chainlink-ccv/protocol" ) +// fallbackLookbackBlocks bounds how far back the first eth_getLogs scan reaches on a +// long-lived chain, so it neither scans from genesis nor exceeds the provider's +// getLogs range limit — in blocks, not wall-clock, since the target event is always +// recent. Mirrors verifier/pkg/sourcereader's DefaultMaxBlockRange. +const fallbackLookbackBlocks uint64 = 1500 + type eventKey struct { chainSelector uint64 msgNum uint64 @@ -177,7 +183,19 @@ func (p *eventPoller[T]) poll() { return } - events, err := p.pollFn(lastScanned+1, latestBlock) + start := lastScanned + 1 + if lastScanned == 0 { + if lookbackStart := fallbackStartBlock(latestBlock); lookbackStart > start { + start = lookbackStart + p.logger.Debug(). + Uint64("fromBlock", start). + Uint64("toBlock", latestBlock). + Str("event", p.eventName). + Msg("Using fallback start block (bounded lookback)") + } + } + + events, err := p.pollFn(start, latestBlock) if err != nil { p.logger.Warn().Err(err).Str("event", p.eventName).Msg("Failed to poll events") return @@ -220,6 +238,13 @@ func (p *eventPoller[T]) poll() { p.lastScannedBlock = latestBlock } +func fallbackStartBlock(latestBlock uint64) uint64 { + if latestBlock > fallbackLookbackBlocks { + return latestBlock - fallbackLookbackBlocks + } + return 0 +} + func (p *eventPoller[T]) addToCache(key eventKey, result pollerResult[T]) { seqKey := eventKey{chainSelector: key.chainSelector, msgNum: key.msgNum} if _, exists := p.cachedBySeqNum[seqKey]; !exists { diff --git a/build/devenv/evm/event_poller_test.go b/build/devenv/evm/event_poller_test.go index ab420dc77..b3867cc07 100644 --- a/build/devenv/evm/event_poller_test.go +++ b/build/devenv/evm/event_poller_test.go @@ -197,3 +197,16 @@ func TestEventPollerByMessageID(t *testing.T) { } }) } + +func TestFallbackStartBlock(t *testing.T) { + t.Parallel() + + // Long-lived chain: the first scan reaches back exactly the lookback window, + // independent of block time. + const latest = uint64(10_000_000) + require.Equal(t, latest-fallbackLookbackBlocks, fallbackStartBlock(latest)) + + // Chain younger than (or exactly at) the lookback window scans from genesis. + require.Equal(t, uint64(0), fallbackStartBlock(fallbackLookbackBlocks)) + require.Equal(t, uint64(0), fallbackStartBlock(100)) +} From 01741ff440feae32d5aef1654bf2497906c26a6d Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 1 Jul 2026 16:13:50 +0100 Subject: [PATCH 02/10] feat: OnchainAssertionOnly in tcapi --- build/devenv/evm/impl.go | 11 +++-- build/devenv/tests/e2e/tcapi/basic/v3.go | 41 +++++++++++-------- .../tests/e2e/tcapi/token_transfer/v3.go | 27 +++++++----- build/devenv/tests/e2e/tcapi/types.go | 9 +++- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/build/devenv/evm/impl.go b/build/devenv/evm/impl.go index 0a49ddab4..da3ead4b4 100644 --- a/build/devenv/evm/impl.go +++ b/build/devenv/evm/impl.go @@ -136,9 +136,8 @@ func extractEthClientFromBackend(client any) (*ethclient.Client, error) { // NewCCIP17EVM creates new smart-contracts wrappers with utility functions for CCIP17EVM implementation. func NewCCIP17EVM(ctx context.Context, logger zerolog.Logger, e *deployment.Environment, chainSelector uint64) (*CCIP17EVM, error) { var ( - onRamp *onramp.OnRamp - offRamp *offramp.OffRamp - onRampPoller eventPoller[cciptestinterfaces.MessageSentEvent] + onRamp *onramp.OnRamp + offRamp *offramp.OffRamp ) chainDetails, err := chainsel.GetChainDetails(chainSelector) if err != nil { @@ -192,7 +191,6 @@ func NewCCIP17EVM(ctx context.Context, logger zerolog.Logger, e *deployment.Envi ethClient: ethClient, onRamp: onRamp, offRamp: offRamp, - onRampPoller: &onRampPoller, }, nil } @@ -203,6 +201,11 @@ func (m *CCIP17EVM) ChainSelector() uint64 { func (m *CCIP17EVM) getOrCreateOnRampPoller() (*eventPoller[cciptestinterfaces.MessageSentEvent], error) { m.pollersMu.Lock() defer m.pollersMu.Unlock() + + if m.onRampPoller != nil { + return m.onRampPoller, nil + } + onRamp := m.onRamp ethClient := m.ethClient diff --git a/build/devenv/tests/e2e/tcapi/basic/v3.go b/build/devenv/tests/e2e/tcapi/basic/v3.go index 590683beb..a6be57da3 100644 --- a/build/devenv/tests/e2e/tcapi/basic/v3.go +++ b/build/devenv/tests/e2e/tcapi/basic/v3.go @@ -111,19 +111,24 @@ func (tc *v3TestCase) Run(ctx context.Context) error { } messageID := sendMessageResult.MessageID - aggregatorClients, err := tc.lib.AllAggregators() - if err != nil { - return fmt.Errorf("failed to get aggregator clients: %w", err) - } - aggregatorClient := aggregatorClients[common.DefaultCommitteeVerifierQualifier] - if tc.aggregatorQualifier != "" && tc.aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { - if client, ok := aggregatorClients[tc.aggregatorQualifier]; ok { - aggregatorClient = client + var aggregatorClient *ccv.AggregatorClient + var indexerMonitor *ccv.IndexerMonitor + if !tc.args.Run.OnchainAssertionOnly { + aggregatorClients, aggErr := tc.lib.AllAggregators() + if aggErr != nil { + return fmt.Errorf("failed to get aggregator clients: %w", aggErr) + } + aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier] + if tc.aggregatorQualifier != "" && tc.aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { + if client, ok := aggregatorClients[tc.aggregatorQualifier]; ok { + aggregatorClient = client + } + } + var monErr error + indexerMonitor, monErr = tc.lib.IndexerMonitor() + if monErr != nil { + return fmt.Errorf("failed to get indexer monitor: %w", monErr) } - } - indexerMonitor, err := tc.lib.IndexerMonitor() - if err != nil { - return fmt.Errorf("failed to get indexer monitor: %w", err) } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor) defer cleanupFn() @@ -138,11 +143,13 @@ func (tc *v3TestCase) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to assert message: %w", err) } - if result.AggregatedResult == nil { - return fmt.Errorf("aggregated result is nil") - } - if len(result.IndexedVerifications.Results) != tc.numExpectedVerifications { - return fmt.Errorf("expected %d indexed verifications, got %d", tc.numExpectedVerifications, len(result.IndexedVerifications.Results)) + if !tc.args.Run.OnchainAssertionOnly { + if result.AggregatedResult == nil { + return fmt.Errorf("aggregated result is nil") + } + if len(result.IndexedVerifications.Results) != tc.numExpectedVerifications { + return fmt.Errorf("expected %d indexed verifications, got %d", tc.numExpectedVerifications, len(result.IndexedVerifications.Results)) + } } e, err := dst.ConfirmExecOnDest(ctx, tc.src, messageKey, execTimeout) diff --git a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go index ab44af377..9f6af284a 100644 --- a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go +++ b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go @@ -138,14 +138,19 @@ func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { } msgID := sendRes.MessageID - aggregatorClients, err := tc.lib.AllAggregators() - if err != nil { - return fmt.Errorf("failed to get aggregator clients: %w", err) - } - aggregatorClient := aggregatorClients[common.DefaultCommitteeVerifierQualifier] - indexerMonitor, err := tc.lib.IndexerMonitor() - if err != nil { - return fmt.Errorf("failed to get indexer monitor: %w", err) + var aggregatorClient *ccv.AggregatorClient + var indexerMonitor *ccv.IndexerMonitor + if !tc.args.Run.OnchainAssertionOnly { + aggregatorClients, aggErr := tc.lib.AllAggregators() + if aggErr != nil { + return fmt.Errorf("failed to get aggregator clients: %w", aggErr) + } + aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier] + var monErr error + indexerMonitor, monErr = tc.lib.IndexerMonitor() + if monErr != nil { + return fmt.Errorf("failed to get indexer monitor: %w", monErr) + } } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor) defer cleanupFn() @@ -160,8 +165,10 @@ func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("assert message: %w", err) } - if res.AggregatedResult == nil { - return fmt.Errorf("aggregated result is nil") + if !tc.args.Run.OnchainAssertionOnly { + if res.AggregatedResult == nil { + return fmt.Errorf("aggregated result is nil") + } } execEvt, err := dst.ConfirmExecOnDest(ctx, tc.src, messageKey, execTimeout) diff --git a/build/devenv/tests/e2e/tcapi/types.go b/build/devenv/tests/e2e/tcapi/types.go index 42e7612f9..171a8e8cf 100644 --- a/build/devenv/tests/e2e/tcapi/types.go +++ b/build/devenv/tests/e2e/tcapi/types.go @@ -49,13 +49,18 @@ type TestCase interface { // DefaultV3ExecutionGasLimit is the execution gas limit used when SendConfig and MessageOptions omit it. const DefaultV3ExecutionGasLimit uint32 = 200_000 -// RunConfig holds optional overrides for wait/confirm timeouts in TCAPI Run methods. -// Zero values use the fallback passed to SentTimeout or ExecTimeout. +// RunConfig holds optional overrides for wait/confirm timeouts and assertion scope +// in TCAPI Run methods. Zero values use the fallback passed to SentTimeout or +// ExecTimeout, and select the full local-devenv assertion behavior. type RunConfig struct { // ConfirmSentTimeout overrides ConfirmSendOnSource when non-zero. ConfirmSentTimeout time.Duration // ConfirmExecTimeout overrides AssertMessage and ConfirmExecOnDest when non-zero. ConfirmExecTimeout time.Duration + // OnchainAssertionOnly skips aggregator/indexer wiring and assertion, for + // environments where those offchain services aren't reachable (e.g. persistent + // env). Default false: wire clients and require offchain assert results. + OnchainAssertionOnly bool } // SentTimeout returns ConfirmSentTimeout when set, otherwise fallback. From 74e6c1ae273261110c68a9c460d7d83deae0579b Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 1 Jul 2026 20:03:57 +0100 Subject: [PATCH 03/10] chore: testing --- build/devenv/services/committeeverifier/base.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/devenv/services/committeeverifier/base.go b/build/devenv/services/committeeverifier/base.go index 8de1975bb..bbd0437a9 100644 --- a/build/devenv/services/committeeverifier/base.go +++ b/build/devenv/services/committeeverifier/base.go @@ -258,10 +258,12 @@ func launchVerifier(ctx context.Context, in *Input, outputs []*blockchain.Output // Register each blockchain output as a chain the node has a signing identity on. // This causes the bootstrapper to sync the node's signing key to JD on connect, // making it available to deployment changesets via ListNodeChainConfigs. + // + // FIXME: testing for _, output := range outputs { if output.ChainID != "" { bootstrapInput.Chains = append(bootstrapInput.Chains, bootstrap.ChainRegistration{ - Type: in.ChainFamily, + Type: output.Family, ID: output.ChainID, }) } From 5ed0842e3869602228774013d133513e03892144 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Wed, 1 Jul 2026 20:35:18 +0100 Subject: [PATCH 04/10] chore: extract nested setup code --- build/devenv/tests/e2e/tcapi/basic/v3.go | 36 +++++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/build/devenv/tests/e2e/tcapi/basic/v3.go b/build/devenv/tests/e2e/tcapi/basic/v3.go index a6be57da3..f559d7d95 100644 --- a/build/devenv/tests/e2e/tcapi/basic/v3.go +++ b/build/devenv/tests/e2e/tcapi/basic/v3.go @@ -114,20 +114,10 @@ func (tc *v3TestCase) Run(ctx context.Context) error { var aggregatorClient *ccv.AggregatorClient var indexerMonitor *ccv.IndexerMonitor if !tc.args.Run.OnchainAssertionOnly { - aggregatorClients, aggErr := tc.lib.AllAggregators() - if aggErr != nil { - return fmt.Errorf("failed to get aggregator clients: %w", aggErr) - } - aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier] - if tc.aggregatorQualifier != "" && tc.aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { - if client, ok := aggregatorClients[tc.aggregatorQualifier]; ok { - aggregatorClient = client - } - } - var monErr error - indexerMonitor, monErr = tc.lib.IndexerMonitor() - if monErr != nil { - return fmt.Errorf("failed to get indexer monitor: %w", monErr) + var setupErr error + aggregatorClient, indexerMonitor, setupErr = setupAggregatorAndIndexer(tc.lib, tc.aggregatorQualifier) + if setupErr != nil { + return setupErr } } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor) @@ -168,6 +158,24 @@ func (tc *v3TestCase) HavePrerequisites(ctx context.Context) bool { return tc.ensureHydrated(ctx) == nil } +func setupAggregatorAndIndexer(lib ccv.Lib, aggregatorQualifier string) (*ccv.AggregatorClient, *ccv.IndexerMonitor, error) { + aggregatorClients, err := lib.AllAggregators() + if err != nil { + return nil, nil, fmt.Errorf("failed to get aggregator clients: %w", err) + } + aggregatorClient := aggregatorClients[common.DefaultCommitteeVerifierQualifier] + if aggregatorQualifier != "" && aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { + if client, ok := aggregatorClients[aggregatorQualifier]; ok { + aggregatorClient = client + } + } + indexerMonitor, err := lib.IndexerMonitor() + if err != nil { + return nil, nil, fmt.Errorf("failed to get indexer monitor: %w", err) + } + return aggregatorClient, indexerMonitor, nil +} + func getCommitteeCCV(resolver chainreg.AddressResolver, ds datastore.DataStore, srcChainSelector uint64, qualifier string) (protocol.CCV, error) { addr, err := resolver.GetCommitteeCCV(ds, srcChainSelector, qualifier) if err != nil { From 19623eb34c216b51f345e5f9638d6b7f141229a7 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Thu, 2 Jul 2026 09:52:21 +0100 Subject: [PATCH 05/10] fix: mirror --- build/devenv/services/committeeverifier/base.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build/devenv/services/committeeverifier/base.go b/build/devenv/services/committeeverifier/base.go index bbd0437a9..d507d3bfa 100644 --- a/build/devenv/services/committeeverifier/base.go +++ b/build/devenv/services/committeeverifier/base.go @@ -255,15 +255,13 @@ func launchVerifier(ctx context.Context, in *Input, outputs []*blockchain.Output envVars["LOG_LEVEL"] = lvl } - // Register each blockchain output as a chain the node has a signing identity on. + // Register each matching chain family blockchain output as a chain the node has a signing identity on. // This causes the bootstrapper to sync the node's signing key to JD on connect, // making it available to deployment changesets via ListNodeChainConfigs. - // - // FIXME: testing for _, output := range outputs { - if output.ChainID != "" { + if output.ChainID != "" && output.Family == in.ChainFamily { bootstrapInput.Chains = append(bootstrapInput.Chains, bootstrap.ChainRegistration{ - Type: output.Family, + Type: in.ChainFamily, ID: output.ChainID, }) } From a274a652f394f9bbddb262b0d0df4735ddad737a Mon Sep 17 00:00:00 2001 From: Makram Kamaleddine Date: Thu, 2 Jul 2026 15:02:34 +0300 Subject: [PATCH 06/10] 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. --- bootstrap/bootstrap.go | 1 + bootstrap/bootstrap_test.go | 7 +++++++ bootstrap/config.go | 28 +++++++++++++++++++++++++++- bootstrap/config_test.go | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index f37ac1602..022789464 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -357,6 +357,7 @@ func buildUpdateNodeRequest( Enabled: true, OcrKeyBundle: &pb.OCR2Config_OCRKeyBundle{ OnchainSigningAddress: addr, + OnchainSigningPubKey: keys.RawPubKeyHex(signingKey.KeyInfo.PublicKey), }, }, }) diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index f36e16f6c..c050d2ccb 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -415,12 +415,19 @@ func TestBuildUpdateNodeRequest(t *testing.T) { require.NotEmpty(t, cc.Ocr2Config.OcrKeyBundle.OnchainSigningAddress) // EVM addresses are 42 chars (0x + 40 hex) require.Len(t, cc.Ocr2Config.OcrKeyBundle.OnchainSigningAddress, 42) + require.NotEmpty(t, cc.Ocr2Config.OcrKeyBundle.OnchainSigningPubKey) } // Both chains must have the same address (same signing key) addr0 := req.ChainConfigs[0].Ocr2Config.OcrKeyBundle.OnchainSigningAddress addr1 := req.ChainConfigs[1].Ocr2Config.OcrKeyBundle.OnchainSigningAddress require.Equal(t, addr0, addr1) + + // The raw public key is identical across chains too: it's the same key, just + // rendered per-family in OnchainSigningAddress. + pubKey0 := req.ChainConfigs[0].Ocr2Config.OcrKeyBundle.OnchainSigningPubKey + pubKey1 := req.ChainConfigs[1].Ocr2Config.OcrKeyBundle.OnchainSigningPubKey + require.Equal(t, pubKey0, pubKey1) }) t.Run("unsupported chain type returns error", func(t *testing.T) { diff --git a/bootstrap/config.go b/bootstrap/config.go index 22ec124af..1853da085 100644 --- a/bootstrap/config.go +++ b/bootstrap/config.go @@ -177,9 +177,35 @@ func (c *Config) validateInfra() []error { if err := c.Server.validate(); err != nil { errs = append(errs, fmt.Errorf("failed to validate 'server' section: %w", err)) } - for i, chain := range c.Chains { + errs = append(errs, validateChains(c.Chains)...) + return errs +} + +// validateChains validates each chain entry and enforces that every entry declares the +// same chain family. A committee verifier binary is built for exactly one chain family +// (e.g. EVM XOR Solana, never both) and pushes one signing key for every declared chain +// under the assumption that they're all the same family; mixing families here would +// silently push a key formatted for the wrong chain type to JD, breaking signing key +// sync for whichever family lost the race — this catches that misconfiguration at load +// time instead. +func validateChains(chains []ChainRegistration) []error { + var errs []error + var firstType string + var firstIndex int + for i, chain := range chains { if err := chain.validate(); err != nil { errs = append(errs, fmt.Errorf("invalid chain at index %d: %w", i, err)) + continue + } + upperType := strings.ToUpper(chain.Type) + if firstType == "" { + firstType, firstIndex = upperType, i + } else if upperType != firstType { + errs = append(errs, fmt.Errorf( + "chain at index %d has type %q but chains must all declare the same family (found %q at index %d): "+ + "a bootstrapper is built for exactly one chain family and pushes one signing key for all declared chains", + i, chain.Type, firstType, firstIndex, + )) } } return errs diff --git a/bootstrap/config_test.go b/bootstrap/config_test.go index e113556e8..b890716b1 100644 --- a/bootstrap/config_test.go +++ b/bootstrap/config_test.go @@ -452,4 +452,38 @@ func TestConfig_validate_Chains(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "invalid chain at index 0") }) + + t.Run("mixed chain families fails validation", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, + Chains: []ChainRegistration{{Type: "EVM", ID: "1"}, {Type: "SOLANA", ID: "mainnet"}}, + } + err := cfg.validate(logger.Test(t), infraMD, true) + require.Error(t, err) + require.Contains(t, err.Error(), `chain at index 1 has type "SOLANA"`) + require.Contains(t, err.Error(), `same family (found "EVM" at index 0)`) + }) + + t.Run("mixed chain families is case-insensitive", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, + Chains: []ChainRegistration{{Type: "evm", ID: "1"}, {Type: "EVM", ID: "137"}}, + } + require.NoError(t, cfg.validate(logger.Test(t), infraMD, true), "same family in different casing must not be flagged as mixed") + }) + + t.Run("an invalid entry does not mask the family the remaining valid entries share", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, + Chains: []ChainRegistration{{Type: "NOTACHAIN", ID: "1"}, {Type: "EVM", ID: "1"}, {Type: "SOLANA", ID: "mainnet"}}, + } + err := cfg.validate(logger.Test(t), infraMD, true) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid chain at index 0") + require.Contains(t, err.Error(), `chain at index 2 has type "SOLANA"`) + require.Contains(t, err.Error(), `same family (found "EVM" at index 1)`) + }) } From b632cc2d252330346f0e390dbb3e308a380a4b09 Mon Sep 17 00:00:00 2001 From: Makram Kamaleddine Date: Thu, 2 Jul 2026 15:19:42 +0300 Subject: [PATCH 07/10] fix compilation error --- bootstrap/config_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/config_test.go b/bootstrap/config_test.go index b890716b1..40e731019 100644 --- a/bootstrap/config_test.go +++ b/bootstrap/config_test.go @@ -459,7 +459,7 @@ func TestConfig_validate_Chains(t *testing.T) { JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, Chains: []ChainRegistration{{Type: "EVM", ID: "1"}, {Type: "SOLANA", ID: "mainnet"}}, } - err := cfg.validate(logger.Test(t), infraMD, true) + err := cfg.validate(true) require.Error(t, err) require.Contains(t, err.Error(), `chain at index 1 has type "SOLANA"`) require.Contains(t, err.Error(), `same family (found "EVM" at index 0)`) @@ -471,7 +471,7 @@ func TestConfig_validate_Chains(t *testing.T) { JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, Chains: []ChainRegistration{{Type: "evm", ID: "1"}, {Type: "EVM", ID: "137"}}, } - require.NoError(t, cfg.validate(logger.Test(t), infraMD, true), "same family in different casing must not be flagged as mixed") + require.NoError(t, cfg.validate(true), "same family in different casing must not be flagged as mixed") }) t.Run("an invalid entry does not mask the family the remaining valid entries share", func(t *testing.T) { @@ -480,7 +480,7 @@ func TestConfig_validate_Chains(t *testing.T) { JD: validJD, Keystore: validKeystore, DB: validDB, Server: validServer, Chains: []ChainRegistration{{Type: "NOTACHAIN", ID: "1"}, {Type: "EVM", ID: "1"}, {Type: "SOLANA", ID: "mainnet"}}, } - err := cfg.validate(logger.Test(t), infraMD, true) + err := cfg.validate(true) require.Error(t, err) require.Contains(t, err.Error(), "invalid chain at index 0") require.Contains(t, err.Error(), `chain at index 2 has type "SOLANA"`) From aaf393c783b05d5163305edf96f6d3337ce33593 Mon Sep 17 00:00:00 2001 From: Makram Kamaleddine Date: Thu, 2 Jul 2026 15:33:03 +0300 Subject: [PATCH 08/10] temp: bump chainlink-ccip --- build/devenv/go.mod | 2 +- build/devenv/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/devenv/go.mod b/build/devenv/go.mod index 0479d5562..482b09f26 100644 --- a/build/devenv/go.mod +++ b/build/devenv/go.mod @@ -27,7 +27,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.104 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260626191803-b2c751b7f789 - github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260626191803-b2c751b7f789 + github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260702120622-0236bd2146a2 github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260608205628-b1fb1b311772 github.com/smartcontractkit/chainlink-ccv/deployment v0.0.2-0.20260616151800-9a3a31c4e194 github.com/smartcontractkit/chainlink-ccv/integration/evm v0.0.0-20260701140628-aa6dcbdd5f9c diff --git a/build/devenv/go.sum b/build/devenv/go.sum index 7fa4d5f03..9d3ac9c3b 100644 --- a/build/devenv/go.sum +++ b/build/devenv/go.sum @@ -1125,8 +1125,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260626191803-b2c751b7f789 h1:YP/nuP0quoea5BeEUwZ9EAY/m+07vnVpSElQ9dTCoG4= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260626191803-b2c751b7f789/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260702120622-0236bd2146a2 h1:q5mWrn84a3Nlx517FZ3Db3UnmvgHS+6SHexel/+HQ1k= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260702120622-0236bd2146a2/go.mod h1:7VW7TwL0yIOQIoe39YwUfwYkt9P79tDR4Pb0h/Q7P3E= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= From e5bd254a02b94492ff4393b6257196bea07819ba Mon Sep 17 00:00:00 2001 From: Jade Park Date: Fri, 3 Jul 2026 13:05:04 +0100 Subject: [PATCH 09/10] feat: skip offchain assertions without flag --- build/devenv/evm/event_poller.go | 4 +- build/devenv/lib.go | 25 ++-- build/devenv/lib_test.go | 58 ++++++++ build/devenv/tests/e2e/tcapi/basic/v3.go | 41 ++---- build/devenv/tests/e2e/tcapi/offchain.go | 41 ++++++ build/devenv/tests/e2e/tcapi/offchain_test.go | 125 ++++++++++++++++++ .../tests/e2e/tcapi/token_transfer/v3.go | 22 +-- build/devenv/tests/e2e/tcapi/types.go | 9 +- 8 files changed, 256 insertions(+), 69 deletions(-) create mode 100644 build/devenv/lib_test.go create mode 100644 build/devenv/tests/e2e/tcapi/offchain.go create mode 100644 build/devenv/tests/e2e/tcapi/offchain_test.go diff --git a/build/devenv/evm/event_poller.go b/build/devenv/evm/event_poller.go index 1aac91bc8..f2359d1fb 100644 --- a/build/devenv/evm/event_poller.go +++ b/build/devenv/evm/event_poller.go @@ -13,8 +13,8 @@ import ( // fallbackLookbackBlocks bounds how far back the first eth_getLogs scan reaches on a // long-lived chain, so it neither scans from genesis nor exceeds the provider's -// getLogs range limit — in blocks, not wall-clock, since the target event is always -// recent. Mirrors verifier/pkg/sourcereader's DefaultMaxBlockRange. +// getLogs range limit. The bound is in blocks, not wall-clock, since the target event +// is always recent. Mirrors verifier/pkg/sourcereader's DefaultMaxBlockRange. const fallbackLookbackBlocks uint64 = 1500 type eventKey struct { diff --git a/build/devenv/lib.go b/build/devenv/lib.go index 6d0945473..616a0f89e 100644 --- a/build/devenv/lib.go +++ b/build/devenv/lib.go @@ -32,8 +32,12 @@ type ChainImpl struct { // // Note that not all methods may be implemented by all backends. // For example, as of writing, a CLDF environment doesn't store indexer -// or aggregator endpoints, so the [Lib.Indexer] and [Lib.AllIndexers] -// methods will return an error. +// or aggregator endpoints. +// +// The plural offchain getters ([Lib.AllAggregators], [Lib.AllIndexers]) return an +// empty result with a nil error when no endpoints are configured, so callers can +// treat absence as a normal state via a length check and reserve a returned error +// for a real construction failure. type Lib interface { // Chains returns a slice of [ChainImpl] objects in an unspecified order. Chains(ctx context.Context) ([]ChainImpl, error) @@ -57,12 +61,13 @@ type Lib interface { // or an error if no indexer client is available. IndexerMonitor() (*IndexerMonitor, error) - // AllIndexers returns all indexer clients available. - // or an error if no indexer clients are available. + // AllIndexers returns all indexer clients available, or (nil, nil) if no + // indexer endpoints are configured. AllIndexers() ([]*client.IndexerClient, error) - // AllAggregators returns a mapping of qualifier name to the client of the aggregator for that qualifier. - // or an error if no aggregator clients are available. + // AllAggregators returns a mapping of qualifier name to the client of the + // aggregator for that qualifier, or an empty map (nil error) if no aggregator + // endpoints are configured. AllAggregators() (map[string]*AggregatorClient, error) } @@ -139,7 +144,7 @@ func (l *libFromCCV) AllAggregators() (map[string]*AggregatorClient, error) { } if len(l.cfg.AggregatorEndpoints) == 0 { - return nil, fmt.Errorf("no aggregator endpoints configured") + return map[string]*AggregatorClient{}, nil } aggregators := make(map[string]*AggregatorClient, len(l.cfg.AggregatorEndpoints)) @@ -194,7 +199,7 @@ func (l *libFromCCV) AllIndexers() ([]*client.IndexerClient, error) { return nil, fmt.Errorf("failed to initialize indexer client: %w", err) } if len(l.cfg.IndexerEndpoints) == 0 { - return nil, fmt.Errorf("no indexer endpoints configured") + return nil, nil } indexers := make([]*client.IndexerClient, 0, len(l.cfg.IndexerEndpoints)) httpClient := &http.Client{ @@ -229,7 +234,7 @@ type libFromCLDF struct { // AllIndexers implements [Lib]. func (l *libFromCLDF) AllIndexers() ([]*client.IndexerClient, error) { - return nil, fmt.Errorf("no indexer clients available in CLDF environment") + return nil, nil } // CLDFEnvironment implements [Lib]. @@ -307,7 +312,7 @@ func (l *libFromCLDF) IndexerMonitor() (*IndexerMonitor, error) { // AllAggregators implements [Lib]. func (l *libFromCLDF) AllAggregators() (map[string]*AggregatorClient, error) { - return nil, fmt.Errorf("no aggregator clients available in CLDF environment") + return map[string]*AggregatorClient{}, nil } // NewLibFromCLDFEnv creates a new [Lib] from a [deployment.Environment]. diff --git a/build/devenv/lib_test.go b/build/devenv/lib_test.go new file mode 100644 index 000000000..76a5ad25c --- /dev/null +++ b/build/devenv/lib_test.go @@ -0,0 +1,58 @@ +package ccv + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestLibFromCCV(cfg *Cfg) *libFromCCV { + return &libFromCCV{ + envOutFile: "test.toml", + cfg: cfg, + l: zerolog.Nop(), + } +} + +// TestLib_OffchainGetters covers the absence contract for both backends: the plural +// getters return an empty result with a nil error when nothing is configured, the +// singular getters return an error, and a real construction failure surfaces as an +// error from the plural getter too. +func TestLib_OffchainGetters(t *testing.T) { + ccvUnconfigured := newTestLibFromCCV(&Cfg{}) + ccvBadAggregatorCert := newTestLibFromCCV(&Cfg{ + AggregatorEndpoints: map[string]string{"default": "127.0.0.1:1"}, + AggregatorCACertFiles: map[string]string{"default": "/nonexistent/ca.pem"}, + }) + cldf := &libFromCLDF{l: zerolog.Nop()} + + tests := []struct { + name string + call func() (any, error) + wantErr bool // false: expect an empty result with a nil error + }{ + {"ccv unconfigured: AllAggregators", func() (any, error) { return ccvUnconfigured.AllAggregators() }, false}, + {"ccv unconfigured: AllIndexers", func() (any, error) { return ccvUnconfigured.AllIndexers() }, false}, + {"ccv unconfigured: Indexer", func() (any, error) { return ccvUnconfigured.Indexer() }, true}, + {"ccv unconfigured: IndexerMonitor", func() (any, error) { return ccvUnconfigured.IndexerMonitor() }, true}, + {"ccv bad aggregator cert: AllAggregators", func() (any, error) { return ccvBadAggregatorCert.AllAggregators() }, true}, + {"cldf: AllAggregators", func() (any, error) { return cldf.AllAggregators() }, false}, + {"cldf: AllIndexers", func() (any, error) { return cldf.AllIndexers() }, false}, + {"cldf: Indexer", func() (any, error) { return cldf.Indexer() }, true}, + {"cldf: IndexerMonitor", func() (any, error) { return cldf.IndexerMonitor() }, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.call() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Empty(t, got) + }) + } +} diff --git a/build/devenv/tests/e2e/tcapi/basic/v3.go b/build/devenv/tests/e2e/tcapi/basic/v3.go index f559d7d95..cf3152398 100644 --- a/build/devenv/tests/e2e/tcapi/basic/v3.go +++ b/build/devenv/tests/e2e/tcapi/basic/v3.go @@ -111,14 +111,9 @@ func (tc *v3TestCase) Run(ctx context.Context) error { } messageID := sendMessageResult.MessageID - var aggregatorClient *ccv.AggregatorClient - var indexerMonitor *ccv.IndexerMonitor - if !tc.args.Run.OnchainAssertionOnly { - var setupErr error - aggregatorClient, indexerMonitor, setupErr = setupAggregatorAndIndexer(tc.lib, tc.aggregatorQualifier) - if setupErr != nil { - return setupErr - } + aggregatorClient, indexerMonitor, err := tcapi.SetupOffchainClients(tc.lib, tc.aggregatorQualifier) + if err != nil { + return err } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor) defer cleanupFn() @@ -133,13 +128,11 @@ func (tc *v3TestCase) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to assert message: %w", err) } - if !tc.args.Run.OnchainAssertionOnly { - if result.AggregatedResult == nil { - return fmt.Errorf("aggregated result is nil") - } - if len(result.IndexedVerifications.Results) != tc.numExpectedVerifications { - return fmt.Errorf("expected %d indexed verifications, got %d", tc.numExpectedVerifications, len(result.IndexedVerifications.Results)) - } + if aggregatorClient != nil && result.AggregatedResult == nil { + return fmt.Errorf("aggregated result is nil") + } + if indexerMonitor != nil && len(result.IndexedVerifications.Results) != tc.numExpectedVerifications { + return fmt.Errorf("expected %d indexed verifications, got %d", tc.numExpectedVerifications, len(result.IndexedVerifications.Results)) } e, err := dst.ConfirmExecOnDest(ctx, tc.src, messageKey, execTimeout) @@ -158,24 +151,6 @@ func (tc *v3TestCase) HavePrerequisites(ctx context.Context) bool { return tc.ensureHydrated(ctx) == nil } -func setupAggregatorAndIndexer(lib ccv.Lib, aggregatorQualifier string) (*ccv.AggregatorClient, *ccv.IndexerMonitor, error) { - aggregatorClients, err := lib.AllAggregators() - if err != nil { - return nil, nil, fmt.Errorf("failed to get aggregator clients: %w", err) - } - aggregatorClient := aggregatorClients[common.DefaultCommitteeVerifierQualifier] - if aggregatorQualifier != "" && aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { - if client, ok := aggregatorClients[aggregatorQualifier]; ok { - aggregatorClient = client - } - } - indexerMonitor, err := lib.IndexerMonitor() - if err != nil { - return nil, nil, fmt.Errorf("failed to get indexer monitor: %w", err) - } - return aggregatorClient, indexerMonitor, nil -} - func getCommitteeCCV(resolver chainreg.AddressResolver, ds datastore.DataStore, srcChainSelector uint64, qualifier string) (protocol.CCV, error) { addr, err := resolver.GetCommitteeCCV(ds, srcChainSelector, qualifier) if err != nil { diff --git a/build/devenv/tests/e2e/tcapi/offchain.go b/build/devenv/tests/e2e/tcapi/offchain.go new file mode 100644 index 000000000..bd8cbde4a --- /dev/null +++ b/build/devenv/tests/e2e/tcapi/offchain.go @@ -0,0 +1,41 @@ +package tcapi + +import ( + "fmt" + + ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" + "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" +) + +// SetupOffchainClients resolves the aggregator and indexer clients from the +// environment. When no offchain endpoints are configured it returns nil clients and +// a nil error; NewTestingContext skips assertion stages for nil clients. A non-nil +// error means a configured client failed to construct. +func SetupOffchainClients(lib ccv.Lib, aggregatorQualifier string) (*ccv.AggregatorClient, *ccv.IndexerMonitor, error) { + aggregatorClients, err := lib.AllAggregators() + if err != nil { + return nil, nil, fmt.Errorf("failed to get aggregator clients: %w", err) + } + var aggregatorClient *ccv.AggregatorClient + if len(aggregatorClients) > 0 { + aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier] + if aggregatorQualifier != "" && aggregatorQualifier != common.DefaultCommitteeVerifierQualifier { + if client, ok := aggregatorClients[aggregatorQualifier]; ok { + aggregatorClient = client + } + } + } + + indexers, err := lib.AllIndexers() + if err != nil { + return nil, nil, fmt.Errorf("failed to get indexer clients: %w", err) + } + var indexerMonitor *ccv.IndexerMonitor + if len(indexers) > 0 { + indexerMonitor, err = lib.IndexerMonitor() + if err != nil { + return nil, nil, fmt.Errorf("failed to get indexer monitor: %w", err) + } + } + return aggregatorClient, indexerMonitor, nil +} diff --git a/build/devenv/tests/e2e/tcapi/offchain_test.go b/build/devenv/tests/e2e/tcapi/offchain_test.go new file mode 100644 index 000000000..3e87db59c --- /dev/null +++ b/build/devenv/tests/e2e/tcapi/offchain_test.go @@ -0,0 +1,125 @@ +package tcapi + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" + "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" + "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" + "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/client" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +// stubLib is a minimal ccv.Lib test double. Absence is expressed by leaving the +// plural-getter fields empty; a construction failure by setting the *Err fields. +type stubLib struct { + aggregators map[string]*ccv.AggregatorClient + aggregatorsErr error + indexers []*client.IndexerClient + allIndexersErr error + indexerMonitor *ccv.IndexerMonitor + indexerMonitorErr error +} + +func (s *stubLib) Chains(context.Context) ([]ccv.ChainImpl, error) { return nil, nil } +func (s *stubLib) ChainsMap(context.Context) (map[uint64]cciptestinterfaces.CCIP17, error) { + return nil, nil +} +func (s *stubLib) CLDFEnvironment() (*deployment.Environment, error) { return nil, nil } +func (s *stubLib) DataStore() (datastore.DataStore, error) { return nil, nil } +func (s *stubLib) Indexer() (*client.IndexerClient, error) { return nil, nil } +func (s *stubLib) IndexerMonitor() (*ccv.IndexerMonitor, error) { + return s.indexerMonitor, s.indexerMonitorErr +} +func (s *stubLib) AllIndexers() ([]*client.IndexerClient, error) { + return s.indexers, s.allIndexersErr +} +func (s *stubLib) AllAggregators() (map[string]*ccv.AggregatorClient, error) { + return s.aggregators, s.aggregatorsErr +} + +var _ ccv.Lib = (*stubLib)(nil) + +// presentIndexers is a non-empty indexer slice for the "configured" branch; the +// element only needs to be non-nil for the length check. +func presentIndexers() []*client.IndexerClient { return []*client.IndexerClient{{}} } + +func TestSetupOffchainClients(t *testing.T) { + defaultAgg := &ccv.AggregatorClient{} + secondaryAgg := &ccv.AggregatorClient{} + + tests := []struct { + name string + lib *stubLib + qualifier string + wantErr bool + wantAgg *ccv.AggregatorClient // nil expects a nil aggregator client + wantIdx bool // expect a non-nil indexer monitor + }{ + { + name: "both absent", + lib: &stubLib{}, + }, + { + name: "both present", + lib: &stubLib{ + aggregators: map[string]*ccv.AggregatorClient{common.DefaultCommitteeVerifierQualifier: defaultAgg}, + indexers: presentIndexers(), + indexerMonitor: &ccv.IndexerMonitor{}, + }, + wantAgg: defaultAgg, + wantIdx: true, + }, + { + name: "qualifier override, indexer absent", + lib: &stubLib{ + aggregators: map[string]*ccv.AggregatorClient{ + common.DefaultCommitteeVerifierQualifier: defaultAgg, + "secondary": secondaryAgg, + }, + }, + qualifier: "secondary", + wantAgg: secondaryAgg, + }, + { + name: "aggregator construction fails", + lib: &stubLib{aggregatorsErr: errors.New("connection refused")}, + wantErr: true, + }, + { + name: "indexer monitor construction fails", + lib: &stubLib{ + indexers: presentIndexers(), + indexerMonitorErr: errors.New("dial tcp: connection refused"), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + agg, idx, err := SetupOffchainClients(tt.lib, tt.qualifier) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantAgg == nil { + assert.Nil(t, agg) + } else { + assert.Same(t, tt.wantAgg, agg) + } + if tt.wantIdx { + assert.NotNil(t, idx) + } else { + assert.Nil(t, idx) + } + }) + } +} diff --git a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go index 9f6af284a..fa00d2f65 100644 --- a/build/devenv/tests/e2e/tcapi/token_transfer/v3.go +++ b/build/devenv/tests/e2e/tcapi/token_transfer/v3.go @@ -138,19 +138,9 @@ func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { } msgID := sendRes.MessageID - var aggregatorClient *ccv.AggregatorClient - var indexerMonitor *ccv.IndexerMonitor - if !tc.args.Run.OnchainAssertionOnly { - aggregatorClients, aggErr := tc.lib.AllAggregators() - if aggErr != nil { - return fmt.Errorf("failed to get aggregator clients: %w", aggErr) - } - aggregatorClient = aggregatorClients[common.DefaultCommitteeVerifierQualifier] - var monErr error - indexerMonitor, monErr = tc.lib.IndexerMonitor() - if monErr != nil { - return fmt.Errorf("failed to get indexer monitor: %w", monErr) - } + aggregatorClient, indexerMonitor, err := tcapi.SetupOffchainClients(tc.lib, "") + if err != nil { + return err } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, chainMap, aggregatorClient, indexerMonitor) defer cleanupFn() @@ -165,10 +155,8 @@ func (tc *tokenTransferV3TestCase) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("assert message: %w", err) } - if !tc.args.Run.OnchainAssertionOnly { - if res.AggregatedResult == nil { - return fmt.Errorf("aggregated result is nil") - } + if aggregatorClient != nil && res.AggregatedResult == nil { + return fmt.Errorf("aggregated result is nil") } execEvt, err := dst.ConfirmExecOnDest(ctx, tc.src, messageKey, execTimeout) diff --git a/build/devenv/tests/e2e/tcapi/types.go b/build/devenv/tests/e2e/tcapi/types.go index 171a8e8cf..42e7612f9 100644 --- a/build/devenv/tests/e2e/tcapi/types.go +++ b/build/devenv/tests/e2e/tcapi/types.go @@ -49,18 +49,13 @@ type TestCase interface { // DefaultV3ExecutionGasLimit is the execution gas limit used when SendConfig and MessageOptions omit it. const DefaultV3ExecutionGasLimit uint32 = 200_000 -// RunConfig holds optional overrides for wait/confirm timeouts and assertion scope -// in TCAPI Run methods. Zero values use the fallback passed to SentTimeout or -// ExecTimeout, and select the full local-devenv assertion behavior. +// RunConfig holds optional overrides for wait/confirm timeouts in TCAPI Run methods. +// Zero values use the fallback passed to SentTimeout or ExecTimeout. type RunConfig struct { // ConfirmSentTimeout overrides ConfirmSendOnSource when non-zero. ConfirmSentTimeout time.Duration // ConfirmExecTimeout overrides AssertMessage and ConfirmExecOnDest when non-zero. ConfirmExecTimeout time.Duration - // OnchainAssertionOnly skips aggregator/indexer wiring and assertion, for - // environments where those offchain services aren't reachable (e.g. persistent - // env). Default false: wire clients and require offchain assert results. - OnchainAssertionOnly bool } // SentTimeout returns ConfirmSentTimeout when set, otherwise fallback. From ff9f3593248a5e89e4cb47166d8cd920ee4f8dd1 Mon Sep 17 00:00:00 2001 From: Jade Park Date: Fri, 3 Jul 2026 14:05:10 +0100 Subject: [PATCH 10/10] chore: lint --- build/devenv/tests/e2e/tcapi/offchain_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/devenv/tests/e2e/tcapi/offchain_test.go b/build/devenv/tests/e2e/tcapi/offchain_test.go index 3e87db59c..2aaf02daf 100644 --- a/build/devenv/tests/e2e/tcapi/offchain_test.go +++ b/build/devenv/tests/e2e/tcapi/offchain_test.go @@ -37,9 +37,11 @@ func (s *stubLib) Indexer() (*client.IndexerClient, error) { return ni func (s *stubLib) IndexerMonitor() (*ccv.IndexerMonitor, error) { return s.indexerMonitor, s.indexerMonitorErr } + func (s *stubLib) AllIndexers() ([]*client.IndexerClient, error) { return s.indexers, s.allIndexersErr } + func (s *stubLib) AllAggregators() (map[string]*ccv.AggregatorClient, error) { return s.aggregators, s.aggregatorsErr }