From 6e06d2a9353b8a960c88870a1f00317cf756f06d Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Mon, 6 Jul 2026 11:10:46 -0300 Subject: [PATCH 01/26] feat(load): Add load metrics --- ccip/devenv/impl.go | 6 +- ccip/devenv/tests/load/gun.go | 170 ++++++++++++--- ccip/devenv/tests/load/gun_canton2evm_test.go | 7 +- .../tests/load/gun_canton2evm_token_test.go | 7 +- ccip/devenv/tests/load/gun_evm2canton_test.go | 7 +- .../tests/load/gun_evm2canton_token_test.go | 7 +- ccip/devenv/tests/load/load_helpers.go | 116 ++++++++++- ccip/devenv/tests/load/metrics.go | 193 ++++++++++++++++++ 8 files changed, 467 insertions(+), 46 deletions(-) create mode 100644 ccip/devenv/tests/load/metrics.go diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 612b1abcf..560ccea07 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -1400,7 +1400,6 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("execute CCIP Send: %w", err) } - c.logger.Info().Str("UpdateID", ccipSendReport.Output.ExecInfo.UpdateID).Msg("CCIP Send executed") update, err := participant.LedgerServices.Update.GetUpdateById(ctx, &apiv2.GetUpdateByIdRequest{ UpdateId: ccipSendReport.Output.ExecInfo.UpdateID, UpdateFormat: &apiv2.UpdateFormat{ @@ -1443,6 +1442,11 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, err } + c.logger.Info(). + Str("UpdateID", ccipSendReport.Output.ExecInfo.UpdateID). + Str("messageID", protocol.Bytes32(parsedSend.messageID).String()). + Uint64("seqNo", parsedSend.seqNo). + Msg("CCIP Send executed") // Set next holdings err = c.setNextHoldings(update.GetTransaction().GetEvents(), hasTokenTransfer, fields.TokenAmount.Amount) diff --git a/ccip/devenv/tests/load/gun.go b/ccip/devenv/tests/load/gun.go index afddfb8df..a975d4a49 100644 --- a/ccip/devenv/tests/load/gun.go +++ b/ccip/devenv/tests/load/gun.go @@ -1,13 +1,14 @@ package load import ( + "context" "fmt" "sync" "sync/atomic" + "testing" "time" - "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/protocol" "github.com/smartcontractkit/chainlink-testing-framework/wasp" @@ -15,6 +16,23 @@ import ( devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" ) +// ConfirmSendFunc confirms a CCIP send on the source chain after SendMessage returns. +// TODO: this is needed because EVM impls method has a bug on prod-testnet checks. +type ConfirmSendFunc func( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + sendResult cciptestinterfaces.MessageSentEvent, +) (cciptestinterfaces.MessageSentEvent, error) + +// LoadGunOptions configures send confirmation and exec timeout for CCIPLoadGun. +type LoadGunOptions struct { + ConfirmSend ConfirmSendFunc + ConfirmExecTimeout time.Duration + SkipExecConfirm bool +} + type loadMessageBuilder func( source cciptestinterfaces.CCIP17, callNum int64, @@ -52,9 +70,11 @@ func (d Destination) BuildMessage( // required; staging/prod runners rely on pre-existing funded accounts. type CCIPLoadGun struct { mu sync.Mutex + flightReady sync.Cond inFlight int32 maxConcurrent int32 calls atomic.Int64 + messageIDs []protocol.Bytes32 source cciptestinterfaces.CCIP17 destinations []Destination @@ -63,8 +83,11 @@ type CCIPLoadGun struct { ccvAddr protocol.UnknownAddress executorAddr protocol.UnknownAddress - confirmSendTimeout time.Duration + confirmSend ConfirmSendFunc confirmExecTimeout time.Duration + skipExecConfirm bool + + metricsCollector *LoadMetricsCollector } // NewCCIPLoadGun wires a CCIP source with one or more destinations for load testing. @@ -72,11 +95,14 @@ func NewCCIPLoadGun( source cciptestinterfaces.CCIP17, destinations []Destination, ccvAddr, executorAddr protocol.UnknownAddress, - confirmExecTimeout time.Duration, + opts LoadGunOptions, ) (*CCIPLoadGun, error) { if source == nil { return nil, fmt.Errorf("CCIPLoadGun: source is nil") } + if opts.ConfirmSend == nil { + return nil, fmt.Errorf("CCIPLoadGun: ConfirmSend is nil") + } if len(destinations) == 0 { return nil, fmt.Errorf("CCIPLoadGun: at least one destination is required") } @@ -95,18 +121,50 @@ func NewCCIPLoadGun( return nil, fmt.Errorf("CCIPLoadGun: destination[%d] mixes token and message-only destinations", i) } } + confirmExecTimeout := opts.ConfirmExecTimeout if confirmExecTimeout <= 0 { confirmExecTimeout = 5 * time.Minute } - return &CCIPLoadGun{ + g := &CCIPLoadGun{ source: source, destinations: destinations, ccvAddr: ccvAddr, executorAddr: executorAddr, - confirmSendTimeout: 30 * time.Second, + confirmSend: opts.ConfirmSend, confirmExecTimeout: confirmExecTimeout, - }, nil + skipExecConfirm: opts.SkipExecConfirm, + metricsCollector: &LoadMetricsCollector{}, + } + g.flightReady.L = &g.mu + + return g, nil +} + +func (g *CCIPLoadGun) acquireSingleFlight() { + g.mu.Lock() + for g.inFlight >= 1 { + g.flightReady.Wait() + } + g.inFlight++ + if g.inFlight > g.maxConcurrent { + g.maxConcurrent = g.inFlight + } + g.mu.Unlock() +} + +func (g *CCIPLoadGun) releaseSingleFlight() { + g.mu.Lock() + g.inFlight-- + if g.inFlight == 0 { + g.flightReady.Broadcast() + } + g.mu.Unlock() +} + +// ConfirmExecTimeout returns the exec confirmation timeout configured for this gun. +func (g *CCIPLoadGun) ConfirmExecTimeout() time.Duration { + return g.confirmExecTimeout } func (g *CCIPLoadGun) nextDestination() Destination { @@ -124,26 +182,10 @@ func (g *CCIPLoadGun) Call(gen *wasp.Generator) *wasp.Response { } t := gen.Cfg.T - var depth int32 - g.mu.Lock() - g.inFlight++ - depth = g.inFlight - if depth > g.maxConcurrent { - g.maxConcurrent = depth - } - g.mu.Unlock() + g.acquireSingleFlight() + defer g.releaseSingleFlight() g.calls.Add(1) - defer func() { - g.mu.Lock() - g.inFlight-- - g.mu.Unlock() - }() - - if depth > 1 { - require.FailNow(t, "overlapping CCIPLoadGun.Call", - "expected single-flight; concurrent depth=%d", depth) - } dest := g.nextDestination() destSelector := dest.Chain.ChainSelector() @@ -151,9 +193,11 @@ func (g *CCIPLoadGun) Call(gen *wasp.Generator) *wasp.Response { callNum := g.calls.Load() fields, opts, err := dest.BuildMessage(g.source, callNum, g.ccvAddr, g.executorAddr) if err != nil { + g.metricsCollector.incrementSendFailure() return &wasp.Response{Failed: true, Error: fmt.Sprintf("BuildMessage (dest=%d): %v", destSelector, err), Duration: time.Since(start)} } + sentTime := time.Now() sendRes, err := g.source.SendMessage( subtestCtx, destSelector, @@ -161,34 +205,70 @@ func (g *CCIPLoadGun) Call(gen *wasp.Generator) *wasp.Response { opts, 3, ) + sendDuration := time.Since(sentTime) if err != nil { + g.metricsCollector.incrementSendFailure() return &wasp.Response{Failed: true, Error: fmt.Sprintf("SendMessage (dest=%d): %v", destSelector, err), Duration: time.Since(start)} } if sendRes.Message == nil { + g.metricsCollector.incrementSendFailure() return &wasp.Response{Failed: true, Error: "SendMessage returned nil message", Duration: time.Since(start)} } seqNo := uint64(sendRes.Message.SequenceNumber) - sentEvent, err := g.source.ConfirmSendOnSource( - subtestCtx, - destSelector, - cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, - g.confirmSendTimeout, - ) + confirmSendStart := time.Now() + sentEvent, err := g.confirmSend(t, subtestCtx, destSelector, seqNo, sendRes) + confirmSendDuration := time.Since(confirmSendStart) if err != nil { - return &wasp.Response{Failed: true, Error: fmt.Sprintf("ConfirmSendOnSource (dest=%d): %v", destSelector, err), Duration: time.Since(start)} + g.metricsCollector.incrementConfirmSendFailure() + return &wasp.Response{Failed: true, Error: fmt.Sprintf("ConfirmSend (dest=%d): %v", destSelector, err), Duration: time.Since(start)} } + g.mu.Lock() + g.messageIDs = append(g.messageIDs, sentEvent.MessageID) + g.mu.Unlock() + + ccv.Plog.Info(). + Str("messageID", sentEvent.MessageID.String()). + Uint64("seqNo", seqNo). + Uint64("destSelector", destSelector). + Msg("Load message confirmed on source") + + sourceChain := g.source.ChainSelector() + record := LoadMessageRecord{ + SeqNo: seqNo, + SourceChain: sourceChain, + DestChain: destSelector, + MessageID: sentEvent.MessageID, + SentTime: sentTime, + SendDuration: sendDuration, + ConfirmSendDuration: confirmSendDuration, + } + + if g.skipExecConfirm { + record.TotalDuration = time.Since(sentTime) + g.metricsCollector.appendRecord(record) + return &wasp.Response{ + Failed: false, + StatusCode: "200", + Duration: time.Since(start), + } + } + + confirmExecStart := time.Now() ev, err := dest.Chain.ConfirmExecOnDest( subtestCtx, g.source.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID}, g.confirmExecTimeout, ) + confirmExecDuration := time.Since(confirmExecStart) if err != nil { + g.metricsCollector.incrementConfirmExecFailure() return &wasp.Response{Failed: true, Error: fmt.Sprintf("ConfirmExecOnDest (dest=%d): %v", destSelector, err), Duration: time.Since(start)} } if ev.State != cciptestinterfaces.ExecutionStateSuccess { + g.metricsCollector.incrementConfirmExecFailure() return &wasp.Response{ Failed: true, Error: fmt.Sprintf("execution state=%s (dest=%d)", ev.State.String(), destSelector), @@ -197,6 +277,11 @@ func (g *CCIPLoadGun) Call(gen *wasp.Generator) *wasp.Response { } } + record.ConfirmExecDuration = confirmExecDuration + record.ExecutedTime = time.Now() + record.TotalDuration = record.ExecutedTime.Sub(sentTime) + g.metricsCollector.appendRecord(record) + return &wasp.Response{ Failed: false, StatusCode: "200", @@ -215,3 +300,24 @@ func (g *CCIPLoadGun) MaxConcurrentObserved() int32 { func (g *CCIPLoadGun) CallCount() int64 { return g.calls.Load() } + +// Metrics returns a copy of successful load message timing records. +func (g *CCIPLoadGun) Metrics() []LoadMessageRecord { + records, _ := g.metricsCollector.snapshot() + return records +} + +// FailureCounts returns phase failure counters from failed WASP calls. +func (g *CCIPLoadGun) FailureCounts() LoadFailureCounts { + _, failures := g.metricsCollector.snapshot() + return failures +} + +// MessageIDs returns a copy of message IDs collected after successful ConfirmSend. +func (g *CCIPLoadGun) MessageIDs() []protocol.Bytes32 { + g.mu.Lock() + defer g.mu.Unlock() + out := make([]protocol.Bytes32, len(g.messageIDs)) + copy(out, g.messageIDs) + return out +} diff --git a/ccip/devenv/tests/load/gun_canton2evm_test.go b/ccip/devenv/tests/load/gun_canton2evm_test.go index 0233a7b49..5fa890a58 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_test.go @@ -90,9 +90,12 @@ func TestCanton2EVM_Load(t *testing.T) { destinations, ccvAddr, executorAddr, - utilstests.WaitTimeout(t), + LoadGunOptions{ + ConfirmSend: cantonSourceConfirmSend(cantonChain), + ConfirmExecTimeout: utilstests.WaitTimeout(t), + }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm", sched, "message_only") + runWASP(t, gun, "canton-load-canton2evm", sched, "message_only", nil) } diff --git a/ccip/devenv/tests/load/gun_canton2evm_token_test.go b/ccip/devenv/tests/load/gun_canton2evm_token_test.go index df3a80bc9..cb53d4a26 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_token_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_token_test.go @@ -78,11 +78,14 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { destinations, ccvAddr, executorAddr, - utilstests.WaitTimeout(t), + LoadGunOptions{ + ConfirmSend: cantonSourceConfirmSend(cantonChain), + ConfirmExecTimeout: utilstests.WaitTimeout(t), + }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer") + runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer", nil) receiverBalanceAfter, err := firstDest.Chain.GetTokenBalance(ctx, firstDest.Receiver, destToken) require.NoError(t, err) diff --git a/ccip/devenv/tests/load/gun_evm2canton_test.go b/ccip/devenv/tests/load/gun_evm2canton_test.go index 034633ae5..86fa2fb4f 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_test.go @@ -66,9 +66,12 @@ func TestEVM2Canton_Load(t *testing.T) { []Destination{cantonDest}, ccvAddr, executorAddr, - utilstests.WaitTimeout(t), + LoadGunOptions{ + ConfirmSend: evmSourceConfirmSend(evmChain), // TODO: this confirmation will change on prod-testnet PR + ConfirmExecTimeout: utilstests.WaitTimeout(t), + }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only") + runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only", nil) } diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index 426e29fed..cd2fc0cc1 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -91,11 +91,14 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { []Destination{cantonDest}, ccvAddr, executorAddr, - utilstests.WaitTimeout(t), + LoadGunOptions{ + ConfirmSend: evmSourceConfirmSend(evmChain), // TODO: this confirmation will change on prod-testnet PR + ConfirmExecTimeout: utilstests.WaitTimeout(t), + }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer") + runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer", nil) totalHoldingsRat, err := testhelpers.GetHoldingsBalance(ctx, receiverParticipant, nil) require.NoError(t, err) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 4bf0b1399..6343a2710 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" "os" + "strings" "testing" "time" @@ -16,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" ccvload "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/load" + ccvmetrics "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/metrics" "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" @@ -29,10 +31,13 @@ import ( ) const ( - envMessageRate = "CANTON_LOAD_MESSAGE_RATE" - envLoadDuration = "CANTON_LOAD_DURATION" - defaultMessageRate = "1/1s" - defaultLoadDuration = 90 * time.Second + envMessageRate = "CANTON_LOAD_MESSAGE_RATE" + envLoadDuration = "CANTON_LOAD_DURATION" + envLoadCallTimeout = "CANTON_LOAD_CALL_TIMEOUT" + defaultMessageRate = "1/1s" + defaultLoadDuration = 90 * time.Second + defaultLoadCallPadding = 2 * time.Minute + confirmSendTimeout = 30 * time.Second ) type scheduleConfig struct { @@ -70,9 +75,71 @@ func loadSchedule(t *testing.T) scheduleConfig { } } -func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string) { +func waspCallTimeout(t *testing.T, gun *CCIPLoadGun, sched scheduleConfig) time.Duration { t.Helper() + if v := strings.TrimSpace(os.Getenv(envLoadCallTimeout)); v != "" { + parsed, err := time.ParseDuration(v) + require.NoError(t, err, "%s=%q invalid", envLoadCallTimeout, v) + return parsed + } + + return gun.ConfirmExecTimeout() + sched.rateUnit + defaultLoadCallPadding +} + +func printLoadMetrics(t *testing.T, gun *CCIPLoadGun) { + t.Helper() + + records := gun.Metrics() + failures := gun.FailureCounts() + PrintPhaseMetricsSummary(t, records, failures, false) + + ccvMetrics := ToCCVMessageMetrics(records) + if len(ccvMetrics) > 0 { + totals := ccvmetrics.MessageTotals{ + Sent: len(ccvMetrics), + Received: len(ccvMetrics), + } + summary := ccvmetrics.CalculateMetricsSummary(ccvMetrics, totals) + ccvmetrics.PrintMetricsSummary(t, summary) + } +} + +func logLoadMessageSummary(t *testing.T, gun *CCIPLoadGun, indexerEndpoints []string) { + t.Helper() + + ids := gun.MessageIDs() + lggr := ccv.Plog + lggr.Info().Int("count", len(ids)).Msg("Load message summary") + + var indexerBase string + if len(indexerEndpoints) > 0 { + indexerBase = strings.TrimSuffix(indexerEndpoints[0], "/") + } + + for i, id := range ids { + msgID := id.String() + ev := lggr.Info().Int("index", i+1).Str("messageID", msgID) + if indexerBase != "" { + ev = ev.Str("indexer", fmt.Sprintf("%s/v1/verifierresults/%s", indexerBase, msgID)) + } + ev.Msg("Load message sent") + } +} + +func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string, indexerEndpoints []string) { + t.Helper() + defer logLoadMessageSummary(t, gun, indexerEndpoints) + + callTimeout := waspCallTimeout(t, gun, sched) + ccv.Plog.Info(). + Str("messageRate", sched.messageRate). + Dur("rateUnit", sched.rateUnit). + Dur("loadDuration", sched.duration). + Dur("callTimeout", callTimeout). + Dur("confirmExecTimeout", gun.ConfirmExecTimeout()). + Msg("WASP load schedule") + labels := map[string]string{ "go_test_name": genName, "branch": "test", @@ -92,6 +159,7 @@ func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfi wasp.Plain(sched.rate, sched.duration), ), RateLimitUnitDuration: sched.rateUnit, + CallTimeout: callTimeout, Gun: gun, Labels: labels, LokiConfig: nil, @@ -104,6 +172,8 @@ func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfi require.Positive(t, gun.CallCount(), "gun should have completed at least one message") require.LessOrEqual(t, gun.MaxConcurrentObserved(), int32(1), "Gun.Call must not overlap (single-flight)") + + printLoadMetrics(t, gun) } func discoverEVMDestinations(t *testing.T, in *ccv.Cfg, chainMap map[uint64]cciptestinterfaces.CCIP17) []Destination { @@ -370,3 +440,39 @@ func resolveEVMSourceAddrs(t *testing.T, lib ccv.Lib, evmSelector uint64) (proto return ccvAddr, executorAddr } + +func cantonSourceConfirmSend(source cciptestinterfaces.CCIP17) ConfirmSendFunc { + return func( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + _ cciptestinterfaces.MessageSentEvent, + ) (cciptestinterfaces.MessageSentEvent, error) { + return source.ConfirmSendOnSource( + ctx, + destSelector, + cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, + confirmSendTimeout, + ) + } +} + +// TODO: this is needed because EVM impls method has a bug on prod-testnet checks. +// currently just a simple wrapper around the method. +func evmSourceConfirmSend(source cciptestinterfaces.CCIP17) ConfirmSendFunc { + return func( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + _ cciptestinterfaces.MessageSentEvent, + ) (cciptestinterfaces.MessageSentEvent, error) { + return source.ConfirmSendOnSource( + ctx, + destSelector, + cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, + confirmSendTimeout, + ) + } +} diff --git a/ccip/devenv/tests/load/metrics.go b/ccip/devenv/tests/load/metrics.go new file mode 100644 index 000000000..4d405a0e9 --- /dev/null +++ b/ccip/devenv/tests/load/metrics.go @@ -0,0 +1,193 @@ +package load + +import ( + "slices" + "sync" + "testing" + "time" + + ccvmetrics "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/metrics" + "github.com/smartcontractkit/chainlink-ccv/protocol" +) + +// LoadMessageRecord captures per-message phase timings for a successful load test call. +type LoadMessageRecord struct { + SeqNo uint64 + SourceChain uint64 + DestChain uint64 + MessageID protocol.Bytes32 + SentTime time.Time + ExecutedTime time.Time + SendDuration time.Duration + ConfirmSendDuration time.Duration + ConfirmExecDuration time.Duration + TotalDuration time.Duration +} + +// LoadFailureCounts tracks failures by load test phase. +type LoadFailureCounts struct { + Send int + ConfirmSend int + ConfirmExec int +} + +// LoadMetricsCollector accumulates successful message records and phase failure counts. +type LoadMetricsCollector struct { + mu sync.Mutex + records []LoadMessageRecord + failures LoadFailureCounts +} + +func (c *LoadMetricsCollector) appendRecord(r LoadMessageRecord) { + c.mu.Lock() + defer c.mu.Unlock() + c.records = append(c.records, r) +} + +func (c *LoadMetricsCollector) incrementSendFailure() { + c.mu.Lock() + defer c.mu.Unlock() + c.failures.Send++ +} + +func (c *LoadMetricsCollector) incrementConfirmSendFailure() { + c.mu.Lock() + defer c.mu.Unlock() + c.failures.ConfirmSend++ +} + +func (c *LoadMetricsCollector) incrementConfirmExecFailure() { + c.mu.Lock() + defer c.mu.Unlock() + c.failures.ConfirmExec++ +} + +func (c *LoadMetricsCollector) snapshot() ([]LoadMessageRecord, LoadFailureCounts) { + c.mu.Lock() + defer c.mu.Unlock() + out := make([]LoadMessageRecord, len(c.records)) + copy(out, c.records) + return out, c.failures +} + +// PercentileStats holds common percentile values for a set of durations. +type PercentileStats struct { + Min time.Duration + Max time.Duration + P90 time.Duration + P95 time.Duration + P99 time.Duration +} + +func calculatePercentiles(durations []time.Duration) PercentileStats { + if len(durations) == 0 { + return PercentileStats{} + } + + slices.Sort(durations) + + p90Index := int(float64(len(durations)) * 0.90) + p95Index := int(float64(len(durations)) * 0.95) + p99Index := int(float64(len(durations)) * 0.99) + + if p90Index >= len(durations) { + p90Index = len(durations) - 1 + } + if p95Index >= len(durations) { + p95Index = len(durations) - 1 + } + if p99Index >= len(durations) { + p99Index = len(durations) - 1 + } + + return PercentileStats{ + Min: durations[0], + Max: durations[len(durations)-1], + P90: durations[p90Index], + P95: durations[p95Index], + P99: durations[p99Index], + } +} + +func logPhasePercentiles(t *testing.T, label string, stats PercentileStats, count int) { + t.Helper() + t.Logf("%s (n=%d):\n"+ + " Min: %v\n"+ + " Max: %v\n"+ + " P90: %v\n"+ + " P95: %v\n"+ + " P99: %v", + label, count, + stats.Min, stats.Max, stats.P90, stats.P95, stats.P99, + ) +} + +// PrintPhaseMetricsSummary prints per-phase timing percentiles for successful load messages. +func PrintPhaseMetricsSummary(t *testing.T, records []LoadMessageRecord, failures LoadFailureCounts, skipExecConfirm bool) { + t.Helper() + + t.Logf("\n" + + "========================================\n" + + " Load Phase Metrics \n" + + "========================================") + + if len(records) == 0 { + t.Logf("No successful load messages recorded") + } else { + sendDurations := make([]time.Duration, len(records)) + confirmSendDurations := make([]time.Duration, len(records)) + for i, r := range records { + sendDurations[i] = r.SendDuration + confirmSendDurations[i] = r.ConfirmSendDuration + } + + logPhasePercentiles(t, "Send", calculatePercentiles(sendDurations), len(records)) + logPhasePercentiles(t, "Confirm Send", calculatePercentiles(confirmSendDurations), len(records)) + + if skipExecConfirm { + sourceConfirmed := make([]time.Duration, len(records)) + for i, r := range records { + sourceConfirmed[i] = r.SendDuration + r.ConfirmSendDuration + } + logPhasePercentiles(t, "Source Confirmed (Send → Confirm Send)", calculatePercentiles(sourceConfirmed), len(records)) + } else { + confirmExecDurations := make([]time.Duration, len(records)) + totalDurations := make([]time.Duration, len(records)) + for i, r := range records { + confirmExecDurations[i] = r.ConfirmExecDuration + totalDurations[i] = r.TotalDuration + } + logPhasePercentiles(t, "Confirm Exec", calculatePercentiles(confirmExecDurations), len(records)) + logPhasePercentiles(t, "Total (E2E)", calculatePercentiles(totalDurations), len(records)) + } + } + + totalFailures := failures.Send + failures.ConfirmSend + failures.ConfirmExec + if totalFailures > 0 { + t.Logf("----------------------------------------\n"+ + "Failures: send=%d confirm_send=%d confirm_exec=%d", + failures.Send, failures.ConfirmSend, failures.ConfirmExec) + } + + t.Logf("========================================") +} + +// ToCCVMessageMetrics maps load records with ExecutedTime set to CCV message metrics. +func ToCCVMessageMetrics(records []LoadMessageRecord) []ccvmetrics.MessageMetrics { + out := make([]ccvmetrics.MessageMetrics, 0, len(records)) + for _, r := range records { + if r.ExecutedTime.IsZero() { + continue + } + out = append(out, ccvmetrics.MessageMetrics{ + SeqNo: r.SeqNo, + MessageID: r.MessageID.String(), + SourceChain: r.SourceChain, + DestChain: r.DestChain, + SentTime: r.SentTime, + ExecutedTime: r.ExecutedTime, + LatencyDuration: r.ExecutedTime.Sub(r.SentTime), + }) + } + return out +} From c9cfe0365781fb4d01ebd964716743868c4f7a8e Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 12:04:37 -0300 Subject: [PATCH 02/26] fix(lint) --- ccip/devenv/tests/load/gun.go | 1 + ccip/devenv/tests/load/gun_canton2evm_test.go | 2 +- .../tests/load/gun_canton2evm_token_test.go | 2 +- ccip/devenv/tests/load/gun_evm2canton_test.go | 2 +- .../tests/load/gun_evm2canton_token_test.go | 2 +- ccip/devenv/tests/load/load_helpers.go | 18 ++++-------------- ccip/devenv/tests/load/metrics.go | 2 ++ 7 files changed, 11 insertions(+), 18 deletions(-) diff --git a/ccip/devenv/tests/load/gun.go b/ccip/devenv/tests/load/gun.go index a975d4a49..9f9e94dcb 100644 --- a/ccip/devenv/tests/load/gun.go +++ b/ccip/devenv/tests/load/gun.go @@ -319,5 +319,6 @@ func (g *CCIPLoadGun) MessageIDs() []protocol.Bytes32 { defer g.mu.Unlock() out := make([]protocol.Bytes32, len(g.messageIDs)) copy(out, g.messageIDs) + return out } diff --git a/ccip/devenv/tests/load/gun_canton2evm_test.go b/ccip/devenv/tests/load/gun_canton2evm_test.go index 5fa890a58..29060312c 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_test.go @@ -97,5 +97,5 @@ func TestCanton2EVM_Load(t *testing.T) { ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm", sched, "message_only", nil) + runWASP(t, gun, "canton-load-canton2evm", sched, "message_only") } diff --git a/ccip/devenv/tests/load/gun_canton2evm_token_test.go b/ccip/devenv/tests/load/gun_canton2evm_token_test.go index cb53d4a26..291426a30 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_token_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_token_test.go @@ -85,7 +85,7 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer", nil) + runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer") receiverBalanceAfter, err := firstDest.Chain.GetTokenBalance(ctx, firstDest.Receiver, destToken) require.NoError(t, err) diff --git a/ccip/devenv/tests/load/gun_evm2canton_test.go b/ccip/devenv/tests/load/gun_evm2canton_test.go index 86fa2fb4f..b33555040 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_test.go @@ -73,5 +73,5 @@ func TestEVM2Canton_Load(t *testing.T) { ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only", nil) + runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only") } diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index cd2fc0cc1..4ad00269d 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -98,7 +98,7 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer", nil) + runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer") totalHoldingsRat, err := testhelpers.GetHoldingsBalance(ctx, receiverParticipant, nil) require.NoError(t, err) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 6343a2710..0ba7e3daa 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -105,31 +105,21 @@ func printLoadMetrics(t *testing.T, gun *CCIPLoadGun) { } } -func logLoadMessageSummary(t *testing.T, gun *CCIPLoadGun, indexerEndpoints []string) { +func logLoadMessageSummary(t *testing.T, gun *CCIPLoadGun) { t.Helper() ids := gun.MessageIDs() lggr := ccv.Plog lggr.Info().Int("count", len(ids)).Msg("Load message summary") - var indexerBase string - if len(indexerEndpoints) > 0 { - indexerBase = strings.TrimSuffix(indexerEndpoints[0], "/") - } - for i, id := range ids { - msgID := id.String() - ev := lggr.Info().Int("index", i+1).Str("messageID", msgID) - if indexerBase != "" { - ev = ev.Str("indexer", fmt.Sprintf("%s/v1/verifierresults/%s", indexerBase, msgID)) - } - ev.Msg("Load message sent") + lggr.Info().Int("index", i+1).Str("messageID", id.String()).Msg("Load message sent") } } -func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string, indexerEndpoints []string) { +func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string) { t.Helper() - defer logLoadMessageSummary(t, gun, indexerEndpoints) + defer logLoadMessageSummary(t, gun) callTimeout := waspCallTimeout(t, gun, sched) ccv.Plog.Info(). diff --git a/ccip/devenv/tests/load/metrics.go b/ccip/devenv/tests/load/metrics.go index 4d405a0e9..38e0d7b80 100644 --- a/ccip/devenv/tests/load/metrics.go +++ b/ccip/devenv/tests/load/metrics.go @@ -67,6 +67,7 @@ func (c *LoadMetricsCollector) snapshot() ([]LoadMessageRecord, LoadFailureCount defer c.mu.Unlock() out := make([]LoadMessageRecord, len(c.records)) copy(out, c.records) + return out, c.failures } @@ -189,5 +190,6 @@ func ToCCVMessageMetrics(records []LoadMessageRecord) []ccvmetrics.MessageMetric LatencyDuration: r.ExecutedTime.Sub(r.SentTime), }) } + return out } From 0ea19d2c45b9694946c1a8441b9f7a49f9899cac Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 16:03:59 -0300 Subject: [PATCH 03/26] fix(e2e): Fix tests holding minting --- ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index 28e2e1a3b..ca7550055 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -52,11 +52,15 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NoError(t, err) }) + // TODO: currently minting 2 holdings of 2k for all the e2e tests. Otherwise one holding might conflict with the other. + // the tests need to be hardened to not rely on this and instead correctly pick the holding that suffices. + require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount)*100)) + require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount)*100)) + t.Run("EOA receiver and default committee verifier", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) // Setup message send - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount))) require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), 0)) ds, err := lib.DataStore() @@ -146,12 +150,6 @@ func TestCanton2EVM_Basic(t *testing.T) { tokenTransferAmount := lane.TransferAmount.Uint64() // Setup message send - require.NoError(t, cantonImpl.MintTokens(ctx, - devenvtests.CantonToEVMTokenSequentialSends*uint64(devenvtests.CantonToEVMFeeAmount), - )) // Holdings for fee - require.NoError(t, cantonImpl.MintTokens(ctx, - devenvtests.CantonToEVMTokenSequentialSends*tokenTransferAmount, - )) // Holdings for token transfer require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) ds, err := lib.DataStore() @@ -237,8 +235,6 @@ func TestCanton2EVM_Basic(t *testing.T) { lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) tokenTransferAmount := lane.TransferAmount.Uint64() - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount))) - require.NoError(t, cantonImpl.MintTokens(ctx, tokenTransferAmount)) require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) receiver, err := evmChain.GetEOAReceiverAddress() @@ -281,8 +277,6 @@ func TestCanton2EVM_Basic(t *testing.T) { lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) tokenTransferAmount := lane.TransferAmount.Uint64() - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount))) - require.NoError(t, cantonImpl.MintTokens(ctx, tokenTransferAmount)) require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) ds, err := lib.DataStore() From 19f7888a28200a7c1848f733ee7e71cd43915487 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 17:15:47 -0300 Subject: [PATCH 04/26] feat(e2e): Token Lane Refactor --- ccip/devenv/constants.go | 26 ++ ccip/devenv/impl.go | 80 +++-- ccip/devenv/manual_execution.go | 58 ++++ ccip/devenv/tests/constants.go | 14 - ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 68 +++-- .../e2e/canton2evm_send_validation_test.go | 12 +- ccip/devenv/tests/e2e/evm2canton_e2e_test.go | 2 +- ccip/devenv/tests/env.go | 33 +++ ccip/devenv/tests/load/gun_canton2evm_test.go | 9 +- .../tests/load/gun_canton2evm_token_test.go | 4 +- .../tests/load/gun_evm2canton_token_test.go | 2 +- ccip/devenv/tests/load/load_helpers.go | 25 +- ccip/devenv/tests/token_lane.go | 273 ++++++++++++++---- ccip/devenv/tests/token_lane_test.go | 185 ++++++++++++ ccip/devenv/tests/token_transfer_config.toml | 51 +++- 15 files changed, 670 insertions(+), 172 deletions(-) create mode 100644 ccip/devenv/constants.go delete mode 100644 ccip/devenv/tests/constants.go create mode 100644 ccip/devenv/tests/env.go create mode 100644 ccip/devenv/tests/token_lane_test.go diff --git a/ccip/devenv/constants.go b/ccip/devenv/constants.go new file mode 100644 index 000000000..c0234b457 --- /dev/null +++ b/ccip/devenv/constants.go @@ -0,0 +1,26 @@ +package devenv + +import "github.com/smartcontractkit/chainlink-ccv/protocol" + +// Shared devenv test constants (message and token paths). + +const ( + // EVMToCantonFinalityConfig is the minimum block-depth FTF (1 confirmation). + EVMToCantonFinalityConfig = protocol.Finality(1) + + // CantonToEVMFeeAmount is the per-message CCIP fee budget in Amulet units for + // message-only sends (200k gas). Kept low for prod-testnet compatibility. + CantonToEVMFeeAmount int64 = 50 + + // CantonToEVMTokenTransferFeeAmount is the per-message fee budget for token transfers + // (500k execution gas), which quote ~127 Amulet per send in devenv. Used only by + // token e2e/load tests; must cover one send and leave enough change for sequential sends. + CantonToEVMTokenTransferFeeAmount int64 = 130 + + // Canton token amounts use 10-decimal fixed point (e.g. 1_000_000_000 = 0.1). + CantonFixedPointScale int64 = 10_000_000_000 + CantonFixedPointToEVMScale int64 = 100_000_000 // fixedPoint * this = EVM wei + + // CantonToEVMTokenSequentialSends is how many token transfers the Canton→EVM e2e subtest sends. + CantonToEVMTokenSequentialSends = 2 +) diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 2bd2fba58..39d9fd3de 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -70,8 +70,9 @@ import ( ) // TODO: move this to share between devenv and integration tests -// and define a const for LINK instrument const AMTInstrument = types.TEXT("Amulet") +const LINKInstrument = types.TEXT("LINK") + const amuletTransferPreapprovalTemplateID = "#splice-amulet:Splice.AmuletRules:TransferPreapproval" var _ cciptestinterfaces.CCIP17 = &Chain{} @@ -551,9 +552,10 @@ func (c *Chain) GetTokenExpansionConfigs( "instrument-id:Amulet", ), }, - PoolType: string(poolRef.Type), - TokenPoolQualifier: poolRef.Qualifier, - AllowedFinalityConfig: finality.Config{WaitForFinality: true}, + PoolType: string(poolRef.Type), + TokenPoolQualifier: poolRef.Qualifier, + // BlockDepth 1: minimum FTF allowed by pool; messages may request finality>=1 via extra args. + AllowedFinalityConfig: finality.Config{BlockDepth: 1}, }, }}, nil } @@ -761,8 +763,9 @@ func (c *Chain) GetTokenTransferConfigs( Type: datastore.ContractType(token_admin_registry.ContractType), Version: token_admin_registry.Version, }, - RemoteChains: remoteChains, - AllowedFinalityConfig: finality.Config{WaitForFinality: true}, + RemoteChains: remoteChains, + // BlockDepth 1: pool must allow message FTF; WaitForFinality-only rejects BlockDepth requests. + AllowedFinalityConfig: finality.Config{BlockDepth: 1}, }}, nil } @@ -1013,39 +1016,28 @@ func (c *Chain) SetupReceive(ctx context.Context) error { // SetupSend sets up Canton sender specific prerequisites for sending a message. // eg: deploy per-party router, deploy ccipsender contract... +// transferInstrument defaults to Amulet under registryAdmin when omitted or Admin is empty. func (c *Chain) SetupSend( ctx context.Context, feeAmountPerMessage uint64, - transferAmountPerMessage uint64, + transferAmountPerMessage *big.Rat, + transferInstrument ...splice_api_token_holding_v1.InstrumentId, ) error { - participant, clientIdx, err := c.ClientParticipant() + participant, _, err := c.ClientParticipant() if err != nil { return fmt.Errorf("no canton participants configured: %w", err) } party := participant.PartyID - // Deploy contracts // - // Router for sender party. routerAddress, err := c.DeployPerPartyRouter(ctx, participant, party) if err != nil { return fmt.Errorf("failed to deploy per-party router: %w", err) } - // Deploy a sender-owned CCIPSender contract. - senderInstanceID := contracts.MustNewInstanceID("devenv-ccipsender") - out, err := operations.ExecuteOperation(c.e.OperationsBundle, sender.Deploy, c.chain, contract.DeployInput[ccipsender.CCIPSender]{ - Qualifier: nil, - ParticipantIndex: clientIdx, - Template: ccipsender.CCIPSender{ - InstanceId: types.TEXT(senderInstanceID), - Owner: types.PARTY(party), - }, - OwnerParty: types.PARTY(party), - }) + senderAddress, err := c.DeployCCIPSender(ctx, participant, party) if err != nil { return fmt.Errorf("failed to deploy ccip sender contract: %w", err) } - senderAddress := contracts.HexToInstanceAddress(out.Output.Address) registryAdmin, err := testhelpers.ResolveRegistryAdmin(ctx, participant) if err != nil { return fmt.Errorf("resolve registry admin: %w", err) @@ -1054,14 +1046,11 @@ func (c *Chain) SetupSend( c.routerAddress = routerAddress c.senderAddress = senderAddress - // Clients setup // _, err = c.getValidatorAPIClients() if err != nil { return fmt.Errorf("get validator API clients: %w", err) } - // Tokens setup // - // Setup fee holding feeTokenInstrument := splice_api_token_holding_v1.InstrumentId{ Admin: types.PARTY(registryAdmin), Id: AMTInstrument, @@ -1085,25 +1074,24 @@ func (c *Chain) SetupSend( c.feeTokenInstrument = feeTokenInstrument c.nextFeeCID = selectedFee[0].ContractID - // End setup, no transfer holdings needed - if transferAmountPerMessage == 0 { + if transferAmountPerMessage == nil || transferAmountPerMessage.Sign() <= 0 { return nil } - // Setup transfer holdings - // TODO: add support for LINK instrument transferTokenInstrument := &splice_api_token_holding_v1.InstrumentId{ Admin: types.PARTY(registryAdmin), Id: AMTInstrument, } + if len(transferInstrument) > 0 && transferInstrument[0].Admin != "" { + transferTokenInstrument = &transferInstrument[0] + } - // Select transfer holding using another call here because fee/token might use different instruments - filters := append(baseFilters, testhelpers.ExcludeCIDs([]string{c.nextFeeCID})) // this filter is here in case fee and transfer use the same instrument + filters := append(baseFilters, testhelpers.ExcludeCIDs([]string{c.nextFeeCID})) transferRows, err := testhelpers.ListHoldingsForInstrument(ctx, participant, transferTokenInstrument, filters...) if err != nil { return fmt.Errorf("list transfer holdings for setup: %w", err) } - transferMin := new(big.Rat).SetUint64(transferAmountPerMessage) + transferMin := new(big.Rat).Set(transferAmountPerMessage) selectedTransfer, err := testhelpers.SelectHoldingsForInstrument(transferRows, []*big.Rat{transferMin}) if err != nil || len(selectedTransfer) == 0 { return fmt.Errorf("select transfer holding for setup: %w", err) @@ -1117,8 +1105,11 @@ func (c *Chain) SetupSend( // MintTokens mint tokens for transfer and fees. To be used on devenv tests only. // this method won't work in staging/prod tests -// TODO: add support for LINK instrument -func (c *Chain) MintTokens(ctx context.Context, amount uint64) error { +func (c *Chain) MintTokens(ctx context.Context, amount *big.Rat) error { + if amount == nil || amount.Sign() <= 0 { + return nil + } + participant, _, err := c.ClientParticipant() if err != nil { return fmt.Errorf("no canton participants configured: %w", err) @@ -1137,7 +1128,7 @@ func (c *Chain) MintTokens(ctx context.Context, amount uint64) error { validatorAPIClients.transferClient, validatorAPIClients.scanClient, party, - strconv.FormatUint(amount, 10), + amount.FloatString(10), ) if err != nil { return fmt.Errorf("failed to mint tokens: %w", err) @@ -1166,7 +1157,8 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("canton SendMessage: token transfer amount must be positive") } - hasTokenTransfer := c.transferTokenInstrument != nil && fields.TokenAmount.Amount != nil && fields.TokenAmount.Amount.Sign() > 0 + hasTokenTransfer := fields.TokenAmount.Amount != nil && + fields.TokenAmount.Amount.Sign() > 0 participant, clientIdx, err := c.ClientParticipant() if err != nil { @@ -1250,9 +1242,8 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint Receiver: hex.EncodeToString(fields.Receiver), } if hasTokenTransfer { - // TODO: move this to the other line also branching by tokenTransfer outgoingMessage.TokenTransfer = &oapiCommon.TokenTransfer{ - Amount: fields.TokenAmount.Amount.String(), + Amount: new(big.Rat).SetFrac(fields.TokenAmount.Amount, big.NewInt(CantonFixedPointScale)).FloatString(10), Token: oapiCommon.InstrumentId{ Admin: oapiCommon.PartyId(c.transferTokenInstrument.Admin), Id: string(c.transferTokenInstrument.Id), @@ -1300,8 +1291,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint // Token Pool var tokenPoolRequiredCCVs []string if hasTokenTransfer { - // Get Token Pool - token := contracts.EncodeInstrumentID(c.feeTokenInstrument) + token := contracts.EncodeInstrumentID(*c.transferTokenInstrument) tokenPoolAddress, err := c.GetTokenPoolForToken(ctx, token) if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("get token pool for token %s: %w", token.String(), err) @@ -1451,7 +1441,11 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint Msg("CCIP Send executed") // Set next holdings - err = c.setNextHoldings(update.GetTransaction().GetEvents(), hasTokenTransfer, fields.TokenAmount.Amount) + var tokenAmount *big.Rat + if hasTokenTransfer { + tokenAmount = new(big.Rat).SetFrac(fields.TokenAmount.Amount, big.NewInt(CantonFixedPointScale)) + } + err = c.setNextHoldings(update.GetTransaction().GetEvents(), hasTokenTransfer, tokenAmount) if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("set next holdings: %w", err) } @@ -1570,7 +1564,7 @@ func parseFirstCCIPMessageSentFromLedgerEvents(events []*apiv2.Event, previousSe // Created events from the send transaction. When no qualifying holding appears in those // events, the corresponding next CID is cleared (empty means no next holding available); // the current send already succeeded and a future SendMessage will fail its holding checks. -func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, tokenAmount *big.Int) error { +func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, tokenAmount *big.Rat) error { participant, _, err := c.ClientParticipant() if err != nil { return fmt.Errorf("no canton participants configured: %w", err) @@ -1621,7 +1615,7 @@ func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, to slices.Clone(refreshFilters), testhelpers.ExcludeCIDs(append(spentCIDs, c.nextFeeCID)), ) - transferValue := new(big.Rat).SetInt(tokenAmount) + transferValue := new(big.Rat).Set(tokenAmount) nextTransferCID, exhaustion, err := pickNextHolding( events, *c.transferTokenInstrument, diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index 5126be778..ad2e74f6f 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math/big" + "os" "strings" "sync" "time" @@ -24,15 +25,24 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" + ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" "github.com/smartcontractkit/chainlink-canton/contracts" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) const perPartyRouterInstanceID = "test-router" +func instanceIDFromEnv(key, defaultID string) contracts.InstanceID { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return contracts.InstanceID(v) + } + return contracts.InstanceID(defaultID) +} + // DeployPerPartyRouter uses the PerPartyRouterFactory to create a new PerPartyRouter instance for the given party. // partyOwner (client participant) exercises CreateRouter with factory disclosures from EDS. // It returns the instance address of the router. If a router already exists for the party, it returns the existing address. @@ -96,6 +106,54 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, clientParticipant cant return routerAddress, nil } +// DeployCCIPSender returns a sender-owned CCIPSender instance address, deploying only when missing. +func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Participant, partyId string) (contracts.InstanceAddress, error) { + var unset contracts.InstanceAddress + if c.senderAddress != unset { + return c.senderAddress, nil + } + + instanceID := instanceIDFromEnv("CANTON_SENDER_INSTANCE_ID", "e2e-ccipsender") + senderAddress := instanceID.RawInstanceAddress(types.PARTY(partyId)).InstanceAddress() + + if _, err := contract.FindActiveContractIDByInstanceAddress( + ctx, + participant.LedgerServices.State, + contract.LedgerQueryParties(participant), + ccipsender.CCIPSender{}.GetTemplateID(), + senderAddress, + ); err == nil { + c.senderAddress = senderAddress + return senderAddress, nil + } + + _, err := operations.ExecuteOperation(c.e.OperationsBundle, sender.Deploy, c.chain, contract.DeployInput[ccipsender.CCIPSender]{ + Qualifier: nil, + ParticipantIndex: c.clientParticipantIndex(), + Template: ccipsender.CCIPSender{ + InstanceId: types.TEXT(instanceID), + Owner: types.PARTY(partyId), + }, + OwnerParty: types.PARTY(partyId), + }) + if err != nil { + if _, findErr := contract.FindActiveContractIDByInstanceAddress( + ctx, + participant.LedgerServices.State, + contract.LedgerQueryParties(participant), + ccipsender.CCIPSender{}.GetTemplateID(), + senderAddress, + ); findErr == nil { + c.senderAddress = senderAddress + return senderAddress, nil + } + return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy ccip sender contract: %w", err) + } + + c.senderAddress = senderAddress + return senderAddress, nil +} + func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Participant, partyId string, receiverFinality int64) (contracts.InstanceAddress, error) { finalityConfig, err := encodeReceiverFinalityConfig(receiverFinality) if err != nil { diff --git a/ccip/devenv/tests/constants.go b/ccip/devenv/tests/constants.go deleted file mode 100644 index 4d16a64a8..000000000 --- a/ccip/devenv/tests/constants.go +++ /dev/null @@ -1,14 +0,0 @@ -package tests - -// Shared devenv test constants (message and token paths). - -const ( - // CantonToEVMFeeAmount is the Canton CCIP send fee in Amulet units. - CantonToEVMFeeAmount int64 = 2_000 - - // EVMDecimalsScale converts Canton token amounts to EVM 18-decimal balance units. - EVMDecimalsScale int64 = 1_000_000_000_000_000_000 - - // CantonToEVMTokenSequentialSends is how many token transfers the Canton→EVM e2e subtest sends. - CantonToEVMTokenSequentialSends = 2 -) diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index ca7550055..1f67f934f 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -54,14 +54,14 @@ func TestCanton2EVM_Basic(t *testing.T) { // TODO: currently minting 2 holdings of 2k for all the e2e tests. Otherwise one holding might conflict with the other. // the tests need to be hardened to not rely on this and instead correctly pick the holding that suffices. - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount)*100)) - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount)*100)) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*100))) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*100))) t.Run("EOA receiver and default committee verifier", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) // Setup message send - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), 0)) + require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) ds, err := lib.DataStore() require.NoError(t, err) @@ -146,11 +146,21 @@ func TestCanton2EVM_Basic(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) // Send params (transfer amount, gas limit, finality) come from token_transfer_config.toml. - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) - tokenTransferAmount := lane.TransferAmount.Uint64() - - // Setup message send - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) + + fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) + sends := cantondevenv.CantonToEVMTokenSequentialSends + feeTotal := new(big.Rat).SetUint64(uint64(sends) * fee) + transferTotalFP := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(sends))) + transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) + transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) + require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) + if lane.TransferInstrument.Admin != "" { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) + } else { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) + } ds, err := lib.DataStore() require.NoError(t, err) @@ -176,8 +186,8 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NoError(t, err) require.NotNil(t, receiverBalanceBefore) - for sendIdx := range devenvtests.CantonToEVMTokenSequentialSends { - t.Logf("Token transfer send %d/%d", sendIdx+1, devenvtests.CantonToEVMTokenSequentialSends) + for sendIdx := range cantondevenv.CantonToEVMTokenSequentialSends { + t.Logf("Token transfer send %d/%d", sendIdx+1, cantondevenv.CantonToEVMTokenSequentialSends) sendMessageResult, err := cantonChain.SendMessage( subtestCtx, evmChain.ChainSelector(), @@ -221,8 +231,8 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NoError(t, err) require.NotNil(t, receiverBalanceAfter) - expectedTransferPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(devenvtests.EVMDecimalsScale)) - totalExpectedTransfer := new(big.Int).Mul(expectedTransferPerMessage, big.NewInt(devenvtests.CantonToEVMTokenSequentialSends)) + expectedTransferPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointToEVMScale)) + totalExpectedTransfer := new(big.Int).Mul(expectedTransferPerMessage, big.NewInt(cantondevenv.CantonToEVMTokenSequentialSends)) expectedReceiverBalanceAfter := new(big.Int).Add(new(big.Int).Set(receiverBalanceBefore), totalExpectedTransfer) t.Logf("EVM receiver token balance: before=%s after=%s totalExpectedTransfer=%s", receiverBalanceBefore.String(), receiverBalanceAfter.String(), totalExpectedTransfer.String()) require.Equal(t, expectedReceiverBalanceAfter, receiverBalanceAfter) @@ -232,10 +242,19 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Run("token transfer with default extraArgs", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) - tokenTransferAmount := lane.TransferAmount.Uint64() - - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) + + fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) + feeTotal := new(big.Rat).SetUint64(fee) + transferTotal := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) + require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) + if lane.TransferInstrument.Admin != "" { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) + } else { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) + } receiver, err := evmChain.GetEOAReceiverAddress() require.NoError(t, err) @@ -274,10 +293,19 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Run("token transfer with zero gas limit", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) - tokenTransferAmount := lane.TransferAmount.Uint64() - - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), tokenTransferAmount)) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) + + fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) + feeTotal := new(big.Rat).SetUint64(fee) + transferTotal := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) + require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) + if lane.TransferInstrument.Admin != "" { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) + } else { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) + } ds, err := lib.DataStore() require.NoError(t, err) diff --git a/ccip/devenv/tests/e2e/canton2evm_send_validation_test.go b/ccip/devenv/tests/e2e/canton2evm_send_validation_test.go index 60d57ba07..62458a55c 100644 --- a/ccip/devenv/tests/e2e/canton2evm_send_validation_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_send_validation_test.go @@ -59,8 +59,8 @@ func setupCanton2EVMSendFixtureWithHoldings(t *testing.T, withFeeHoldings bool) require.True(t, ok) if withFeeHoldings { - require.NoError(t, cantonImpl.MintTokens(ctx, uint64(devenvtests.CantonToEVMFeeAmount))) - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), 0)) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)))) + require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) } ds, err := lib.DataStore() @@ -229,8 +229,8 @@ func TestCanton2EVM_SendValidation(t *testing.T) { t.Run("Unsupported token on lane", func(t *testing.T) { f := setupCanton2EVMSendFixture(t) - require.NoError(t, f.cantonImpl.MintTokens(f.ctx, 1000)) - require.NoError(t, f.cantonImpl.SetupSend(f.ctx, uint64(devenvtests.CantonToEVMFeeAmount), 1000)) + require.NoError(t, f.cantonImpl.MintTokens(f.ctx, big.NewRat(1000, 1))) + require.NoError(t, f.cantonImpl.SetupSend(f.ctx, uint64(cantondevenv.CantonToEVMFeeAmount), big.NewRat(1000, 1))) f.cantonImpl.SetTransferTokenInstrumentForTest(splice_api_token_holding_v1.InstrumentId{ Admin: types.PARTY("unsupported-token-admin"), Id: types.TEXT("UnsupportedToken"), @@ -252,8 +252,8 @@ func TestCanton2EVM_SendValidation(t *testing.T) { t.Run("Zero token amount", func(t *testing.T) { f := setupCanton2EVMSendFixture(t) - require.NoError(t, f.cantonImpl.MintTokens(f.ctx, 1000)) - require.NoError(t, f.cantonImpl.SetupSend(f.ctx, uint64(devenvtests.CantonToEVMFeeAmount), 1000)) + require.NoError(t, f.cantonImpl.MintTokens(f.ctx, big.NewRat(1000, 1))) + require.NoError(t, f.cantonImpl.SetupSend(f.ctx, uint64(cantondevenv.CantonToEVMFeeAmount), big.NewRat(1000, 1))) err := f.send( cciptestinterfaces.MessageFields{ diff --git a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go index 69b45eb03..23af4ac8f 100644 --- a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go +++ b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go @@ -126,7 +126,7 @@ func TestEVM2Canton_Basic(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) // Send params (transfer amount, gas limit, finality) come from token_transfer_config.toml. - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, srcSelector, []uint64{dstSelector}) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, srcSelector, []uint64{dstSelector}) srcToken := lane.SrcToken srcSender, err := srcChain.GetEOAReceiverAddress() require.NoError(t, err) diff --git a/ccip/devenv/tests/env.go b/ccip/devenv/tests/env.go new file mode 100644 index 000000000..f414d8076 --- /dev/null +++ b/ccip/devenv/tests/env.go @@ -0,0 +1,33 @@ +package tests + +import ( + "fmt" + "strings" +) + +// CCIPEnv names a CCIP e2e target environment. +type CCIPEnv string + +const ( + EnvDevenv CCIPEnv = "devenv" + EnvProdTestnet CCIPEnv = "prod-testnet" +) + +// ParseCCIPEnv validates and returns a CCIPEnv from its string form. +func ParseCCIPEnv(s string) (CCIPEnv, error) { + switch CCIPEnv(strings.TrimSpace(s)) { + case EnvDevenv, EnvProdTestnet: + return CCIPEnv(strings.TrimSpace(s)), nil + case "staging": + return "", fmt.Errorf("ccip env %q is reserved but not yet supported", s) + case "mainnet": + return "", fmt.Errorf("ccip env %q is reserved but not yet supported", s) + default: + return "", fmt.Errorf("unknown ccip env %q: want devenv or prod-testnet", s) + } +} + +// IsRemote reports whether the environment targets live testnet infrastructure. +func (e CCIPEnv) IsRemote() bool { + return e == EnvProdTestnet +} diff --git a/ccip/devenv/tests/load/gun_canton2evm_test.go b/ccip/devenv/tests/load/gun_canton2evm_test.go index 29060312c..ccf6998d8 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_test.go @@ -2,6 +2,7 @@ package load import ( "fmt" + "math/big" "os" "testing" @@ -79,11 +80,11 @@ func TestCanton2EVM_Load(t *testing.T) { if estimatedMessages == 0 { estimatedMessages = 1 } - mintAmount := estimatedMessages * uint64(devenvtests.CantonToEVMFeeAmount) * mintBufferNumerator / mintBufferDenominator + mintAmount := estimatedMessages * uint64(cantondevenv.CantonToEVMFeeAmount) * mintBufferNumerator / mintBufferDenominator t.Logf("Pre-mint: estimatedMessages=%d feePerMessage=%d totalFeeMint=%d", - estimatedMessages, devenvtests.CantonToEVMFeeAmount, mintAmount) - require.NoError(t, cantonImpl.MintTokens(ctx, mintAmount)) - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(devenvtests.CantonToEVMFeeAmount), 0)) + estimatedMessages, cantondevenv.CantonToEVMFeeAmount, mintAmount) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(mintAmount))) + require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) gun, err := NewCCIPLoadGun( cantonChain, diff --git a/ccip/devenv/tests/load/gun_canton2evm_token_test.go b/ccip/devenv/tests/load/gun_canton2evm_token_test.go index 291426a30..2c69b6d72 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_token_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_token_test.go @@ -50,7 +50,7 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { evmSelectors := discoverEVMTokenSelectors(t, in) require.NotEmpty(t, evmSelectors, "need at least one EVM token destination in the env file") - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, cantonChain.ChainSelector(), evmSelectors) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), evmSelectors) t.Logf("Token lane: pool=%s transfer=%s", lane.PoolRef.Qualifier, lane.TransferAmount.String()) destinations := discoverEVMTokenDestinations(t, in, chainMap, lane) @@ -91,7 +91,7 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { require.NoError(t, err) require.NotNil(t, receiverBalanceAfter) - expectedPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(devenvtests.EVMDecimalsScale)) + expectedPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointToEVMScale)) expectedDelta := new(big.Int).Mul(expectedPerMessage, big.NewInt(gun.CallCount())) expectedBalance := new(big.Int).Add(new(big.Int).Set(receiverBalanceBefore), expectedDelta) t.Logf("EVM receiver token balance: before=%s after=%s expectedDelta=%s calls=%d", diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index 4ad00269d..eb44409e6 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -52,7 +52,7 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { require.True(t, ok, "Canton dest chain must be *devenv.Chain") require.NoError(t, cantonImpl.SetupReceive(ctx)) - lane := devenvtests.ResolveTokenLane(t, in, lib, chainMap, evmChain.ChainSelector(), []uint64{cantonChain.ChainSelector()}) + lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, evmChain.ChainSelector(), []uint64{cantonChain.ChainSelector()}) t.Logf("Token lane: pool=%s transfer=%s srcToken=%x", lane.PoolRef.Qualifier, lane.TransferAmount.String(), diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 0ba7e3daa..7f2d820d8 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -265,17 +265,20 @@ func setupCantonTokenLoadHoldings( t.Helper() estimated := estimateMessages(sched) - feeMint := estimated * uint64(devenvtests.CantonToEVMFeeAmount) - transferMint := estimated * lane.TransferAmount.Uint64() - t.Logf("Pre-mint: estimatedMessages=%d feeMint=%d transferMint=%d", - estimated, feeMint, transferMint) - require.NoError(t, cantonImpl.MintTokens(ctx, feeMint)) - require.NoError(t, cantonImpl.MintTokens(ctx, transferMint)) - require.NoError(t, cantonImpl.SetupSend( - ctx, - uint64(devenvtests.CantonToEVMFeeAmount), - lane.TransferAmount.Uint64(), - )) + fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) + feeTotal := new(big.Rat).SetUint64(estimated * fee) + transferTotalFP := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(estimated))) + transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) + transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + t.Logf("Pre-mint: estimatedMessages=%d feeTotal=%s transferTotal=%s", + estimated, feeTotal.FloatString(10), transferTotal.FloatString(10)) + require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) + require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) + if lane.TransferInstrument.Admin != "" { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) + } else { + require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) + } } func evmLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.UnknownAddress) Destination { diff --git a/ccip/devenv/tests/token_lane.go b/ccip/devenv/tests/token_lane.go index 6ca23d332..ad1607e74 100644 --- a/ccip/devenv/tests/token_lane.go +++ b/ccip/devenv/tests/token_lane.go @@ -21,7 +21,10 @@ import ( "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/go-daml/pkg/types" "github.com/stretchr/testify/require" + + splice_api_token_holding_v1 "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" ) const ( @@ -34,8 +37,12 @@ const ( // TokenLane describes a resolved token pool pairing for load or e2e tests. type TokenLane struct { PoolRef datastore.AddressRef - // TransferAmount is the per-message token transfer amount. + // TransferAmount is the per-message token transfer amount (wei for EVM→Canton, 10^10 fixed-point for Canton→EVM). TransferAmount *big.Int + // TransferInstrumentID is a TOML hint only (e.g. Amulet, LINK); prod resolution uses TransferInstrument. + TransferInstrumentID string + // TransferInstrument is the resolved Canton transfer instrument when Canton is the source. + TransferInstrument splice_api_token_holding_v1.InstrumentId // ExecutionGasLimit is the per-message execution gas limit. ExecutionGasLimit uint32 // FinalityConfig is the per-message finality configuration. @@ -49,23 +56,42 @@ type TokenLane struct { } type tokenConfigTOML struct { + Devenv tokenDirectionsTOML `toml:"devenv"` + ProdTestnet tokenDirectionsTOML `toml:"prod-testnet"` +} + +type tokenDirectionsTOML struct { EVMToCanton tokenDirectionTOML `toml:"evm_to_canton"` CantonToEVM tokenDirectionTOML `toml:"canton_to_evm"` } type tokenDirectionTOML struct { - PoolType string `toml:"pool_type"` - PoolVersion string `toml:"pool_version"` - PoolQualifier string `toml:"pool_qualifier"` - TransferAmount string `toml:"transfer_amount"` - ExecutionGasLimit uint32 `toml:"execution_gas_limit"` - FinalityConfig uint32 `toml:"finality_config"` + PoolType string `toml:"pool_type"` + PoolVersion string `toml:"pool_version"` + PoolQualifier string `toml:"pool_qualifier"` + TransferAmount string `toml:"transfer_amount"` + ExecutionGasLimit uint32 `toml:"execution_gas_limit"` + FinalityConfig uint32 `toml:"finality_config"` + RemotePoolType string `toml:"remote_pool_type"` + RemotePoolVersion string `toml:"remote_pool_version"` + RemotePoolQualifier string `toml:"remote_pool_qualifier"` + TransferInstrumentID string `toml:"transfer_instrument_id"` +} + +type tokenDirectionParsed struct { + PoolRef datastore.AddressRef + RemotePoolRef *datastore.AddressRef + TransferAmount *big.Int + TransferInstrumentID string + ExecutionGasLimit uint32 + FinalityConfig protocol.Finality } // ResolveTokenLane loads send params from TOML, matches the pool on the source chain, // and resolves source/destination token addresses for the requested destinations. func ResolveTokenLane( t *testing.T, + env CCIPEnv, in *ccv.Cfg, lib ccv.Lib, chainMap map[uint64]cciptestinterfaces.CCIP17, @@ -74,68 +100,64 @@ func ResolveTokenLane( ) TokenLane { t.Helper() - // Load the CLDF environment and datastore used to resolve on-chain addresses. - env, err := lib.CLDFEnvironment() + cldfEnv, err := lib.CLDFEnvironment() require.NoError(t, err) - require.NotNil(t, env) + require.NotNil(t, cldfEnv) srcChain, ok := chainMap[srcSelector] require.True(t, ok, "source chain %d not in harness chain map", srcSelector) - // Pick the TOML block from the source chain family (Canton<->EVM only). direction := directionEVMToCanton if isCantonSelector(srcSelector) { direction = directionCantonToEVM } - // Read pool identity and per-message send params from token_transfer_config.toml. - poolRef, transferAmount, gasLimit, finalityConfig := loadTokenDirection(t, direction) + dir := loadTokenDirection(t, env, direction) - // List token transfer configs deployed on the source chain for the requested destinations. srcProvider := tokenConfigProvider(srcChain) - cfgs, err := srcProvider.GetTokenTransferConfigs(env, srcSelector, destSelectors, in.EnvironmentTopology) + cfgs, err := srcProvider.GetTokenTransferConfigs(cldfEnv, srcSelector, destSelectors, in.EnvironmentTopology) require.NoError(t, err, "get token transfer configs for source chain %d", srcSelector) - // Require exactly one config whose pool ref matches the TOML declaration. - cfg := selectTokenConfig(t, cfgs, poolRef, srcSelector) + cfg, matched := trySelectTokenConfig(cfgs, dir.PoolRef) + if !matched { + t.Fatalf("no token transfer config on chain %d matches pool %s (have %s)", + srcSelector, poolRefString(dir.PoolRef), poolRefsString(cfgs)) + } - // Fail fast if any requested destination is missing from that lane's RemoteChains. for _, sel := range destSelectors { if _, present := cfg.RemoteChains[sel]; !present { t.Fatalf("destination %d not configured for pool %s on chain %d (have %v)", - sel, poolRefString(poolRef), srcSelector, sortedRemoteSelectors(cfg)) + sel, poolRefString(dir.PoolRef), srcSelector, sortedRemoteSelectors(cfg)) } } - // Assemble the lane with TOML send params; token addresses are filled in below. lane := TokenLane{ - PoolRef: poolRef, - TransferAmount: transferAmount, - ExecutionGasLimit: gasLimit, - FinalityConfig: finalityConfig, - DestTokenBySelector: make(map[uint64]protocol.UnknownAddress, len(destSelectors)), + PoolRef: dir.PoolRef, + TransferAmount: dir.TransferAmount, + TransferInstrumentID: dir.TransferInstrumentID, + ExecutionGasLimit: dir.ExecutionGasLimit, + FinalityConfig: dir.FinalityConfig, + DestTokenBySelector: make(map[uint64]protocol.UnknownAddress, len(destSelectors)), } - // EVM source: resolve the ERC-20 from the matched config's TokenRef in the datastore. - // Canton source: instrument is chosen in SetupSend, so SrcToken stays empty. if !isCantonSelector(srcSelector) { - srcToken, err := resolveTokenRef(env.DataStore, srcSelector, cfg.TokenRef) + srcToken, err := resolveTokenRef(cldfEnv.DataStore, srcSelector, cfg.TokenRef) require.NoError(t, err, "resolve source token on chain %d", srcSelector) lane.SrcToken = srcToken + } else { + lane.TransferInstrument = resolveCantonTransferInstrument(t, cldfEnv.DataStore, srcSelector, cfg.TokenRef, dir.PoolRef, dir.TransferInstrumentID) } - // EVM destinations: look up each dest chain's config for the remote pool and resolve TokenRef. - // Canton destinations are skipped (no EVM-style token address for balance checks). for _, sel := range destSelectors { if isCantonSelector(sel) { continue } - lane.DestTokenBySelector[sel] = resolveDestToken(t, env, in, chainMap, srcSelector, sel, cfg.RemoteChains[sel], poolRef) + lane.DestTokenBySelector[sel] = resolveDestToken(t, cldfEnv, in, chainMap, srcSelector, sel, cfg.RemoteChains[sel], dir.PoolRef) } return lane } -func loadTokenDirection(t *testing.T, direction string) (poolRef datastore.AddressRef, transferAmount *big.Int, gasLimit uint32, finalityConfig protocol.Finality) { +func loadTokenDirection(t *testing.T, env CCIPEnv, direction string) tokenDirectionParsed { t.Helper() path := os.Getenv(envTokenTestConfig) @@ -147,40 +169,88 @@ func loadTokenDirection(t *testing.T, direction string) (poolRef datastore.Addre _, err := toml.DecodeFile(path, &cfg) require.NoError(t, err, "decode token transfer config %q (set %s to override)", path, envTokenTestConfig) + dirs, err := tokenDirectionsForEnv(cfg, env) + require.NoError(t, err, "%s: no token transfer config for env %q", path, env) + var dir tokenDirectionTOML switch direction { case directionEVMToCanton: - dir = cfg.EVMToCanton + dir = dirs.EVMToCanton case directionCantonToEVM: - dir = cfg.CantonToEVM + dir = dirs.CantonToEVM default: t.Fatalf("unknown token transfer direction %q (expected %q or %q)", direction, directionEVMToCanton, directionCantonToEVM) } - require.NotEmpty(t, dir.PoolType, "%s: pool_type is required for direction %q", path, direction) - require.NotEmpty(t, dir.PoolVersion, "%s: pool_version is required for direction %q", path, direction) - require.NotEmpty(t, dir.PoolQualifier, "%s: pool_qualifier is required for direction %q", path, direction) + require.NotEmpty(t, dir.PoolType, "%s: pool_type is required for env %q direction %q", path, env, direction) + require.NotEmpty(t, dir.PoolVersion, "%s: pool_version is required for env %q direction %q", path, env, direction) + require.NotEmpty(t, dir.PoolQualifier, "%s: pool_qualifier is required for env %q direction %q", path, env, direction) version, err := semver.NewVersion(dir.PoolVersion) - require.NoError(t, err, "%s: invalid pool_version %q for direction %q", path, dir.PoolVersion, direction) + require.NoError(t, err, "%s: invalid pool_version %q for env %q direction %q", path, dir.PoolVersion, env, direction) - amount, ok := new(big.Int).SetString(strings.TrimSpace(dir.TransferAmount), 10) - require.True(t, ok && amount.Sign() > 0, "%s: transfer_amount %q must be a positive integer for direction %q", path, dir.TransferAmount, direction) + amountStr := strings.TrimSpace(dir.TransferAmount) + intAmount, ok := new(big.Int).SetString(amountStr, 10) + switch direction { + case directionEVMToCanton: + require.True(t, ok && intAmount.Sign() > 0, "%s: transfer_amount %q must be a positive integer wei for env %q direction %q", path, dir.TransferAmount, env, direction) + case directionCantonToEVM: + require.True(t, ok && intAmount.Sign() > 0, "%s: transfer_amount %q must be a positive integer fixed-point (10^10 scale) for env %q direction %q", path, dir.TransferAmount, env, direction) + default: + t.Fatalf("unknown token transfer direction %q", direction) + } + + require.NotZero(t, dir.ExecutionGasLimit, "%s: execution_gas_limit is required for env %q direction %q", path, env, direction) + + parsed := tokenDirectionParsed{ + PoolRef: datastore.AddressRef{ + Type: datastore.ContractType(dir.PoolType), + Version: version, + Qualifier: dir.PoolQualifier, + }, + TransferAmount: intAmount, + TransferInstrumentID: strings.TrimSpace(dir.TransferInstrumentID), + ExecutionGasLimit: dir.ExecutionGasLimit, + FinalityConfig: protocol.Finality(dir.FinalityConfig), + } - require.NotZero(t, dir.ExecutionGasLimit, "%s: execution_gas_limit is required for direction %q", path, direction) + if dir.RemotePoolType != "" || dir.RemotePoolVersion != "" || dir.RemotePoolQualifier != "" { + require.NotEmpty(t, dir.RemotePoolType, "%s: remote_pool_type is required when remote pool fields are set (env %q direction %q)", path, env, direction) + require.NotEmpty(t, dir.RemotePoolVersion, "%s: remote_pool_version is required when remote pool fields are set (env %q direction %q)", path, env, direction) + require.NotEmpty(t, dir.RemotePoolQualifier, "%s: remote_pool_qualifier is required when remote pool fields are set (env %q direction %q)", path, env, direction) - poolRef = datastore.AddressRef{ - Type: datastore.ContractType(dir.PoolType), - Version: version, - Qualifier: dir.PoolQualifier, + remoteVersion, err := semver.NewVersion(dir.RemotePoolVersion) + require.NoError(t, err, "%s: invalid remote_pool_version %q for env %q direction %q", path, dir.RemotePoolVersion, env, direction) + + remoteRef := datastore.AddressRef{ + Type: datastore.ContractType(dir.RemotePoolType), + Version: remoteVersion, + Qualifier: dir.RemotePoolQualifier, + } + parsed.RemotePoolRef = &remoteRef } - return poolRef, amount, dir.ExecutionGasLimit, protocol.Finality(dir.FinalityConfig) + return parsed } -func selectTokenConfig(t *testing.T, cfgs []tokenscore.TokenTransferConfig, poolRef datastore.AddressRef, srcSelector uint64) tokenscore.TokenTransferConfig { - t.Helper() +func tokenDirectionsForEnv(cfg tokenConfigTOML, env CCIPEnv) (tokenDirectionsTOML, error) { + switch env { + case EnvDevenv: + if cfg.Devenv.EVMToCanton.PoolType == "" && cfg.Devenv.CantonToEVM.PoolType == "" { + return tokenDirectionsTOML{}, fmt.Errorf("missing [devenv.*] sections") + } + return cfg.Devenv, nil + case EnvProdTestnet: + if cfg.ProdTestnet.EVMToCanton.PoolType == "" && cfg.ProdTestnet.CantonToEVM.PoolType == "" { + return tokenDirectionsTOML{}, fmt.Errorf("missing [prod-testnet.*] sections") + } + return cfg.ProdTestnet, nil + default: + return tokenDirectionsTOML{}, fmt.Errorf("unsupported ccip env %q", env) + } +} +func trySelectTokenConfig(cfgs []tokenscore.TokenTransferConfig, poolRef datastore.AddressRef) (tokenscore.TokenTransferConfig, bool) { matches := make([]tokenscore.TokenTransferConfig, 0, 1) for _, cfg := range cfgs { if poolRefEqual(cfg.TokenPoolRef, poolRef) { @@ -189,16 +259,10 @@ func selectTokenConfig(t *testing.T, cfgs []tokenscore.TokenTransferConfig, pool } switch len(matches) { case 1: - return matches[0] - case 0: - t.Fatalf("no token transfer config on chain %d matches pool %s (have %s)", - srcSelector, poolRefString(poolRef), poolRefsString(cfgs)) + return matches[0], true default: - t.Fatalf("pool %s matched %d configs on chain %d (expected one): %s", - poolRefString(poolRef), len(matches), srcSelector, poolRefsString(matches)) + return tokenscore.TokenTransferConfig{}, false } - - return tokenscore.TokenTransferConfig{} } func resolveDestToken( @@ -243,6 +307,102 @@ func tokenConfigProvider(chain cciptestinterfaces.CCIP17) cciptestinterfaces.Tok return evm.NewEmptyCCIP17EVM() } +func resolveCantonTransferInstrument( + t *testing.T, + ds datastore.DataStore, + chainSelector uint64, + tokenRef datastore.AddressRef, + poolRef datastore.AddressRef, + transferInstrumentIDHint string, +) splice_api_token_holding_v1.InstrumentId { + t.Helper() + + addrRef, err := ds.Addresses().Get(datastore.NewAddressRefKey(chainSelector, tokenRef.Type, tokenRef.Version, tokenRef.Qualifier)) + require.NoError(t, err, "resolve Canton token ref %s on chain %d", poolRefString(tokenRef), chainSelector) + + poolAddrRef, err := ds.Addresses().Get(datastore.NewAddressRefKey(chainSelector, poolRef.Type, poolRef.Version, poolRef.Qualifier)) + require.NoError(t, err, "resolve Canton pool ref %s on chain %d", poolRefString(poolRef), chainSelector) + + instrument, err := resolveInstrumentFromTokenRefWithFallback(addrRef, poolAddrRef, transferInstrumentIDHint) + require.NoError(t, err, "parse instrument from token ref %s on chain %d", poolRefString(tokenRef), chainSelector) + + return instrument +} + +func resolveInstrumentFromTokenRefWithFallback( + tokenRef datastore.AddressRef, + poolRef datastore.AddressRef, + transferInstrumentIDHint string, +) (splice_api_token_holding_v1.InstrumentId, error) { + if instrument, err := parseInstrumentIDFromTokenRefLabels(tokenRef); err == nil { + return instrument, nil + } + + ccipOwner := ccipOwnerFromRefLabels(poolRef) + if ccipOwner == "" { + return splice_api_token_holding_v1.InstrumentId{}, fmt.Errorf( + "tokenRef labels must include instrument-admin: and instrument-id:", + ) + } + + instrumentID := normalizeTransferInstrumentID(transferInstrumentIDHint) + if instrumentID == "" { + return splice_api_token_holding_v1.InstrumentId{}, fmt.Errorf( + "transfer_instrument_id is required when token ref labels are missing", + ) + } + + return splice_api_token_holding_v1.InstrumentId{ + Admin: types.PARTY(ccipOwner), + Id: types.TEXT(instrumentID), + }, nil +} + +func parseInstrumentIDFromTokenRefLabels(tokenRef datastore.AddressRef) (splice_api_token_holding_v1.InstrumentId, error) { + var instrumentAdmin, instrumentIDText string + for _, label := range tokenRef.Labels.List() { + switch { + case strings.HasPrefix(label, "instrument-admin:"): + instrumentAdmin = strings.TrimSpace(strings.TrimPrefix(label, "instrument-admin:")) + case strings.HasPrefix(label, "instrument-id:"): + instrumentIDText = strings.TrimSpace(strings.TrimPrefix(label, "instrument-id:")) + } + } + if instrumentAdmin == "" || instrumentIDText == "" { + return splice_api_token_holding_v1.InstrumentId{}, fmt.Errorf( + "tokenRef labels must include instrument-admin: and instrument-id:", + ) + } + + return splice_api_token_holding_v1.InstrumentId{ + Admin: types.PARTY(instrumentAdmin), + Id: types.TEXT(instrumentIDText), + }, nil +} + +func ccipOwnerFromRefLabels(ref datastore.AddressRef) string { + for _, label := range ref.Labels.List() { + if party, ok := strings.CutPrefix(label, "ccip-owner:"); ok { + return strings.TrimSpace(party) + } + if idx := strings.LastIndex(label, "@ccipOwner::"); idx >= 0 { + return strings.TrimSpace(label[idx+1:]) + } + } + + return "" +} + +func normalizeTransferInstrumentID(hint string) string { + hint = strings.TrimSpace(hint) + switch strings.ToLower(hint) { + case "link": + return "link-token" + default: + return hint + } +} + func resolveTokenRef(ds datastore.DataStore, chainSelector uint64, ref datastore.AddressRef) (protocol.UnknownAddress, error) { addrRef, err := ds.Addresses().Get(datastore.NewAddressRefKey(chainSelector, ref.Type, ref.Version, ref.Qualifier)) if err != nil { @@ -280,7 +440,6 @@ func defaultTokenConfigPath() string { return filepath.Join(filepath.Dir(file), defaultTokenConfigFile) } -// Helpers for logging // func poolRefsString(cfgs []tokenscore.TokenTransferConfig) string { if len(cfgs) == 0 { return "none" diff --git a/ccip/devenv/tests/token_lane_test.go b/ccip/devenv/tests/token_lane_test.go new file mode 100644 index 000000000..88f9ba2d2 --- /dev/null +++ b/ccip/devenv/tests/token_lane_test.go @@ -0,0 +1,185 @@ +package tests + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/BurntSushi/toml" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-canton/ccip/devenv" +) + +func TestTokenDirectionsForEnv(t *testing.T) { + t.Parallel() + + cfg, err := decodeTokenConfigTOML(defaultTokenConfigPath()) + require.NoError(t, err) + + devenvDirs, err := tokenDirectionsForEnv(cfg, EnvDevenv) + require.NoError(t, err) + require.Equal(t, "BurnMintTokenPool", devenvDirs.EVMToCanton.PoolType) + require.Contains(t, devenvDirs.EVMToCanton.PoolQualifier, "BurnMintTokenPool 2.0.0 [default]") + require.Equal(t, "LockReleaseTokenPool", devenvDirs.CantonToEVM.PoolType) + + prodDirs, err := tokenDirectionsForEnv(cfg, EnvProdTestnet) + require.NoError(t, err) + require.Equal(t, "TEST", prodDirs.EVMToCanton.PoolQualifier) + require.Equal(t, "LINK", prodDirs.EVMToCanton.RemotePoolQualifier) + require.Equal(t, "LINK", prodDirs.CantonToEVM.PoolQualifier) + require.Equal(t, "100", prodDirs.CantonToEVM.TransferAmount) + require.Equal(t, "link-token", prodDirs.CantonToEVM.TransferInstrumentID) +} + +func TestLoadTokenDirection_envSelection(t *testing.T) { + t.Setenv(envTokenTestConfig, defaultTokenConfigPath()) + + devenvDir := loadTokenDirection(t, EnvDevenv, directionEVMToCanton) + require.Equal(t, "BurnMintTokenPool", string(devenvDir.PoolRef.Type)) + require.Contains(t, devenvDir.PoolRef.Qualifier, "BurnMintTokenPool 2.0.0 [default]") + require.Nil(t, devenvDir.RemotePoolRef) + + prodDir := loadTokenDirection(t, EnvProdTestnet, directionEVMToCanton) + require.Equal(t, "TEST", prodDir.PoolRef.Qualifier) + require.NotNil(t, prodDir.RemotePoolRef) + require.Equal(t, "LINK", prodDir.RemotePoolRef.Qualifier) +} + +func TestLoadTokenDirection_transferAmountParsing(t *testing.T) { + t.Setenv(envTokenTestConfig, defaultTokenConfigPath()) + + evmDir := loadTokenDirection(t, EnvDevenv, directionEVMToCanton) + require.Equal(t, "100000000001", evmDir.TransferAmount.String()) + + cantonDir := loadTokenDirection(t, EnvDevenv, directionCantonToEVM) + require.Equal(t, big.NewInt(1000), cantonDir.TransferAmount) + + prodCantonDir := loadTokenDirection(t, EnvProdTestnet, directionCantonToEVM) + require.Equal(t, big.NewInt(100), prodCantonDir.TransferAmount) + require.Equal(t, "link-token", prodCantonDir.TransferInstrumentID) + + expectedWei := new(big.Int).Mul(prodCantonDir.TransferAmount, big.NewInt(devenv.CantonFixedPointToEVMScale)) + require.Equal(t, "10000000000", expectedWei.String()) +} + +func TestLoadTokenDirection_missingEnvSection(t *testing.T) { + path := filepath.Join(t.TempDir(), "token_transfer_config.toml") + require.NoError(t, os.WriteFile(path, []byte(` +[devenv.evm_to_canton] +pool_type = "BurnMintTokenPool" +pool_version = "2.0.0" +pool_qualifier = "TEST" +transfer_amount = "1" +execution_gas_limit = 1 +finality_config = 0 +`), 0o600)) + + t.Setenv(envTokenTestConfig, path) + + _, err := tokenDirectionsForEnv(mustDecodeTokenConfig(t, path), EnvProdTestnet) + require.Error(t, err) + require.Contains(t, err.Error(), "prod-testnet") +} + +func mustDecodeTokenConfig(t *testing.T, path string) tokenConfigTOML { + t.Helper() + + cfg, err := decodeTokenConfigTOML(path) + require.NoError(t, err) + + return cfg +} + +func decodeTokenConfigTOML(path string) (tokenConfigTOML, error) { + var cfg tokenConfigTOML + _, err := toml.DecodeFile(path, &cfg) + + return cfg, err +} + +func TestParseInstrumentIDFromTokenRefLabels(t *testing.T) { + t.Parallel() + + ccipOwner := "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551" + tokenRef := datastore.AddressRef{ + Labels: datastore.NewLabelSet( + "instrument-admin:"+ccipOwner, + "instrument-id:link-token", + ), + } + + instrument, err := parseInstrumentIDFromTokenRefLabels(tokenRef) + require.NoError(t, err) + require.Equal(t, ccipOwner, string(instrument.Admin)) + require.Equal(t, "link-token", string(instrument.Id)) + + _, err = parseInstrumentIDFromTokenRefLabels(datastore.AddressRef{}) + require.Error(t, err) + require.Contains(t, err.Error(), "instrument-admin") +} + +func TestNormalizeTransferInstrumentID(t *testing.T) { + t.Parallel() + + require.Equal(t, "link-token", normalizeTransferInstrumentID("LINK")) + require.Equal(t, "link-token", normalizeTransferInstrumentID("link")) + require.Equal(t, "link-token", normalizeTransferInstrumentID(" link ")) + require.Equal(t, "amulet", normalizeTransferInstrumentID("Amulet")) + require.Equal(t, "link-token", normalizeTransferInstrumentID("link-token")) +} + +func TestCCIPOwnerFromRefLabels(t *testing.T) { + t.Parallel() + + ccipOwner := "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551" + + fromPrefix := datastore.AddressRef{ + Labels: datastore.NewLabelSet("ccip-owner:" + ccipOwner), + } + require.Equal(t, ccipOwner, ccipOwnerFromRefLabels(fromPrefix)) + + fromSuffix := datastore.AddressRef{ + Labels: datastore.NewLabelSet("burnminttokenpool-LINK@" + ccipOwner), + } + require.Equal(t, ccipOwner, ccipOwnerFromRefLabels(fromSuffix)) + + require.Empty(t, ccipOwnerFromRefLabels(datastore.AddressRef{})) +} + +func TestResolveInstrumentFromTokenRefWithFallback(t *testing.T) { + t.Parallel() + + ccipOwner := "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551" + + tokenWithLabels := datastore.AddressRef{ + Labels: datastore.NewLabelSet( + "instrument-admin:"+ccipOwner, + "instrument-id:link-token", + ), + } + poolRef := datastore.AddressRef{ + Labels: datastore.NewLabelSet("burnminttokenpool-LINK@" + ccipOwner), + } + + instrument, err := resolveInstrumentFromTokenRefWithFallback(tokenWithLabels, poolRef, "LINK") + require.NoError(t, err) + require.Equal(t, ccipOwner, string(instrument.Admin)) + require.Equal(t, "link-token", string(instrument.Id)) + + tokenWithoutLabels := datastore.AddressRef{} + instrument, err = resolveInstrumentFromTokenRefWithFallback(tokenWithoutLabels, poolRef, "LINK") + require.NoError(t, err) + require.Equal(t, ccipOwner, string(instrument.Admin)) + require.Equal(t, "link-token", string(instrument.Id)) + + _, err = resolveInstrumentFromTokenRefWithFallback(tokenWithoutLabels, datastore.AddressRef{}, "LINK") + require.Error(t, err) + require.Contains(t, err.Error(), "instrument-admin") + + _, err = resolveInstrumentFromTokenRefWithFallback(tokenWithoutLabels, poolRef, "") + require.Error(t, err) + require.Contains(t, err.Error(), "transfer_instrument_id") +} diff --git a/ccip/devenv/tests/token_transfer_config.toml b/ccip/devenv/tests/token_transfer_config.toml index c3bbfe7ee..10530c621 100644 --- a/ccip/devenv/tests/token_transfer_config.toml +++ b/ccip/devenv/tests/token_transfer_config.toml @@ -1,24 +1,49 @@ -# Token lane inputs for devenv e2e/load tests. +# Token lane inputs for CCIP e2e/load tests. # +# Env selection follows -ccip-env / CCIP_ENV (same as CCIP harness). # Token identity (pool_type / pool_version / pool_qualifier) is REQUIRED and is # matched against the source chain's GetTokenTransferConfigs to discover the lane. # Numeric send params (transfer_amount / execution_gas_limit / finality_config) # are optional; when omitted, code-level per-direction defaults are used. # -# Override the file path with the CANTON_TOKEN_TEST_CONFIG env var. +# Override the entire file path with CANTON_TOKEN_TEST_CONFIG. -[evm_to_canton] -pool_type = "BurnMintTokenPool" -pool_version = "2.0.0" +[devenv.evm_to_canton] +pool_type = "BurnMintTokenPool" +pool_version = "2.0.0" pool_qualifier = "TEST (BurnMintTokenPool 2.0.0 [default], LockReleaseTokenPool 2.0.0 [default])::BurnMintTokenPool 2.0.0 [default]" -transfer_amount = "100000000000" -execution_gas_limit = 200000 -finality_config = 0 +transfer_amount = "100000000001" +execution_gas_limit = 200001 +# Per-message FTF (1-block depth); requires Canton pool remoteChainCfg BlockDepth >= 1 (see impl.go). +finality_config = 1 -[canton_to_evm] -pool_type = "LockReleaseTokenPool" -pool_version = "2.0.0" +[devenv.canton_to_evm] +pool_type = "LockReleaseTokenPool" +pool_version = "2.0.0" pool_qualifier = "TEST (BurnMintTokenPool 2.0.0 [default], LockReleaseTokenPool 2.0.0 [default])::LockReleaseTokenPool 2.0.0 [default]" -transfer_amount = "1000" +transfer_amount = "1000" execution_gas_limit = 500000 -finality_config = 1 +finality_config = 1 + +[prod-testnet.evm_to_canton] +pool_type = "BurnMintTokenPool" +pool_version = "2.0.0" +pool_qualifier = "TEST" +transfer_amount = "1000000000000001" +execution_gas_limit = 200001 +finality_config = 1 +remote_pool_type = "BurnMintTokenPool" +remote_pool_version = "2.0.0" +remote_pool_qualifier = "LINK" + +[prod-testnet.canton_to_evm] +pool_type = "BurnMintTokenPool" +pool_version = "2.0.0" +pool_qualifier = "LINK" +transfer_amount = "100" +execution_gas_limit = 500000 +finality_config = 1 +remote_pool_type = "BurnMintTokenPool" +remote_pool_version = "2.0.0" +remote_pool_qualifier = "TEST" +transfer_instrument_id = "link-token" From a0b7f765ddec379b5732198ba0e19683db14ecd9 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 18:06:28 -0300 Subject: [PATCH 05/26] fix(tests): tests token + lint fix --- ccip/devenv/impl.go | 47 +++++++++++++++----- ccip/devenv/manual_execution.go | 3 +- ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 20 +++------ ccip/devenv/tests/load/load_helpers.go | 3 +- ccip/devenv/tests/token_lane_test.go | 2 +- deployment/sequences/token_pools.go | 6 +-- 6 files changed, 49 insertions(+), 32 deletions(-) diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 39d9fd3de..3fd4a1517 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -223,9 +223,11 @@ type Chain struct { validatorAPIClients validatorAPIClients feeTokenInstrument splice_api_token_holding_v1.InstrumentId + feeAmountPerMessage uint64 // per-message fee budget; used when rotating fee holdings after send nextFeeCID string // holding CID to be used as fee on next message send transferTokenInstrument *splice_api_token_holding_v1.InstrumentId nextTransferCID string // holding CID to be used as transfer on next message send + sendsLeft uint64 // remaining sends in current setup batch; 0 = always rotate // verifierObs is injected post-construction by test runners (see SetVerifierObservation). // Required by ConfirmExecOnDest to fetch verifier results from aggregator/indexer. @@ -1072,9 +1074,12 @@ func (c *Chain) SetupSend( } c.feeTokenInstrument = feeTokenInstrument + c.feeAmountPerMessage = feeAmountPerMessage c.nextFeeCID = selectedFee[0].ContractID if transferAmountPerMessage == nil || transferAmountPerMessage.Sign() <= 0 { + c.transferTokenInstrument = nil + c.nextTransferCID = "" return nil } @@ -1103,6 +1108,17 @@ func (c *Chain) SetupSend( return nil } +// SetSequentialSends limits holding rotation to the next sends messages in this setup batch. +// After the send whose on-chain seq equals nextSeq+sends, setNextHoldings is skipped. +// Pass 0 to always rotate (open-ended load). +func (c *Chain) SetSequentialSends(sends int) { + if sends <= 0 { + c.sendsLeft = 0 + return + } + c.sendsLeft = uint64(sends) +} + // MintTokens mint tokens for transfer and fees. To be used on devenv tests only. // this method won't work in staging/prod tests func (c *Chain) MintTokens(ctx context.Context, amount *big.Rat) error { @@ -1440,16 +1456,6 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint Uint64("seqNo", parsedSend.seqNo). Msg("CCIP Send executed") - // Set next holdings - var tokenAmount *big.Rat - if hasTokenTransfer { - tokenAmount = new(big.Rat).SetFrac(fields.TokenAmount.Amount, big.NewInt(CantonFixedPointScale)) - } - err = c.setNextHoldings(update.GetTransaction().GetEvents(), hasTokenTransfer, tokenAmount) - if err != nil { - return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("set next holdings: %w", err) - } - event := cciptestinterfaces.MessageSentEvent{ MessageID: parsedSend.messageID, ReceiptIssuers: nil, // TODO: add them later, not currently needed @@ -1462,6 +1468,24 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint c.lastSentSeq = parsedSend.seqNo c.lastSentEvent = event + c.sendsLeft-- + if c.sendsLeft == 0 { + c.logger.Info(). + Uint64("sendsLeft", c.sendsLeft). + Msg("Skipping holding rotation after last planned send in batch") + + return event, nil + } + + var tokenAmount *big.Rat + if hasTokenTransfer { + tokenAmount = new(big.Rat).SetFrac(fields.TokenAmount.Amount, big.NewInt(CantonFixedPointScale)) + } + err = c.setNextHoldings(update.GetTransaction().GetEvents(), hasTokenTransfer, tokenAmount) + if err != nil { + return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("set next holdings: %w", err) + } + return event, nil } @@ -1590,10 +1614,11 @@ func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, to testhelpers.ExcludeCIDs(spentCIDs), } + feeMin := new(big.Rat).SetUint64(c.feeAmountPerMessage) nextFeeCID, exhaustion, err := pickNextHolding( events, c.feeTokenInstrument, - big.NewRat(0, 1), // TODO: we'll have to consider real fee here once we go load tests + feeMin, refreshFilters..., ) if err != nil && !exhaustion { diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index ad2e74f6f..444d85623 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -40,6 +40,7 @@ func instanceIDFromEnv(key, defaultID string) contracts.InstanceID { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return contracts.InstanceID(v) } + return contracts.InstanceID(defaultID) } @@ -149,8 +150,8 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici } return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy ccip sender contract: %w", err) } - c.senderAddress = senderAddress + return senderAddress, nil } diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index 1f67f934f..5a57847a7 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -54,8 +54,8 @@ func TestCanton2EVM_Basic(t *testing.T) { // TODO: currently minting 2 holdings of 2k for all the e2e tests. Otherwise one holding might conflict with the other. // the tests need to be hardened to not rely on this and instead correctly pick the holding that suffices. - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*100))) - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*100))) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*1000))) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*1000))) t.Run("EOA receiver and default committee verifier", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) @@ -150,17 +150,13 @@ func TestCanton2EVM_Basic(t *testing.T) { fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) sends := cantondevenv.CantonToEVMTokenSequentialSends - feeTotal := new(big.Rat).SetUint64(uint64(sends) * fee) - transferTotalFP := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(sends))) - transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) - require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } + cantonImpl.SetSequentialSends(sends) ds, err := lib.DataStore() require.NoError(t, err) @@ -245,16 +241,13 @@ func TestCanton2EVM_Basic(t *testing.T) { lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - feeTotal := new(big.Rat).SetUint64(fee) - transferTotal := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) - require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } + cantonImpl.SetSequentialSends(1) receiver, err := evmChain.GetEOAReceiverAddress() require.NoError(t, err) @@ -296,16 +289,13 @@ func TestCanton2EVM_Basic(t *testing.T) { lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - feeTotal := new(big.Rat).SetUint64(fee) - transferTotal := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) - require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } + cantonImpl.SetSequentialSends(1) ds, err := lib.DataStore() require.NoError(t, err) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 7f2d820d8..115a8e532 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -267,7 +267,7 @@ func setupCantonTokenLoadHoldings( estimated := estimateMessages(sched) fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) feeTotal := new(big.Rat).SetUint64(estimated * fee) - transferTotalFP := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(estimated))) + transferTotalFP := new(big.Int).Mul(lane.TransferAmount, new(big.Int).SetUint64(estimated)) transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) t.Logf("Pre-mint: estimatedMessages=%d feeTotal=%s transferTotal=%s", @@ -279,6 +279,7 @@ func setupCantonTokenLoadHoldings( } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } + cantonImpl.SetSequentialSends(int(estimated)) } func evmLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.UnknownAddress) Destination { diff --git a/ccip/devenv/tests/token_lane_test.go b/ccip/devenv/tests/token_lane_test.go index 88f9ba2d2..2c2470779 100644 --- a/ccip/devenv/tests/token_lane_test.go +++ b/ccip/devenv/tests/token_lane_test.go @@ -127,7 +127,7 @@ func TestNormalizeTransferInstrumentID(t *testing.T) { require.Equal(t, "link-token", normalizeTransferInstrumentID("LINK")) require.Equal(t, "link-token", normalizeTransferInstrumentID("link")) require.Equal(t, "link-token", normalizeTransferInstrumentID(" link ")) - require.Equal(t, "amulet", normalizeTransferInstrumentID("Amulet")) + require.Equal(t, "Amulet", normalizeTransferInstrumentID("Amulet")) require.Equal(t, "link-token", normalizeTransferInstrumentID("link-token")) } diff --git a/deployment/sequences/token_pools.go b/deployment/sequences/token_pools.go index 582916259..be082f958 100644 --- a/deployment/sequences/token_pools.go +++ b/deployment/sequences/token_pools.go @@ -592,12 +592,12 @@ var DeployTokenPoolForToken = operations.NewSequence( return ccipsequences.OnChainOutput{}, fmt.Errorf("tokenRef.address is required") } tokenRef := datastore.AddressRef{ - Address: tokenAddress, - Type: datastore.ContractType("Token"), - // TODO: what should this be set to? + Address: tokenAddress, + Type: datastore.ContractType("Token"), Version: input.TokenPoolVersion, Qualifier: qualifier, ChainSelector: input.ChainSelector, + Labels: input.TokenRef.Labels, } if mcmsEnabled && len(proposalOutputs) > 0 { From 6f70df2370a07429a0201d54ee68b2df417625e5 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 22:19:56 -0300 Subject: [PATCH 06/26] try fixing tokens holdings issue --- ccip/devenv/impl.go | 106 +++++++++++++++---- ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 4 +- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 3fd4a1517..3f56bc14d 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -1068,14 +1068,14 @@ func (c *Chain) SetupSend( return fmt.Errorf("list fee holdings for setup: %w", err) } feeMin := new(big.Rat).SetUint64(feeAmountPerMessage) - selectedFee, err := testhelpers.SelectHoldingsForInstrument(feeRows, []*big.Rat{feeMin}) - if err != nil || len(selectedFee) == 0 { + selectedFee, err := c.selectHolding("setup-fee", feeTokenInstrument, feeRows, feeMin) + if err != nil { return fmt.Errorf("select fee holding for setup: %w", err) } c.feeTokenInstrument = feeTokenInstrument c.feeAmountPerMessage = feeAmountPerMessage - c.nextFeeCID = selectedFee[0].ContractID + c.nextFeeCID = selectedFee.ContractID if transferAmountPerMessage == nil || transferAmountPerMessage.Sign() <= 0 { c.transferTokenInstrument = nil @@ -1097,13 +1097,13 @@ func (c *Chain) SetupSend( return fmt.Errorf("list transfer holdings for setup: %w", err) } transferMin := new(big.Rat).Set(transferAmountPerMessage) - selectedTransfer, err := testhelpers.SelectHoldingsForInstrument(transferRows, []*big.Rat{transferMin}) - if err != nil || len(selectedTransfer) == 0 { + selectedTransfer, err := c.selectHolding("setup-transfer", *transferTokenInstrument, transferRows, transferMin) + if err != nil { return fmt.Errorf("select transfer holding for setup: %w", err) } c.transferTokenInstrument = transferTokenInstrument - c.nextTransferCID = selectedTransfer[0].ContractID + c.nextTransferCID = selectedTransfer.ContractID return nil } @@ -1615,18 +1615,17 @@ func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, to } feeMin := new(big.Rat).SetUint64(c.feeAmountPerMessage) - nextFeeCID, exhaustion, err := pickNextHolding( + nextFeeCID, feeExhausted, err := c.pickNextHolding( events, c.feeTokenInstrument, feeMin, refreshFilters..., ) - if err != nil && !exhaustion { + if err != nil && !feeExhausted { return fmt.Errorf("refresh next fee holding from update: %w", err) } - if !exhaustion { + if !feeExhausted { c.nextFeeCID = nextFeeCID - c.logger.Info().Str("NextFeeCID", c.nextFeeCID).Msg("Selected next fee holding") } else { c.nextFeeCID = "" c.logger.Info().Msg("No next fee holding available after send; clearing for future sends") @@ -1641,18 +1640,17 @@ func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, to testhelpers.ExcludeCIDs(append(spentCIDs, c.nextFeeCID)), ) transferValue := new(big.Rat).Set(tokenAmount) - nextTransferCID, exhaustion, err := pickNextHolding( + nextTransferCID, transferExhausted, err := c.pickNextHolding( events, *c.transferTokenInstrument, transferValue, transferFilters..., ) - if err != nil && !exhaustion { + if err != nil && !transferExhausted { return fmt.Errorf("refresh next transfer holding from update: %w", err) } - if !exhaustion { + if !transferExhausted { c.nextTransferCID = nextTransferCID - c.logger.Info().Str("NextTransferCID", c.nextTransferCID).Msg("Selected next transfer holding") } else { c.nextTransferCID = "" c.logger.Info().Msg("No next transfer holding available after send; clearing for future sends") @@ -1663,27 +1661,93 @@ func (c *Chain) setNextHoldings(events []*apiv2.Event, hasTokenTransfer bool, to // pickNextHolding chooses an unused holding for the next send from Created events in the // send transaction only. Returns an empty CID when nothing meets minAmount (not an error). -func pickNextHolding( +func (c *Chain) pickNextHolding( events []*apiv2.Event, instrument splice_api_token_holding_v1.InstrumentId, minAmount *big.Rat, filters ...testhelpers.Filter, ) (string, bool, error) { - minAmounts := []*big.Rat{minAmount} - fromEvents, err := testhelpers.ListedHoldingsFromTransactionEventsForInstrument(events, instrument, filters...) if err != nil { return "", false, err } if len(fromEvents) == 0 { - return "", true, nil // no qualifying Created events → exhaustion, next send will fail + c.logger.Info(). + Str("Source", "rotate"). + Str("InstrumentAdmin", string(instrument.Admin)). + Str("InstrumentId", string(instrument.Id)). + Str("MinRequired", minAmount.FloatString(10)). + Int("Candidates", 0). + Msg("No qualifying holding in send events") + + return "", true, nil + } + + picked, err := c.selectHolding("rotate", instrument, fromEvents, minAmount) + if err != nil { + return "", true, err + } + + return picked.ContractID, false, nil +} + +func (c *Chain) selectHolding( + source string, + instrument splice_api_token_holding_v1.InstrumentId, + rows []testhelpers.ListedHolding, + min *big.Rat, +) (testhelpers.ListedHolding, error) { + if min == nil { + min = big.NewRat(0, 1) } - picked, err := testhelpers.SelectHoldingsForInstrument(fromEvents, minAmounts) + + picked, err := testhelpers.SelectHoldingsForInstrument(rows, []*big.Rat{min}) if err != nil || len(picked) == 0 { - return "", true, fmt.Errorf("refresh next fee holding from update: %w", err) + c.logHoldingSelectionFailure(source, instrument, min, rows, err) + if err != nil { + return testhelpers.ListedHolding{}, err + } + + return testhelpers.ListedHolding{}, fmt.Errorf("no qualifying holding for %s", source) } - return picked[0].ContractID, false, nil + c.logger.Info(). + Str("Source", source). + Str("ContractID", picked[0].ContractID). + Str("Amount", picked[0].Amount.FloatString(10)). + Str("InstrumentAdmin", string(instrument.Admin)). + Str("InstrumentId", string(instrument.Id)). + Str("MinRequired", min.FloatString(10)). + Int("Candidates", len(rows)). + Msg("Selected holding") + + return picked[0], nil +} + +func (c *Chain) logHoldingSelectionFailure( + source string, + instrument splice_api_token_holding_v1.InstrumentId, + min *big.Rat, + rows []testhelpers.ListedHolding, + selectErr error, +) { + logEvent := c.logger.Info(). + Str("Source", source). + Str("InstrumentAdmin", string(instrument.Admin)). + Str("InstrumentId", string(instrument.Id)). + Str("MinRequired", min.FloatString(10)). + Int("Candidates", len(rows)) + if selectErr != nil { + logEvent = logEvent.Err(selectErr) + } + logEvent.Msg("No qualifying holding selected") + for _, row := range rows { + c.logger.Debug(). + Str("Source", source). + Str("ContractID", row.ContractID). + Str("Amount", row.Amount.FloatString(10)). + Msg("Holding candidate") + } } func (c *Chain) waitOneSentEventBySeqNo(ctx context.Context, to, seq uint64, timeout time.Duration) (cciptestinterfaces.MessageSentEvent, error) { diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index 5a57847a7..da46bf6e1 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -54,8 +54,8 @@ func TestCanton2EVM_Basic(t *testing.T) { // TODO: currently minting 2 holdings of 2k for all the e2e tests. Otherwise one holding might conflict with the other. // the tests need to be hardened to not rely on this and instead correctly pick the holding that suffices. - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*1000))) - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*1000))) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*10000))) + require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*10000))) t.Run("EOA receiver and default committee verifier", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) From 29e75371a6acc69ad24ead4715943f8af66dda50 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 22:48:34 -0300 Subject: [PATCH 07/26] raise canton transfer amount --- ccip/devenv/tests/token_transfer_config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccip/devenv/tests/token_transfer_config.toml b/ccip/devenv/tests/token_transfer_config.toml index 10530c621..e6ff634e7 100644 --- a/ccip/devenv/tests/token_transfer_config.toml +++ b/ccip/devenv/tests/token_transfer_config.toml @@ -21,7 +21,7 @@ finality_config = 1 pool_type = "LockReleaseTokenPool" pool_version = "2.0.0" pool_qualifier = "TEST (BurnMintTokenPool 2.0.0 [default], LockReleaseTokenPool 2.0.0 [default])::LockReleaseTokenPool 2.0.0 [default]" -transfer_amount = "1000" +transfer_amount = "10000000001" execution_gas_limit = 500000 finality_config = 1 From 1b3d61df4c630f2bf267af4ca5f9d0e9a37246a3 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 23:12:19 -0300 Subject: [PATCH 08/26] fix setupSend first hold picking --- ccip/devenv/impl.go | 24 ++++++++++++++------ ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 7 +++--- ccip/devenv/tests/load/load_helpers.go | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 3f56bc14d..b08a5e95e 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -1018,11 +1018,16 @@ func (c *Chain) SetupReceive(ctx context.Context) error { // SetupSend sets up Canton sender specific prerequisites for sending a message. // eg: deploy per-party router, deploy ccipsender contract... +// +// feePerMessage is stored and used as the per-send fee budget in SendMessage and holding rotation. +// transferPerMessage is the per-send transfer amount (nil or ≤0 → message-only). Initial holding +// selection uses perMessage × sendsLeft when SetSequentialSends was called with sends > 0. +// // transferInstrument defaults to Amulet under registryAdmin when omitted or Admin is empty. func (c *Chain) SetupSend( ctx context.Context, - feeAmountPerMessage uint64, - transferAmountPerMessage *big.Rat, + feePerMessage uint64, + transferPerMessage *big.Rat, transferInstrument ...splice_api_token_holding_v1.InstrumentId, ) error { participant, _, err := c.ClientParticipant() @@ -1067,17 +1072,20 @@ func (c *Chain) SetupSend( if err != nil { return fmt.Errorf("list fee holdings for setup: %w", err) } - feeMin := new(big.Rat).SetUint64(feeAmountPerMessage) + feeMin := new(big.Rat).SetUint64(feePerMessage) + if c.sendsLeft > 0 { + feeMin.Mul(feeMin, new(big.Rat).SetUint64(c.sendsLeft)) + } selectedFee, err := c.selectHolding("setup-fee", feeTokenInstrument, feeRows, feeMin) if err != nil { return fmt.Errorf("select fee holding for setup: %w", err) } c.feeTokenInstrument = feeTokenInstrument - c.feeAmountPerMessage = feeAmountPerMessage + c.feeAmountPerMessage = feePerMessage c.nextFeeCID = selectedFee.ContractID - if transferAmountPerMessage == nil || transferAmountPerMessage.Sign() <= 0 { + if transferPerMessage == nil || transferPerMessage.Sign() <= 0 { c.transferTokenInstrument = nil c.nextTransferCID = "" return nil @@ -1096,7 +1104,10 @@ func (c *Chain) SetupSend( if err != nil { return fmt.Errorf("list transfer holdings for setup: %w", err) } - transferMin := new(big.Rat).Set(transferAmountPerMessage) + transferMin := new(big.Rat).Set(transferPerMessage) + if c.sendsLeft > 0 { + transferMin.Mul(transferMin, new(big.Rat).SetUint64(c.sendsLeft)) + } selectedTransfer, err := c.selectHolding("setup-transfer", *transferTokenInstrument, transferRows, transferMin) if err != nil { return fmt.Errorf("select transfer holding for setup: %w", err) @@ -1110,7 +1121,6 @@ func (c *Chain) SetupSend( // SetSequentialSends limits holding rotation to the next sends messages in this setup batch. // After the send whose on-chain seq equals nextSeq+sends, setNextHoldings is skipped. -// Pass 0 to always rotate (open-ended load). func (c *Chain) SetSequentialSends(sends int) { if sends <= 0 { c.sendsLeft = 0 diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index da46bf6e1..3d14fb393 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -61,6 +61,7 @@ func TestCanton2EVM_Basic(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) // Setup message send + cantonImpl.SetSequentialSends(1) require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) ds, err := lib.DataStore() @@ -151,12 +152,12 @@ func TestCanton2EVM_Basic(t *testing.T) { fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) sends := cantondevenv.CantonToEVMTokenSequentialSends transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + cantonImpl.SetSequentialSends(sends) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } - cantonImpl.SetSequentialSends(sends) ds, err := lib.DataStore() require.NoError(t, err) @@ -242,12 +243,12 @@ func TestCanton2EVM_Basic(t *testing.T) { fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + cantonImpl.SetSequentialSends(1) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } - cantonImpl.SetSequentialSends(1) receiver, err := evmChain.GetEOAReceiverAddress() require.NoError(t, err) @@ -290,12 +291,12 @@ func TestCanton2EVM_Basic(t *testing.T) { fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + cantonImpl.SetSequentialSends(1) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } - cantonImpl.SetSequentialSends(1) ds, err := lib.DataStore() require.NoError(t, err) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 115a8e532..d29538203 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -274,12 +274,12 @@ func setupCantonTokenLoadHoldings( estimated, feeTotal.FloatString(10), transferTotal.FloatString(10)) require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) + cantonImpl.SetSequentialSends(int(estimated)) if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) } - cantonImpl.SetSequentialSends(int(estimated)) } func evmLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.UnknownAddress) Destination { From 4e05025bd61e67d56b38c3c7777123f2558289d7 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 7 Jul 2026 23:25:46 -0300 Subject: [PATCH 09/26] fix lint + unit tests --- ccip/devenv/impl.go | 16 ++++++++-------- ccip/devenv/manual_execution.go | 1 + ccip/devenv/tests/load/load_helpers.go | 4 +++- ccip/devenv/tests/token_lane.go | 2 ++ ccip/devenv/tests/token_lane_test.go | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index b08a5e95e..51f63077e 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -1705,15 +1705,15 @@ func (c *Chain) selectHolding( source string, instrument splice_api_token_holding_v1.InstrumentId, rows []testhelpers.ListedHolding, - min *big.Rat, + minAmount *big.Rat, ) (testhelpers.ListedHolding, error) { - if min == nil { - min = big.NewRat(0, 1) + if minAmount == nil { + minAmount = big.NewRat(0, 1) } - picked, err := testhelpers.SelectHoldingsForInstrument(rows, []*big.Rat{min}) + picked, err := testhelpers.SelectHoldingsForInstrument(rows, []*big.Rat{minAmount}) if err != nil || len(picked) == 0 { - c.logHoldingSelectionFailure(source, instrument, min, rows, err) + c.logHoldingSelectionFailure(source, instrument, minAmount, rows, err) if err != nil { return testhelpers.ListedHolding{}, err } @@ -1727,7 +1727,7 @@ func (c *Chain) selectHolding( Str("Amount", picked[0].Amount.FloatString(10)). Str("InstrumentAdmin", string(instrument.Admin)). Str("InstrumentId", string(instrument.Id)). - Str("MinRequired", min.FloatString(10)). + Str("MinRequired", minAmount.FloatString(10)). Int("Candidates", len(rows)). Msg("Selected holding") @@ -1737,7 +1737,7 @@ func (c *Chain) selectHolding( func (c *Chain) logHoldingSelectionFailure( source string, instrument splice_api_token_holding_v1.InstrumentId, - min *big.Rat, + minAmount *big.Rat, rows []testhelpers.ListedHolding, selectErr error, ) { @@ -1745,7 +1745,7 @@ func (c *Chain) logHoldingSelectionFailure( Str("Source", source). Str("InstrumentAdmin", string(instrument.Admin)). Str("InstrumentId", string(instrument.Id)). - Str("MinRequired", min.FloatString(10)). + Str("MinRequired", minAmount.FloatString(10)). Int("Candidates", len(rows)) if selectErr != nil { logEvent = logEvent.Err(selectErr) diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index 444d85623..df58d74d9 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -148,6 +148,7 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici c.senderAddress = senderAddress return senderAddress, nil } + return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy ccip sender contract: %w", err) } c.senderAddress = senderAddress diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index d29538203..10641fddf 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -3,6 +3,7 @@ package load import ( "context" "fmt" + "math" "math/big" "os" "strings" @@ -274,7 +275,8 @@ func setupCantonTokenLoadHoldings( estimated, feeTotal.FloatString(10), transferTotal.FloatString(10)) require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) - cantonImpl.SetSequentialSends(int(estimated)) + require.LessOrEqual(t, estimated, uint64(math.MaxInt)) + cantonImpl.SetSequentialSends(int(estimated)) //nolint:gosec // bounded by require above if lane.TransferInstrument.Admin != "" { require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) } else { diff --git a/ccip/devenv/tests/token_lane.go b/ccip/devenv/tests/token_lane.go index ad1607e74..85a9f4e37 100644 --- a/ccip/devenv/tests/token_lane.go +++ b/ccip/devenv/tests/token_lane.go @@ -239,11 +239,13 @@ func tokenDirectionsForEnv(cfg tokenConfigTOML, env CCIPEnv) (tokenDirectionsTOM if cfg.Devenv.EVMToCanton.PoolType == "" && cfg.Devenv.CantonToEVM.PoolType == "" { return tokenDirectionsTOML{}, fmt.Errorf("missing [devenv.*] sections") } + return cfg.Devenv, nil case EnvProdTestnet: if cfg.ProdTestnet.EVMToCanton.PoolType == "" && cfg.ProdTestnet.CantonToEVM.PoolType == "" { return tokenDirectionsTOML{}, fmt.Errorf("missing [prod-testnet.*] sections") } + return cfg.ProdTestnet, nil default: return tokenDirectionsTOML{}, fmt.Errorf("unsupported ccip env %q", env) diff --git a/ccip/devenv/tests/token_lane_test.go b/ccip/devenv/tests/token_lane_test.go index 2c2470779..07fe0c8df 100644 --- a/ccip/devenv/tests/token_lane_test.go +++ b/ccip/devenv/tests/token_lane_test.go @@ -55,7 +55,7 @@ func TestLoadTokenDirection_transferAmountParsing(t *testing.T) { require.Equal(t, "100000000001", evmDir.TransferAmount.String()) cantonDir := loadTokenDirection(t, EnvDevenv, directionCantonToEVM) - require.Equal(t, big.NewInt(1000), cantonDir.TransferAmount) + require.Equal(t, big.NewInt(10000000001), cantonDir.TransferAmount) prodCantonDir := loadTokenDirection(t, EnvProdTestnet, directionCantonToEVM) require.Equal(t, big.NewInt(100), prodCantonDir.TransferAmount) From 271d3532f60518addd24b859514dc72dcc7fd658 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 00:00:35 -0300 Subject: [PATCH 10/26] fix load tests CI --- .github/actions/ccip-load-test/action.yml | 28 ++------ .github/workflows/ccip-load-command.yml | 18 +++++ .github/workflows/ccip-load-tests.yml | 69 +++++++++++++++----- ccip/devenv/tests/token_transfer_config.toml | 2 +- 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index 18ededfb7..d85c38c4b 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -1,7 +1,8 @@ name: Run CCIP load test description: >- - Shared setup and execution for CCIP WASP load tests against local devenv or - prod-testnet infrastructure. + Shared execution for CCIP WASP load tests against local devenv or prod-testnet + infrastructure. Call setup-ccip-devenv and upload-ccip-devenv-logs separately + when you want distinct workflow steps (see ccip-load-tests.yml). inputs: ccip_env: @@ -77,16 +78,6 @@ inputs: runs: using: composite steps: - - name: Setup CCIP devenv - if: inputs.ccip_env == 'devenv' - uses: ./.github/actions/setup-ccip-devenv - with: - canton-ref: ${{ inputs.canton_ref }} - canton-path: ${{ inputs.canton_path }} - ccv-iam-role: ${{ inputs.ccv-iam-role }} - jd-registry: ${{ inputs.jd-registry }} - jd-image: ${{ inputs.jd-image }} - - name: Install Go (prod-testnet) if: inputs.ccip_env == 'prod-testnet' uses: actions/setup-go@v6 @@ -116,7 +107,7 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=devenv -run "$TEST_RUN" + go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" - name: Run load tests (prod-testnet) if: inputs.ccip_env == 'prod-testnet' @@ -144,13 +135,4 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" - - - name: Upload devenv logs - if: always() && inputs.ccip_env == 'devenv' - uses: ./.github/actions/upload-ccip-devenv-logs - with: - canton-path: ${{ inputs.canton_path }} - test-package-dir: load - log-dump-suffix: ccip-load-tests - artifact-name: container-logs-ccip-load-tests + go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" diff --git a/.github/workflows/ccip-load-command.yml b/.github/workflows/ccip-load-command.yml index 1016f0022..cfcf4db12 100644 --- a/.github/workflows/ccip-load-command.yml +++ b/.github/workflows/ccip-load-command.yml @@ -79,6 +79,15 @@ jobs: echo "test_timeout=30m" >> "$GITHUB_OUTPUT" fi + - name: Setup CCIP devenv + if: steps.params.outputs.ccip_env == 'devenv' + uses: ./.github/actions/setup-ccip-devenv + with: + canton-path: . + ccv-iam-role: ${{ secrets.CCV_IAM_ROLE }} + jd-registry: ${{ secrets.JD_REGISTRY }} + jd-image: ${{ secrets.JD_IMAGE }} + - name: Run CCIP load test uses: ./.github/actions/ccip-load-test with: @@ -98,6 +107,15 @@ jobs: CANTON_OKTA_CLIENT_SECRET_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET }} CCIP_PROD_TESTNET_PRIVATE_KEY: ${{ secrets.CCIP_PROD_TESTNET_PRIVATE_KEY }} + - name: Upload devenv logs + if: always() && steps.params.outputs.ccip_env == 'devenv' + uses: ./.github/actions/upload-ccip-devenv-logs + with: + canton-path: . + test-package-dir: load + log-dump-suffix: ccip-load-tests + artifact-name: container-logs-ccip-load-tests + - name: Update comment if: always() uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 diff --git a/.github/workflows/ccip-load-tests.yml b/.github/workflows/ccip-load-tests.yml index 77e9b655b..8fd9e3656 100644 --- a/.github/workflows/ccip-load-tests.yml +++ b/.github/workflows/ccip-load-tests.yml @@ -104,23 +104,60 @@ jobs: echo "test_timeout=${{ inputs.test_timeout }}" >> "$GITHUB_OUTPUT" fi - - name: Run CCIP load test - uses: ./.github/actions/ccip-load-test + - name: Setup CCIP devenv + if: inputs.ccip_env == 'devenv' + uses: ./.github/actions/setup-ccip-devenv with: - ccip_env: ${{ inputs.ccip_env }} - direction: ${{ inputs.direction }} - message_rate: ${{ steps.prod_defaults.outputs.message_rate }} - load_duration: ${{ steps.prod_defaults.outputs.load_duration }} - test_timeout: ${{ steps.prod_defaults.outputs.test_timeout }} - config_file: ${{ inputs.config_file }} - canton_path: ${{ steps.prod_defaults.outputs.canton_path }} - canton_ref: ${{ inputs.canton_ref }} - skip_exec_confirm: ${{ inputs.skip_exec_confirm }} - confirm_exec_timeout: ${{ inputs.confirm_exec_timeout }} - CANTON_OKTA_AUTHORIZER_TESTNET: ${{ secrets.CANTON_OKTA_AUTHORIZER_TESTNET }} - CANTON_OKTA_CLIENT_ID_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_ID_TESTNET }} - CANTON_OKTA_CLIENT_SECRET_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET }} - CCIP_PROD_TESTNET_PRIVATE_KEY: ${{ secrets.CCIP_PROD_TESTNET_PRIVATE_KEY }} + canton-ref: ${{ inputs.canton_ref }} + canton-path: ${{ steps.prod_defaults.outputs.canton_path }} ccv-iam-role: ${{ secrets.CCV_IAM_ROLE }} jd-registry: ${{ secrets.JD_REGISTRY }} jd-image: ${{ secrets.JD_IMAGE }} + + - name: Install Go (prod-testnet) + if: inputs.ccip_env == 'prod-testnet' + uses: actions/setup-go@v6 + with: + cache: true + go-version-file: ${{ steps.prod_defaults.outputs.canton_path }}/go.mod + cache-dependency-path: ${{ steps.prod_defaults.outputs.canton_path }}/go.sum + + - name: Download Go dependencies (prod-testnet) + if: inputs.ccip_env == 'prod-testnet' + working-directory: ${{ steps.prod_defaults.outputs.canton_path }} + run: go mod download + + - name: Run CCIP load test (${{ inputs.direction }}) + working-directory: ${{ steps.prod_defaults.outputs.canton_path }}/ccip/devenv/tests/load + env: + CANTON_LOAD_MESSAGE_RATE: ${{ steps.prod_defaults.outputs.message_rate }} + CANTON_LOAD_DURATION: ${{ steps.prod_defaults.outputs.load_duration }} + CCIP_ENV: ${{ inputs.ccip_env == 'prod-testnet' && 'prod-testnet' || '' }} + CCIP_CONFIG_FILE: ${{ inputs.config_file }} + CANTON_AUTH_TYPE: ${{ inputs.ccip_env == 'prod-testnet' && 'clientCredentials' || '' }} + CANTON_AUTH_URL: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_AUTHORIZER_TESTNET || '' }} + CANTON_CLIENT_ID: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_CLIENT_ID_TESTNET || '' }} + CANTON_CLIENT_SECRET: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET || '' }} + CANTON_PARTY_ID: ${{ inputs.ccip_env == 'prod-testnet' && 'u_d53a15c42af6::1220c250c23c55120f7c758bccc5cbc739629015ab921594e1c29656981f985bffa7' || '' }} + CANTON_GRPC_URL: ${{ inputs.ccip_env == 'prod-testnet' && 'testnet.cv1.bcy-v.metalhosts.com:443' || '' }} + CANTON_CONFIRM_EXEC_TIMEOUT: ${{ inputs.confirm_exec_timeout }} + CANTON_LOAD_SKIP_EXEC_CONFIRM: ${{ inputs.skip_exec_confirm }} + PRIVATE_KEY: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CCIP_PROD_TESTNET_PRIVATE_KEY || '' }} + run: | + case "${{ inputs.direction }}" in + canton2evm) TEST_RUN='^TestCanton2EVM_Load$' ;; + evm2canton) TEST_RUN='^TestEVM2Canton_Load$' ;; + canton2evm-token) TEST_RUN='^TestCanton2EVM_TokenLoad$' ;; + evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; + *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; + esac + go test -timeout "${{ steps.prod_defaults.outputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" + + - name: Upload devenv logs + if: always() && inputs.ccip_env == 'devenv' + uses: ./.github/actions/upload-ccip-devenv-logs + with: + canton-path: ${{ steps.prod_defaults.outputs.canton_path }} + test-package-dir: load + log-dump-suffix: ccip-load-tests + artifact-name: container-logs-ccip-load-tests diff --git a/ccip/devenv/tests/token_transfer_config.toml b/ccip/devenv/tests/token_transfer_config.toml index e6ff634e7..31f5691f5 100644 --- a/ccip/devenv/tests/token_transfer_config.toml +++ b/ccip/devenv/tests/token_transfer_config.toml @@ -1,6 +1,6 @@ # Token lane inputs for CCIP e2e/load tests. # -# Env selection follows -ccip-env / CCIP_ENV (same as CCIP harness). +# Env selection uses CCIP_ENV (prod-testnet) or defaults to devenv in tests. # Token identity (pool_type / pool_version / pool_qualifier) is REQUIRED and is # matched against the source chain's GetTokenTransferConfigs to discover the lane. # Numeric send params (transfer_amount / execution_gas_limit / finality_config) From af323c10d4f059811acf82c648d79b64b3da970f Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 00:32:35 -0300 Subject: [PATCH 11/26] feat(tests): env agnostic boostrapper for e2e/load --- ccip/devenv/tests/e2e/canton2evm_e2e_test.go | 165 ++++------- ccip/devenv/tests/e2e/evm2canton_e2e_test.go | 107 +++----- ccip/devenv/tests/env.go | 59 ++++ ccip/devenv/tests/env_test.go | 35 +++ ccip/devenv/tests/helpers.go | 259 +++++++++++++++++- ccip/devenv/tests/load/gun_canton2evm_test.go | 75 ++--- .../tests/load/gun_canton2evm_token_test.go | 75 +++-- ccip/devenv/tests/load/gun_evm2canton_test.go | 54 +--- .../tests/load/gun_evm2canton_token_test.go | 70 ++--- ccip/devenv/tests/load/load_helpers.go | 239 ++++++++-------- 10 files changed, 656 insertions(+), 482 deletions(-) create mode 100644 ccip/devenv/tests/env_test.go diff --git a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go index 3d14fb393..82c618caa 100644 --- a/ccip/devenv/tests/e2e/canton2evm_e2e_test.go +++ b/ccip/devenv/tests/e2e/canton2evm_e2e_test.go @@ -1,10 +1,8 @@ package canton import ( - "fmt" "math/big" "testing" - "time" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" @@ -13,8 +11,6 @@ import ( "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,53 +26,26 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Skip("skipping Canton2EVM_Basic test in short mode") } - configPath := "../../env-canton-evm-out.toml" - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) - - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath) - require.NoError(t, err) + boot := devenvtests.BootstrapE2E(t, devenvtests.ParseEnvFromFlag(t)) ctx := ccv.Plog.WithContext(t.Context()) - chainMap, err := lib.ChainsMap(ctx) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) - - evmChain := devenvtests.GetChainFromMap(t, blockchain.TypeAnvil, in, chainMap) - cantonChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonImpl, ok := cantonChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton chain cantonImpl must be *devenv.Chain") - - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) - - // TODO: currently minting 2 holdings of 2k for all the e2e tests. Otherwise one holding might conflict with the other. - // the tests need to be hardened to not rely on this and instead correctly pick the holding that suffices. - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*10000))) - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(uint64(cantondevenv.CantonToEVMFeeAmount)*10000))) - t.Run("EOA receiver and default committee verifier", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - // Setup message send - cantonImpl.SetSequentialSends(1) - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) + boot.SetupCantonSend(t, ctx, 0) + receiver := boot.ResolveEVMReceiver(t) - ds, err := lib.DataStore() - require.NoError(t, err) - receiver, err := evmChain.GetEOAReceiverAddress() + ds, err := boot.Lib.DataStore() require.NoError(t, err) ccvAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(canton_committee_verifier.ContractType), canton_committee_verifier.Version.String(), devenvcommon.DefaultCommitteeVerifierQualifier, "canton committee verifier", ) executorAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(executor.ContractType), executor.Version.String(), devenvcommon.DefaultExecutorQualifier, @@ -87,14 +56,14 @@ func TestCanton2EVM_Basic(t *testing.T) { receiver, ccvAddr, executorAddr, - cantonChain.ChainSelector(), - evmChain.ChainSelector(), + boot.Canton.ChainSelector(), + boot.EVM.ChainSelector(), ) t.Logf("Sending Canton -> EVM message") - sendMessageResult, err := cantonChain.SendMessage( + sendMessageResult, err := boot.Canton.SendMessage( subtestCtx, - evmChain.ChainSelector(), + boot.EVM.ChainSelector(), cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("canton2evm tcapi test"), @@ -115,29 +84,24 @@ func TestCanton2EVM_Basic(t *testing.T) { ) require.NoError(t, err) require.NotNil(t, sendMessageResult.Message) - // require.NotEmpty(t, sendMessageResult.ReceiptIssuers) seqNo := uint64(sendMessageResult.Message.SequenceNumber) - t.Logf( - "SendMessage accepted: seqNo=%d", - seqNo, - // len(sendMessageResult.ReceiptIssuers), - ) + t.Logf("SendMessage accepted: seqNo=%d", seqNo) - t.Logf("Waiting for CCIPMessageSent event: from=%d to=%d seq=%d", cantonChain.ChainSelector(), evmChain.ChainSelector(), seqNo) - sentEvent, err := cantonChain.ConfirmSendOnSource(subtestCtx, evmChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, 30*time.Second) + t.Logf("Waiting for CCIPMessageSent event: from=%d to=%d seq=%d", boot.Canton.ChainSelector(), boot.EVM.ChainSelector(), seqNo) + sentEvent, err := boot.Canton.ConfirmSendOnSource(subtestCtx, boot.EVM.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, devenvtests.ConfirmSendTimeout(t, boot.Env)) require.NoError(t, err) t.Logf("CCIPMessageSent event: %+v", sentEvent) t.Logf("Asserting message propagated through aggregator/indexer: messageID=%x", sentEvent.MessageID[:]) - result := devenvtests.AssertSingleVerifierResult(t, subtestCtx, lib, sentEvent.MessageID) + result := devenvtests.AssertSingleVerifierResult(t, subtestCtx, boot.Lib, sentEvent.MessageID) t.Logf( "Message assertion succeeded: aggregated=true indexerResults=%+v", result.IndexedVerifications.Results, ) - t.Logf("Waiting for execution event on EVM: from=%d seq=%d", cantonChain.ChainSelector(), seqNo) - ev, err := evmChain.ConfirmExecOnDest(subtestCtx, cantonChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) + t.Logf("Waiting for execution event on EVM: from=%d seq=%d", boot.Canton.ChainSelector(), seqNo) + ev, err := boot.EVM.ConfirmExecOnDest(subtestCtx, boot.Canton.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) require.NoError(t, err) assert.Equal(t, cciptestinterfaces.ExecutionStateSuccess, ev.State) t.Logf("Execution event: %+v", ev) @@ -146,48 +110,37 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Run("EOA receiver and default committee verifier token transfer", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - // Send params (transfer amount, gas limit, finality) come from token_transfer_config.toml. - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) - - fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - sends := cantondevenv.CantonToEVMTokenSequentialSends - transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - cantonImpl.SetSequentialSends(sends) - if lane.TransferInstrument.Admin != "" { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) - } else { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) - } + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, boot.Canton.ChainSelector(), []uint64{boot.EVM.ChainSelector()}) + boot.SetupCantonTokenSend(t, ctx, lane, cantondevenv.CantonToEVMTokenSequentialSends) - ds, err := lib.DataStore() - require.NoError(t, err) - receiver, err := evmChain.GetEOAReceiverAddress() + receiver := boot.ResolveEVMReceiver(t) + + ds, err := boot.Lib.DataStore() require.NoError(t, err) ccvAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(canton_committee_verifier.ContractType), canton_committee_verifier.Version.String(), devenvcommon.DefaultCommitteeVerifierQualifier, "canton committee verifier", ) executorAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(executor.ContractType), executor.Version.String(), devenvcommon.DefaultExecutorQualifier, "source executor", ) - require.NoError(t, err) - destTokenAddress := lane.DestTokenBySelector[evmChain.ChainSelector()] - receiverBalanceBefore, err := evmChain.GetTokenBalance(subtestCtx, receiver, destTokenAddress) + destTokenAddress := lane.DestTokenBySelector[boot.EVM.ChainSelector()] + receiverBalanceBefore, err := boot.EVM.GetTokenBalance(subtestCtx, receiver, destTokenAddress) require.NoError(t, err) require.NotNil(t, receiverBalanceBefore) for sendIdx := range cantondevenv.CantonToEVMTokenSequentialSends { t.Logf("Token transfer send %d/%d", sendIdx+1, cantondevenv.CantonToEVMTokenSequentialSends) - sendMessageResult, err := cantonChain.SendMessage( + sendMessageResult, err := boot.Canton.SendMessage( subtestCtx, - evmChain.ChainSelector(), + boot.EVM.ChainSelector(), cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("canton2evm token transfer"), @@ -214,17 +167,24 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NotNil(t, sendMessageResult.Message.TokenTransfer) seqNo := uint64(sendMessageResult.Message.SequenceNumber) - sentEvent, err := cantonChain.ConfirmSendOnSource(subtestCtx, evmChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) + sentEvent, err := boot.Canton.ConfirmSendOnSource(subtestCtx, boot.EVM.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, devenvtests.ConfirmSendTimeout(t, boot.Env)) require.NoError(t, err) require.NotNil(t, sentEvent.Message) require.NotNil(t, sentEvent.Message.TokenTransfer) - ev, err := evmChain.ConfirmExecOnDest(subtestCtx, cantonChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) + t.Logf("Asserting message propagated through aggregator/indexer: messageID=%x", sentEvent.MessageID[:]) + result := devenvtests.AssertSingleVerifierResult(t, subtestCtx, boot.Lib, sentEvent.MessageID) + t.Logf( + "Message assertion succeeded: aggregated=true indexerResults=%+v", + result.IndexedVerifications.Results, + ) + + ev, err := boot.EVM.ConfirmExecOnDest(subtestCtx, boot.Canton.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) require.NoError(t, err) require.Equal(t, cciptestinterfaces.ExecutionStateSuccess, ev.State) } - receiverBalanceAfter, err := evmChain.GetTokenBalance(subtestCtx, receiver, destTokenAddress) + receiverBalanceAfter, err := boot.EVM.GetTokenBalance(subtestCtx, receiver, destTokenAddress) require.NoError(t, err) require.NotNil(t, receiverBalanceAfter) @@ -239,23 +199,14 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Run("token transfer with default extraArgs", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, boot.Canton.ChainSelector(), []uint64{boot.EVM.ChainSelector()}) + boot.SetupCantonTokenSend(t, ctx, lane, 1) - fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - cantonImpl.SetSequentialSends(1) - if lane.TransferInstrument.Admin != "" { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) - } else { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) - } - - receiver, err := evmChain.GetEOAReceiverAddress() - require.NoError(t, err) + receiver := boot.ResolveEVMReceiver(t) - sendMessageResult, err := cantonChain.SendMessage( + sendMessageResult, err := boot.Canton.SendMessage( subtestCtx, - evmChain.ChainSelector(), + boot.EVM.ChainSelector(), cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("canton2evm token transfer default extraArgs"), @@ -274,10 +225,10 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NotNil(t, sendMessageResult.Message.TokenTransfer) seqNo := uint64(sendMessageResult.Message.SequenceNumber) - sentEvent, err := cantonChain.ConfirmSendOnSource(subtestCtx, evmChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) + sentEvent, err := boot.Canton.ConfirmSendOnSource(subtestCtx, boot.EVM.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, devenvtests.ConfirmSendTimeout(t, boot.Env)) require.NoError(t, err) - ev, err := evmChain.ConfirmExecOnDest(subtestCtx, cantonChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID}, tests.WaitTimeout(t)) + ev, err := boot.EVM.ConfirmExecOnDest(subtestCtx, boot.Canton.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID}, tests.WaitTimeout(t)) require.NoError(t, err) require.Equal(t, cciptestinterfaces.ExecutionStateSuccess, ev.State) }) @@ -287,39 +238,31 @@ func TestCanton2EVM_Basic(t *testing.T) { t.Run("token transfer with zero gas limit", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), []uint64{evmChain.ChainSelector()}) + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, boot.Canton.ChainSelector(), []uint64{boot.EVM.ChainSelector()}) + boot.SetupCantonTokenSend(t, ctx, lane, 1) - fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - cantonImpl.SetSequentialSends(1) - if lane.TransferInstrument.Admin != "" { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) - } else { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) - } + receiver := boot.ResolveEVMReceiver(t) - ds, err := lib.DataStore() - require.NoError(t, err) - receiver, err := evmChain.GetEOAReceiverAddress() + ds, err := boot.Lib.DataStore() require.NoError(t, err) ccvAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(canton_committee_verifier.ContractType), canton_committee_verifier.Version.String(), devenvcommon.DefaultCommitteeVerifierQualifier, "canton committee verifier", ) executorAddr := devenvtests.GetContractAddress( - t, ds, cantonChain.ChainSelector(), + t, ds, boot.Canton.ChainSelector(), datastore.ContractType(executor.ContractType), executor.Version.String(), devenvcommon.DefaultExecutorQualifier, "source executor", ) - sendMessageResult, err := cantonChain.SendMessage( + sendMessageResult, err := boot.Canton.SendMessage( subtestCtx, - evmChain.ChainSelector(), + boot.EVM.ChainSelector(), cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("canton2evm token transfer zero gas"), @@ -345,10 +288,10 @@ func TestCanton2EVM_Basic(t *testing.T) { require.NotNil(t, sendMessageResult.Message) seqNo := uint64(sendMessageResult.Message.SequenceNumber) - sentEvent, err := cantonChain.ConfirmSendOnSource(subtestCtx, evmChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, tests.WaitTimeout(t)) + sentEvent, err := boot.Canton.ConfirmSendOnSource(subtestCtx, boot.EVM.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, devenvtests.ConfirmSendTimeout(t, boot.Env)) require.NoError(t, err) - ev, err := evmChain.ConfirmExecOnDest(subtestCtx, cantonChain.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID}, tests.WaitTimeout(t)) + ev, err := boot.EVM.ConfirmExecOnDest(subtestCtx, boot.Canton.ChainSelector(), cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID}, tests.WaitTimeout(t)) require.NoError(t, err) require.Equal(t, cciptestinterfaces.ExecutionStateSuccess, ev.State) }) diff --git a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go index 23af4ac8f..da0a4a118 100644 --- a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go +++ b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go @@ -1,27 +1,23 @@ package canton import ( - "fmt" "math/big" "testing" - "time" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/proxy" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/versioned_verifier_resolver" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" + ccldf "github.com/smartcontractkit/chainlink-ccv/build/devenv/cldf" "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory "github.com/smartcontractkit/chainlink-ccv/protocol" - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // register Canton ImplFactory - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) @@ -32,38 +28,24 @@ func TestEVM2Canton_Basic(t *testing.T) { t.Skip("skipping EVM2Canton_Basic test in short mode") } - configPath := "../../env-canton-evm-out.toml" - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) - - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath) - require.NoError(t, err) + boot := devenvtests.BootstrapE2E(t, devenvtests.ParseEnvFromFlag(t)) + ctx := ccv.Plog.WithContext(t.Context()) + boot.SetupCantonReceive(t, ctx) - chainMap, err := lib.ChainsMap(t.Context()) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) - - srcChain := devenvtests.GetChainFromMap(t, blockchain.TypeAnvil, in, chainMap) - dstChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonDest, ok := dstChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton dest chain must be *devenv.Chain") - require.NoError(t, cantonDest.SetupReceive(ccv.Plog.WithContext(t.Context()))) - - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) - - srcSelector := srcChain.ChainSelector() - dstSelector := dstChain.ChainSelector() - receiverParticipant, _, err := cantonDest.ClientParticipant() + srcSelector := boot.EVM.ChainSelector() + dstSelector := boot.Canton.ChainSelector() + _, opsEnv, err := ccldf.NewCLDFOperationsEnvironment(boot.Cfg.Blockchains, boot.Cfg.CLDF.DataStore) require.NoError(t, err) + var receiverParticipant canton.Participant + if chains := opsEnv.BlockChains.CantonChains(); len(chains[dstSelector].Participants) > 0 { + receiverParticipant = chains[dstSelector].Participants[0] + } require.NotEmpty(t, receiverParticipant.PartyID) - receiver, err := cantonDest.GetEOAReceiverAddress() + receiver, err := boot.Canton.GetEOAReceiverAddress() require.NoError(t, err) - ds, err := lib.DataStore() + ds, err := boot.Lib.DataStore() require.NoError(t, err) ccvAddr := devenvtests.GetContractAddress( t, ds, srcSelector, @@ -83,39 +65,26 @@ func TestEVM2Canton_Basic(t *testing.T) { t.Run("message transfer", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - seqNo, err := srcChain.GetExpectedNextSequenceNumber(subtestCtx, dstSelector) + seqNo, err := boot.EVM.GetExpectedNextSequenceNumber(subtestCtx, dstSelector) require.NoError(t, err) - sendMessageResult, err := srcChain.SendMessage(subtestCtx, dstSelector, cciptestinterfaces.MessageFields{ + sendMessageResult, err := boot.EVM.SendMessage(subtestCtx, dstSelector, cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("Hello message transfer from EVM!"), - }, cciptestinterfaces.MessageOptions{ - ExecutionGasLimit: 200_000, - FinalityConfig: 0, - Executor: executorAddress, - CCVs: []protocol.CCV{ - { - CCVAddress: ccvAddr, - Args: []byte{}, - ArgsLen: 0, - }, - }, - }, 3) + }, devenvtests.EVMToCantonMessageOptions(200_000, executorAddress, ccvAddr), 3) require.NoError(t, err) require.NotNil(t, sendMessageResult.Message) - sentEvent, err := srcChain.ConfirmSendOnSource(subtestCtx, dstSelector, cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, 15*time.Second) + sentEvent, err := boot.ConfirmEVMSendOnSource(t, subtestCtx, dstSelector, seqNo, sendMessageResult) require.NoError(t, err) require.NotNil(t, sentEvent.Message) require.Nil(t, sentEvent.Message.TokenTransfer) execKey := cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID} - executionStateChangedEvent, err := cantonDest.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, utilstests.WaitTimeout(t)) + executionStateChangedEvent, err := boot.Canton.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, devenvtests.ConfirmExecTimeout(t)) require.NoError(t, err) require.Equal(t, cciptestinterfaces.ExecutionStateSuccess, executionStateChangedEvent.State) - // testing idempotency of ConfirmExecOnDest: a second call - // must return the same event without re-executing. - idempotentEvent, err := cantonDest.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, utilstests.WaitTimeout(t)) + idempotentEvent, err := boot.Canton.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, devenvtests.ConfirmExecTimeout(t)) require.NoError(t, err) require.Equal(t, executionStateChangedEvent.State, idempotentEvent.State) require.Equal(t, executionStateChangedEvent.MessageNumber, idempotentEvent.MessageNumber) @@ -125,14 +94,25 @@ func TestEVM2Canton_Basic(t *testing.T) { t.Run("token transfer", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) - // Send params (transfer amount, gas limit, finality) come from token_transfer_config.toml. - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, srcSelector, []uint64{dstSelector}) + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, srcSelector, []uint64{dstSelector}) + t.Logf("Token lane: pool=%s srcToken=%x transfer=%s", + lane.PoolRef.Qualifier, lane.SrcToken, lane.TransferAmount.String()) + srcToken := lane.SrcToken - srcSender, err := srcChain.GetEOAReceiverAddress() - require.NoError(t, err) - seqNo, err := srcChain.GetExpectedNextSequenceNumber(subtestCtx, dstSelector) + srcSender := boot.ResolveEVMReceiver(t) + + if boot.Env.IsRemote() { + senderBalance, err := boot.EVM.GetTokenBalance(subtestCtx, srcSender, srcToken) + require.NoError(t, err) + require.NotNil(t, senderBalance) + require.GreaterOrEqual(t, senderBalance.Cmp(lane.TransferAmount), 0, + "EVM sender token balance %s is below transfer amount %s; fund PRIVATE_KEY wallet with TEST tokens on Sepolia", + senderBalance.String(), lane.TransferAmount.String()) + t.Log("prod-testnet: ensure PRIVATE_KEY wallet has Sepolia ETH for send and possible router approve tx") + } + seqNo, err := boot.EVM.GetExpectedNextSequenceNumber(subtestCtx, dstSelector) require.NoError(t, err) - sendMessageResult, err := srcChain.SendMessage(subtestCtx, dstSelector, cciptestinterfaces.MessageFields{ + sendMessageResult, err := boot.EVM.SendMessage(subtestCtx, dstSelector, cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("Hello token transfer from EVM!"), TokenAmount: cciptestinterfaces.TokenAmount{ @@ -155,15 +135,12 @@ func TestEVM2Canton_Basic(t *testing.T) { require.NotNil(t, sendMessageResult.Message) require.NotNil(t, sendMessageResult.Message.TokenTransfer) - sentEvent, err := srcChain.ConfirmSendOnSource(subtestCtx, dstSelector, cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, 15*time.Second) + sentEvent, err := boot.ConfirmEVMSendOnSource(t, subtestCtx, dstSelector, seqNo, sendMessageResult) require.NoError(t, err) require.NotNil(t, sentEvent.Message) require.NotNil(t, sentEvent.Message.TokenTransfer) - // Pre-exec assertions on the verifier result are kept here (cheap aggregator/indexer - // re-read) so the token transfer assertions stay co-located with the test body. - // ConfirmExecOnDest below performs its own fetch internally for execution. - result := devenvtests.AssertSingleVerifierResult(t, subtestCtx, lib, sentEvent.MessageID) + result := devenvtests.AssertSingleVerifierResult(t, subtestCtx, boot.Lib, sentEvent.MessageID) vr := result.IndexedVerifications.Results[0].VerifierResult require.NotNil(t, vr.Message.TokenTransfer) require.NotNil(t, vr.Message.TokenTransfer.Amount) @@ -171,7 +148,7 @@ func TestEVM2Canton_Basic(t *testing.T) { require.Positive(t, vr.Message.TokenTransfer.Amount.Cmp(big.NewInt(0)), "token transfer amount must be positive") execKey := cciptestinterfaces.MessageEventKey{SeqNum: seqNo, MessageID: sentEvent.MessageID} - executionStateChangedEvent, err := cantonDest.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, utilstests.WaitTimeout(t)) + executionStateChangedEvent, err := boot.Canton.ConfirmExecOnDest(subtestCtx, srcSelector, execKey, devenvtests.ConfirmExecTimeout(t)) require.NoError(t, err) require.Equal(t, cciptestinterfaces.ExecutionStateSuccess, executionStateChangedEvent.State) @@ -180,10 +157,10 @@ func TestEVM2Canton_Basic(t *testing.T) { totalHoldingsFloat, _ := new(big.Float).SetRat(totalHoldingsRat).Float64() t.Logf("Canton receiver total holdings after execute: %.10f", totalHoldingsFloat) - srcBalanceAfter, err := srcChain.GetTokenBalance(subtestCtx, srcSender, srcToken) + srcBalanceAfter, err := boot.EVM.GetTokenBalance(subtestCtx, srcSender, srcToken) require.NoError(t, err) require.NotNil(t, srcBalanceAfter) - dstBalanceAfter, err := cantonDest.GetTokenBalance(subtestCtx, receiver, nil) + dstBalanceAfter, err := boot.Canton.GetTokenBalance(subtestCtx, receiver, nil) require.NoError(t, err) require.NotNil(t, dstBalanceAfter) t.Logf("Token balances after execute: evm_sender=%s canton_receiver=%s", srcBalanceAfter.String(), dstBalanceAfter.String()) diff --git a/ccip/devenv/tests/env.go b/ccip/devenv/tests/env.go index f414d8076..458b6457a 100644 --- a/ccip/devenv/tests/env.go +++ b/ccip/devenv/tests/env.go @@ -1,10 +1,18 @@ package tests import ( + "flag" "fmt" + "os" + "path/filepath" "strings" + "testing" + + "github.com/stretchr/testify/require" ) +const envConfigFile = "CCIP_CONFIG_FILE" + // CCIPEnv names a CCIP e2e target environment. type CCIPEnv string @@ -13,6 +21,20 @@ const ( EnvProdTestnet CCIPEnv = "prod-testnet" ) +var ccipEnvFlag = flag.String( + "ccip-env", + defaultFromEnv("CCIP_ENV", string(EnvDevenv)), + "CCIP e2e environment: devenv (default) or prod-testnet", +) + +func defaultFromEnv(key, fallback string) string { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return v + } + + return fallback +} + // ParseCCIPEnv validates and returns a CCIPEnv from its string form. func ParseCCIPEnv(s string) (CCIPEnv, error) { switch CCIPEnv(strings.TrimSpace(s)) { @@ -27,7 +49,44 @@ func ParseCCIPEnv(s string) (CCIPEnv, error) { } } +// ConfigPath returns the CCV env output TOML filename under ccip/devenv. +func (e CCIPEnv) ConfigPath() string { + switch e { + case EnvDevenv: + return "env-canton-evm-out.toml" + case EnvProdTestnet: + return "env-prod-testnet-out.toml" + default: + return "" + } +} + +// ResolveConfigPath returns the CCV env output TOML filename under ccip/devenv. +// When CCIP_CONFIG_FILE is set, its basename is used instead of the default for env. +func ResolveConfigPath(env CCIPEnv) string { + if override := strings.TrimSpace(os.Getenv(envConfigFile)); override != "" { + return filepath.Base(override) + } + + return env.ConfigPath() +} + // IsRemote reports whether the environment targets live testnet infrastructure. func (e CCIPEnv) IsRemote() bool { return e == EnvProdTestnet } + +// ParseEnvFromFlag reads the -ccip-env flag (defaulting from CCIP_ENV). +func ParseEnvFromFlag(t *testing.T) CCIPEnv { + t.Helper() + + t.Logf("CCIP_ENV env=%q ccip-env flag=%q", + os.Getenv("CCIP_ENV"), + *ccipEnvFlag, + ) + + env, err := ParseCCIPEnv(*ccipEnvFlag) + require.NoError(t, err) + + return env +} diff --git a/ccip/devenv/tests/env_test.go b/ccip/devenv/tests/env_test.go new file mode 100644 index 000000000..5ebbbd6e0 --- /dev/null +++ b/ccip/devenv/tests/env_test.go @@ -0,0 +1,35 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResolveConfigPath(t *testing.T) { + t.Setenv(envConfigFile, "") + + require.Equal(t, "env-canton-evm-out.toml", ResolveConfigPath(EnvDevenv)) + require.Equal(t, "env-prod-testnet-out.toml", ResolveConfigPath(EnvProdTestnet)) + + t.Setenv(envConfigFile, "env-prod-testnet.ci.toml") + require.Equal(t, "env-prod-testnet.ci.toml", ResolveConfigPath(EnvProdTestnet)) + + t.Setenv(envConfigFile, "ccip/devenv/custom.toml") + require.Equal(t, "custom.toml", ResolveConfigPath(EnvDevenv)) +} + +func TestParseCCIPEnv(t *testing.T) { + t.Parallel() + + env, err := ParseCCIPEnv("devenv") + require.NoError(t, err) + require.Equal(t, EnvDevenv, env) + + env, err = ParseCCIPEnv("prod-testnet") + require.NoError(t, err) + require.Equal(t, EnvProdTestnet, env) + + _, err = ParseCCIPEnv("unknown") + require.Error(t, err) +} diff --git a/ccip/devenv/tests/helpers.go b/ccip/devenv/tests/helpers.go index 8d6e77795..257c6e10d 100644 --- a/ccip/devenv/tests/helpers.go +++ b/ccip/devenv/tests/helpers.go @@ -2,23 +2,278 @@ package tests import ( "context" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" "testing" "time" "github.com/Masterminds/semver/v3" + gethcrypto "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/tcapi" "github.com/smartcontractkit/chainlink-ccv/protocol" - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" + "github.com/smartcontractkit/chainlink-canton/testhelpers" ) +const ( + envConfirmExecTimeout = "CANTON_CONFIRM_EXEC_TIMEOUT" + defaultConfirmExecTimeout = 5 * time.Minute + + envConfirmSendTimeout = "CANTON_CONFIRM_SEND_TIMEOUT" +) + +// ConfirmExecTimeout returns the timeout for Canton ConfirmExecOnDest polling. +// Default is 5 minutes; override with CANTON_CONFIRM_EXEC_TIMEOUT (e.g. "10m"). +func ConfirmExecTimeout(t *testing.T) time.Duration { + t.Helper() + + timeout := defaultConfirmExecTimeout + if d := os.Getenv(envConfirmExecTimeout); d != "" { + parsed, err := time.ParseDuration(d) + require.NoError(t, err, "%s=%q invalid", envConfirmExecTimeout, d) + timeout = parsed + } + + return timeout +} + +// ConfirmSendTimeout returns the timeout for ConfirmSendOnSource polling. +// Defaults: devenv 15s, prod-testnet 10m; override with CANTON_CONFIRM_SEND_TIMEOUT (e.g. "30s"). +func ConfirmSendTimeout(t *testing.T, env CCIPEnv) time.Duration { + t.Helper() + + var timeout time.Duration + switch env { + case EnvDevenv: + timeout = 15 * time.Second + case EnvProdTestnet: + timeout = 10 * time.Minute + default: + timeout = 15 * time.Second + } + + if d := os.Getenv(envConfirmSendTimeout); d != "" { + parsed, err := time.ParseDuration(d) + require.NoError(t, err, "%s=%q invalid", envConfirmSendTimeout, d) + timeout = parsed + } + + return timeout +} + +// EVMToCantonMessageOptions returns standard message options for EVM→Canton sends with FTF. +func EVMToCantonMessageOptions(gasLimit uint32, executor, ccvAddr protocol.UnknownAddress) cciptestinterfaces.MessageOptions { + return cciptestinterfaces.MessageOptions{ + ExecutionGasLimit: gasLimit, + FinalityConfig: cantondevenv.EVMToCantonFinalityConfig, + Executor: executor, + CCVs: []protocol.CCV{ + {CCVAddress: ccvAddr, Args: []byte{}, ArgsLen: 0}, + }, + } +} + +// E2EBootstrap holds shared CCIP e2e setup for a selected environment. +type E2EBootstrap struct { + Env CCIPEnv + Cfg *ccv.Cfg + Lib ccv.Lib + ChainMap map[uint64]cciptestinterfaces.CCIP17 + Canton *cantondevenv.Chain + EVM cciptestinterfaces.CCIP17 +} + +// BootstrapE2E loads config, wires verifier observation, and resolves Canton + EVM chains. +func BootstrapE2E(t *testing.T, env CCIPEnv) E2EBootstrap { + t.Helper() + + if env.IsRemote() && os.Getenv("CANTON_GRPC_URL") == "" { + t.Skip("CANTON_GRPC_URL unset: not configured for remote Canton") + } + + configPath := filepath.Join("..", "..", ResolveConfigPath(env)) + in, err := ccv.LoadOutput[ccv.Cfg](configPath) + require.NoError(t, err) + + lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath) + require.NoError(t, err) + + ctx := t.Context() + chainMap, err := lib.ChainsMap(ctx) + require.NoError(t, err) + require.NoError(t, WireVerifierObservationFromLib(lib, chainMap)) + + evmChain := GetChainFromMap(t, blockchain.TypeAnvil, in, chainMap) + cantonChain := GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) + cantonImpl, ok := cantonChain.(*cantondevenv.Chain) + require.True(t, ok, "Canton chain must be *devenv.Chain") + + if !env.IsRemote() { + t.Cleanup(func() { + _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) + require.NoError(t, err) + }) + } + + return E2EBootstrap{ + Env: env, + Cfg: in, + Lib: lib, + ChainMap: chainMap, + Canton: cantonImpl, + EVM: evmChain, + } +} + +// SetupCantonSend prepares Canton for send in both envs. +// devenv mints fee Amulet before SetupSend; prod-testnet assumes the party is already funded. +func (b E2EBootstrap) SetupCantonSend(t *testing.T, ctx context.Context, transferAmount uint64) { + t.Helper() + + fee := uint64(cantondevenv.CantonToEVMFeeAmount) + if !b.Env.IsRemote() { + require.NoError(t, b.Canton.MintTokens(ctx, new(big.Rat).SetUint64(fee))) + } + b.Canton.SetSequentialSends(1) + require.NoError(t, b.Canton.SetupSend(ctx, fee, new(big.Rat).SetUint64(transferAmount))) +} + +// SetupCantonTokenSend prepares Canton for token sends (fee + transfer holdings). +// Devenv mints Amulet; prod-testnet logs required Amulet and transfer instrument balances. +func (b E2EBootstrap) SetupCantonTokenSend(t *testing.T, ctx context.Context, lane TokenLane, sends int) { + t.Helper() + + require.Positive(t, sends) + + fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) + feeTotal := new(big.Rat).SetInt64(int64(sends) * cantondevenv.CantonToEVMTokenTransferFeeAmount) + transferTotalFP := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(sends+1000))) // add a buffer to avoid flakyness + transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) + transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) + + if !b.Env.IsRemote() { + require.NoError(t, b.Canton.MintTokens(ctx, feeTotal)) + require.NoError(t, b.Canton.MintTokens(ctx, transferTotal)) + } else { + instrumentLabel := lane.TransferInstrumentID + if instrumentLabel == "" { + instrumentLabel = string(cantondevenv.AMTInstrument) + } + t.Logf("prod-testnet: ensure Canton party holds at least %s Amulet for fees (%d sends × %d)", + feeTotal.FloatString(10), sends, cantondevenv.CantonToEVMTokenTransferFeeAmount) + t.Logf("prod-testnet: ensure Canton party holds at least %s %s for transfers (%d sends × %s)", + transferTotal.FloatString(10), instrumentLabel, sends, transferPerSend.FloatString(10)) + if lane.TransferInstrument.Admin != "" { + participant, _, err := b.Canton.ClientParticipant() + require.NoError(t, err) + inst := &lane.TransferInstrument + filters := []testhelpers.Filter{ + testhelpers.WithHoldingOwner(participant.PartyID), + testhelpers.WithUnlockedHoldingsOnly(), + } + rows, err := testhelpers.ListHoldingsForInstrument(ctx, participant, inst, filters...) + require.NoError(t, err) + balance, err := testhelpers.GetHoldingsBalance(ctx, participant, inst, filters...) + require.NoError(t, err) + t.Logf("prod-testnet: unlocked holdings for instrument admin=%s id=%s: count=%d total=%s", + lane.TransferInstrument.Admin, lane.TransferInstrument.Id, len(rows), balance.FloatString(10)) + } + } + + b.Canton.SetSequentialSends(sends) + if lane.TransferInstrument.Admin != "" { + require.NoError(t, b.Canton.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) + } else { + require.NoError(t, b.Canton.SetupSend(ctx, fee, transferPerSend)) + } +} + +// SetupCantonReceive deploys the client party's PerPartyRouter before inbound messages +// are executed on Canton (e.g. EVM→Canton). +func (b E2EBootstrap) SetupCantonReceive(t *testing.T, ctx context.Context) { + t.Helper() + require.NoError(t, b.Canton.SetupReceive(ctx)) +} + +// ResolveEVMReceiver returns the EVM wallet address derived from PRIVATE_KEY on prod-testnet, +// or the devenv EOA otherwise. Used as the Canton→EVM message receiver, the EVM→Canton sender, +// and in load tests for EVM-side balance checks. +func (b E2EBootstrap) ResolveEVMReceiver(t *testing.T) protocol.UnknownAddress { + t.Helper() + + if b.Env.IsRemote() { + pkHex := strings.TrimSpace(os.Getenv("PRIVATE_KEY")) + require.NotEmpty(t, pkHex, "PRIVATE_KEY required for prod-testnet EVM receiver") + pkHex = strings.TrimPrefix(pkHex, "0x") + pk, err := gethcrypto.HexToECDSA(pkHex) + require.NoError(t, err) + addr := gethcrypto.PubkeyToAddress(pk.PublicKey) + + return protocol.UnknownAddress(addr.Bytes()) + } + + receiver, err := b.EVM.GetEOAReceiverAddress() + require.NoError(t, err) + + return receiver +} + +// ConfirmEVMSendOnSource confirms an EVM-side CCIP send after SendMessage. +// +// On devenv we poll CCIPMessageSent via ConfirmSendOnSource (local Anvil, small block range). +// On prod-testnet we use sendResult from SendMessage (tx receipt) only: ConfirmSendOnSource +// should be used here but is broken in chainlink-ccv devenv — the event poller scans from +// block 1 to latest on public RPCs and times out with 504 Gateway Timeout. +func (b E2EBootstrap) ConfirmEVMSendOnSource( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + sendResult cciptestinterfaces.MessageSentEvent, +) (cciptestinterfaces.MessageSentEvent, error) { + t.Helper() + + if b.Env.IsRemote() { + return sendResult, nil + } + + return b.EVM.ConfirmSendOnSource( + ctx, + destSelector, + cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, + ConfirmSendTimeout(t, b.Env), + ) +} + +// ConfirmCantonSendOnSource confirms a Canton-side CCIP send after SendMessage. +// Canton tracks the last sent event in memory and polls by sequence number when needed. +func (b E2EBootstrap) ConfirmCantonSendOnSource( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, +) (cciptestinterfaces.MessageSentEvent, error) { + t.Helper() + + return b.Canton.ConfirmSendOnSource( + ctx, + destSelector, + cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, + ConfirmSendTimeout(t, b.Env), + ) +} + func GetContractAddress( t *testing.T, ds datastore.DataStore, @@ -129,7 +384,7 @@ func AssertSingleVerifierResult( result, err := cantondevenv.AssertMessageWithVerifierObservation(ctx, obs, messageID, tcapi.AssertMessageOptions{ TickInterval: time.Second, - Timeout: utilstests.WaitTimeout(t), + Timeout: ConfirmExecTimeout(t), ExpectedVerifierResults: 1, AssertVerifierLogs: false, AssertExecutorLogs: false, diff --git a/ccip/devenv/tests/load/gun_canton2evm_test.go b/ccip/devenv/tests/load/gun_canton2evm_test.go index ccf6998d8..60bdba18f 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_test.go @@ -1,20 +1,14 @@ package load import ( - "fmt" "math/big" - "os" "testing" - chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // registers Canton via init + "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" ) @@ -28,11 +22,13 @@ const ( // TestCanton2EVM_Load runs WASP RPS=1 against the real Canton→EVM path (message-only), // round-robining across every EVM destination found in the env file. // -// Requires a running devenv and ../../env-canton-evm-out.toml (same as the basic e2e test). +// Devenv: requires a running devenv and env-canton-evm-out.toml; pre-mints fee holdings +// and calls SetupSend once before WASP starts. // -// Devenv-specific: this test pre-mints fee holdings and calls SetupSend once before WASP -// starts. The CCIPLoadGun itself is environment-agnostic so the same gun can be reused by -// a future staging/prod runner that assumes pre-funded accounts. +// Prod-testnet: send-only load (Canton send + confirm send, no ConfirmExecOnDest on EVM). +// Set CANTON_GRPC_URL, CANTON_PARTY_ID, CANTON_AUTH_*, PRIVATE_KEY (EVM message receiver), +// CANTON_LOAD_SKIP_EXEC_CONFIRM=true, and pre-fund the Canton party (~50 Amulet per message). +// Verify delivery via indexer/CCIP ops — the test does not assert EVM execution on prod. // //nolint:paralleltest // Canton holdings must stay 1-wide; shares env with e2e. func TestCanton2EVM_Load(t *testing.T) { @@ -40,63 +36,48 @@ func TestCanton2EVM_Load(t *testing.T) { t.Skip("skipping Canton→EVM load test in short mode") } - configPath := "../../env-canton-evm-out.toml" - if _, err := os.Stat(configPath); err != nil { - t.Skipf("skipping Canton→EVM load test: %v (start devenv to generate %s)", err, configPath) - } - - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) - + env := devenvtests.ParseEnvFromFlag(t) + t.Logf("env: %s", env) + boot := devenvtests.BootstrapE2E(t, env) ctx := ccv.Plog.WithContext(t.Context()) - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath, chainsel.FamilyEVM, chainsel.FamilyCanton) - require.NoError(t, err) - - chainMap, err := lib.ChainsMap(ctx) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) - cantonChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonImpl, ok := cantonChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton chain must be *cantondevenv.Chain") + skipExec := loadSkipExecConfirm(t) - destinations := discoverEVMDestinations(t, in, chainMap) + destinations := discoverEVMDestinationsFromBoot(t, boot) require.NotEmpty(t, destinations, "need at least one EVM destination in the env file") t.Logf("Canton→EVM load destinations: %d EVM chain(s)", len(destinations)) for _, d := range destinations { t.Logf(" - selector=%d receiver=%x", d.Chain.ChainSelector(), d.Receiver) } - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) - - ccvAddr, executorAddr := resolveCantonSourceAddrs(t, lib, cantonChain.ChainSelector()) + ccvAddr, executorAddr := resolveCantonSourceAddrs(t, boot.Lib, boot.Canton.ChainSelector()) sched := loadSchedule(t) - estimatedMessages := uint64(sched.rate) * uint64(sched.duration/sched.rateUnit) - if estimatedMessages == 0 { - estimatedMessages = 1 + estimatedMessages := estimateMessages(sched) + requiredAmulet := estimatedMessages * uint64(devenv.CantonToEVMFeeAmount) * mintBufferNumerator / mintBufferDenominator + if boot.Env.IsRemote() { + t.Logf("Prod: ensure Canton party holds at least %d Amulet (estimatedMessages=%d feePerMessage=%d)", + requiredAmulet, estimatedMessages, devenv.CantonToEVMFeeAmount) + } else { + t.Logf("Pre-mint: estimatedMessages=%d feePerMessage=%d totalFeeMint=%d", + estimatedMessages, devenv.CantonToEVMFeeAmount, requiredAmulet) + require.NoError(t, boot.Canton.MintTokens(ctx, new(big.Rat).SetUint64(requiredAmulet))) } - mintAmount := estimatedMessages * uint64(cantondevenv.CantonToEVMFeeAmount) * mintBufferNumerator / mintBufferDenominator - t.Logf("Pre-mint: estimatedMessages=%d feePerMessage=%d totalFeeMint=%d", - estimatedMessages, cantondevenv.CantonToEVMFeeAmount, mintAmount) - require.NoError(t, cantonImpl.MintTokens(ctx, new(big.Rat).SetUint64(mintAmount))) - require.NoError(t, cantonImpl.SetupSend(ctx, uint64(cantondevenv.CantonToEVMFeeAmount), nil)) + require.NoError(t, boot.Canton.SetupSend(ctx, uint64(devenv.CantonToEVMFeeAmount), nil)) gun, err := NewCCIPLoadGun( - cantonChain, + boot.Canton, destinations, ccvAddr, executorAddr, LoadGunOptions{ - ConfirmSend: cantonSourceConfirmSend(cantonChain), - ConfirmExecTimeout: utilstests.WaitTimeout(t), + ConfirmSend: CantonSourceConfirmSend(boot), + ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), + SkipExecConfirm: skipExec, }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm", sched, "message_only") + runWASP(t, gun, "canton-load-canton2evm", sched, "message_only", skipExec, boot.Cfg.IndexerEndpoints) } diff --git a/ccip/devenv/tests/load/gun_canton2evm_token_test.go b/ccip/devenv/tests/load/gun_canton2evm_token_test.go index 2c69b6d72..66edab1d0 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_token_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_token_test.go @@ -1,26 +1,25 @@ package load import ( - "fmt" "math/big" - "os" "testing" - chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // registers Canton via init + "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" ) // TestCanton2EVM_TokenLoad runs WASP RPS=1 against the Canton→EVM token transfer path. // -// Requires a running devenv and ../../env-canton-evm-out.toml. +// Devenv: requires a running devenv and env-canton-evm-out.toml; pre-mints fee + transfer +// holdings via SetupCantonTokenSend before WASP starts. +// +// Prod-testnet: requires a pre-funded Canton party (Amulet + transfer instrument) and +// PRIVATE_KEY wallet with Sepolia ETH for execution gas. Full exec confirm; no EVM balance +// assert on prod — verify delivery via indexer/CCIP ops. // //nolint:paralleltest // Canton holdings must stay 1-wide; shares env with e2e. func TestCanton2EVM_TokenLoad(t *testing.T) { @@ -28,73 +27,61 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { t.Skip("skipping Canton→EVM token load test in short mode") } - configPath := "../../env-canton-evm-out.toml" - if _, err := os.Stat(configPath); err != nil { - t.Skipf("skipping Canton→EVM token load test: %v (start devenv to generate %s)", err, configPath) - } - - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) + env := devenvtests.ParseEnvFromFlag(t) + boot := devenvtests.BootstrapE2E(t, env) ctx := ccv.Plog.WithContext(t.Context()) - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath, chainsel.FamilyEVM, chainsel.FamilyCanton) - require.NoError(t, err) - chainMap, err := lib.ChainsMap(ctx) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) - - cantonChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonImpl, ok := cantonChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton chain must be *cantondevenv.Chain") - - evmSelectors := discoverEVMTokenSelectors(t, in) + evmSelectors := discoverEVMTokenSelectors(t, boot.Cfg) require.NotEmpty(t, evmSelectors, "need at least one EVM token destination in the env file") - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, cantonChain.ChainSelector(), evmSelectors) + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, boot.Canton.ChainSelector(), evmSelectors) t.Logf("Token lane: pool=%s transfer=%s", lane.PoolRef.Qualifier, lane.TransferAmount.String()) - destinations := discoverEVMTokenDestinations(t, in, chainMap, lane) + destinations := discoverEVMTokenDestinationsFromBoot(t, boot, lane) require.NotEmpty(t, destinations, "need at least one EVM token destination in the env file") t.Logf("Canton→EVM token load destinations: %d EVM chain(s)", len(destinations)) + for _, d := range destinations { + t.Logf(" - selector=%d receiver=%x", d.Chain.ChainSelector(), d.Receiver) + } + evmReceiver := boot.ResolveEVMReceiver(t) firstDest := destinations[0] destToken := lane.DestTokenBySelector[firstDest.Chain.ChainSelector()] - receiverBalanceBefore, err := firstDest.Chain.GetTokenBalance(ctx, firstDest.Receiver, destToken) + receiverBalanceBefore, err := firstDest.Chain.GetTokenBalance(ctx, evmReceiver, destToken) require.NoError(t, err) require.NotNil(t, receiverBalanceBefore) - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) - - ccvAddr, executorAddr := resolveCantonSourceAddrs(t, lib, cantonChain.ChainSelector()) sched := loadSchedule(t) + estimatedMessages := estimateMessages(sched) + boot.SetupCantonTokenSend(t, ctx, lane, int(estimatedMessages)) - setupCantonTokenLoadHoldings(t, ctx, cantonImpl, sched, lane) + ccvAddr, executorAddr := resolveCantonSourceAddrs(t, boot.Lib, boot.Canton.ChainSelector()) gun, err := NewCCIPLoadGun( - cantonChain, + boot.Canton, destinations, ccvAddr, executorAddr, LoadGunOptions{ - ConfirmSend: cantonSourceConfirmSend(cantonChain), - ConfirmExecTimeout: utilstests.WaitTimeout(t), + ConfirmSend: CantonSourceConfirmSend(boot), + ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), + SkipExecConfirm: false, }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer") + runWASP(t, gun, "canton-load-canton2evm-token", sched, "token_transfer", false, boot.Cfg.IndexerEndpoints) - receiverBalanceAfter, err := firstDest.Chain.GetTokenBalance(ctx, firstDest.Receiver, destToken) + receiverBalanceAfter, err := firstDest.Chain.GetTokenBalance(ctx, evmReceiver, destToken) require.NoError(t, err) require.NotNil(t, receiverBalanceAfter) - expectedPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointToEVMScale)) + expectedPerMessage := new(big.Int).Mul(lane.TransferAmount, big.NewInt(devenv.CantonFixedPointToEVMScale)) expectedDelta := new(big.Int).Mul(expectedPerMessage, big.NewInt(gun.CallCount())) - expectedBalance := new(big.Int).Add(new(big.Int).Set(receiverBalanceBefore), expectedDelta) t.Logf("EVM receiver token balance: before=%s after=%s expectedDelta=%s calls=%d", receiverBalanceBefore.String(), receiverBalanceAfter.String(), expectedDelta.String(), gun.CallCount()) - require.Equal(t, expectedBalance, receiverBalanceAfter) + if !boot.Env.IsRemote() { + expectedBalance := new(big.Int).Add(new(big.Int).Set(receiverBalanceBefore), expectedDelta) + require.Equal(t, expectedBalance, receiverBalanceAfter) + } } diff --git a/ccip/devenv/tests/load/gun_evm2canton_test.go b/ccip/devenv/tests/load/gun_evm2canton_test.go index b33555040..7d57f5d1f 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_test.go @@ -1,27 +1,21 @@ package load import ( - "fmt" - "os" "testing" - chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // registers Canton via init - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" ) // TestEVM2Canton_Load runs WASP RPS=1 against the real EVM→Canton path (message-only). // -// Requires a running devenv and ../../env-canton-evm-out.toml (same as the basic e2e test). -// EVM source accounts are pre-funded by devenv; no Canton MintTokens/SetupSend. +// Devenv: requires a running devenv and env-canton-evm-out.toml; EVM accounts are pre-funded. +// Prod-testnet: set CANTON_GRPC_URL, CANTON_PARTY_ID, CANTON_AUTH_*, and PRIVATE_KEY; Canton +// party must already be funded (no MintTokens on this path). // //nolint:paralleltest // single-flight exec on Canton dest; shares env with e2e. func TestEVM2Canton_Load(t *testing.T) { @@ -29,49 +23,29 @@ func TestEVM2Canton_Load(t *testing.T) { t.Skip("skipping EVM→Canton load test in short mode") } - configPath := "../../env-canton-evm-out.toml" - if _, err := os.Stat(configPath); err != nil { - t.Skipf("skipping EVM→Canton load test: %v (start devenv to generate %s)", err, configPath) - } - - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) - + env := devenvtests.ParseEnvFromFlag(t) + boot := devenvtests.BootstrapE2E(t, env) ctx := ccv.Plog.WithContext(t.Context()) - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath, chainsel.FamilyEVM, chainsel.FamilyCanton) - require.NoError(t, err) - - chainMap, err := lib.ChainsMap(ctx) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) + boot.SetupCantonReceive(t, ctx) - evmChain := devenvtests.GetChainFromMap(t, blockchain.TypeAnvil, in, chainMap) - cantonChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonImpl, ok := cantonChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton dest chain must be *devenv.Chain") - require.NoError(t, cantonImpl.SetupReceive(ctx)) - cantonDest := discoverCantonDest(t, in, chainMap) + cantonDest := discoverCantonDestFromBoot(t, boot) t.Logf("EVM→Canton load: source=%d dest=%d receiver=%x", - evmChain.ChainSelector(), cantonDest.Chain.ChainSelector(), cantonDest.Receiver) - - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) + boot.EVM.ChainSelector(), cantonDest.Chain.ChainSelector(), cantonDest.Receiver) - ccvAddr, executorAddr := resolveEVMSourceAddrs(t, lib, evmChain.ChainSelector()) + ccvAddr, executorAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( - evmChain, + boot.EVM, []Destination{cantonDest}, ccvAddr, executorAddr, LoadGunOptions{ - ConfirmSend: evmSourceConfirmSend(evmChain), // TODO: this confirmation will change on prod-testnet PR - ConfirmExecTimeout: utilstests.WaitTimeout(t), + ConfirmSend: EVMSourceConfirmSend(boot), + ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), + SkipExecConfirm: false, }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only") + runWASP(t, gun, "canton-load-evm2canton", loadSchedule(t), "message_only", false, boot.Cfg.IndexerEndpoints) } diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index eb44409e6..24f608228 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -1,28 +1,24 @@ package load import ( - "fmt" "math/big" - "os" "testing" - chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // registers Canton via init - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) // TestEVM2Canton_TokenLoad runs WASP RPS=1 against the EVM→Canton token transfer path. // -// Requires a running devenv and ../../env-canton-evm-out.toml. +// Devenv: requires a running devenv and env-canton-evm-out.toml; EVM sender is pre-funded. +// +// Prod-testnet: requires PRIVATE_KEY wallet with TEST tokens and Sepolia ETH on Sepolia, +// plus a pre-funded Canton party for execution fees. // //nolint:paralleltest // single-flight exec on Canton dest; shares env with e2e. func TestEVM2Canton_TokenLoad(t *testing.T) { @@ -30,75 +26,59 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { t.Skip("skipping EVM→Canton token load test in short mode") } - configPath := "../../env-canton-evm-out.toml" - if _, err := os.Stat(configPath); err != nil { - t.Skipf("skipping EVM→Canton token load test: %v (start devenv to generate %s)", err, configPath) - } - - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) + env := devenvtests.ParseEnvFromFlag(t) + boot := devenvtests.BootstrapE2E(t, env) ctx := ccv.Plog.WithContext(t.Context()) - lib, err := ccv.NewLibFromCCVEnv(&ccv.Plog, configPath, chainsel.FamilyEVM, chainsel.FamilyCanton) - require.NoError(t, err) + boot.SetupCantonReceive(t, ctx) - chainMap, err := lib.ChainsMap(ctx) - require.NoError(t, err) - require.NoError(t, devenvtests.WireVerifierObservationFromLib(lib, chainMap)) - - evmChain := devenvtests.GetChainFromMap(t, blockchain.TypeAnvil, in, chainMap) - cantonChain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - cantonImpl, ok := cantonChain.(*cantondevenv.Chain) - require.True(t, ok, "Canton dest chain must be *devenv.Chain") - require.NoError(t, cantonImpl.SetupReceive(ctx)) - - lane := devenvtests.ResolveTokenLane(t, devenvtests.EnvDevenv, in, lib, chainMap, evmChain.ChainSelector(), []uint64{cantonChain.ChainSelector()}) + lane := devenvtests.ResolveTokenLane(t, boot.Env, boot.Cfg, boot.Lib, boot.ChainMap, boot.EVM.ChainSelector(), []uint64{boot.Canton.ChainSelector()}) t.Logf("Token lane: pool=%s transfer=%s srcToken=%x", lane.PoolRef.Qualifier, lane.TransferAmount.String(), lane.SrcToken) - receiverParticipant, _, err := cantonImpl.ClientParticipant() + receiverParticipant, _, err := boot.Canton.ClientParticipant() require.NoError(t, err) require.NotEmpty(t, receiverParticipant.PartyID) - receiver, err := cantonChain.GetEOAReceiverAddress() + receiver, err := boot.Canton.GetEOAReceiverAddress() require.NoError(t, err) - cantonDest := cantonTokenLoadDestination(cantonChain, receiver, lane) + cantonDest := cantonTokenLoadDestination(boot.Canton, receiver, lane) sched := loadSchedule(t) estimatedMessages := estimateMessages(sched) - evmSender, err := evmChain.GetEOAReceiverAddress() - require.NoError(t, err) - senderBalance, err := evmChain.GetTokenBalance(ctx, evmSender, lane.SrcToken) + evmSender := boot.ResolveEVMReceiver(t) + senderBalance, err := boot.EVM.GetTokenBalance(ctx, evmSender, lane.SrcToken) require.NoError(t, err) requiredBalance := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(estimatedMessages))) t.Logf("EVM sender token balance=%s requiredForRun=%s (estimatedMessages=%d; devenv pre-funds sender)", senderBalance.String(), requiredBalance.String(), estimatedMessages) - if senderBalance.Cmp(requiredBalance) < 0 { + if boot.Env.IsRemote() { + require.GreaterOrEqual(t, senderBalance.Cmp(requiredBalance), 0, + "EVM sender token balance %s is below required %s; fund PRIVATE_KEY wallet with TEST tokens on Sepolia", + senderBalance.String(), requiredBalance.String()) + t.Log("prod-testnet: ensure PRIVATE_KEY wallet has Sepolia ETH for send and possible router approve tx") + } else if senderBalance.Cmp(requiredBalance) < 0 { t.Logf("warning: EVM sender balance may be insufficient for full run") } - t.Cleanup(func() { - _, err := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) - require.NoError(t, err) - }) - - ccvAddr, executorAddr := resolveEVMSourceAddrs(t, lib, evmChain.ChainSelector()) + ccvAddr, executorAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( - evmChain, + boot.EVM, []Destination{cantonDest}, ccvAddr, executorAddr, LoadGunOptions{ - ConfirmSend: evmSourceConfirmSend(evmChain), // TODO: this confirmation will change on prod-testnet PR - ConfirmExecTimeout: utilstests.WaitTimeout(t), + ConfirmSend: EVMSourceConfirmSend(boot), + ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), + SkipExecConfirm: false, }, ) require.NoError(t, err) - runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer") + runWASP(t, gun, "canton-load-evm2canton-token", sched, "token_transfer", false, boot.Cfg.IndexerEndpoints) totalHoldingsRat, err := testhelpers.GetHoldingsBalance(ctx, receiverParticipant, nil) require.NoError(t, err) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index 10641fddf..c77732911 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -3,8 +3,6 @@ package load import ( "context" "fmt" - "math" - "math/big" "os" "strings" "testing" @@ -25,20 +23,20 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/wasp" "github.com/stretchr/testify/require" - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" canton_committee_verifier "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/executor" ) const ( - envMessageRate = "CANTON_LOAD_MESSAGE_RATE" - envLoadDuration = "CANTON_LOAD_DURATION" - envLoadCallTimeout = "CANTON_LOAD_CALL_TIMEOUT" - defaultMessageRate = "1/1s" - defaultLoadDuration = 90 * time.Second - defaultLoadCallPadding = 2 * time.Minute - confirmSendTimeout = 30 * time.Second + envMessageRate = "CANTON_LOAD_MESSAGE_RATE" + envLoadDuration = "CANTON_LOAD_DURATION" + envLoadSkipExecConfirm = "CANTON_LOAD_SKIP_EXEC_CONFIRM" + envLoadCallTimeout = "CANTON_LOAD_CALL_TIMEOUT" + defaultMessageRate = "1/1s" + defaultLoadDuration = 90 * time.Second + defaultLoadCallPadding = 2 * time.Minute + defaultSendOnlyCallBudget = 5 * time.Minute ) type scheduleConfig struct { @@ -76,7 +74,19 @@ func loadSchedule(t *testing.T) scheduleConfig { } } -func waspCallTimeout(t *testing.T, gun *CCIPLoadGun, sched scheduleConfig) time.Duration { +func loadSkipExecConfirm(t *testing.T) bool { + t.Helper() + + v := strings.TrimSpace(os.Getenv(envLoadSkipExecConfirm)) + switch strings.ToLower(v) { + case "1", "true", "yes": + return true + default: + return false + } +} + +func waspCallTimeout(t *testing.T, gun *CCIPLoadGun, sched scheduleConfig, skipExecConfirm bool) time.Duration { t.Helper() if v := strings.TrimSpace(os.Getenv(envLoadCallTimeout)); v != "" { @@ -84,51 +94,67 @@ func waspCallTimeout(t *testing.T, gun *CCIPLoadGun, sched scheduleConfig) time. require.NoError(t, err, "%s=%q invalid", envLoadCallTimeout, v) return parsed } + if skipExecConfirm { + return defaultSendOnlyCallBudget + sched.rateUnit + } return gun.ConfirmExecTimeout() + sched.rateUnit + defaultLoadCallPadding } -func printLoadMetrics(t *testing.T, gun *CCIPLoadGun) { +func printLoadMetrics(t *testing.T, gun *CCIPLoadGun, skipExecConfirm bool) { t.Helper() records := gun.Metrics() failures := gun.FailureCounts() - PrintPhaseMetricsSummary(t, records, failures, false) - - ccvMetrics := ToCCVMessageMetrics(records) - if len(ccvMetrics) > 0 { - totals := ccvmetrics.MessageTotals{ - Sent: len(ccvMetrics), - Received: len(ccvMetrics), + PrintPhaseMetricsSummary(t, records, failures, skipExecConfirm) + + if !skipExecConfirm { + ccvMetrics := ToCCVMessageMetrics(records) + if len(ccvMetrics) > 0 { + totals := ccvmetrics.MessageTotals{ + Sent: len(ccvMetrics), + Received: len(ccvMetrics), + } + summary := ccvmetrics.CalculateMetricsSummary(ccvMetrics, totals) + ccvmetrics.PrintMetricsSummary(t, summary) } - summary := ccvmetrics.CalculateMetricsSummary(ccvMetrics, totals) - ccvmetrics.PrintMetricsSummary(t, summary) } } -func logLoadMessageSummary(t *testing.T, gun *CCIPLoadGun) { +func logLoadMessageSummary(t *testing.T, gun *CCIPLoadGun, indexerEndpoints []string) { t.Helper() ids := gun.MessageIDs() lggr := ccv.Plog lggr.Info().Int("count", len(ids)).Msg("Load message summary") + var indexerBase string + if len(indexerEndpoints) > 0 { + indexerBase = strings.TrimSuffix(indexerEndpoints[0], "/") + } + for i, id := range ids { - lggr.Info().Int("index", i+1).Str("messageID", id.String()).Msg("Load message sent") + msgID := id.String() + ev := lggr.Info().Int("index", i+1).Str("messageID", msgID) + if indexerBase != "" { + ev = ev.Str("indexer", fmt.Sprintf("%s/v1/verifierresults/%s", indexerBase, msgID)) + } + ev.Msg("Load message sent") } } -func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string) { +func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfig, scenario string, skipExecConfirm bool, indexerEndpoints []string) { t.Helper() - defer logLoadMessageSummary(t, gun) + defer logLoadMessageSummary(t, gun, indexerEndpoints) - callTimeout := waspCallTimeout(t, gun, sched) + callTimeout := waspCallTimeout(t, gun, sched, skipExecConfirm) ccv.Plog.Info(). Str("messageRate", sched.messageRate). Dur("rateUnit", sched.rateUnit). Dur("loadDuration", sched.duration). Dur("callTimeout", callTimeout). Dur("confirmExecTimeout", gun.ConfirmExecTimeout()). + Bool("skipExecConfirm", skipExecConfirm). Msg("WASP load schedule") labels := map[string]string{ @@ -141,6 +167,9 @@ func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfi if scenario != "" { labels["scenario"] = scenario } + if skipExecConfirm { + labels["skip_exec_confirm"] = "true" + } p := wasp.NewProfile().Add(wasp.NewGenerator(&wasp.Config{ T: t, @@ -164,15 +193,17 @@ func runWASP(t *testing.T, gun *CCIPLoadGun, genName string, sched scheduleConfi require.LessOrEqual(t, gun.MaxConcurrentObserved(), int32(1), "Gun.Call must not overlap (single-flight)") - printLoadMetrics(t, gun) + printLoadMetrics(t, gun, skipExecConfirm) } -func discoverEVMDestinations(t *testing.T, in *ccv.Cfg, chainMap map[uint64]cciptestinterfaces.CCIP17) []Destination { +func discoverEVMDestinationsFromBoot(t *testing.T, boot devenvtests.E2EBootstrap) []Destination { t.Helper() + receiver := boot.ResolveEVMReceiver(t) + dests := make([]Destination, 0) seen := make(map[uint64]struct{}) - for _, bc := range in.Blockchains { + for _, bc := range boot.Cfg.Blockchains { if bc.Type != blockchain.TypeAnvil { continue } @@ -181,12 +212,9 @@ func discoverEVMDestinations(t *testing.T, in *ccv.Cfg, chainMap map[uint64]ccip if _, dup := seen[details.ChainSelector]; dup { continue } - chain, ok := chainMap[details.ChainSelector] + chain, ok := boot.ChainMap[details.ChainSelector] require.True(t, ok, "EVM chain %d not in harness chain map", details.ChainSelector) - receiver, err := chain.GetEOAReceiverAddress() - require.NoError(t, err) - dests = append(dests, evmLoadDestination(chain, receiver)) seen[details.ChainSelector] = struct{}{} } @@ -218,24 +246,27 @@ func discoverEVMTokenSelectors(t *testing.T, in *ccv.Cfg) []uint64 { return selectors } -func discoverEVMTokenDestinations( - t *testing.T, - in *ccv.Cfg, - chainMap map[uint64]cciptestinterfaces.CCIP17, - lane devenvtests.TokenLane, -) []Destination { +func discoverEVMTokenDestinationsFromBoot(t *testing.T, boot devenvtests.E2EBootstrap, lane devenvtests.TokenLane) []Destination { t.Helper() - selectors := discoverEVMTokenSelectors(t, in) - dests := make([]Destination, 0, len(selectors)) - for _, selector := range selectors { - chain, ok := chainMap[selector] - require.True(t, ok, "EVM chain %d not in harness chain map", selector) + receiver := boot.ResolveEVMReceiver(t) - receiver, err := chain.GetEOAReceiverAddress() - require.NoError(t, err) + dests := make([]Destination, 0) + seen := make(map[uint64]struct{}) + for _, bc := range boot.Cfg.Blockchains { + if bc.Type != blockchain.TypeAnvil { + continue + } + details, err := chainsel.GetChainDetailsByChainIDAndFamily(bc.ChainID, chainsel.FamilyEVM) + require.NoError(t, err, "resolve chain selector for chainID=%s", bc.ChainID) + if _, dup := seen[details.ChainSelector]; dup { + continue + } + chain, ok := boot.ChainMap[details.ChainSelector] + require.True(t, ok, "EVM chain %d not in harness chain map", details.ChainSelector) dests = append(dests, evmTokenLoadDestination(chain, receiver, lane)) + seen[details.ChainSelector] = struct{}{} } return dests @@ -254,36 +285,6 @@ func estimateMessages(sched scheduleConfig) uint64 { return estimated } -// setupCantonTokenLoadHoldings pre-mints two separate Amulet holdings (fee + transfer) and -// calls SetupSend once, matching the e2e canton2evm token transfer pattern. -func setupCantonTokenLoadHoldings( - t *testing.T, - ctx context.Context, - cantonImpl *cantondevenv.Chain, - sched scheduleConfig, - lane devenvtests.TokenLane, -) { - t.Helper() - - estimated := estimateMessages(sched) - fee := uint64(cantondevenv.CantonToEVMTokenTransferFeeAmount) - feeTotal := new(big.Rat).SetUint64(estimated * fee) - transferTotalFP := new(big.Int).Mul(lane.TransferAmount, new(big.Int).SetUint64(estimated)) - transferTotal := new(big.Rat).SetFrac(transferTotalFP, big.NewInt(cantondevenv.CantonFixedPointScale)) - transferPerSend := new(big.Rat).SetFrac(lane.TransferAmount, big.NewInt(cantondevenv.CantonFixedPointScale)) - t.Logf("Pre-mint: estimatedMessages=%d feeTotal=%s transferTotal=%s", - estimated, feeTotal.FloatString(10), transferTotal.FloatString(10)) - require.NoError(t, cantonImpl.MintTokens(ctx, feeTotal)) - require.NoError(t, cantonImpl.MintTokens(ctx, transferTotal)) - require.LessOrEqual(t, estimated, uint64(math.MaxInt)) - cantonImpl.SetSequentialSends(int(estimated)) //nolint:gosec // bounded by require above - if lane.TransferInstrument.Admin != "" { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend, lane.TransferInstrument)) - } else { - require.NoError(t, cantonImpl.SetupSend(ctx, fee, transferPerSend)) - } -} - func evmLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.UnknownAddress) Destination { destSelector := chain.ChainSelector() return Destination{ @@ -317,7 +318,7 @@ func evmTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol. Receiver: receiver, Data: fmt.Appendf(nil, "canton2evm token load n=%d dest=%d", callNum, destSelector), TokenAmount: cciptestinterfaces.TokenAmount{ - Amount: new(big.Int).Set(lane.TransferAmount), + Amount: lane.TransferAmount, }, }, cciptestinterfaces.MessageOptions{ ExecutionGasLimit: lane.ExecutionGasLimit, @@ -331,14 +332,39 @@ func evmTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol. } } -func discoverCantonDest(t *testing.T, in *ccv.Cfg, chainMap map[uint64]cciptestinterfaces.CCIP17) Destination { +func discoverCantonDestFromBoot(t *testing.T, boot devenvtests.E2EBootstrap) Destination { t.Helper() - chain := devenvtests.GetChainFromMap(t, blockchain.TypeCanton, in, chainMap) - receiver, err := chain.GetEOAReceiverAddress() + receiver, err := boot.Canton.GetEOAReceiverAddress() require.NoError(t, err) - return cantonLoadDestination(chain, receiver) + return cantonLoadDestination(boot.Canton, receiver) +} + +// EVMSourceConfirmSend returns a ConfirmSendFunc that delegates to BootstrapE2E.ConfirmEVMSendOnSource. +func EVMSourceConfirmSend(boot devenvtests.E2EBootstrap) ConfirmSendFunc { + return func( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + sendResult cciptestinterfaces.MessageSentEvent, + ) (cciptestinterfaces.MessageSentEvent, error) { + return boot.ConfirmEVMSendOnSource(t, ctx, destSelector, seqNo, sendResult) + } +} + +// CantonSourceConfirmSend returns a ConfirmSendFunc that delegates to BootstrapE2E.ConfirmCantonSendOnSource. +func CantonSourceConfirmSend(boot devenvtests.E2EBootstrap) ConfirmSendFunc { + return func( + t *testing.T, + ctx context.Context, + destSelector uint64, + seqNo uint64, + _ cciptestinterfaces.MessageSentEvent, + ) (cciptestinterfaces.MessageSentEvent, error) { + return boot.ConfirmCantonSendOnSource(t, ctx, destSelector, seqNo) + } } func cantonLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.UnknownAddress) Destination { @@ -348,16 +374,9 @@ func cantonLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.Un Receiver: receiver, buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, executorAddr protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { return cciptestinterfaces.MessageFields{ - Receiver: receiver, - Data: fmt.Appendf(nil, "evm2canton load n=%d dest=%d", callNum, destSelector), - }, cciptestinterfaces.MessageOptions{ - ExecutionGasLimit: 200_000, - FinalityConfig: 0, - Executor: executorAddr, - CCVs: []protocol.CCV{ - {CCVAddress: ccvAddr, Args: []byte{}, ArgsLen: 0}, - }, - }, nil + Receiver: receiver, + Data: fmt.Appendf(nil, "evm2canton load n=%d dest=%d", callNum, destSelector), + }, devenvtests.EVMToCantonMessageOptions(200_000, executorAddr, ccvAddr), nil }, } } @@ -374,7 +393,7 @@ func cantonTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protoc Receiver: receiver, Data: fmt.Appendf(nil, "evm2canton token load n=%d dest=%d", callNum, destSelector), TokenAmount: cciptestinterfaces.TokenAmount{ - Amount: new(big.Int).Set(lane.TransferAmount), + Amount: lane.TransferAmount, TokenAddress: lane.SrcToken, }, }, cciptestinterfaces.MessageOptions{ @@ -436,39 +455,3 @@ func resolveEVMSourceAddrs(t *testing.T, lib ccv.Lib, evmSelector uint64) (proto return ccvAddr, executorAddr } - -func cantonSourceConfirmSend(source cciptestinterfaces.CCIP17) ConfirmSendFunc { - return func( - t *testing.T, - ctx context.Context, - destSelector uint64, - seqNo uint64, - _ cciptestinterfaces.MessageSentEvent, - ) (cciptestinterfaces.MessageSentEvent, error) { - return source.ConfirmSendOnSource( - ctx, - destSelector, - cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, - confirmSendTimeout, - ) - } -} - -// TODO: this is needed because EVM impls method has a bug on prod-testnet checks. -// currently just a simple wrapper around the method. -func evmSourceConfirmSend(source cciptestinterfaces.CCIP17) ConfirmSendFunc { - return func( - t *testing.T, - ctx context.Context, - destSelector uint64, - seqNo uint64, - _ cciptestinterfaces.MessageSentEvent, - ) (cciptestinterfaces.MessageSentEvent, error) { - return source.ConfirmSendOnSource( - ctx, - destSelector, - cciptestinterfaces.MessageEventKey{SeqNum: seqNo}, - confirmSendTimeout, - ) - } -} From d760ee476fe7ea2439fb29d50a8b70127f6cea0a Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 11:14:42 -0300 Subject: [PATCH 12/26] fix(load): remove unecessary variable override for devenv --- .github/workflows/ccip-load-tests.yml | 2 +- ccip/devenv/tests/env.go | 5 ++++- ccip/devenv/tests/env_test.go | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ccip-load-tests.yml b/.github/workflows/ccip-load-tests.yml index 8fd9e3656..389a62203 100644 --- a/.github/workflows/ccip-load-tests.yml +++ b/.github/workflows/ccip-load-tests.yml @@ -133,7 +133,7 @@ jobs: CANTON_LOAD_MESSAGE_RATE: ${{ steps.prod_defaults.outputs.message_rate }} CANTON_LOAD_DURATION: ${{ steps.prod_defaults.outputs.load_duration }} CCIP_ENV: ${{ inputs.ccip_env == 'prod-testnet' && 'prod-testnet' || '' }} - CCIP_CONFIG_FILE: ${{ inputs.config_file }} + CCIP_CONFIG_FILE: ${{ inputs.ccip_env == 'prod-testnet' && inputs.config_file || '' }} CANTON_AUTH_TYPE: ${{ inputs.ccip_env == 'prod-testnet' && 'clientCredentials' || '' }} CANTON_AUTH_URL: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_AUTHORIZER_TESTNET || '' }} CANTON_CLIENT_ID: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_CLIENT_ID_TESTNET || '' }} diff --git a/ccip/devenv/tests/env.go b/ccip/devenv/tests/env.go index 458b6457a..ed2a2b019 100644 --- a/ccip/devenv/tests/env.go +++ b/ccip/devenv/tests/env.go @@ -62,8 +62,11 @@ func (e CCIPEnv) ConfigPath() string { } // ResolveConfigPath returns the CCV env output TOML filename under ccip/devenv. -// When CCIP_CONFIG_FILE is set, its basename is used instead of the default for env. +// On prod-testnet, CCIP_CONFIG_FILE overrides the default when set. func ResolveConfigPath(env CCIPEnv) string { + if !env.IsRemote() { + return env.ConfigPath() + } if override := strings.TrimSpace(os.Getenv(envConfigFile)); override != "" { return filepath.Base(override) } diff --git a/ccip/devenv/tests/env_test.go b/ccip/devenv/tests/env_test.go index 5ebbbd6e0..190a50ac3 100644 --- a/ccip/devenv/tests/env_test.go +++ b/ccip/devenv/tests/env_test.go @@ -16,7 +16,8 @@ func TestResolveConfigPath(t *testing.T) { require.Equal(t, "env-prod-testnet.ci.toml", ResolveConfigPath(EnvProdTestnet)) t.Setenv(envConfigFile, "ccip/devenv/custom.toml") - require.Equal(t, "custom.toml", ResolveConfigPath(EnvDevenv)) + require.Equal(t, "env-canton-evm-out.toml", ResolveConfigPath(EnvDevenv)) + require.Equal(t, "custom.toml", ResolveConfigPath(EnvProdTestnet)) } func TestParseCCIPEnv(t *testing.T) { From 0a95f44545bdf5cc9e189133bccc1952fbfc4e0e Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 11:57:29 -0300 Subject: [PATCH 13/26] fix(load): add buffer to messages estimation --- ccip/devenv/tests/load/gun_canton2evm_token_test.go | 3 +-- ccip/devenv/tests/load/load_helpers.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ccip/devenv/tests/load/gun_canton2evm_token_test.go b/ccip/devenv/tests/load/gun_canton2evm_token_test.go index 66edab1d0..0bc1f2b2b 100644 --- a/ccip/devenv/tests/load/gun_canton2evm_token_test.go +++ b/ccip/devenv/tests/load/gun_canton2evm_token_test.go @@ -52,8 +52,7 @@ func TestCanton2EVM_TokenLoad(t *testing.T) { require.NotNil(t, receiverBalanceBefore) sched := loadSchedule(t) - estimatedMessages := estimateMessages(sched) - boot.SetupCantonTokenSend(t, ctx, lane, int(estimatedMessages)) + boot.SetupCantonTokenSend(t, ctx, lane, sequentialSendsForLoad(sched)) ccvAddr, executorAddr := resolveCantonSourceAddrs(t, boot.Lib, boot.Canton.ChainSelector()) diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index c77732911..e404a3329 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -37,6 +37,11 @@ const ( defaultLoadDuration = 90 * time.Second defaultLoadCallPadding = 2 * time.Minute defaultSendOnlyCallBudget = 5 * time.Minute + + // waspSequentialSendBuffer is added to estimateMessages when calling SetSequentialSends + // for token load. WASP always fires more Call() invocations than estimateMessages predicts + // (schedule boundaries, generator resume, post-window drain). + waspSequentialSendBuffer = 3 ) type scheduleConfig struct { @@ -272,6 +277,13 @@ func discoverEVMTokenDestinationsFromBoot(t *testing.T, boot devenvtests.E2EBoot return dests } +// sequentialSendsForLoad returns SetSequentialSends budget for token load setup. +func sequentialSendsForLoad(sched scheduleConfig) int { + estimated := estimateMessages(sched) + //nolint:gosec // estimateMessages is positive; buffer is a small fixed constant + return int(estimated + waspSequentialSendBuffer) +} + func estimateMessages(sched scheduleConfig) uint64 { // rate and rateUnit are validated positive in loadSchedule. if sched.rate <= 0 { From b47f527012e14ba5e88a1a5efb72bb2bde00aa22 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 13:04:02 -0300 Subject: [PATCH 14/26] feat(load): Connect load tests to prod-testnet --- .github/actions/ccip-load-test/action.yml | 28 +- .github/workflows/ccip-load-command.yml | 134 ------- .github/workflows/ccip-load-tests.yml | 77 ++-- .github/workflows/chatops.yml | 1 - .gitignore | 2 + Makefile | 31 ++ ccip/devenv/README.md | 367 ++++++++++++++++-- ccip/devenv/cldf.go | 93 +++-- ccip/devenv/cldf_auth.go | 157 ++++++++ ccip/devenv/cldf_auth_test.go | 201 ++++++++++ ccip/devenv/env-prod-testnet.ci.toml | 52 +++ ccip/devenv/impl.go | 13 +- ccip/devenv/manual_execution.go | 244 ++++++++---- ccip/devenv/manual_execution_test.go | 38 ++ ccip/devenv/tests/e2e/main_test.go | 12 + .../canton_prod_testnet_connection_test.go | 71 ++++ ccip/devenv/tests/token_lane.go | 125 +++++- ccip/devenv/verifier_observation.go | 40 +- ccip/devenv/verifier_observation_test.go | 22 ++ 19 files changed, 1357 insertions(+), 351 deletions(-) delete mode 100644 .github/workflows/ccip-load-command.yml create mode 100644 ccip/devenv/cldf_auth.go create mode 100644 ccip/devenv/cldf_auth_test.go create mode 100644 ccip/devenv/env-prod-testnet.ci.toml create mode 100644 ccip/devenv/manual_execution_test.go create mode 100644 ccip/devenv/tests/e2e/main_test.go create mode 100644 ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go create mode 100644 ccip/devenv/verifier_observation_test.go diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index d85c38c4b..18ededfb7 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -1,8 +1,7 @@ name: Run CCIP load test description: >- - Shared execution for CCIP WASP load tests against local devenv or prod-testnet - infrastructure. Call setup-ccip-devenv and upload-ccip-devenv-logs separately - when you want distinct workflow steps (see ccip-load-tests.yml). + Shared setup and execution for CCIP WASP load tests against local devenv or + prod-testnet infrastructure. inputs: ccip_env: @@ -78,6 +77,16 @@ inputs: runs: using: composite steps: + - name: Setup CCIP devenv + if: inputs.ccip_env == 'devenv' + uses: ./.github/actions/setup-ccip-devenv + with: + canton-ref: ${{ inputs.canton_ref }} + canton-path: ${{ inputs.canton_path }} + ccv-iam-role: ${{ inputs.ccv-iam-role }} + jd-registry: ${{ inputs.jd-registry }} + jd-image: ${{ inputs.jd-image }} + - name: Install Go (prod-testnet) if: inputs.ccip_env == 'prod-testnet' uses: actions/setup-go@v6 @@ -107,7 +116,7 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" + go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=devenv -run "$TEST_RUN" - name: Run load tests (prod-testnet) if: inputs.ccip_env == 'prod-testnet' @@ -135,4 +144,13 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" + go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" + + - name: Upload devenv logs + if: always() && inputs.ccip_env == 'devenv' + uses: ./.github/actions/upload-ccip-devenv-logs + with: + canton-path: ${{ inputs.canton_path }} + test-package-dir: load + log-dump-suffix: ccip-load-tests + artifact-name: container-logs-ccip-load-tests diff --git a/.github/workflows/ccip-load-command.yml b/.github/workflows/ccip-load-command.yml deleted file mode 100644 index cfcf4db12..000000000 --- a/.github/workflows/ccip-load-command.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: CCIP Load (ChatOps) -on: - repository_dispatch: - types: [ccip-load-command] - -concurrency: - group: ccip-load-pr-${{ github.event.client_payload.pull_request.number }} - cancel-in-progress: false - -permissions: {} - -jobs: - load: - name: CCIP load (ChatOps) - if: | - github.event.client_payload.slash_command.command == 'ccip-load' && - github.event.client_payload.pull_request.head.repo.full_name == github.repository - permissions: - contents: read - id-token: write - pull-requests: write - runs-on: ubuntu-latest-8cores-32GB - timeout-minutes: 120 - steps: - - name: Setup GitHub Token - id: setup-github-token - uses: smartcontractkit/.github/actions/setup-github-token@ef78fa97bf3c77de6563db1175422703e9e6674f # setup-github-token@0.2.1 - with: - aws-role-arn: ${{ secrets.GATI_AWS_ROLE_CANTON_CICD }} - aws-lambda-url: ${{ secrets.GATI_AWS_LABDA_URL_INTEGRATIONS }} - aws-region: ${{ secrets.GATI_AWS_REGION }} - - - name: Post comment - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 - with: - repo-token: ${{ steps.setup-github-token.outputs.access-token }} - refresh-message-position: true - issue: ${{ github.event.client_payload.github.payload.issue.number }} - message: | - CCIP load test running... - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - - - name: Check out PR branch - uses: actions/checkout@v6 - with: - ref: ${{ github.event.client_payload.pull_request.head.sha }} - token: ${{ steps.setup-github-token.outputs.access-token }} - - - name: Resolve inputs - id: params - env: - NAMED_CCIP_ENV: ${{ github.event.client_payload.slash_command.args.named.ccip_env }} - NAMED_DIRECTION: ${{ github.event.client_payload.slash_command.args.named.direction }} - NAMED_MESSAGE_RATE: ${{ github.event.client_payload.slash_command.args.named.message_rate }} - NAMED_LOAD_DURATION: ${{ github.event.client_payload.slash_command.args.named.load_duration }} - NAMED_TEST_TIMEOUT: ${{ github.event.client_payload.slash_command.args.named.test_timeout }} - NAMED_CONFIG_FILE: ${{ github.event.client_payload.slash_command.args.named.config_file }} - NAMED_SKIP_EXEC_CONFIRM: ${{ github.event.client_payload.slash_command.args.named.skip_exec_confirm }} - NAMED_CONFIRM_EXEC_TIMEOUT: ${{ github.event.client_payload.slash_command.args.named.confirm_exec_timeout }} - NAMED_PARTY_ID: ${{ github.event.client_payload.slash_command.args.named.party_id }} - NAMED_GRPC_URL: ${{ github.event.client_payload.slash_command.args.named.grpc_url }} - run: | - echo "ccip_env=${NAMED_CCIP_ENV:-prod-testnet}" >> "$GITHUB_OUTPUT" - echo "direction=${NAMED_DIRECTION:-canton2evm}" >> "$GITHUB_OUTPUT" - echo "message_rate=${NAMED_MESSAGE_RATE:-1/45s}" >> "$GITHUB_OUTPUT" - echo "load_duration=${NAMED_LOAD_DURATION:-2m}" >> "$GITHUB_OUTPUT" - echo "config_file=${NAMED_CONFIG_FILE:-env-prod-testnet.ci.toml}" >> "$GITHUB_OUTPUT" - echo "skip_exec_confirm=${NAMED_SKIP_EXEC_CONFIRM:-true}" >> "$GITHUB_OUTPUT" - echo "confirm_exec_timeout=${NAMED_CONFIRM_EXEC_TIMEOUT:-10m}" >> "$GITHUB_OUTPUT" - echo "party_id=${NAMED_PARTY_ID:-u_d53a15c42af6::1220c250c23c55120f7c758bccc5cbc739629015ab921594e1c29656981f985bffa7}" >> "$GITHUB_OUTPUT" - echo "grpc_url=${NAMED_GRPC_URL:-testnet.cv1.bcy-v.metalhosts.com:443}" >> "$GITHUB_OUTPUT" - - direction="${NAMED_DIRECTION:-canton2evm}" - if [ -n "${NAMED_TEST_TIMEOUT}" ]; then - echo "test_timeout=${NAMED_TEST_TIMEOUT}" >> "$GITHUB_OUTPUT" - elif [ "$direction" = "evm2canton" ]; then - echo "test_timeout=45m" >> "$GITHUB_OUTPUT" - else - echo "test_timeout=30m" >> "$GITHUB_OUTPUT" - fi - - - name: Setup CCIP devenv - if: steps.params.outputs.ccip_env == 'devenv' - uses: ./.github/actions/setup-ccip-devenv - with: - canton-path: . - ccv-iam-role: ${{ secrets.CCV_IAM_ROLE }} - jd-registry: ${{ secrets.JD_REGISTRY }} - jd-image: ${{ secrets.JD_IMAGE }} - - - name: Run CCIP load test - uses: ./.github/actions/ccip-load-test - with: - ccip_env: ${{ steps.params.outputs.ccip_env }} - direction: ${{ steps.params.outputs.direction }} - message_rate: ${{ steps.params.outputs.message_rate }} - load_duration: ${{ steps.params.outputs.load_duration }} - test_timeout: ${{ steps.params.outputs.test_timeout }} - config_file: ${{ steps.params.outputs.config_file }} - canton_path: . - skip_exec_confirm: ${{ steps.params.outputs.skip_exec_confirm }} - confirm_exec_timeout: ${{ steps.params.outputs.confirm_exec_timeout }} - party_id: ${{ steps.params.outputs.party_id }} - grpc_url: ${{ steps.params.outputs.grpc_url }} - CANTON_OKTA_AUTHORIZER_TESTNET: ${{ secrets.CANTON_OKTA_AUTHORIZER_TESTNET }} - CANTON_OKTA_CLIENT_ID_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_ID_TESTNET }} - CANTON_OKTA_CLIENT_SECRET_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET }} - CCIP_PROD_TESTNET_PRIVATE_KEY: ${{ secrets.CCIP_PROD_TESTNET_PRIVATE_KEY }} - - - name: Upload devenv logs - if: always() && steps.params.outputs.ccip_env == 'devenv' - uses: ./.github/actions/upload-ccip-devenv-logs - with: - canton-path: . - test-package-dir: load - log-dump-suffix: ccip-load-tests - artifact-name: container-logs-ccip-load-tests - - - name: Update comment - if: always() - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 - with: - repo-token: ${{ steps.setup-github-token.outputs.access-token }} - refresh-message-position: true - issue: ${{ github.event.client_payload.github.payload.issue.number }} - message-success: | - CCIP load test passed. - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - message-failure: | - CCIP load test failed. - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - message-cancelled: | - CCIP load test cancelled. - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/ccip-load-tests.yml b/.github/workflows/ccip-load-tests.yml index 389a62203..18a1e00b0 100644 --- a/.github/workflows/ccip-load-tests.yml +++ b/.github/workflows/ccip-load-tests.yml @@ -89,7 +89,7 @@ jobs: echo "load_duration=${{ inputs.load_duration }}" >> "$GITHUB_OUTPUT" fi if [ "${{ inputs.test_timeout }}" = "40m" ]; then - if [ "${{ inputs.direction }}" = "evm2canton" ]; then + if [ "${{ inputs.direction }}" = "evm2canton" ] || [ "${{ inputs.direction }}" = "evm2canton-token" ]; then echo "test_timeout=45m" >> "$GITHUB_OUTPUT" else echo "test_timeout=30m" >> "$GITHUB_OUTPUT" @@ -97,6 +97,12 @@ jobs: else echo "test_timeout=${{ inputs.test_timeout }}" >> "$GITHUB_OUTPUT" fi + direction="${{ inputs.direction }}" + if [ "$direction" = "canton2evm" ]; then + echo "skip_exec_confirm=true" >> "$GITHUB_OUTPUT" + else + echo "skip_exec_confirm=false" >> "$GITHUB_OUTPUT" + fi else echo "canton_path=chainlink-canton" >> "$GITHUB_OUTPUT" echo "message_rate=${{ inputs.message_rate }}" >> "$GITHUB_OUTPUT" @@ -104,60 +110,23 @@ jobs: echo "test_timeout=${{ inputs.test_timeout }}" >> "$GITHUB_OUTPUT" fi - - name: Setup CCIP devenv - if: inputs.ccip_env == 'devenv' - uses: ./.github/actions/setup-ccip-devenv + - name: Run CCIP load test + uses: ./.github/actions/ccip-load-test with: - canton-ref: ${{ inputs.canton_ref }} - canton-path: ${{ steps.prod_defaults.outputs.canton_path }} + ccip_env: ${{ inputs.ccip_env }} + direction: ${{ inputs.direction }} + message_rate: ${{ steps.prod_defaults.outputs.message_rate }} + load_duration: ${{ steps.prod_defaults.outputs.load_duration }} + test_timeout: ${{ steps.prod_defaults.outputs.test_timeout }} + config_file: ${{ inputs.config_file }} + canton_path: ${{ steps.prod_defaults.outputs.canton_path }} + canton_ref: ${{ inputs.canton_ref }} + skip_exec_confirm: ${{ inputs.ccip_env == 'prod-testnet' && steps.prod_defaults.outputs.skip_exec_confirm || inputs.skip_exec_confirm }} + confirm_exec_timeout: ${{ inputs.confirm_exec_timeout }} + CANTON_OKTA_AUTHORIZER_TESTNET: ${{ secrets.CANTON_OKTA_AUTHORIZER_TESTNET }} + CANTON_OKTA_CLIENT_ID_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_ID_TESTNET }} + CANTON_OKTA_CLIENT_SECRET_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET }} + CCIP_PROD_TESTNET_PRIVATE_KEY: ${{ secrets.CCIP_PROD_TESTNET_PRIVATE_KEY }} ccv-iam-role: ${{ secrets.CCV_IAM_ROLE }} jd-registry: ${{ secrets.JD_REGISTRY }} jd-image: ${{ secrets.JD_IMAGE }} - - - name: Install Go (prod-testnet) - if: inputs.ccip_env == 'prod-testnet' - uses: actions/setup-go@v6 - with: - cache: true - go-version-file: ${{ steps.prod_defaults.outputs.canton_path }}/go.mod - cache-dependency-path: ${{ steps.prod_defaults.outputs.canton_path }}/go.sum - - - name: Download Go dependencies (prod-testnet) - if: inputs.ccip_env == 'prod-testnet' - working-directory: ${{ steps.prod_defaults.outputs.canton_path }} - run: go mod download - - - name: Run CCIP load test (${{ inputs.direction }}) - working-directory: ${{ steps.prod_defaults.outputs.canton_path }}/ccip/devenv/tests/load - env: - CANTON_LOAD_MESSAGE_RATE: ${{ steps.prod_defaults.outputs.message_rate }} - CANTON_LOAD_DURATION: ${{ steps.prod_defaults.outputs.load_duration }} - CCIP_ENV: ${{ inputs.ccip_env == 'prod-testnet' && 'prod-testnet' || '' }} - CCIP_CONFIG_FILE: ${{ inputs.ccip_env == 'prod-testnet' && inputs.config_file || '' }} - CANTON_AUTH_TYPE: ${{ inputs.ccip_env == 'prod-testnet' && 'clientCredentials' || '' }} - CANTON_AUTH_URL: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_AUTHORIZER_TESTNET || '' }} - CANTON_CLIENT_ID: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_CLIENT_ID_TESTNET || '' }} - CANTON_CLIENT_SECRET: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET || '' }} - CANTON_PARTY_ID: ${{ inputs.ccip_env == 'prod-testnet' && 'u_d53a15c42af6::1220c250c23c55120f7c758bccc5cbc739629015ab921594e1c29656981f985bffa7' || '' }} - CANTON_GRPC_URL: ${{ inputs.ccip_env == 'prod-testnet' && 'testnet.cv1.bcy-v.metalhosts.com:443' || '' }} - CANTON_CONFIRM_EXEC_TIMEOUT: ${{ inputs.confirm_exec_timeout }} - CANTON_LOAD_SKIP_EXEC_CONFIRM: ${{ inputs.skip_exec_confirm }} - PRIVATE_KEY: ${{ inputs.ccip_env == 'prod-testnet' && secrets.CCIP_PROD_TESTNET_PRIVATE_KEY || '' }} - run: | - case "${{ inputs.direction }}" in - canton2evm) TEST_RUN='^TestCanton2EVM_Load$' ;; - evm2canton) TEST_RUN='^TestEVM2Canton_Load$' ;; - canton2evm-token) TEST_RUN='^TestCanton2EVM_TokenLoad$' ;; - evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; - *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; - esac - go test -timeout "${{ steps.prod_defaults.outputs.test_timeout }}" -v -count 1 -run "$TEST_RUN" - - - name: Upload devenv logs - if: always() && inputs.ccip_env == 'devenv' - uses: ./.github/actions/upload-ccip-devenv-logs - with: - canton-path: ${{ steps.prod_defaults.outputs.canton_path }} - test-package-dir: load - log-dump-suffix: ccip-load-tests - artifact-name: container-logs-ccip-load-tests diff --git a/.github/workflows/chatops.yml b/.github/workflows/chatops.yml index 6b0fae541..bad73e698 100644 --- a/.github/workflows/chatops.yml +++ b/.github/workflows/chatops.yml @@ -26,4 +26,3 @@ jobs: permission: write commands: | auto-fix - ccip-load diff --git a/.gitignore b/.gitignore index 161187529..4a9e611eb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ scripts/prod_testnet/.env eds/staging_testnet_canton.local.toml eds/prod_testnet_canton.local.toml eds/prod_testnet_canton.local.secrets.toml +ccip/devenv/.env.staging.local +ccip/devenv/.env.prod-testnet.local scripts/prod_mainnet/.env scripts/prod_mainnet/.env.example .env.prod-testnet.local diff --git a/Makefile b/Makefile index 57e1ec0dd..a82681afc 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,15 @@ run-canton2evm-load: ## Canton→EVM WASP load (requires running devenv + env-ca run-evm2canton-load: ## EVM→Canton WASP load (requires running devenv + env-canton-evm-out.toml). cd ccip/devenv/tests/load && go test -timeout 15m -v -count 1 -run '^TestEVM2Canton_Load$$' +.PHONY: run-evm2canton-load-prod +run-evm2canton-load-prod: ## EVM→Canton WASP load on prod-testnet (Canton TestNet + Sepolia; set CANTON_* and PRIVATE_KEY). + cd ccip/devenv/tests/load && go test -timeout 45m -v -count 1 -ccip-env=prod-testnet -run '^TestEVM2Canton_Load$$' + +.PHONY: run-canton2evm-load-prod +run-canton2evm-load-prod: ## Canton→EVM WASP load on prod-testnet (send-only; set CANTON_* and PRIVATE_KEY). + cd ccip/devenv/tests/load && CANTON_LOAD_SKIP_EXEC_CONFIRM=true go test -timeout 30m -v -count=1 \ + -ccip-env=prod-testnet -run '^TestCanton2EVM_Load$$' + .PHONY: run-canton2evm-token-load run-canton2evm-token-load: ## Canton→EVM token WASP load (requires running devenv + env-canton-evm-out.toml). cd ccip/devenv/tests/load && go test -timeout 20m -v -count 1 -run '^TestCanton2EVM_TokenLoad$$' @@ -115,6 +124,28 @@ run-canton2evm-token-load: ## Canton→EVM token WASP load (requires running dev run-evm2canton-token-load: ## EVM→Canton token WASP load (requires running devenv + env-canton-evm-out.toml). cd ccip/devenv/tests/load && go test -timeout 20m -v -count 1 -run '^TestEVM2Canton_TokenLoad$$' +.PHONY: run-canton2evm-token-load-prod +run-canton2evm-token-load-prod: ## Canton→EVM token WASP load on prod-testnet (set CANTON_* and PRIVATE_KEY). + cd ccip/devenv/tests/load && go test -timeout 30m -v -count=1 \ + -ccip-env=prod-testnet -run '^TestCanton2EVM_TokenLoad$$' + +.PHONY: run-evm2canton-token-load-prod +run-evm2canton-token-load-prod: ## EVM→Canton token WASP load on prod-testnet (set CANTON_* and PRIVATE_KEY). + cd ccip/devenv/tests/load && go test -timeout 45m -v -count=1 \ + -ccip-env=prod-testnet -run '^TestEVM2Canton_TokenLoad$$' + +.PHONY: run-evm2canton-token-e2e-prod +run-evm2canton-token-e2e-prod: ## EVM→Canton token e2e on prod-testnet. + cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + -ccip-env=prod-testnet \ + -run '^TestEVM2Canton_Basic$/^token_transfer$$' + +.PHONY: run-canton2evm-token-e2e-prod +run-canton2evm-token-e2e-prod: ## Canton→EVM token e2e on prod-testnet. + cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + -ccip-env=prod-testnet \ + -run '^TestCanton2EVM_Basic$/^EOA receiver and default committee verifier token transfer$$' + .PHONY: build-run-e2e-tests build-run-e2e-tests: start-devenv run-e2e-tests diff --git a/ccip/devenv/README.md b/ccip/devenv/README.md index 05e11518c..f0458db33 100644 --- a/ccip/devenv/README.md +++ b/ccip/devenv/README.md @@ -7,7 +7,7 @@ environment that includes Canton. Make sure to checkout [chainlink-ccv](https://github.com/smartcontractkit/chainlink-ccv/) in a parallel directory, i.e. so that chainlink-canton and chainlink-ccv have the same parent directory. This is needed so that we can build -needed docker images. Use ref **`695181f87033217a197386366e1ab563198f482e`** (matches `go.mod` and CI). +needed docker images. ## Spin up an environment @@ -30,13 +30,54 @@ After the environment is spun up, you can run a test like: make run-e2e-tests ``` +### E2E environment selection (`-ccip-env` / `CCIP_ENV`) + +Message e2e tests run against **local devenv** by default or **Canton TestNet + Sepolia** when prod-testnet is selected. Set the environment by name (not TOML path): + +| Value | Config file | Remote | +|---|---|---| +| `devenv` (default) | [`env-canton-evm-out.toml`](./env-canton-evm-out.toml) | no | +| `prod-testnet` | [`env-prod-testnet-out.toml`](./env-prod-testnet-out.toml) | yes | + +Use the `-ccip-env` flag or `CCIP_ENV` env var (flag wins if both are set): + +```bash +# Local devenv (default) +cd ccip/devenv/tests/e2e && go test -v -run 'TestCanton2EVM_Basic/EOA' -count=1 + +# Prod testnet +CCIP_ENV=prod-testnet \ + CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_*=... \ + PRIVATE_KEY=... \ + go test -timeout 8m -v -count=1 -ccip-env=prod-testnet \ + -run 'TestEVM2Canton_Basic/message|TestCanton2EVM_Basic/EOA' +``` + +**Prod prerequisites** + +- Canton party wallet funded with at least **50 Amulet units** (message fee) +- Canton auth env vars (`CANTON_GRPC_URL`, `CANTON_PARTY_ID`, `CANTON_AUTH_*`) — see [Prod testnet connection smoke test](#prod-testnet-connection-smoke-test) +- Sepolia gas via `PRIVATE_KEY` (EVM sender / receiver for prod runs) + +**Optional instance ID overrides** (defaults: `test-router`, `e2e-ccipsender`, `e2e-receiver`): + +| Env var | Default | +|---|---| +| `CANTON_ROUTER_INSTANCE_ID` | `test-router` | +| `CANTON_SENDER_INSTANCE_ID` | `e2e-ccipsender` | +| `CANTON_RECEIVER_INSTANCE_ID` | `e2e-receiver` | + +Token e2e: **EVM→Canton token** and **Canton→EVM token** are supported on prod-testnet (see [EVM→Canton token e2e (prod-testnet)](#evm→canton-token-e2e-prod-testnet) and [Canton→EVM token e2e (prod-testnet)](#canton→evm-token-e2e-prod-testnet)). A second prod run reuses existing router/sender/receiver contracts on ledger when instance IDs match. + ## Load tests Load tests live in `ccip/devenv/tests/load`. They use [WASP](https://pkg.go.dev/github.com/smartcontractkit/chainlink-testing-framework/wasp) and run sequentially (RPS=1) because Canton holdings are single-flight. -### Canton → EVM load (requires devenv) +### Canton → EVM load + +Sequential Canton→EVM messages round-robined across every EVM destination in the env file. -Sequential Canton→EVM messages round-robined across every EVM destination in the env file. Requires `make start-devenv` so `ccip/devenv/env-canton-evm-out.toml` exists. +**Devenv** (requires `make start-devenv` so `ccip/devenv/env-canton-evm-out.toml` exists): pre-mints fee holdings and calls `SetupSend` once before WASP starts. Full send + EVM exec confirmation per message. Schedule is configured via env vars (defaults are `1/1s` for 90s): @@ -64,9 +105,32 @@ cd ccip/devenv/tests/load && go test -timeout 15m -v -count 1 -run '^TestCanton2 If the out file is missing the test skips with a hint. -### Canton → EVM token load (requires devenv) +**Prod-testnet** (Canton TestNet + Sepolia): send-only message load — Canton send + confirm send, no `ConfirmExecOnDest` on EVM (executor not available on prod). Set `CANTON_LOAD_SKIP_EXEC_CONFIRM=true`. Verify delivery via indexer/CCIP ops; the test does not assert EVM execution. -Separate test from message-only load: `TestCanton2EVM_TokenLoad`. Resolves the token lane declared in [`token_transfer_config.toml`](./tests/token_transfer_config.toml) (see [Token lane configuration](#token-lane-configuration)) against the source chain's `GetTokenTransferConfigs`, validating every destination has the lane. Pre-mints Canton fee + transfer holdings, runs WASP, then asserts EVM receiver token balance delta. +Prerequisites: `CANTON_GRPC_URL`, `CANTON_PARTY_ID`, `CANTON_AUTH_*`, `PRIVATE_KEY` (EVM message receiver wallet), and a pre-funded Canton party (~50 Amulet per message at `CantonToEVMFeeAmount`). + +```bash +CCIP_ENV=prod-testnet \ +CANTON_LOAD_SKIP_EXEC_CONFIRM=true \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_CLIENT_ID=... CANTON_AUTH_CLIENT_SECRET=... \ +PRIVATE_KEY=0x... \ +CANTON_LOAD_MESSAGE_RATE=1/10s \ +CANTON_LOAD_DURATION=5m \ +go test -timeout 30m -v -count=1 -ccip-env=prod-testnet \ + -run '^TestCanton2EVM_Load$' ./ccip/devenv/tests/load/ +``` + +Or from repo root: + +```bash +make run-canton2evm-load-prod +``` + +### Canton → EVM token load + +Separate test from message-only load: `TestCanton2EVM_TokenLoad`. Resolves the token lane declared in [`token_transfer_config.toml`](./tests/token_transfer_config.toml) (see [Token lane configuration](#token-lane-configuration)) against the source chain's `GetTokenTransferConfigs`, validating every destination has the lane. Runs WASP with full exec confirm; on devenv, asserts EVM receiver token balance delta. + +**Devenv** (requires running devenv + `env-canton-evm-out.toml`): pre-mints Canton fee + transfer holdings via `SetupCantonTokenSend`. ```bash make run-canton2evm-token-load @@ -78,9 +142,35 @@ Equivalent: cd ccip/devenv/tests/load && go test -timeout 20m -v -count 1 -run '^TestCanton2EVM_TokenLoad$' ``` -### EVM → Canton load (requires devenv) +**Prod-testnet** (Canton TestNet + Sepolia): full per-message confirmation (send → receipt on EVM → `ConfirmExecOnDest`). Pre-fund the Canton party before the run: + +- **Amulet** for CCIP fees: `estimatedMessages × 130` (see `CantonToEVMTokenTransferFeeAmount`) +- **LINK** (`link-token`): `estimatedMessages × 100` fixed-point per send (from `[prod-testnet.canton_to_evm]` `transfer_amount`) + +Also set `PRIVATE_KEY` to a Sepolia wallet with ETH for execution gas on the EVM receiver. The test logs EVM receiver balance before/after but does **not** assert balance on prod — verify delivery via indexer/CCIP ops. + +```bash +CCIP_ENV=prod-testnet \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_CLIENT_ID=... CANTON_AUTH_CLIENT_SECRET=... \ +PRIVATE_KEY=0x... \ +CANTON_LOAD_MESSAGE_RATE=1/30s \ +CANTON_LOAD_DURATION=5m \ +CANTON_CONFIRM_EXEC_TIMEOUT=10m \ +go test -timeout 30m -v -count=1 -ccip-env=prod-testnet \ + -run '^TestCanton2EVM_TokenLoad$' ./ccip/devenv/tests/load/ +``` + +Or from repo root: + +```bash +make run-canton2evm-token-load-prod +``` + +### EVM → Canton load + +Sequential EVM→Canton messages against the Canton destination in the env file. Uses the same schedule env vars as Canton→EVM (`CANTON_LOAD_MESSAGE_RATE`, `CANTON_LOAD_DURATION`). -Sequential EVM→Canton messages against the Canton destination in the env file. Uses the same schedule env vars as Canton→EVM (`CANTON_LOAD_MESSAGE_RATE`, `CANTON_LOAD_DURATION`). EVM accounts are pre-funded by devenv; no Canton pre-mint. +**Devenv** (requires running devenv + `env-canton-evm-out.toml`): EVM accounts are pre-funded by devenv; no Canton pre-mint. Example — 1 message every 20 seconds for 10 minutes: @@ -99,9 +189,32 @@ Equivalent: cd ccip/devenv/tests/load && go test -timeout 15m -v -count 1 -run '^TestEVM2Canton_Load$' ``` -### EVM → Canton token load (requires devenv) +**Prod-testnet** (Canton TestNet + Sepolia): message-only load with full per-message confirmation (send → receipt on EVM → `ConfirmExecOnDest` on Canton). Use a conservative rate — each iteration is synchronous (~30–60s end-to-end on prod), so WASP RPS=1 is effectively bounded by confirm latency. Budget for Sepolia gas per send plus Canton execution fees. Token load is also supported on prod (see [Canton → EVM token load](#canton--evm-token-load) and [EVM → Canton token load](#evm--canton-token-load)). + +Prerequisites: `CANTON_GRPC_URL`, `CANTON_PARTY_ID`, `CANTON_AUTH_*`, `PRIVATE_KEY` (Sepolia sender/receiver wallet), and a pre-funded Canton party. + +```bash +CCIP_ENV=prod-testnet \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_CLIENT_ID=... CANTON_AUTH_CLIENT_SECRET=... \ +PRIVATE_KEY=0x... \ +CANTON_LOAD_MESSAGE_RATE=1/30s \ +CANTON_LOAD_DURATION=5m \ +CANTON_CONFIRM_EXEC_TIMEOUT=10m \ +go test -timeout 45m -v -count=1 -ccip-env=prod-testnet \ + -run '^TestEVM2Canton_Load$' ./ccip/devenv/tests/load/ +``` + +Or from repo root: + +```bash +make run-evm2canton-load-prod +``` + +### EVM → Canton token load + +Separate test from message-only load: `TestEVM2Canton_TokenLoad`. Resolves the token lane declared in [`token_transfer_config.toml`](./tests/token_transfer_config.toml) (see [Token lane configuration](#token-lane-configuration)), logs EVM sender balance vs estimated transfer need, runs WASP, logs Canton holdings post-run. -Separate test from message-only load: `TestEVM2Canton_TokenLoad`. Resolves the token lane declared in [`token_transfer_config.toml`](./tests/token_transfer_config.toml) (see [Token lane configuration](#token-lane-configuration)), logs EVM sender balance vs estimated transfer need (devenv pre-funds sender), runs WASP, logs Canton holdings post-run. +**Devenv** (requires running devenv + `env-canton-evm-out.toml`): EVM sender is pre-funded by devenv. ```bash make run-evm2canton-token-load @@ -113,61 +226,205 @@ Equivalent: cd ccip/devenv/tests/load && go test -timeout 20m -v -count 1 -run '^TestEVM2Canton_TokenLoad$' ``` +**Prod-testnet** (Canton TestNet + Sepolia): full per-message confirmation. Fund `PRIVATE_KEY` wallet with: + +- **TEST** ERC-20 tokens on Sepolia: `estimatedMessages × transfer_amount` from `[prod-testnet.evm_to_canton]` (default `1000000000000000000` wei per send) +- **Sepolia ETH** for send and possible router approve tx + +Also ensure the Canton party is funded for execution fees. + +```bash +CCIP_ENV=prod-testnet \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_CLIENT_ID=... CANTON_AUTH_CLIENT_SECRET=... \ +PRIVATE_KEY=0x... \ +CANTON_LOAD_MESSAGE_RATE=1/30s \ +CANTON_LOAD_DURATION=5m \ +CANTON_CONFIRM_EXEC_TIMEOUT=10m \ +go test -timeout 45m -v -count=1 -ccip-env=prod-testnet \ + -run '^TestEVM2Canton_TokenLoad$' ./ccip/devenv/tests/load/ +``` + +Or from repo root: + +```bash +make run-evm2canton-token-load-prod +``` + +### EVM→Canton token e2e (prod-testnet) + +Single EVM→Canton token transfer end-to-end on Canton TestNet + Sepolia via `TestEVM2Canton_Basic/token_transfer`. + +**Prerequisites** + +- `CCIP_ENV=prod-testnet` and `CCIP_CONFIG_FILE=env-prod-testnet.ci.toml` (or local `env-prod-testnet-out.toml`) +- Canton auth env vars (`CANTON_GRPC_URL`, `CANTON_PARTY_ID`, `CANTON_AUTH_*`) — see [Prod testnet connection smoke test](#prod-testnet-connection-smoke-test) +- `PRIVATE_KEY` wallet funded with: + - **TEST** ERC-20 tokens on Sepolia (≥ `transfer_amount` from `[prod-testnet.evm_to_canton]` in `token_transfer_config.toml`, default `1000000000000000000`) + - **Sepolia ETH** for send and possible router approve tx +- Canton party wallet funded for execution fees + +```bash +CCIP_ENV=prod-testnet \ +CCIP_CONFIG_FILE=env-prod-testnet.ci.toml \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_*=... \ +PRIVATE_KEY=0x... \ +CANTON_CONFIRM_EXEC_TIMEOUT=10m \ +make run-evm2canton-token-e2e-prod +``` + +Equivalent: + +```bash +cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + -ccip-env=prod-testnet \ + -run '^TestEVM2Canton_Basic$/^token_transfer$' +``` + +### Canton→EVM token e2e (prod-testnet) + +Two sequential Canton→EVM LINK transfers end-to-end on Canton TestNet + Sepolia via `TestCanton2EVM_Basic/EOA receiver and default committee verifier token transfer`. + +**Prerequisites** + +- `CCIP_ENV=prod-testnet` and `CCIP_CONFIG_FILE=env-prod-testnet.ci.toml` (or local `env-prod-testnet-out.toml`) +- Canton auth env vars (`CANTON_GRPC_URL`, `CANTON_PARTY_ID`, `CANTON_AUTH_*`) — see [Prod testnet connection smoke test](#prod-testnet-connection-smoke-test) +- Canton party wallet funded with: + - **Amulet** for CCIP fees (≥ `260` for 2 sends at `CantonToEVMTokenTransferFeeAmount` = 130) + - **LINK** on Canton (`link-token`, ≥ `200` fixed-point for 2 sends at `transfer_amount` = `"100"` from `[prod-testnet.canton_to_evm]`) +- `PRIVATE_KEY` wallet on Sepolia with ETH for execution gas on the EVM receiver + +```bash +CCIP_ENV=prod-testnet \ +CCIP_CONFIG_FILE=env-prod-testnet.ci.toml \ +CANTON_GRPC_URL=... CANTON_PARTY_ID=... CANTON_AUTH_*=... \ +PRIVATE_KEY=0x... \ +CANTON_CONFIRM_EXEC_TIMEOUT=10m \ +make run-canton2evm-token-e2e-prod +``` + +Equivalent: + +```bash +cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + -ccip-env=prod-testnet \ + -run '^TestCanton2EVM_Basic$/^EOA receiver and default committee verifier token transfer$' +``` + +Expected EVM receiver token delta: `2 × 100000000000` wei TEST (= `2 × 0.0000001` LINK at `transfer_amount` = `"100"` fixed-point). + ### Token lane configuration -Token transfer tests (both e2e and load) declare the token lane to send in [`tests/token_transfer_config.toml`](./tests/token_transfer_config.toml). The test resolves the declared **token pool identity** against the source chain's `GetTokenTransferConfigs`, then validates that every destination chain has that lane configured before running. The committed file holds the devenv defaults. +Token transfer tests (both e2e and load) declare the token lane to send in [`tests/token_transfer_config.toml`](./tests/token_transfer_config.toml). The test resolves the declared **token pool identity** against the source chain's `GetTokenTransferConfigs`, then validates that every destination chain has that lane configured before running. On prod-testnet, when combo discovery fails (simple `TEST`/`LINK` qualifiers), the resolver falls back to direct datastore lookups using `remote_pool_*` fields. -Override the file path with `CANTON_TOKEN_TEST_CONFIG`: +Env selection follows `-ccip-env` / `CCIP_ENV` (same as the CCIP harness). Override the entire file path with `CANTON_TOKEN_TEST_CONFIG`: ```bash CANTON_TOKEN_TEST_CONFIG=/path/to/custom.toml make run-canton2evm-token-load ``` -The file has one block per direction (`[evm_to_canton]`, `[canton_to_evm]`): +The file uses env-keyed sections (`[devenv.*]`, `[prod-testnet.*]`) with one block per direction (`evm_to_canton`, `canton_to_evm`): | Key | Required | Meaning | |---|---|---| | `pool_type` | yes | token pool contract type on the source chain (e.g. `BurnMintTokenPool`, `LockReleaseTokenPool`) | | `pool_version` | yes | semantic version of the token pool (e.g. `2.0.0`) | | `pool_qualifier` | yes | datastore qualifier identifying the exact pool | -| `transfer_amount` | no | per-message token amount (string integer); falls back to a per-direction default | +| `transfer_amount` | no | per-message token amount; **integer wei** for `evm_to_canton`, **integer 10^10 fixed-point** for `canton_to_evm` (see below) | | `execution_gas_limit` | no | per-message execution gas limit; falls back to a per-direction default | | `finality_config` | no | per-message finality config; falls back to a per-direction default | +| `remote_pool_type` | prod fallback | remote pool type for prod datastore fallback | +| `remote_pool_version` | prod fallback | remote pool version for prod datastore fallback | +| `remote_pool_qualifier` | prod fallback | remote pool qualifier for prod datastore fallback | +| `transfer_instrument_id` | Canton→EVM | Canton transfer instrument when Canton is source (e.g. `LINK`; defaults to Amulet) | + +**`transfer_amount` format by direction** + +| Direction | Format | Example | Sent amount | +|---|---|---|---| +| `evm_to_canton` | Integer **wei** | `"100000000000"` | `100000000000` wei on Sepolia TEST | +| `canton_to_evm` | Integer **10^10 fixed-point** | `"1000"` | `0.0000001` LINK (`1000` fixed-point units) | +| `canton_to_evm` send boundary | same fixed-point in `TokenAmount.Amount` | `"1000"` | passed directly to `TokenAmount.Amount` | +| `canton_to_evm` EVM assert | `fixedPoint × 10^8` wei | `1000` → `100000000000` wei | matches reciprocal EVM→Canton | + +Fixed-point scale: `1000000000` = `0.1` LINK, `1000` = `0.0000001` LINK (same 10-decimal scale as Canton NUMERIC). Token **identity** (`pool_type` / `pool_version` / `pool_qualifier`) is always required and is never defaulted; only the numeric send params have code-level fallbacks. If the qualifier matches zero or multiple pools, or a destination lacks the lane, the test fails fast listing the available pool refs / selectors. ```toml -[evm_to_canton] +[devenv.evm_to_canton] pool_type = "BurnMintTokenPool" pool_version = "2.0.0" pool_qualifier = "TEST (BurnMintTokenPool 2.0.0 [default], LockReleaseTokenPool 2.0.0 [default])::BurnMintTokenPool 2.0.0 [default]" transfer_amount = "100000000000" execution_gas_limit = 200000 -finality_config = 0 +finality_config = 1 -[canton_to_evm] -pool_type = "LockReleaseTokenPool" +[prod-testnet.evm_to_canton] +pool_type = "BurnMintTokenPool" pool_version = "2.0.0" -pool_qualifier = "TEST (BurnMintTokenPool 2.0.0 [default], LockReleaseTokenPool 2.0.0 [default])::LockReleaseTokenPool 2.0.0 [default]" -transfer_amount = "1000" +pool_qualifier = "TEST" +transfer_amount = "1000000000000000000" +execution_gas_limit = 200000 +finality_config = 1 +remote_pool_type = "BurnMintTokenPool" +remote_pool_version = "2.0.0" +remote_pool_qualifier = "LINK" + +[prod-testnet.canton_to_evm] +pool_type = "BurnMintTokenPool" +pool_version = "2.0.0" +pool_qualifier = "LINK" +transfer_amount = "100" execution_gas_limit = 500000 finality_config = 1 +remote_pool_type = "BurnMintTokenPool" +remote_pool_version = "2.0.0" +remote_pool_qualifier = "TEST" +transfer_instrument_id = "link-token" ``` ### CI (on demand) -The **CCIP Canton Load Tests** workflow (`ccip-load-tests.yml`) can be triggered manually from GitHub Actions -(`workflow_dispatch`). It reuses the same devenv setup as the CCIP E2E workflow and runs one of the load tests depending on the `direction` input. Inputs: +Load tests use the composite action (`.github/actions/ccip-load-test`) from **CCIP Canton Load Tests** (`ccip-load-tests.yml`) via manual `workflow_dispatch`. -| Input | Default | Maps to | -|---|---|---| -| `direction` | `canton2evm` | `canton2evm` → `TestCanton2EVM_Load`; `evm2canton` → `TestEVM2Canton_Load`; `canton2evm-token` → `TestCanton2EVM_TokenLoad`; `evm2canton-token` → `TestEVM2Canton_TokenLoad` | -| `message_rate` | `1/1s` | `CANTON_LOAD_MESSAGE_RATE` | -| `load_duration` | `90s` | `CANTON_LOAD_DURATION` | -| `test_timeout` | `40m` | `go test -timeout` | -| `canton_ref` | workflow ref | chainlink-canton checkout | +#### Manual workflow (`workflow_dispatch`) -chainlink-ccv is pinned to **`695181f87033217a197386366e1ab563198f482e`** in `.github/actions/setup-ccip-devenv` and `go.mod` (same as CCIP E2E). +| Input | Default (devenv) | Default (prod-testnet) | Maps to | +|---|---|---|---| +| `ccip_env` | `devenv` | select `prod-testnet` | `-ccip-env` | +| `direction` | `canton2evm` | same | test `-run` regex | +| `message_rate` | `1/1s` | `1/45s` (when left at devenv default) | `CANTON_LOAD_MESSAGE_RATE` | +| `load_duration` | `90s` | `2m` (when left at devenv default) | `CANTON_LOAD_DURATION` | +| `test_timeout` | `40m` | `30m` / `45m` for evm2canton or evm2canton-token | `go test -timeout` | +| `config_file` | — | `env-prod-testnet.ci.toml` | `CCIP_CONFIG_FILE` | +| `skip_exec_confirm` | — | `true` for canton2evm only; `false` for token + evm2canton | `CANTON_LOAD_SKIP_EXEC_CONFIRM` | +| `confirm_exec_timeout` | — | `10m` | `CANTON_CONFIRM_EXEC_TIMEOUT` | +| `canton_ref` | workflow ref | devenv only | chainlink-canton checkout | + +**Devenv** spins up Docker via `setup-ccip-devenv` (same as CCIP E2E). **Prod-testnet** hits live Canton TestNet + Sepolia with no local devenv. + +#### Prod-testnet config file + +`*-out.toml` files are gitignored (local devenv output). CI uses the committed snapshot [`env-prod-testnet.ci.toml`](./env-prod-testnet.ci.toml) instead. Override with `config_file=` or `CCIP_CONFIG_FILE` when running locally: + +```bash +CCIP_CONFIG_FILE=env-prod-testnet.ci.toml CCIP_ENV=prod-testnet go test ... +``` + +#### GitHub secrets (prod-testnet CI) + +Workflows pass these secrets to `.github/actions/ccip-load-test` using the same names (1:1); the composite action maps them to test env vars: + +| Secret (input name) | Env var | +|---|---| +| `CANTON_OKTA_AUTHORIZER_TESTNET` | `CANTON_AUTH_URL` | +| `CANTON_OKTA_CLIENT_ID_TESTNET` | `CANTON_CLIENT_ID` | +| `CANTON_OKTA_CLIENT_SECRET_TESTNET` | `CANTON_CLIENT_SECRET` | +| `CCIP_PROD_TESTNET_PRIVATE_KEY` | `PRIVATE_KEY` | + +Devenv secrets unchanged: `CCV_IAM_ROLE`, `JD_REGISTRY`, `JD_IMAGE`. + +chainlink-ccv is pinned in `.github/actions/setup-ccip-devenv` (devenv only). ## Shortcut @@ -177,3 +434,55 @@ If you want to build docker images, spin up a new env, and run the test in a sin # From repo root make build-run-e2e-tests ``` + +## Prod testnet connection smoke test + +Minimal Canton-only connectivity check against real testnet infrastructure. Uses [`env-prod-testnet-out.toml`](./env-prod-testnet-out.toml) (Canton TestNet ↔ Sepolia message lane: contract refs, indexer URLs, EDS URL, verifier issuers); auth secrets and party identity come from environment variables. + +The test is opt-in: it skips unless `CANTON_GRPC_URL` is set (even though the TOML file includes default URLs). This keeps CI green while allowing manual or workflow-triggered runs. + +### Environment variables + +| Variable | Required | Notes | +|---|---|---| +| `CANTON_PARTY_ID` | yes | Ledger party to query; skips `GetUser` when set | +| `CANTON_AUTH_URL` | yes | OIDC issuer | +| `CANTON_CLIENT_ID` | yes | OAuth2 client ID | +| `CANTON_AUTH_TYPE` | no | `authorizationCode` (local), `clientCredentials` (CI), `static`, `insecureStatic` | +| `CANTON_USER_ID` | no | Required for `clientCredentials`; optional for `authorizationCode` (extracted from token `sub` after login) | +| `CANTON_CLIENT_SECRET` | CI only | Required when `CANTON_AUTH_TYPE=clientCredentials` | +| `CANTON_JWT` | static only | For `static` / `insecureStatic` auth | +| `CANTON_GRPC_URL` | opt-in signal | Must be set to run the test; overrides TOML gRPC URL | +| `CANTON_VALIDATOR_API_URL` | no | Overrides TOML validator API URL | + +**Local (browser Okta login):** + +```bash +export CANTON_GRPC_URL='testnet.cv1.bcy-v.metalhosts.com:443' +export CANTON_PARTY_ID='u_0e0328cbbcb7::1220c250c23c55120f7c758bccc5cbc739629015ab921594e1c29656981f985bffa7' +export CANTON_AUTH_URL='https://smartcontract.okta.com/oauth2/austsuml9q2WhPBMM5d7' +export CANTON_CLIENT_ID='0oau1l22b1Jv3dcih5d7' +export CANTON_AUTH_TYPE='authorizationCode' +``` + +**CI (`clientCredentials`, no browser):** + +```bash +export CANTON_GRPC_URL='testnet.cv1.bcy-v.metalhosts.com:443' +export CANTON_PARTY_ID='...' +export CANTON_AUTH_URL='https://smartcontract.okta.com/oauth2/austsuml9q2WhPBMM5d7' +export CANTON_CLIENT_ID='0oau1l22b1Jv3dcih5d7' +export CANTON_AUTH_TYPE='clientCredentials' +export CANTON_CLIENT_SECRET='...' +export CANTON_USER_ID='...' +``` + +### Run command + +```bash +cd ccip/devenv/tests/integration && go test -v -run TestIntegration_CantonProdTestnet_Connection -count=1 +``` + +Use `-ccip-env=prod-testnet` (or `CCIP_ENV=prod-testnet`) so the test loads `env-prod-testnet-out.toml` locally, or `CCIP_CONFIG_FILE=env-prod-testnet.ci.toml` for the committed CI snapshot; default devenv config is `env-canton-evm-out.toml` via `-ccip-env=devenv`. + +The test connects via `NewCLDF`, asserts `PartyID` is set, and lists holdings for the party (empty balance is OK). diff --git a/ccip/devenv/cldf.go b/ccip/devenv/cldf.go index 01a6e1bb0..c1bb3750a 100644 --- a/ccip/devenv/cldf.go +++ b/ccip/devenv/cldf.go @@ -4,14 +4,13 @@ import ( "context" "fmt" - adminv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2/admin" chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_canton_provider "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "google.golang.org/grpc" - cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" - cldf_canton_provider "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider" + "github.com/smartcontractkit/chainlink-canton/commonconfig" ) func NewCLDF(ctx context.Context, b *blockchain.Input) (cldf_chain.BlockChain, uint64, error) { @@ -24,46 +23,80 @@ func NewCLDF(ctx context.Context, b *blockchain.Input) (cldf_chain.BlockChain, u Participants: make([]cldf_canton_provider.ParticipantConfig, len(b.Out.NetworkSpecificData.CantonData.ExternalEndpoints.Participants)), } + presetPartyID := resolvePartyID() + internalParticipants := b.Out.NetworkSpecificData.CantonData.InternalEndpoints.Participants + for i, config := range b.Out.NetworkSpecificData.CantonData.ExternalEndpoints.Participants { - authProvider := authentication.NewInsecureStaticProvider(config.JWT) - // Get Primary Party for user - ledgerApiConn, err := grpc.NewClient( - config.GRPCLedgerAPIURL, - grpc.WithTransportCredentials(authProvider.TransportCredentials()), - grpc.WithPerRPCCredentials(authProvider.PerRPCCredentials()), - ) + authCfg := resolveAuthConfig(config.JWT, config.UserID) + authProvider, err := newAuthProvider(ctx, authCfg) if err != nil { - return nil, 0, fmt.Errorf("failed to create gRPC connection to Ledger API for Canton participant %d: %w", i+1, err) + return nil, 0, fmt.Errorf("create auth provider for Canton participant %d: %w", i+1, err) + } + + userID := resolveUserID(config.UserID) + if userID == "" && authCfg.Type == commonconfig.AuthTypeAuthorizationCode { + userID, err = userIDFromToken(ctx, authProvider) + if err != nil { + return nil, 0, fmt.Errorf("resolve user id for Canton participant %d: %w", i+1, err) + } } - userResp, err := adminv2.NewUserManagementServiceClient(ledgerApiConn).GetUser(context.Background(), &adminv2.GetUserRequest{UserId: config.UserID}) + + grpcURL := resolveGRPCLedgerURL(config.GRPCLedgerAPIURL) + party, err := func() (string, error) { + conn, err := grpc.NewClient( + grpcURL, + grpc.WithTransportCredentials(authProvider.TransportCredentials()), + grpc.WithPerRPCCredentials(authProvider.PerRPCCredentials()), + ) + if err != nil { + return "", fmt.Errorf("create gRPC connection to Ledger API for Canton participant %d: %w", i+1, err) + } + defer conn.Close() + + party, err := resolveParticipantParty(ctx, conn, userID, presetPartyID) + if err != nil { + return "", fmt.Errorf("resolve party for Canton participant %d: %w", i+1, err) + } + + return party, nil + }() if err != nil { - return nil, 0, fmt.Errorf("failed to get user info for user %s for Canton participant %d: %w", config.UserID, i+1, err) + return nil, 0, err } - party := userResp.GetUser().GetPrimaryParty() - if party == "" { - return nil, 0, fmt.Errorf("no primary party found for user %s for Canton participant %d", config.UserID, i+1) + + var internalEndpoints *cldf_canton_provider.Endpoints + if i < len(internalParticipants) { + internal := internalParticipants[i] + if endpointsNonEmpty( + internal.JSONLedgerAPIURL, + internal.GRPCLedgerAPIURL, + internal.AdminAPIURL, + internal.ValidatorAPIURL, + ) { + internalEndpoints = &cldf_canton_provider.Endpoints{ + JSONLedgerAPIURL: internal.JSONLedgerAPIURL, + GRPCLedgerAPIURL: internal.GRPCLedgerAPIURL, + AdminAPIURL: internal.AdminAPIURL, + ValidatorAPIURL: internal.ValidatorAPIURL, + } + } } - _ = ledgerApiConn.Close() providerConfig.Participants[i] = cldf_canton_provider.ParticipantConfig{ Endpoints: cldf_canton_provider.Endpoints{ JSONLedgerAPIURL: config.JSONLedgerAPIURL, - GRPCLedgerAPIURL: config.GRPCLedgerAPIURL, + GRPCLedgerAPIURL: grpcURL, AdminAPIURL: config.AdminAPIURL, - ValidatorAPIURL: config.ValidatorAPIURL, - }, - InternalEndpoints: &cldf_canton_provider.Endpoints{ - JSONLedgerAPIURL: b.Out.NetworkSpecificData.CantonData.InternalEndpoints.Participants[i].JSONLedgerAPIURL, - GRPCLedgerAPIURL: b.Out.NetworkSpecificData.CantonData.InternalEndpoints.Participants[i].GRPCLedgerAPIURL, - AdminAPIURL: b.Out.NetworkSpecificData.CantonData.InternalEndpoints.Participants[i].AdminAPIURL, - ValidatorAPIURL: b.Out.NetworkSpecificData.CantonData.InternalEndpoints.Participants[i].ValidatorAPIURL, + ValidatorAPIURL: resolveValidatorAPIURL(config.ValidatorAPIURL), }, - UserID: config.UserID, - PartyID: party, - AuthProvider: authProvider, + InternalEndpoints: internalEndpoints, + UserID: userID, + PartyID: party, + AuthProvider: authProvider, } } - p, err := cldf_canton_provider.NewRPCChainProvider(d.ChainSelector, providerConfig).Initialize(context.TODO()) + + p, err := cldf_canton_provider.NewRPCChainProvider(d.ChainSelector, providerConfig).Initialize(ctx) if err != nil { return nil, 0, err } diff --git a/ccip/devenv/cldf_auth.go b/ccip/devenv/cldf_auth.go new file mode 100644 index 000000000..25ac66143 --- /dev/null +++ b/ccip/devenv/cldf_auth.go @@ -0,0 +1,157 @@ +package devenv + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strings" + + adminv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2/admin" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" + "google.golang.org/grpc" + + "github.com/smartcontractkit/chainlink-canton/commonconfig" + "github.com/smartcontractkit/chainlink-canton/deployment/authentication/authorizationcode" +) + +func resolveAuthConfig(tomlJWT, tomlUserID string) commonconfig.AuthConfig { + jwt := envTrim("CANTON_JWT") + if jwt == "" { + jwt = tomlJWT + } + + authType := envTrim("CANTON_AUTH_TYPE") + switch { + case authType == "" && jwt != "": + authType = commonconfig.AuthTypeInsecureStatic + case authType == "": + authType = commonconfig.AuthTypeAuthorizationCode + } + + authURL := envTrim("CANTON_AUTH_URL") + clientID := envTrim("CANTON_CLIENT_ID") + clientSecret := envTrim("CANTON_CLIENT_SECRET") + if authType != commonconfig.AuthTypeClientCredentials { + clientSecret = "" + } + + return commonconfig.AuthConfig{ + Type: authType, + UserID: resolveUserID(tomlUserID), + JWT: jwt, + AuthURL: authURL, + ClientID: clientID, + ClientSecret: clientSecret, + } +} + +func resolveUserID(tomlUserID string) string { + if userID := envTrim("CANTON_USER_ID"); userID != "" { + return userID + } + + return tomlUserID +} + +func resolvePartyID() string { + return envTrim("CANTON_PARTY_ID") +} + +func resolveGRPCLedgerURL(tomlURL string) string { + if url := envTrim("CANTON_GRPC_URL"); url != "" { + return url + } + + return tomlURL +} + +func resolveValidatorAPIURL(tomlURL string) string { + if url := envTrim("CANTON_VALIDATOR_API_URL"); url != "" { + return url + } + + return tomlURL +} + +func newAuthProvider(ctx context.Context, authCfg commonconfig.AuthConfig) (authentication.Provider, error) { + if authCfg.Type == commonconfig.AuthTypeAuthorizationCode && authCfg.UserID == "" { + return authorizationcode.NewDiscoveryProvider(ctx, authCfg.AuthURL, authCfg.ClientID) + } + + return authCfg.NewProvider(ctx) +} + +func userIDFromToken(ctx context.Context, provider authentication.Provider) (string, error) { + _ = ctx + + token, err := provider.TokenSource().Token() + if err != nil { + return "", fmt.Errorf("get oauth token: %w", err) + } + + sub, err := jwtSubject(token.AccessToken) + if err != nil { + return "", fmt.Errorf("extract user id from token sub: %w", err) + } + if sub == "" { + return "", fmt.Errorf("token sub claim is empty") + } + + return sub, nil +} + +func jwtSubject(accessToken string) (string, error) { + parts := strings.Split(accessToken, ".") + if len(parts) != 3 { + return "", fmt.Errorf("invalid JWT format") + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("decode JWT payload: %w", err) + } + + var claims struct { + Sub string `json:"sub"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return "", fmt.Errorf("parse JWT payload: %w", err) + } + + return claims.Sub, nil +} + +func resolveParticipantParty(ctx context.Context, conn *grpc.ClientConn, userID, partyID string) (string, error) { + if party := strings.TrimSpace(partyID); party != "" { + return party, nil + } + + if strings.TrimSpace(userID) == "" { + return "", fmt.Errorf("party ID not preset and user ID unset") + } + + userResp, err := adminv2.NewUserManagementServiceClient(conn).GetUser(ctx, &adminv2.GetUserRequest{UserId: userID}) + if err != nil { + return "", fmt.Errorf("get user %s: %w", userID, err) + } + + party := userResp.GetUser().GetPrimaryParty() + if party == "" { + return "", fmt.Errorf("no primary party found for user %s", userID) + } + + return party, nil +} + +func endpointsNonEmpty(jsonURL, grpcURL, adminURL, validatorURL string) bool { + return strings.TrimSpace(jsonURL) != "" || + strings.TrimSpace(grpcURL) != "" || + strings.TrimSpace(adminURL) != "" || + strings.TrimSpace(validatorURL) != "" +} + +func envTrim(key string) string { + return strings.TrimSpace(os.Getenv(key)) +} diff --git a/ccip/devenv/cldf_auth_test.go b/ccip/devenv/cldf_auth_test.go new file mode 100644 index 000000000..29600a4e2 --- /dev/null +++ b/ccip/devenv/cldf_auth_test.go @@ -0,0 +1,201 @@ +package devenv + +import ( + "context" + "testing" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/oauth" + + "github.com/smartcontractkit/chainlink-canton/commonconfig" +) + +// validJWT is a well-formed JWT with sub "1234567890". +const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" + +func clearCantonEnv(t *testing.T) { + t.Helper() + for _, key := range []string{ + "CANTON_AUTH_TYPE", + "CANTON_AUTH_URL", + "CANTON_CLIENT_ID", + "CANTON_CLIENT_SECRET", + "CANTON_JWT", + "CANTON_USER_ID", + "CANTON_PARTY_ID", + "CANTON_GRPC_URL", + "CANTON_VALIDATOR_API_URL", + } { + t.Setenv(key, "") + } +} + +func TestEnvTrim(t *testing.T) { + clearCantonEnv(t) + + require.Equal(t, "", envTrim("CANTON_PARTY_ID")) + + t.Setenv("CANTON_PARTY_ID", "party-1") + require.Equal(t, "party-1", envTrim("CANTON_PARTY_ID")) + + t.Setenv("CANTON_PARTY_ID", " party-2 ") + require.Equal(t, "party-2", envTrim("CANTON_PARTY_ID")) +} + +func TestResolveAuthConfig(t *testing.T) { + tests := []struct { + name string + env map[string]string + tomlJWT string + tomlUserID string + want commonconfig.AuthConfig + }{ + { + name: "toml jwt defaults to insecureStatic", + tomlJWT: validJWT, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeInsecureStatic, + JWT: validJWT, + }, + }, + { + name: "no jwt defaults to authorizationCode", + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeAuthorizationCode, + }, + }, + { + name: "env overrides toml", + env: map[string]string{ + "CANTON_AUTH_TYPE": commonconfig.AuthTypeClientCredentials, + "CANTON_JWT": "env-jwt", + "CANTON_USER_ID": "env-user", + "CANTON_AUTH_URL": "https://auth.example.com/", + "CANTON_CLIENT_ID": "env-client", + "CANTON_CLIENT_SECRET": "env-secret", + }, + tomlJWT: validJWT, + tomlUserID: "toml-user", + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + UserID: "env-user", + JWT: "env-jwt", + AuthURL: "https://auth.example.com/", + ClientID: "env-client", + ClientSecret: "env-secret", + }, + }, + { + name: "client secret cleared for authorizationCode", + env: map[string]string{ + "CANTON_AUTH_TYPE": commonconfig.AuthTypeAuthorizationCode, + "CANTON_AUTH_URL": "https://auth.example.com/", + "CANTON_CLIENT_ID": "env-client", + "CANTON_CLIENT_SECRET": "should-not-apply", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeAuthorizationCode, + AuthURL: "https://auth.example.com/", + ClientID: "env-client", + ClientSecret: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearCantonEnv(t) + for key, value := range tt.env { + t.Setenv(key, value) + } + + got := resolveAuthConfig(tt.tomlJWT, tt.tomlUserID) + require.Equal(t, tt.want, got) + }) + } +} + +func TestResolveUserID(t *testing.T) { + clearCantonEnv(t) + + require.Equal(t, "toml-user", resolveUserID("toml-user")) + + t.Setenv("CANTON_USER_ID", "env-user") + require.Equal(t, "env-user", resolveUserID("toml-user")) +} + +func TestResolvePartyID(t *testing.T) { + clearCantonEnv(t) + + require.Equal(t, "", resolvePartyID()) + + t.Setenv("CANTON_PARTY_ID", "party::1220abc") + require.Equal(t, "party::1220abc", resolvePartyID()) +} + +func TestResolveGRPCLedgerURL(t *testing.T) { + clearCantonEnv(t) + + require.Equal(t, "toml-host:443", resolveGRPCLedgerURL("toml-host:443")) + + t.Setenv("CANTON_GRPC_URL", "env-host:443") + require.Equal(t, "env-host:443", resolveGRPCLedgerURL("toml-host:443")) +} + +func TestResolveValidatorAPIURL(t *testing.T) { + clearCantonEnv(t) + + require.Equal(t, "https://toml.example/validator/", resolveValidatorAPIURL("https://toml.example/validator/")) + + t.Setenv("CANTON_VALIDATOR_API_URL", "https://env.example/validator/") + require.Equal(t, "https://env.example/validator/", resolveValidatorAPIURL("https://toml.example/validator/")) +} + +func TestJWTSubject(t *testing.T) { + t.Parallel() + + sub, err := jwtSubject(validJWT) + require.NoError(t, err) + require.Equal(t, "1234567890", sub) +} + +func TestUserIDFromToken(t *testing.T) { + userID, err := userIDFromToken(context.Background(), testAuthProvider{token: validJWT}) + require.NoError(t, err) + require.Equal(t, "1234567890", userID) +} + +func TestEndpointsNonEmpty(t *testing.T) { + t.Parallel() + + require.False(t, endpointsNonEmpty("", "", "", "")) + require.True(t, endpointsNonEmpty("", "grpc:443", "", "")) +} + +func TestResolveAuthConfig_envJWTOverridesToml(t *testing.T) { + clearCantonEnv(t) + + t.Setenv("CANTON_JWT", validJWT) + got := resolveAuthConfig("toml-jwt-should-lose", "") + require.Equal(t, validJWT, got.JWT) + require.Equal(t, commonconfig.AuthTypeInsecureStatic, got.Type) +} + +type testAuthProvider struct { + token string +} + +func (p testAuthProvider) TokenSource() oauth2.TokenSource { + return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.token}) +} + +func (p testAuthProvider) TransportCredentials() credentials.TransportCredentials { + return authentication.NewInsecureStaticProvider(p.token).TransportCredentials() +} + +func (p testAuthProvider) PerRPCCredentials() credentials.PerRPCCredentials { + return oauth.TokenSource{TokenSource: p.TokenSource()} +} diff --git a/ccip/devenv/env-prod-testnet.ci.toml b/ccip/devenv/env-prod-testnet.ci.toml new file mode 100644 index 000000000..6f5b5bf35 --- /dev/null +++ b/ccip/devenv/env-prod-testnet.ci.toml @@ -0,0 +1,52 @@ +version = 0 +indexer_endpoints = [ + "https://indexer-1.testnet.ccip.chain.link", + "https://indexer-2.testnet.ccip.chain.link", +] + +[cldf] +addresses = [ + '[{"address":"0x181Ac7dC295f1C8C87342d07CFaBA90bC477DB5d","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"OnRamp","version":"2.0.0"},{"address":"0xc6A246A9AcdAaE651708706494720F79C3E5d0A1","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"OffRamp","version":"2.0.0"},{"address":"0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59","chainSelector":16015286601757825753,"labels":[],"qualifier":"0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59-Router","type":"Router","version":"1.2.0"},{"address":"0x8632C3025FAFdD85A299211FD5838b5fBE2df816","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"FeeQuoter","version":"2.0.0"},{"address":"0x8f3ee3c77D2B27c32306a89D367654F959Db223D","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"CommitteeVerifierResolver","version":"2.0.0"},{"address":"0x66f9E0738a4a6fe54aE62DEd00Ca1F72bDecc092","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"ExecutorProxy","version":"2.0.0"},{"address":"0x78874Df86F55b207d5584093C17B59220E8c6B15","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"Executor","version":"2.0.0"},{"address":"0x097D90c9d3E0B50Ca60e1ae45F6A81010f9FB534","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"WETH9","version":"1.0.0"},{"address":"0x779877A7B0D9E8603169DdbD7836e478b4624789","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"LinkToken","version":"1.0.0"},{"address":"0x5185b41F1588FC8C541360709C992794925D484C","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"BurnMintTokenPool","version":"2.0.0"},{"address":"0xeEe6675b20fE5950eb51361b93021D076289F612","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"BurnMintERC20WithDrip","version":"1.5.0"},{"address":"0xA0a507CE0709D3D40F71166c730a860aa29f3491","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"AdvancedPoolHooks","version":"2.0.0"}]', + '[{"address":"0x03519eac48d545c4d0ecdc3e3022e443d9e878867827eecb37d5e5a60ae0c989","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OnRamp","version":"2.0.0"},{"address":"0xd03375cb15a7179bfbfa7cfb843250a6b26e4ddc7a4c30e7bc1777cf3afbf580","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OffRamp","version":"2.0.0"},{"address":"0x18a00775f6781bfe3032d1b9ceeee225416fb36143218969818df860cb1e29c6","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"FeeQuoter","version":"2.0.0"},{"address":"0x45f673fb23aa33fdaa07e7ae7ea0d37218bcff8e63575a5582a2356cbfaa8883","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonGlobalConfig","version":"2.0.0"},{"address":"0x665cfb57b33b2b74383e99af880ff7d967ad85ef5966ac912f9a3cd4faaee5f9","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonPerPartyRouterFactory","version":"2.0.0"},{"address":"0xe8df5a00cc82df74bae0ca2a032e4d5a7fb5424c1194c4036caabe5fd9f40f81","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"TokenAdminRegistry","version":"2.0.0"},{"address":"0xd4678dbaa95efbf0631162b4fbbae08bf42bbde19038692fec94b6f6b5528fec","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"RMNRemote","version":"2.0.0"},{"address":"0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe","chainSelector":9268731218649498074,"labels":[],"qualifier":"default","type":"CommitteeVerifier","version":"2.0.0"},{"address":"0x4b19a247edd769630a136840ef703bd55ca3eda382317ce4384f2aadd47ddbaa","chainSelector":9268731218649498074,"labels":[],"qualifier":"default","type":"Executor","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"BurnMintTokenPool","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"CantonBurnMintTokenPool","version":"2.0.0"},{"address":"39c6212f2291365bed155b2533abbd7ddf340498ccbbb358cee07b83106986ad","chainSelector":9268731218649498074,"labels":["instrument-admin:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551","instrument-id:link-token","ccip-owner:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"Token","version":"2.0.0"},{"address":"0x576182aab988a0804a1aa13081902c076ed6108c1162a04b3e971e871a608527","chainSelector":9268731218649498074,"labels":["linkregistry@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"","type":"LinkRegistry","version":"2.0.0"}]', +] +env_metadata = "{\"metadata\":{\"cantonConfigs\":{\"eds_url\":\"https://eds.testnet.ccip.chain.link\"},\"offchainConfigs\":{\"indexers\":{\"indexer-1\":{\"verifiers\":[{\"issuerAddresses\":[\"0x8f3ee3c77D2B27c32306a89D367654F959Db223D\",\"0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe\"],\"name\":\"default-verifier\"}]}}}}}" + +[[blockchains]] +container_name = "ethereum-testnet-sepolia" +chain_id = "11155111" +type = "anvil" +out.type = "anvil" +out.use_cache = true +out.chain_id = "11155111" +out.family = "evm" +out.container_name = "ethereum-testnet-sepolia" +out.nodes = [ + { http_url = "https://rpcs.cldev.sh/16015286601757825753", internal_http_url = "https://rpcs.cldev.sh/16015286601757825753", internal_ws_url = "wss://rpcs.cldev.sh/16015286601757825753", ws_url = "wss://rpcs.cldev.sh/16015286601757825753" }, +] + +[[blockchains]] +container_name = "canton-testnet" +chain_id = "TestNet" +type = "canton" +out.type = "canton" +out.use_cache = true +out.chain_id = "TestNet" +out.family = "canton" +out.container_name = "canton-testnet" +out.nodes = [] + +[blockchains.out.network_specific_data.canton_data.external_endpoints] + +[[blockchains.out.network_specific_data.canton_data.external_endpoints.participants]] +grpc_ledger_api_url = "testnet.cv1.bcy-v.metalhosts.com:443" +json_ledger_api_url = "https://testnet.cv1.bcy-v.metalhosts.com/api/json/" +validator_api_url = "https://testnet.cv1.bcy-v.metalhosts.com/api/validator/" +user_id = "" +jwt = "" + +[[blockchains.out.network_specific_data.canton_data.internal_endpoints.participants]] +grpc_ledger_api_url = "testnet.cv1.bcy-v.metalhosts.com:443" +json_ledger_api_url = "https://testnet.cv1.bcy-v.metalhosts.com/api/json/" +validator_api_url = "https://testnet.cv1.bcy-v.metalhosts.com/api/validator/" +user_id = "" +jwt = "" diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 51f63077e..dd472c1a9 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -23,7 +23,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-ccip/deployment/finality" "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" @@ -44,7 +43,6 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/clientapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" ccipevents "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" @@ -219,6 +217,7 @@ type Chain struct { // Send setup prerequisites routerAddress contracts.InstanceAddress senderAddress contracts.InstanceAddress + receiverAddress contracts.InstanceAddress registryAdmin string validatorAPIClients validatorAPIClients @@ -230,7 +229,7 @@ type Chain struct { sendsLeft uint64 // remaining sends in current setup batch; 0 = always rotate // verifierObs is injected post-construction by test runners (see SetVerifierObservation). - // Required by ConfirmExecOnDest to fetch verifier results from aggregator/indexer. + // Required by ConfirmExecOnDest to fetch verifier results from indexer (aggregator optional). verifierObs VerifierObservation // partyMutexes serializes manual execution per receiver party so that @@ -943,9 +942,9 @@ func (c *Chain) ConfirmSendOnSource(ctx context.Context, to uint64, key cciptest // 1. Lock per receiver party (PerPartyRouter CID is consumed on every Execute). // 2. Idempotency: if an ExecutionStateChanged for (from, seqNo, messageID) already // exists on the ledger, parse and return it without re-executing. -// 3. Otherwise fetch the verifier result via verifier observation (aggregator + -// indexer), translate the verifier dest address to its hashed instance address, -// and call ManuallyExecuteMessage. +// 3. Otherwise fetch the verifier result via verifier observation (indexer; +// aggregator optional), translate the verifier dest address to its hashed +// instance address, and call ManuallyExecuteMessage. // // Both SeqNum AND MessageID must be set on key: they key the idempotency lookup and // the verifier-result fetch respectively. EVM-side ConfirmExecOnDest is permissive @@ -1277,7 +1276,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint } } // TODO come up with a better way of doing this - routerCid, err := contract.FindActiveContractIDByInstanceAddress(ctx, participant.LedgerServices.State, []string{party}, ccipruntime.PerPartyRouter{}.GetTemplateID(), c.routerAddress) + routerCid, err := c.findPerPartyRouterCidByParty(ctx, participant, party) if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("find active contract ID for router at address %s: %w", c.routerAddress, err) } diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index df58d74d9..05e451178 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -20,9 +20,9 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" @@ -34,41 +34,39 @@ import ( "github.com/smartcontractkit/chainlink-canton/testhelpers" ) -const perPartyRouterInstanceID = "test-router" - func instanceIDFromEnv(key, defaultID string) contracts.InstanceID { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return contracts.InstanceID(v) } - return contracts.InstanceID(defaultID) } -// DeployPerPartyRouter uses the PerPartyRouterFactory to create a new PerPartyRouter instance for the given party. -// partyOwner (client participant) exercises CreateRouter with factory disclosures from EDS. -// It returns the instance address of the router. If a router already exists for the party, it returns the existing address. -func (c *Chain) DeployPerPartyRouter(ctx context.Context, clientParticipant canton.Participant, partyOwner string) (routerAddress contracts.InstanceAddress, err error) { - perPartyRouterFactoryDisclosure, err := c.GetPerPartyRouterFactoryDisclosure(ctx, partyOwner) - if err != nil { - return contracts.InstanceAddress{}, fmt.Errorf("failed to get canton per party router factory disclosure: %w", err) +// DeployPerPartyRouter uses the PerPartyRouterFactory to create a PerPartyRouter instance for the given party. +// It returns the partyOwner-based instance address used in CCIP protocol fields, reusing an existing router on ledger when present. +func (c *Chain) DeployPerPartyRouter(ctx context.Context, participant canton.Participant, partyId string) (routerAddress contracts.InstanceAddress, err error) { + var unset contracts.InstanceAddress + routerInstanceID := instanceIDFromEnv("CANTON_ROUTER_INSTANCE_ID", "test-router") + if c.routerAddress != unset { + if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { + return contracts.InstanceAddress{}, fmt.Errorf("verify cached per-party router: %w", findErr) + } else if ok && found == c.routerAddress { + return c.routerAddress, nil + } + c.routerAddress = unset } - c.logger.Debug().Str("ContractId", perPartyRouterFactoryDisclosure.ContractId).Msg("Resolved per-party router factory disclosure") - routerInstanceID := contracts.InstanceID(perPartyRouterInstanceID) - ccipOwner := perPartyRouterFactoryDisclosure.Address.Owner() - routerAddress = routerInstanceID.RawInstanceAddress(types.PARTY(ccipOwner)).InstanceAddress() + if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { + return contracts.InstanceAddress{}, fmt.Errorf("find existing per-party router: %w", findErr) + } else if ok { + c.routerAddress = found + return found, nil + } - // Return early only if the router already exists at the expected instance address. - _, err = contract.FindActiveContractIDByInstanceAddress( - ctx, - clientParticipant.LedgerServices.State, - []string{partyOwner}, - ccipruntime.PerPartyRouter{}.GetTemplateID(), - routerAddress, - ) - if err == nil { - return routerAddress, nil + perPartyRouterFactoryDisclosure, err := c.GetPerPartyRouterFactoryDisclosure(ctx, partyId) + if err != nil { + return contracts.InstanceAddress{}, fmt.Errorf("failed to get canton per party router factory disclosure: %w", err) } + c.logger.Debug().Str("ContractId", perPartyRouterFactoryDisclosure.ContractId).Msg("Resolved per-party router factory address") _, err = operations.ExecuteOperation( c.e.OperationsBundle, @@ -80,31 +78,104 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, clientParticipant cant ParticipantIndex: c.clientParticipantIndex(), DisclosedContracts: contract.DisclosedContractsFromProto(perPartyRouterFactoryDisclosure.DisclosedContracts), Args: ccipruntime.CreateRouter{ - PartyOwner: types.PARTY(partyOwner), + PartyOwner: types.PARTY(partyId), InstanceId: types.TEXT(routerInstanceID.String()), }, }, operations.WithForceExecute[contract.ChoiceInput[ccipruntime.CreateRouter], canton.Chain](), ) if err != nil { - return contracts.InstanceAddress{}, fmt.Errorf("failed to create per-party router: %w", err) + if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { + return contracts.InstanceAddress{}, fmt.Errorf("create per-party router: %w (find after failure: %v)", err, findErr) + } else if ok { + c.routerAddress = found + return found, nil + } + return contracts.InstanceAddress{}, fmt.Errorf("create per-party router: %w", err) } - _, err = contract.FindActiveContractIDByInstanceAddress( - ctx, - clientParticipant.LedgerServices.State, - []string{partyOwner}, - ccipruntime.PerPartyRouter{}.GetTemplateID(), - routerAddress, - ) + found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID) + if findErr != nil { + return contracts.InstanceAddress{}, fmt.Errorf("find per-party router after create: %w", findErr) + } + if !ok { + return contracts.InstanceAddress{}, fmt.Errorf("per-party router not found after create for party %s", partyId) + } + + c.routerAddress = found + return found, nil +} + +func (c *Chain) findPerPartyRouterByParty( + ctx context.Context, + participant canton.Participant, + partyId string, + preferredInstanceID contracts.InstanceID, +) (contracts.InstanceAddress, string, bool, error) { + templateID := contracts.TemplateIDFromBinding(ccipruntime.PerPartyRouter{}).ToLedgerIdentifier() + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) if err != nil { - return contracts.InstanceAddress{}, fmt.Errorf( - "per-party router not found at %s for party %s after CreateRouter: %w", - routerAddress, partyOwner, err, - ) + return contracts.InstanceAddress{}, "", false, err + } + + var fallback contracts.InstanceAddress + var fallbackCid string + var hasFallback bool + for _, ac := range activeContracts { + created := ac.GetCreatedEvent() + if created == nil { + continue + } + instanceId, partyOwner, ok := perPartyRouterFieldsFromCreated(created) + if !ok { + c.logger.Debug().Msg("Skipping PerPartyRouter active contract missing instanceId or partyOwner") + continue + } + if partyOwner != partyId { + continue + } + addr := contracts.InstanceID(instanceId).RawInstanceAddress(types.PARTY(partyId)).InstanceAddress() + cid := created.GetContractId() + if contracts.InstanceID(instanceId) == preferredInstanceID { + return addr, cid, true, nil + } + if !hasFallback { + fallback = addr + fallbackCid = cid + hasFallback = true + } + } + if hasFallback { + return fallback, fallbackCid, true, nil } - return routerAddress, nil + return contracts.InstanceAddress{}, "", false, nil +} + +func perPartyRouterFieldsFromCreated(created *apiv2.CreatedEvent) (instanceId, partyOwner string, ok bool) { + for _, field := range created.GetCreateArguments().GetFields() { + switch field.GetLabel() { + case "instanceId": + instanceId = field.GetValue().GetText() + case "partyOwner": + partyOwner = field.GetValue().GetParty() + } + } + return instanceId, partyOwner, instanceId != "" && partyOwner != "" +} + +// findPerPartyRouterCidByParty resolves the ledger contract ID for a PerPartyRouter owned by partyId. +// PerPartyRouter is signed by ccipOwner, so instance-address lookup must use partyOwner (see OnRamp.daml). +func (c *Chain) findPerPartyRouterCidByParty(ctx context.Context, participant canton.Participant, partyId string) (string, error) { + routerInstanceID := instanceIDFromEnv("CANTON_ROUTER_INSTANCE_ID", "test-router") + _, cid, ok, err := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("no active PerPartyRouter found for party %s", partyId) + } + return cid, nil } // DeployCCIPSender returns a sender-owned CCIPSender instance address, deploying only when missing. @@ -148,25 +219,43 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici c.senderAddress = senderAddress return senderAddress, nil } - return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy ccip sender contract: %w", err) } - c.senderAddress = senderAddress + c.senderAddress = senderAddress return senderAddress, nil } func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Participant, partyId string, receiverFinality int64) (contracts.InstanceAddress, error) { + var unset contracts.InstanceAddress + if c.receiverAddress != unset { + return c.receiverAddress, nil + } + + instanceID := instanceIDFromEnv("CANTON_RECEIVER_INSTANCE_ID", "e2e-receiver") + receiverAddress := instanceID.RawInstanceAddress(types.PARTY(partyId)).InstanceAddress() + + if _, err := contract.FindActiveContractIDByInstanceAddress( + ctx, + participant.LedgerServices.State, + contract.LedgerQueryParties(participant), + ccipreceiver.CCIPReceiver{}.GetTemplateID(), + receiverAddress, + ); err == nil { + c.receiverAddress = receiverAddress + return receiverAddress, nil + } + finalityConfig, err := encodeReceiverFinalityConfig(receiverFinality) if err != nil { return contracts.InstanceAddress{}, fmt.Errorf("failed to encode receiver finality config: %w", err) } - // Deploy receiver contract - out, err := operations.ExecuteOperation(c.e.OperationsBundle, receiver.Deploy, c.chain, contract.DeployInput[ccipreceiver.CCIPReceiver]{ + _, err = operations.ExecuteOperation(c.e.OperationsBundle, receiver.Deploy, c.chain, contract.DeployInput[ccipreceiver.CCIPReceiver]{ Qualifier: nil, ParticipantIndex: c.clientParticipantIndex(), Template: ccipreceiver.CCIPReceiver{ + InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), RequiredCCVs: nil, OptionalCCVs: nil, @@ -176,10 +265,20 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti OwnerParty: types.PARTY(partyId), }) if err != nil { + if _, findErr := contract.FindActiveContractIDByInstanceAddress( + ctx, + participant.LedgerServices.State, + contract.LedgerQueryParties(participant), + ccipreceiver.CCIPReceiver{}.GetTemplateID(), + receiverAddress, + ); findErr == nil { + c.receiverAddress = receiverAddress + return receiverAddress, nil + } return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy receiver contract: %w", err) } - receiverAddress := contracts.HexToInstanceAddress(out.Output.Address) + c.receiverAddress = receiverAddress return receiverAddress, nil } @@ -220,12 +319,9 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes } encodedMessageHex := hex.EncodeToString(encodedMessage) - routerCid, err := contract.FindActiveContractIDByInstanceAddress(ctx, participant.LedgerServices.State, []string{participant.PartyID}, ccipruntime.PerPartyRouter{}.GetTemplateID(), routerAddress) + routerCid, err := c.findPerPartyRouterCidByParty(ctx, participant, executingParty) if err != nil { - return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf( - "per-party router not found for party %s at %s; call SetupReceive or SetupSend on the client participant before executing messages: %w", - executingParty, routerAddress, err, - ) + return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get router contract ID: %w", err) } c.logger.Debug().Str("InstanceAddress", routerAddress.String()).Str("ContractId", routerCid).Msg("Resolved PerPartyRouter contract") @@ -258,7 +354,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes executeArgs.CcvInputs[i] = ccipreceiver.CCVInput{ CcvCid: types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), VerifierResults: types.TEXT(hex.EncodeToString(vr)), - Context: ccvExecuteDisclosure.ChoiceContext, + Context: ccvExecuteDisclosure.ChoiceContext, } disclosedContracts = append(disclosedContracts, ccvExecuteDisclosure.DisclosedContracts...) } @@ -279,7 +375,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes executeArgs.TokenTransfer = &ccipreceiver.TokenTransferInput{ TokenPoolCid: types.CONTRACT_ID(tokenPoolDisclosure.ContractId), TokenReceiverParty: types.PARTY(executingParty), - Context: tokenPoolDisclosure.ChoiceContext, + Context: tokenPoolDisclosure.ChoiceContext, } disclosedContracts = append(disclosedContracts, tokenPoolDisclosure.DisclosedContracts...) } @@ -455,10 +551,10 @@ func (c *Chain) lockForParty(party string) func() { func (c *Chain) findExistingExecutionState( ctx context.Context, sourceChainSelector, seqNo uint64, messageID protocol.Bytes32, ) (cciptestinterfaces.ExecutionStateChangedEvent, bool, error) { - participant, _, err := c.ClientParticipant() - if err != nil { - return cciptestinterfaces.ExecutionStateChangedEvent{}, false, fmt.Errorf("findExistingExecutionState: %w", err) + if len(c.chain.Participants) == 0 { + return cciptestinterfaces.ExecutionStateChangedEvent{}, false, fmt.Errorf("findExistingExecutionState: no participants on chain") } + participant := c.chain.Participants[0] templateID := contracts.TemplateIDFromBinding(events.ExecutionStateChanged{}).ToLedgerIdentifier() activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) @@ -502,8 +598,8 @@ func verifierAssertTimeout(timeout time.Duration) time.Duration { return timeout } -// fetchVerifierResult queries the aggregator + indexer for the verifier output for -// messageID. Caller must have wired [VerifierObservation] on the chain first. +// fetchVerifierResult queries the indexer (aggregator optional) for the verifier +// output for messageID. Caller must have wired [VerifierObservation] on the chain first. func (c *Chain) fetchVerifierResult(ctx context.Context, messageID protocol.Bytes32, timeout time.Duration) (verifierResult, error) { if !c.verifierObs.wired() { return verifierResult{}, fmt.Errorf("verifier observation not wired") @@ -519,9 +615,6 @@ func (c *Chain) fetchVerifierResult(ctx context.Context, messageID protocol.Byte if err != nil { return verifierResult{}, fmt.Errorf("assertMessage: %w", err) } - if res.AggregatedResult == nil { - return verifierResult{}, fmt.Errorf("aggregated verifier result missing") - } if len(res.IndexedVerifications.Results) != 1 { return verifierResult{}, fmt.Errorf("expected 1 indexed verifier result, got %d", len(res.IndexedVerifications.Results)) } @@ -554,17 +647,28 @@ func encodeReceiverFinalityConfig(finality int64) (ccipcodec.FinalityConfig, err } } -// hashInstanceAddress decodes a verifier result's VerifierDestAddress on Canton. -// On Canton the verifier result carries the raw instance address as a hex-encoded -// string (bytes); to look up its disclosure from EDS we need the hashed instance -// address. This helper is the non-test counterpart of getHashedInstanceAddress -// previously kept in evm2canton_e2e_test.go. -func hashInstanceAddress(rawInstanceAddressBytes protocol.UnknownAddress) (protocol.UnknownAddress, error) { - rawInstanceAddressStr := string(rawInstanceAddressBytes.Bytes()) - rawInstanceAddress, err := contracts.RawInstanceAddressFromString(rawInstanceAddressStr) - if err != nil { - return nil, fmt.Errorf("hashInstanceAddress: %w", err) +// hashInstanceAddress resolves a verifier result's VerifierDestAddress to the hashed +// Canton instance address used for EDS disclosure lookup. +// +// The indexer may return either format depending on environment: +// - devenv: raw instance address string bytes ("instanceId@owner") +// - prod-testnet: already-hashed 32-byte InstanceAddress (from verifier config) +func hashInstanceAddress(addr protocol.UnknownAddress) (protocol.UnknownAddress, error) { + if len(addr) == 0 { + return nil, fmt.Errorf("empty verifier dest address") + } + + if raw, err := contracts.RawInstanceAddressFromString(string(addr)); err == nil { + return protocol.UnknownAddress(raw.InstanceAddress().Bytes()), nil + } + + if len(addr) == contracts.InstanceAddressLength { + return addr, nil + } + + if hexAddr, err := protocol.NewUnknownAddressFromHex(string(addr)); err == nil && len(hexAddr) == contracts.InstanceAddressLength { + return hexAddr, nil } - return protocol.UnknownAddress(rawInstanceAddress.InstanceAddress().Bytes()), nil + return nil, fmt.Errorf("unrecognized verifier dest address format (len=%d)", len(addr)) } diff --git a/ccip/devenv/manual_execution_test.go b/ccip/devenv/manual_execution_test.go new file mode 100644 index 000000000..8875f8cef --- /dev/null +++ b/ccip/devenv/manual_execution_test.go @@ -0,0 +1,38 @@ +package devenv + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-ccv/protocol" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-canton/contracts" +) + +func TestHashInstanceAddress_rawInstanceAddress(t *testing.T) { + t.Parallel() + + raw := "test-verifier@ccvOwner::1220abcd" + addr, err := hashInstanceAddress(protocol.UnknownAddress(raw)) + require.NoError(t, err) + + expectedRaw, err := contracts.RawInstanceAddressFromString(raw) + require.NoError(t, err) + require.Equal(t, protocol.UnknownAddress(expectedRaw.InstanceAddress().Bytes()), addr) +} + +func TestHashInstanceAddress_alreadyHashed(t *testing.T) { + t.Parallel() + + hashed := protocol.UnknownAddress(contracts.HexToInstanceAddress("0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe").Bytes()) + addr, err := hashInstanceAddress(hashed) + require.NoError(t, err) + require.Equal(t, hashed, addr) +} + +func TestHashInstanceAddress_empty(t *testing.T) { + t.Parallel() + + _, err := hashInstanceAddress(nil) + require.Error(t, err) +} diff --git a/ccip/devenv/tests/e2e/main_test.go b/ccip/devenv/tests/e2e/main_test.go new file mode 100644 index 000000000..bf115df39 --- /dev/null +++ b/ccip/devenv/tests/e2e/main_test.go @@ -0,0 +1,12 @@ +package canton + +import ( + "flag" + "os" + "testing" +) + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} diff --git a/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go b/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go new file mode 100644 index 000000000..30542fc2c --- /dev/null +++ b/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go @@ -0,0 +1,71 @@ +package integration + +import ( + "flag" + "os" + "path/filepath" + "testing" + + ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" + _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/stretchr/testify/require" + + _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // register canton impl factory + cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" + devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" + "github.com/smartcontractkit/chainlink-canton/testhelpers" +) + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestIntegration_CantonProdTestnet_Connection(t *testing.T) { + if testing.Short() { + t.Skip("skipping prod testnet connection smoke test in short mode") + } + if os.Getenv("CANTON_GRPC_URL") == "" { + t.Skip("CANTON_GRPC_URL unset: not configured for real Canton testnet") + } + + env := devenvtests.ParseEnvFromFlag(t) + configPath := filepath.Join("..", "..", devenvtests.ResolveConfigPath(env)) + in, err := ccv.LoadOutput[ccv.Cfg](configPath) + require.NoError(t, err) + + var cantonBlockchain *blockchain.Input + for _, bc := range in.Blockchains { + if bc.Type == blockchain.TypeCanton && bc.ChainID == "TestNet" { + cantonBlockchain = bc + break + } + } + require.NotNil(t, cantonBlockchain, "need Canton TestNet blockchain in %s", configPath) + + ctx := t.Context() + chainBC, _, err := cantondevenv.NewCLDF(ctx, cantonBlockchain) + require.NoError(t, err) + + chain, ok := chainBC.(*canton.Chain) + require.True(t, ok, "expected *canton.Chain, got %T", chainBC) + require.NotEmpty(t, chain.Participants) + + participant := chain.Participants[0] + require.NotEmpty(t, participant.PartyID, "participant should be connected with a party ID") + t.Logf("connected participant user_id=%s party_id=%s grpc=%s", participant.UserID, participant.PartyID, participant.Endpoints.GRPCLedgerAPIURL) + + holdings, err := testhelpers.ListHoldingsForInstrument( + ctx, + participant, + nil, + testhelpers.WithHoldingOwner(participant.PartyID), + ) + require.NoError(t, err) + t.Logf("listed %d holdings for party %s", len(holdings), participant.PartyID) + for _, holding := range holdings { + t.Logf("holding contract_id=%s instrument=%s amount=%s", holding.ContractID, holding.View.InstrumentId.Id, holding.Amount) + } +} diff --git a/ccip/devenv/tests/token_lane.go b/ccip/devenv/tests/token_lane.go index 85a9f4e37..4180ec6e9 100644 --- a/ccip/devenv/tests/token_lane.go +++ b/ccip/devenv/tests/token_lane.go @@ -119,8 +119,11 @@ func ResolveTokenLane( cfg, matched := trySelectTokenConfig(cfgs, dir.PoolRef) if !matched { - t.Fatalf("no token transfer config on chain %d matches pool %s (have %s)", - srcSelector, poolRefString(dir.PoolRef), poolRefsString(cfgs)) + if !env.IsRemote() { + t.Fatalf("no token transfer config on chain %d matches pool %s (have %s)", + srcSelector, poolRefString(dir.PoolRef), poolRefsString(cfgs)) + } + return resolveTokenLaneFromDatastore(t, cldfEnv, dir, srcSelector, destSelectors) } for _, sel := range destSelectors { @@ -252,6 +255,124 @@ func tokenDirectionsForEnv(cfg tokenConfigTOML, env CCIPEnv) (tokenDirectionsTOM } } +func resolveTokenLaneFromDatastore( + t *testing.T, + env *deployment.Environment, + dir tokenDirectionParsed, + srcSelector uint64, + destSelectors []uint64, +) TokenLane { + t.Helper() + + requireAddressRefInDatastore(t, env.DataStore, srcSelector, dir.PoolRef, "source pool") + + require.NotNil(t, dir.RemotePoolRef, "remote_pool_* required for prod datastore fallback") + + for _, sel := range destSelectors { + if isCantonSelector(sel) { + requireAddressRefInDatastore(t, env.DataStore, sel, *dir.RemotePoolRef, "remote pool") + } + } + + lane := TokenLane{ + PoolRef: dir.PoolRef, + TransferAmount: dir.TransferAmount, + TransferInstrumentID: dir.TransferInstrumentID, + ExecutionGasLimit: dir.ExecutionGasLimit, + FinalityConfig: dir.FinalityConfig, + DestTokenBySelector: make(map[uint64]protocol.UnknownAddress, len(destSelectors)), + } + + if !isCantonSelector(srcSelector) { + srcToken, err := resolveSrcTokenFromDatastore(env.DataStore, srcSelector) + require.NoError(t, err, "resolve source token on chain %d from datastore", srcSelector) + lane.SrcToken = srcToken + } else { + tokenRef := datastore.AddressRef{ + Type: datastore.ContractType("Token"), + Version: semver.MustParse("2.0.0"), + Qualifier: dir.PoolRef.Qualifier, + } + lane.TransferInstrument = resolveCantonTransferInstrument(t, env.DataStore, srcSelector, tokenRef, dir.PoolRef, dir.TransferInstrumentID) + } + + for _, sel := range destSelectors { + if isCantonSelector(sel) { + continue + } + destToken, err := resolveDestTokenFromDatastore(env.DataStore, sel, *dir.RemotePoolRef) + require.NoError(t, err, "resolve destination token on chain %d from datastore", sel) + lane.DestTokenBySelector[sel] = destToken + } + + return lane +} + +func resolveSrcTokenFromDatastore(ds datastore.DataStore, chainSelector uint64) (protocol.UnknownAddress, error) { + candidates := []datastore.AddressRef{ + { + Type: datastore.ContractType("BurnMintERC20WithDrip"), + Version: semver.MustParse("1.5.0"), + Qualifier: "TEST", + }, + { + Type: datastore.ContractType("BurnMintERC20WithDripToken"), + Version: semver.MustParse("1.0.0"), + Qualifier: "", + }, + } + + var lastErr error + for _, ref := range candidates { + token, err := resolveTokenRef(ds, chainSelector, ref) + if err == nil { + return token, nil + } + lastErr = err + } + + return protocol.UnknownAddress{}, fmt.Errorf("no source token candidate in datastore on chain %d: %w", chainSelector, lastErr) +} + +func resolveDestTokenFromDatastore(ds datastore.DataStore, chainSelector uint64, remotePoolRef datastore.AddressRef) (protocol.UnknownAddress, error) { + candidates := []datastore.AddressRef{ + { + Type: datastore.ContractType("BurnMintERC20WithDrip"), + Version: semver.MustParse("1.5.0"), + Qualifier: remotePoolRef.Qualifier, + }, + { + Type: datastore.ContractType("BurnMintERC20WithDripToken"), + Version: semver.MustParse("1.0.0"), + Qualifier: remotePoolRef.Qualifier, + }, + } + + var lastErr error + for _, ref := range candidates { + token, err := resolveTokenRef(ds, chainSelector, ref) + if err == nil { + return token, nil + } + lastErr = err + } + + return protocol.UnknownAddress{}, fmt.Errorf("no destination token candidate in datastore on chain %d: %w", chainSelector, lastErr) +} + +func requireAddressRefInDatastore( + t *testing.T, + ds datastore.DataStore, + chainSelector uint64, + ref datastore.AddressRef, + label string, +) { + t.Helper() + + _, err := ds.Addresses().Get(datastore.NewAddressRefKey(chainSelector, ref.Type, ref.Version, ref.Qualifier)) + require.NoError(t, err, "%s %s not found on chain %d", label, poolRefString(ref), chainSelector) +} + func trySelectTokenConfig(cfgs []tokenscore.TokenTransferConfig, poolRef datastore.AddressRef) (tokenscore.TokenTransferConfig, bool) { matches := make([]tokenscore.TokenTransferConfig, 0, 1) for _, cfg := range cfgs { diff --git a/ccip/devenv/verifier_observation.go b/ccip/devenv/verifier_observation.go index ac12e11ce..dc495ce6f 100644 --- a/ccip/devenv/verifier_observation.go +++ b/ccip/devenv/verifier_observation.go @@ -11,56 +11,58 @@ import ( ) // VerifierObservation holds off-chain clients used to wait for CCIP verifier -// results (aggregator + indexer). ConfirmExecOnDest needs these; it does not -// need ChainsMap or other Lib methods. +// results (indexer required; aggregator optional). ConfirmExecOnDest needs these; +// it does not need ChainsMap or other Lib methods. // // Build from a CCV env Lib via [VerifierObservationFromLib] (requires -// [ccv.NewLibFromCCVEnv] — CLDF-only Lib backends cannot provide aggregator/indexer). +// [ccv.NewLibFromCCVEnv] — CLDF-only Lib backends cannot provide indexer). type VerifierObservation struct { AggregatorClient *ccv.AggregatorClient IndexerMonitor *ccv.IndexerMonitor } func (o VerifierObservation) wired() bool { - return o.AggregatorClient != nil && o.IndexerMonitor != nil + return o.IndexerMonitor != nil } -// VerifierObservationFromLib extracts aggregator and indexer clients from lib. +// VerifierObservationFromLib extracts indexer (required) and optional aggregator +// clients from lib. func VerifierObservationFromLib(lib ccv.Lib) (VerifierObservation, error) { if lib == nil { return VerifierObservation{}, fmt.Errorf("VerifierObservationFromLib: lib is nil") } - aggregatorClients, err := lib.AllAggregators() + indexerMonitor, err := lib.IndexerMonitor() if err != nil { - return VerifierObservation{}, fmt.Errorf("all aggregators: %w", err) + return VerifierObservation{}, fmt.Errorf("indexer monitor: %w", err) } - aggregatorClient, ok := aggregatorClients[devenvcommon.DefaultCommitteeVerifierQualifier] - if !ok || aggregatorClient == nil { - return VerifierObservation{}, fmt.Errorf("no aggregator client for qualifier %q", devenvcommon.DefaultCommitteeVerifierQualifier) + + obs := VerifierObservation{ + IndexerMonitor: indexerMonitor, } - indexerMonitor, err := lib.IndexerMonitor() + aggregatorClients, err := lib.AllAggregators() if err != nil { - return VerifierObservation{}, fmt.Errorf("indexer monitor: %w", err) + return obs, nil + } + aggregatorClient, ok := aggregatorClients[devenvcommon.DefaultCommitteeVerifierQualifier] + if ok && aggregatorClient != nil { + obs.AggregatorClient = aggregatorClient } - return VerifierObservation{ - AggregatorClient: aggregatorClient, - IndexerMonitor: indexerMonitor, - }, nil + return obs, nil } // AssertMessageWithVerifierObservation waits for verifier results for messageID -// using aggregator and indexer only (no chain map). +// using indexer (and aggregator when configured). func AssertMessageWithVerifierObservation( ctx context.Context, obs VerifierObservation, messageID protocol.Bytes32, opts tcapi.AssertMessageOptions, ) (tcapi.AssertionResult, error) { - if !obs.wired() { - return tcapi.AssertionResult{}, fmt.Errorf("verifier observation not wired (aggregator and indexer required)") + if obs.IndexerMonitor == nil { + return tcapi.AssertionResult{}, fmt.Errorf("verifier observation not wired (indexer required)") } testCtx, cleanupFn := tcapi.NewTestingContext(ctx, nil, obs.AggregatorClient, obs.IndexerMonitor) diff --git a/ccip/devenv/verifier_observation_test.go b/ccip/devenv/verifier_observation_test.go new file mode 100644 index 000000000..f8fd6412c --- /dev/null +++ b/ccip/devenv/verifier_observation_test.go @@ -0,0 +1,22 @@ +package devenv + +import ( + "testing" + + ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" + "github.com/stretchr/testify/require" +) + +func TestVerifierObservation_wired_indexerOnly(t *testing.T) { + obs := VerifierObservation{ + IndexerMonitor: &ccv.IndexerMonitor{}, + } + require.True(t, obs.wired()) +} + +func TestVerifierObservation_wired_indexerNil(t *testing.T) { + obs := VerifierObservation{ + AggregatorClient: &ccv.AggregatorClient{}, + } + require.False(t, obs.wired()) +} From c1774c8c36637e5ad83feaa703949432666ac4a0 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 14:03:15 -0300 Subject: [PATCH 15/26] fix(load): prod-testnet needs v1_0_0 bindings --- .github/actions/ccip-load-test/action.yml | 2 +- Makefile | 12 +- ccip/devenv/README.md | 13 +++ ccip/devenv/constants.go | 15 ++- ccip/devenv/execute_op_devenv.go | 7 ++ ccip/devenv/execute_op_prod.go | 24 ++++ ccip/devenv/fee_quoter_limits.go | 30 ++--- ccip/devenv/impl.go | 103 +++++++++--------- .../devenv/ledgerbind/context_adapt_devenv.go | 23 ++++ ccip/devenv/ledgerbind/context_adapt_prod.go | 50 +++++++++ ccip/devenv/ledgerbind/doc.go | 7 ++ ccip/devenv/ledgerbind/exports_devenv.go | 58 ++++++++++ ccip/devenv/ledgerbind/exports_prod.go | 54 +++++++++ ccip/devenv/ledgerbind/inputs_devenv.go | 64 +++++++++++ ccip/devenv/ledgerbind/inputs_prod.go | 64 +++++++++++ ccip/devenv/manual_execution.go | 58 +++++----- ccip/devenv/participant.go | 7 ++ ccip/devenv/send_op_devenv.go | 7 ++ ccip/devenv/send_op_prod.go | 24 ++++ ccip/devenv/tests/e2e/evm2canton_e2e_test.go | 26 +---- ccip/devenv/tests/env.go | 2 +- ccip/devenv/tests/helpers.go | 13 ++- ccip/devenv/tests/load/gun_evm2canton_test.go | 4 +- .../tests/load/gun_evm2canton_token_test.go | 4 +- ccip/devenv/tests/load/load_helpers.go | 16 +-- 25 files changed, 539 insertions(+), 148 deletions(-) create mode 100644 ccip/devenv/execute_op_devenv.go create mode 100644 ccip/devenv/execute_op_prod.go create mode 100644 ccip/devenv/ledgerbind/context_adapt_devenv.go create mode 100644 ccip/devenv/ledgerbind/context_adapt_prod.go create mode 100644 ccip/devenv/ledgerbind/doc.go create mode 100644 ccip/devenv/ledgerbind/exports_devenv.go create mode 100644 ccip/devenv/ledgerbind/exports_prod.go create mode 100644 ccip/devenv/ledgerbind/inputs_devenv.go create mode 100644 ccip/devenv/ledgerbind/inputs_prod.go create mode 100644 ccip/devenv/send_op_devenv.go create mode 100644 ccip/devenv/send_op_prod.go diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index 18ededfb7..afec3fc88 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -144,7 +144,7 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" + go test -tags=prodledger -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" - name: Upload devenv logs if: always() && inputs.ccip_env == 'devenv' diff --git a/Makefile b/Makefile index a82681afc..a638a47d0 100644 --- a/Makefile +++ b/Makefile @@ -109,11 +109,11 @@ run-evm2canton-load: ## EVM→Canton WASP load (requires running devenv + env-ca .PHONY: run-evm2canton-load-prod run-evm2canton-load-prod: ## EVM→Canton WASP load on prod-testnet (Canton TestNet + Sepolia; set CANTON_* and PRIVATE_KEY). - cd ccip/devenv/tests/load && go test -timeout 45m -v -count 1 -ccip-env=prod-testnet -run '^TestEVM2Canton_Load$$' + cd ccip/devenv/tests/load && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml go test -tags=prodledger -timeout 45m -v -count 1 -ccip-env=prod-testnet -run '^TestEVM2Canton_Load$$' .PHONY: run-canton2evm-load-prod run-canton2evm-load-prod: ## Canton→EVM WASP load on prod-testnet (send-only; set CANTON_* and PRIVATE_KEY). - cd ccip/devenv/tests/load && CANTON_LOAD_SKIP_EXEC_CONFIRM=true go test -timeout 30m -v -count=1 \ + cd ccip/devenv/tests/load && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml CANTON_LOAD_SKIP_EXEC_CONFIRM=true go test -tags=prodledger -timeout 30m -v -count=1 \ -ccip-env=prod-testnet -run '^TestCanton2EVM_Load$$' .PHONY: run-canton2evm-token-load @@ -126,23 +126,23 @@ run-evm2canton-token-load: ## EVM→Canton token WASP load (requires running dev .PHONY: run-canton2evm-token-load-prod run-canton2evm-token-load-prod: ## Canton→EVM token WASP load on prod-testnet (set CANTON_* and PRIVATE_KEY). - cd ccip/devenv/tests/load && go test -timeout 30m -v -count=1 \ + cd ccip/devenv/tests/load && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml go test -tags=prodledger -timeout 30m -v -count=1 \ -ccip-env=prod-testnet -run '^TestCanton2EVM_TokenLoad$$' .PHONY: run-evm2canton-token-load-prod run-evm2canton-token-load-prod: ## EVM→Canton token WASP load on prod-testnet (set CANTON_* and PRIVATE_KEY). - cd ccip/devenv/tests/load && go test -timeout 45m -v -count=1 \ + cd ccip/devenv/tests/load && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml go test -tags=prodledger -timeout 45m -v -count=1 \ -ccip-env=prod-testnet -run '^TestEVM2Canton_TokenLoad$$' .PHONY: run-evm2canton-token-e2e-prod run-evm2canton-token-e2e-prod: ## EVM→Canton token e2e on prod-testnet. - cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + cd ccip/devenv/tests/e2e && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml go test -tags=prodledger -timeout 15m -v -count=1 \ -ccip-env=prod-testnet \ -run '^TestEVM2Canton_Basic$/^token_transfer$$' .PHONY: run-canton2evm-token-e2e-prod run-canton2evm-token-e2e-prod: ## Canton→EVM token e2e on prod-testnet. - cd ccip/devenv/tests/e2e && go test -timeout 15m -v -count=1 \ + cd ccip/devenv/tests/e2e && CCIP_CONFIG_FILE=env-prod-testnet.ci.toml go test -tags=prodledger -timeout 15m -v -count=1 \ -ccip-env=prod-testnet \ -run '^TestCanton2EVM_Basic$/^EOA receiver and default committee verifier token transfer$$' diff --git a/ccip/devenv/README.md b/ccip/devenv/README.md index f0458db33..ab1592192 100644 --- a/ccip/devenv/README.md +++ b/ccip/devenv/README.md @@ -69,6 +69,19 @@ CCIP_ENV=prod-testnet \ Token e2e: **EVM→Canton token** and **Canton→EVM token** are supported on prod-testnet (see [EVM→Canton token e2e (prod-testnet)](#evm→canton-token-e2e-prod-testnet) and [Canton→EVM token e2e (prod-testnet)](#canton→evm-token-e2e-prod-testnet)). A second prod run reuses existing router/sender/receiver contracts on ledger when instance IDs match. +### Ledger bindings (`-tags=prodledger`) + +Devenv tests compile against `bindings/generated/latest` (current dev DAML module layout). Prod-testnet/mainnet contracts on ledger still use the older layout in `bindings/generated/v1_0_0`. + +Build prod-targeting tests with `-tags=prodledger` so devenv code resolves the correct template IDs (e.g. `CCIP.PerPartyRouter` vs `CCIP.RuntimeV1.PerPartyRouter`). Devenv runs omit the tag. + +| Target | Build tag | Bindings | +|---|---|---| +| devenv (default) | _(none)_ | `bindings/generated/latest` | +| prod-testnet / mainnet | `-tags=prodledger` | `bindings/generated/v1_0_0` | + +Implementation lives in [`ccip/devenv/ledgerbind/`](./ledgerbind/). + ## Load tests Load tests live in `ccip/devenv/tests/load`. They use [WASP](https://pkg.go.dev/github.com/smartcontractkit/chainlink-testing-framework/wasp) and run sequentially (RPS=1) because Canton holdings are single-flight. diff --git a/ccip/devenv/constants.go b/ccip/devenv/constants.go index c0234b457..a27016f47 100644 --- a/ccip/devenv/constants.go +++ b/ccip/devenv/constants.go @@ -1,6 +1,9 @@ package devenv -import "github.com/smartcontractkit/chainlink-ccv/protocol" +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccv/protocol" +) // Shared devenv test constants (message and token paths). @@ -10,12 +13,12 @@ const ( // CantonToEVMFeeAmount is the per-message CCIP fee budget in Amulet units for // message-only sends (200k gas). Kept low for prod-testnet compatibility. - CantonToEVMFeeAmount int64 = 50 + CantonToEVMFeeAmount int64 = 5 // CantonToEVMTokenTransferFeeAmount is the per-message fee budget for token transfers // (500k execution gas), which quote ~127 Amulet per send in devenv. Used only by // token e2e/load tests; must cover one send and leave enough change for sequential sends. - CantonToEVMTokenTransferFeeAmount int64 = 130 + CantonToEVMTokenTransferFeeAmount int64 = 13 // Canton token amounts use 10-decimal fixed point (e.g. 1_000_000_000 = 0.1). CantonFixedPointScale int64 = 10_000_000_000 @@ -24,3 +27,9 @@ const ( // CantonToEVMTokenSequentialSends is how many token transfers the Canton→EVM e2e subtest sends. CantonToEVMTokenSequentialSends = 2 ) + +// EVMToCantonNoExecutionExecutor is Client.NO_EXECUTION_ADDRESS on EVM source chains. +// Canton dest execution is manual (ManuallyExecuteMessage); no EVM Executor supports Canton. +var EVMToCantonNoExecutionExecutor = protocol.UnknownAddress( + common.HexToAddress("0xEBa517d200000000000000000000000000000000").Bytes(), +) diff --git a/ccip/devenv/execute_op_devenv.go b/ccip/devenv/execute_op_devenv.go new file mode 100644 index 000000000..767ad836a --- /dev/null +++ b/ccip/devenv/execute_op_devenv.go @@ -0,0 +1,7 @@ +//go:build !prodledger + +package devenv + +import receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + +var ccipReceiverExecuteOperation = receiverop.Execute diff --git a/ccip/devenv/execute_op_prod.go b/ccip/devenv/execute_op_prod.go new file mode 100644 index 000000000..9e00c8287 --- /dev/null +++ b/ccip/devenv/execute_op_prod.go @@ -0,0 +1,24 @@ +//go:build prodledger + +package devenv + +import ( + ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var prodReceiverEncoder = ccipreceiver.NewContract("", "CCIP.CCIPReceiver", "CCIPReceiver").Encoder() + +var ccipReceiverExecuteOperation = contract.NewExercise(contract.ExerciseParams[ccipreceiver.Execute]{ + Name: "canton/ccip/receiver/execute", + Version: receiver.Version, + Description: "Calls the Execute choice on a CCIP Receiver contract", + ContractType: receiver.ContractType, + Validate: func(input ccipreceiver.Execute) error { + return nil + }, + Template: ccipreceiver.CCIPReceiver{}, + Method: ccipreceiver.CCIPReceiver{}.Execute, + EncodeMethod: prodReceiverEncoder.Execute, +}) diff --git a/ccip/devenv/fee_quoter_limits.go b/ccip/devenv/fee_quoter_limits.go index 4f96b1128..f4c56d624 100644 --- a/ccip/devenv/fee_quoter_limits.go +++ b/ccip/devenv/fee_quoter_limits.go @@ -11,7 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/go-daml/pkg/service/ledger" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" "github.com/smartcontractkit/chainlink-canton/contracts" feequoterop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/fee_quoter" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" @@ -49,12 +49,12 @@ func (c *Chain) GetMaxPerMsgGasLimit(ctx context.Context, remoteChainSelector ui return uint32(cfg.MaxPerMsgGasLimit), nil } -func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uint64) (core.FeeQuoterDestChainConfig, error) { +func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uint64) (ledgerbind.FeeQuoterDestChainConfig, error) { if c.e == nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("canton chain environment is nil") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("canton chain environment is nil") } if len(c.chain.Participants) == 0 { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("no canton participants configured") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("no canton participants configured") } feeQuoterRef, err := c.e.DataStore.Addresses().Get(datastore.NewAddressRefKey( @@ -64,7 +64,7 @@ func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uin "", )) if err != nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("resolve FeeQuoter address: %w", err) + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("resolve FeeQuoter address: %w", err) } participant := c.chain.Participants[0] @@ -75,22 +75,22 @@ func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uin ctx, participant.LedgerServices.State, []string{party}, - core.FeeQuoter{}.GetTemplateID(), + ledgerbind.FeeQuoter{}.GetTemplateID(), feeQuoterAddress, ) if err != nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("find active FeeQuoter contract: %w", err) + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("find active FeeQuoter contract: %w", err) } createArgs := activeContract.GetCreatedEvent().GetCreateArguments() if createArgs == nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("FeeQuoter create arguments missing") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("FeeQuoter create arguments missing") } return destChainConfigFromFeeQuoterCreateArgs(createArgs, remoteChainSelector) } -func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChainSelector uint64) (core.FeeQuoterDestChainConfig, error) { +func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChainSelector uint64) (ledgerbind.FeeQuoterDestChainConfig, error) { selectorKey := strconv.FormatUint(remoteChainSelector, 10) for _, field := range createArgs.GetFields() { if field.GetLabel() != "destChainConfigs" { @@ -98,7 +98,7 @@ func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChai } genMap := field.GetValue().GetGenMap() if genMap == nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs is not a GenMap") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs is not a GenMap") } for _, entry := range genMap.GetEntries() { key := strings.TrimSuffix(entry.GetKey().GetNumeric(), ".") @@ -107,18 +107,18 @@ func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChai } record := entry.GetValue().GetRecord() if record == nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("dest chain config value is not a record") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("dest chain config value is not a record") } - var cfg core.FeeQuoterDestChainConfig + var cfg ledgerbind.FeeQuoterDestChainConfig if err := ledger.RecordToStruct(record, &cfg); err != nil { - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("parse dest chain config record: %w", err) + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("parse dest chain config record: %w", err) } return cfg, nil } - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("no FeeQuoter dest config for chain selector %d", remoteChainSelector) + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("no FeeQuoter dest config for chain selector %d", remoteChainSelector) } - return core.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs field not found on FeeQuoter") + return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs field not found on FeeQuoter") } diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index dd472c1a9..65d4ca6da 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -43,20 +43,17 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/clientapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" - ccipevents "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" - ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_transfer_instruction_v1" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" "github.com/smartcontractkit/chainlink-canton/contracts" cantonchangesets "github.com/smartcontractkit/chainlink-canton/deployment/changesets" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" executor2 "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/executor" feequoterop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/fee_quoter" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/global_config" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/token_admin_registry" dsutils "github.com/smartcontractkit/chainlink-canton/deployment/utils/datastore" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" @@ -1209,17 +1206,19 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint } } - maxDataBytes, err := c.GetMaxDataBytes(ctx, dest) - if err != nil { - return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf( - "canton SendMessage: resolve max data bytes for destination %d: %w", dest, err, - ) - } - if len(fields.Data) > int(maxDataBytes) { - return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf( - "canton SendMessage: payload exceeds destination maxDataBytes (%d > %d)", - len(fields.Data), maxDataBytes, - ) + if !c.isRemote() { + maxDataBytes, err := c.GetMaxDataBytes(ctx, dest) + if err != nil { + return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf( + "canton SendMessage: resolve max data bytes for destination %d: %w", dest, err, + ) + } + if len(fields.Data) > int(maxDataBytes) { + return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf( + "canton SendMessage: payload exceeds destination maxDataBytes (%d > %d)", + len(fields.Data), maxDataBytes, + ) + } } sendLog := c.logger.Info().Str("NextFeeCID", c.nextFeeCID) @@ -1281,29 +1280,29 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("find active contract ID for router at address %s: %w", c.routerAddress, err) } var disclosedContracts []*apiv2.DisclosedContract - sendArgs := ccipsender.Send{ + sendArgs := ledgerbind.Send{ RouterCid: types.CONTRACT_ID(routerCid), DestinationChainSelector: types.NUMERIC(strconv.FormatUint(dest, 10)), - Message: clientapi.Canton2AnyMessage{ + Message: ledgerbind.Canton2AnyMessage{ Receiver: types.TEXT(hex.EncodeToString(fields.Receiver)), Payload: types.TEXT(hex.EncodeToString(fields.Data)), - FeeToken: c.feeTokenInstrument, - ExtraArgs: clientapi.ExtraArgs{ - V3: &clientapi.GenericExtraArgsV3{ + FeeToken: ledgerbind.AdaptInstrumentId(c.feeTokenInstrument), + ExtraArgs: ledgerbind.ExtraArgs{ + V3: &ledgerbind.GenericExtraArgsV3{ GasLimit: types.INT64(opts.ExecutionGasLimit), - Executor: clientapi.ExecutorExtraArg{ - ExecutorUseDefault: &clientapi.ExecutorUseDefault{}, + Executor: ledgerbind.ExecutorExtraArg{ + ExecutorUseDefault: &ledgerbind.ExecutorUseDefault{}, }, }, }, }, - FeeTokenInput: ccipsender.FeeTokenInput{ + FeeTokenInput: ledgerbind.FeeTokenInput{ SenderInputCids: []types.CONTRACT_ID{types.CONTRACT_ID(c.nextFeeCID)}, FeeTokenTransferFactory: types.CONTRACT_ID(feeTransferFactorycid), - FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + FeeTokenExtraArgs: ledgerbind.AdaptExtraArgs(splice_api_token_metadata_v1.ExtraArgs{ Context: splice_api_token_metadata_v1.ChoiceContext{Values: testhelpers.ExtractChoiceContextValues(feeTransferFactoryChoiceContextValue)}, Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, - }, + }), }, } @@ -1327,18 +1326,19 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get Token Pool Send disclosure for token pool at address %s: %w", tokenPoolAddress.String(), err) } - sendArgs.Message.TokenTransfer = &clientapi.TokenTransfer{ - Token: splice_api_token_holding_v1.InstrumentId{ + sendArgs.Message.TokenTransfer = &ledgerbind.TokenTransfer{ + Token: ledgerbind.AdaptInstrumentId(splice_api_token_holding_v1.InstrumentId{ Admin: types.PARTY(outgoingMessage.TokenTransfer.Token.Admin), Id: types.TEXT(outgoingMessage.TokenTransfer.Token.Id), - }, + }), Amount: types.NUMERIC(outgoingMessage.TokenTransfer.Amount), } - sendArgs.TokenTransferInput = &ccipsender.TokenTransferInput{ - SenderInputCids: []types.CONTRACT_ID{types.CONTRACT_ID(c.nextTransferCID)}, - TokenPoolCid: types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), - Context: tokenPoolSendDisclosure.ChoiceContext, - } + tokenTransferInput := ledgerbind.NewTokenTransferInput( + []types.CONTRACT_ID{types.CONTRACT_ID(c.nextTransferCID)}, + types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), + tokenPoolSendDisclosure.ChoiceContext, + ) + sendArgs.TokenTransferInput = &tokenTransferInput tokenPoolRequiredCCVs = tokenPoolSendDisclosure.RequiredCCVs disclosedContracts = append(disclosedContracts, tokenPoolSendDisclosure.DisclosedContracts...) } @@ -1348,7 +1348,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get CCIP Send disclosure: %w", err) } - sendArgs.Context = ccipSendDisclosure.ChoiceContext + sendArgs.Context = ledgerbind.AdaptChoiceContext(ccipSendDisclosure.ChoiceContext) sendArgs.FeeTokenInput.FeeTokenConfigCid = types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid) disclosedContracts = append(disclosedContracts, ccipSendDisclosure.DisclosedContracts...) @@ -1367,15 +1367,15 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get CCV Send disclosure for CCV %q: %w", v, err) } - sendArgs.CcvSendInputs = append(sendArgs.CcvSendInputs, ccipsender.CCVSendInput{ - CcvAddress: ccvSendDisclosure.Address.Binding(), - CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), - Context: ccvSendDisclosure.ChoiceContext, - }) - sendArgs.Message.ExtraArgs.V3.Ccvs = append(sendArgs.Message.ExtraArgs.V3.Ccvs, clientapi.CCVExtraArg{ - CcvAddress: ccvSendDisclosure.Address.Binding(), - CcvArgs: "", - }) + sendArgs.CcvSendInputs = append(sendArgs.CcvSendInputs, ledgerbind.NewCCVSendInput( + ccvSendDisclosure.Address, + types.CONTRACT_ID(ccvSendDisclosure.ContractId), + ccvSendDisclosure.ChoiceContext, + )) + sendArgs.Message.ExtraArgs.V3.Ccvs = append(sendArgs.Message.ExtraArgs.V3.Ccvs, ledgerbind.NewCCVExtraArg( + ccvSendDisclosure.Address, + "", + )) allCCVs = append(allCCVs, ccvSendDisclosure.Address.InstanceAddress().Hex()) disclosedContracts = append(disclosedContracts, ccvSendDisclosure.DisclosedContracts...) } @@ -1393,10 +1393,11 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get Executor Send disclosure for Executor %q: %w", *ccipSendDisclosure.Executor, err) } - sendArgs.ExecutorInput = &ccipsender.ExecutorInput{ - ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), - Context: executorSendDisclosure.ChoiceContext, - } + executorInput := ledgerbind.NewExecutorInput( + types.CONTRACT_ID(executorSendDisclosure.ContractId), + executorSendDisclosure.ChoiceContext, + ) + sendArgs.ExecutorInput = &executorInput disclosedContracts = append(disclosedContracts, executorSendDisclosure.DisclosedContracts...) } @@ -1407,7 +1408,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint disclosedContracts = testhelpers.DeduplicateDisclosedContracts(disclosedContracts...) // Call CCIPSend - ccipSendReport, err := operations.ExecuteOperation(c.e.OperationsBundle, sender.Send, c.chain, contract.ChoiceInput[ccipsender.Send]{ + ccipSendReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipSenderSendOperation, c.chain, contract.ChoiceInput[ledgerbind.Send]{ InstanceAddress: c.senderAddress, ParticipantIndex: clientIdx, Args: sendArgs, @@ -1429,7 +1430,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint { IdentifierFilter: &apiv2.CumulativeFilter_TemplateFilter{ TemplateFilter: &apiv2.TemplateFilter{ - TemplateId: contracts.TemplateIDFromBinding(ccipevents.CCIPMessageSent{}).ToLedgerIdentifier(), + TemplateId: contracts.TemplateIDFromBinding(ledgerbind.CCIPMessageSent{}).ToLedgerIdentifier(), IncludeCreatedEventBlob: true, }, }, @@ -1512,7 +1513,7 @@ type ccipMessageSentFromSendUpdate struct { // before this send; returned seqNo is either previousSeq+1 or the sequence from the encoded payload // when that path is present. func parseFirstCCIPMessageSentFromLedgerEvents(events []*apiv2.Event, previousSeq uint64) (ccipMessageSentFromSendUpdate, error) { - messageSentTemplateID := contracts.TemplateIDFromBinding(ccipevents.CCIPMessageSent{}) + messageSentTemplateID := contracts.TemplateIDFromBinding(ledgerbind.CCIPMessageSent{}) // Find CCIPMessageSent event in the events var created *apiv2.CreatedEvent @@ -1533,7 +1534,7 @@ func parseFirstCCIPMessageSentFromLedgerEvents(events []*apiv2.Event, previousSe return ccipMessageSentFromSendUpdate{}, fmt.Errorf("no CCIPMessageSent event found in sender transaction") } - parsed, err := bindings.UnmarshalCreatedEvent[ccipevents.CCIPMessageSent](created) + parsed, err := bindings.UnmarshalCreatedEvent[ledgerbind.CCIPMessageSent](created) if err != nil { return ccipMessageSentFromSendUpdate{}, fmt.Errorf("unmarshal CCIPMessageSent created event: %w", err) } diff --git a/ccip/devenv/ledgerbind/context_adapt_devenv.go b/ccip/devenv/ledgerbind/context_adapt_devenv.go new file mode 100644 index 000000000..a9097625d --- /dev/null +++ b/ccip/devenv/ledgerbind/context_adapt_devenv.go @@ -0,0 +1,23 @@ +//go:build !prodledger + +package ledgerbind + +import ( + latestholding "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" + latestmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" +) + +// AdaptChoiceContext maps caller-side choice context into ledger Send bindings. +func AdaptChoiceContext(ctx latestmeta.ChoiceContext) latestmeta.ChoiceContext { + return ctx +} + +// AdaptExtraArgs maps caller-side token extra args into ledger Send bindings. +func AdaptExtraArgs(args latestmeta.ExtraArgs) latestmeta.ExtraArgs { + return args +} + +// AdaptInstrumentId maps caller-side instrument IDs into ledger Send bindings. +func AdaptInstrumentId(id latestholding.InstrumentId) latestholding.InstrumentId { + return id +} diff --git a/ccip/devenv/ledgerbind/context_adapt_prod.go b/ccip/devenv/ledgerbind/context_adapt_prod.go new file mode 100644 index 000000000..9440c58cc --- /dev/null +++ b/ccip/devenv/ledgerbind/context_adapt_prod.go @@ -0,0 +1,50 @@ +//go:build prodledger + +package ledgerbind + +import ( + "encoding/json" + "fmt" + + latestholding "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" + latestmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" + prodholding "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/splice/splice_api_token_holding_v1" + prodmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/splice/splice_api_token_metadata_v1" +) + +func adaptViaJSON[T any](in any) (T, error) { + var out T + data, err := json.Marshal(in) + if err != nil { + return out, fmt.Errorf("marshal for ledger binding adapt: %w", err) + } + if err := json.Unmarshal(data, &out); err != nil { + return out, fmt.Errorf("unmarshal for ledger binding adapt: %w", err) + } + + return out, nil +} + +func mustAdapt[T any](in any) T { + out, err := adaptViaJSON[T](in) + if err != nil { + panic(err) + } + + return out +} + +// AdaptChoiceContext maps caller-side choice context into v1_0_0 ledger Send bindings. +func AdaptChoiceContext(ctx latestmeta.ChoiceContext) prodmeta.ChoiceContext { + return mustAdapt[prodmeta.ChoiceContext](ctx) +} + +// AdaptExtraArgs maps caller-side token extra args into v1_0_0 ledger Send bindings. +func AdaptExtraArgs(args latestmeta.ExtraArgs) prodmeta.ExtraArgs { + return mustAdapt[prodmeta.ExtraArgs](args) +} + +// AdaptInstrumentId maps caller-side instrument IDs into v1_0_0 ledger Send bindings. +func AdaptInstrumentId(id latestholding.InstrumentId) prodholding.InstrumentId { + return mustAdapt[prodholding.InstrumentId](id) +} diff --git a/ccip/devenv/ledgerbind/doc.go b/ccip/devenv/ledgerbind/doc.go new file mode 100644 index 000000000..a1be143c4 --- /dev/null +++ b/ccip/devenv/ledgerbind/doc.go @@ -0,0 +1,7 @@ +// Package ledgerbind selects CCIP ledger bindings at compile time. +// +// Devenv (default): bindings/generated/latest — current dev DAML layout. +// +// Prod / deployed ledger: go build -tags=prodledger +// Uses bindings/generated/v1_0_0 — template IDs matching Canton TestNet/mainnet today. +package ledgerbind diff --git a/ccip/devenv/ledgerbind/exports_devenv.go b/ccip/devenv/ledgerbind/exports_devenv.go new file mode 100644 index 000000000..75cf3bf66 --- /dev/null +++ b/ccip/devenv/ledgerbind/exports_devenv.go @@ -0,0 +1,58 @@ +//go:build !prodledger + +package ledgerbind + +import ( + rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipapi" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/clientapi" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" + ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" + ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" +) + +type ( + CreateRouter = rt.CreateRouter + PerPartyRouter = rt.PerPartyRouter + + FinalityConfig = ccipcodec.FinalityConfig + + ExecutionStateChanged = events.ExecutionStateChanged + CCIPMessageSent = events.CCIPMessageSent + + Canton2AnyMessage = clientapi.Canton2AnyMessage + ExtraArgs = clientapi.ExtraArgs + GenericExtraArgsV3 = clientapi.GenericExtraArgsV3 + ExecutorExtraArg = clientapi.ExecutorExtraArg + ExecutorUseDefault = clientapi.ExecutorUseDefault + TokenTransfer = clientapi.TokenTransfer + CCVExtraArg = clientapi.CCVExtraArg + + ApplyPriceUpdatersUpdate = core.ApplyPriceUpdatersUpdate + UpdatePrices = core.UpdatePrices + PriceUpdates = core.PriceUpdates + TokenPriceUpdate = core.TokenPriceUpdate + FeeQuoter = core.FeeQuoter + FeeQuoterDestChainConfig = core.FeeQuoterDestChainConfig + + CCIPReceiver = ccipreceiver.CCIPReceiver + CCIPSender = ccipsender.CCIPSender + Send = ccipsender.Send + FeeTokenInput = ccipsender.FeeTokenInput + TokenTransferInput = ccipsender.TokenTransferInput + CCVSendInput = ccipsender.CCVSendInput + ExecutorInput = ccipsender.ExecutorInput + + ReceiverExecute = ccipreceiver.Execute + ReceiverCCVInput = ccipreceiver.CCVInput + ReceiverTokenTransferInput = ccipreceiver.TokenTransferInput +) + +const ( + MessageExecutionStateUNTOUCHED = ccipapi.MessageExecutionStateUNTOUCHED + MessageExecutionStateIN_PROGRESS = ccipapi.MessageExecutionStateIN_PROGRESS + MessageExecutionStateSUCCESS = ccipapi.MessageExecutionStateSUCCESS + MessageExecutionStateFAILURE = ccipapi.MessageExecutionStateFAILURE +) diff --git a/ccip/devenv/ledgerbind/exports_prod.go b/ccip/devenv/ledgerbind/exports_prod.go new file mode 100644 index 000000000..d7159d50e --- /dev/null +++ b/ccip/devenv/ledgerbind/exports_prod.go @@ -0,0 +1,54 @@ +//go:build prodledger + +package ledgerbind + +import ( + rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/ccipruntime" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/core" + ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" + ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" +) + +type ( + CreateRouter = rt.CreateRouter + PerPartyRouter = rt.PerPartyRouter + + FinalityConfig = core.FinalityConfig + + ExecutionStateChanged = core.ExecutionStateChanged + CCIPMessageSent = core.CCIPMessageSent + + Canton2AnyMessage = core.Canton2AnyMessage + ExtraArgs = core.ExtraArgs + GenericExtraArgsV3 = core.GenericExtraArgsV3 + ExecutorExtraArg = core.ExecutorExtraArg + ExecutorUseDefault = core.ExecutorUseDefault + TokenTransfer = core.TokenTransfer + CCVExtraArg = core.CCVExtraArg + + ApplyPriceUpdatersUpdate = core.ApplyPriceUpdatersUpdate + UpdatePrices = core.UpdatePrices + PriceUpdates = core.PriceUpdates + TokenPriceUpdate = core.TokenPriceUpdate + FeeQuoter = core.FeeQuoter + FeeQuoterDestChainConfig = core.FeeQuoterDestChainConfig + + CCIPReceiver = ccipreceiver.CCIPReceiver + CCIPSender = ccipsender.CCIPSender + Send = ccipsender.Send + FeeTokenInput = ccipsender.FeeTokenInput + TokenTransferInput = ccipsender.TokenTransferInput + CCVSendInput = ccipsender.CCVSendInput + ExecutorInput = ccipsender.ExecutorInput + + ReceiverExecute = ccipreceiver.Execute + ReceiverCCVInput = ccipreceiver.CCVInput + ReceiverTokenTransferInput = ccipreceiver.TokenTransferInput +) + +const ( + MessageExecutionStateUNTOUCHED = core.MessageExecutionStateUNTOUCHED + MessageExecutionStateIN_PROGRESS = core.MessageExecutionStateIN_PROGRESS + MessageExecutionStateSUCCESS = core.MessageExecutionStateSUCCESS + MessageExecutionStateFAILURE = core.MessageExecutionStateFAILURE +) diff --git a/ccip/devenv/ledgerbind/inputs_devenv.go b/ccip/devenv/ledgerbind/inputs_devenv.go new file mode 100644 index 000000000..8bdbd8d7b --- /dev/null +++ b/ccip/devenv/ledgerbind/inputs_devenv.go @@ -0,0 +1,64 @@ +//go:build !prodledger + +package ledgerbind + +import ( + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/chainlink/chainlinkapi" + latestmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" + "github.com/smartcontractkit/chainlink-canton/contracts" + "github.com/smartcontractkit/go-daml/pkg/types" +) + +func rawInstanceAddressBinding(addr contracts.RawInstanceAddress) chainlinkapi.RawInstanceAddress { + return chainlinkapi.RawInstanceAddress{Unpack: types.TEXT(addr)} +} + +func NewCCVSendInput(ccvAddress contracts.RawInstanceAddress, ccvCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) CCVSendInput { + return CCVSendInput{ + CcvAddress: rawInstanceAddressBinding(ccvAddress), + CcvCid: ccvCid, + Context: AdaptChoiceContext(ctx), + } +} + +func NewCCVExtraArg(ccvAddress contracts.RawInstanceAddress, ccvArgs string) CCVExtraArg { + return CCVExtraArg{ + CcvAddress: rawInstanceAddressBinding(ccvAddress), + CcvArgs: types.TEXT(ccvArgs), + } +} + +func NewExecutorInput(executorCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) ExecutorInput { + return ExecutorInput{ + ExecutorCid: executorCid, + Context: AdaptChoiceContext(ctx), + } +} + +func NewTokenTransferInput(senderInputCids []types.CONTRACT_ID, tokenPoolCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) TokenTransferInput { + return TokenTransferInput{ + SenderInputCids: senderInputCids, + TokenPoolCid: tokenPoolCid, + Context: AdaptChoiceContext(ctx), + } +} + +func NewReceiverCCVInput(ccvCid types.CONTRACT_ID, verifierResults types.TEXT, ctx latestmeta.ChoiceContext) ReceiverCCVInput { + return ReceiverCCVInput{ + CcvCid: ccvCid, + VerifierResults: verifierResults, + Context: AdaptChoiceContext(ctx), + } +} + +func NewReceiverTokenTransferInput( + tokenPoolCid types.CONTRACT_ID, + tokenReceiverParty types.PARTY, + ctx latestmeta.ChoiceContext, +) ReceiverTokenTransferInput { + return ReceiverTokenTransferInput{ + TokenPoolCid: tokenPoolCid, + TokenReceiverParty: tokenReceiverParty, + Context: AdaptChoiceContext(ctx), + } +} diff --git a/ccip/devenv/ledgerbind/inputs_prod.go b/ccip/devenv/ledgerbind/inputs_prod.go new file mode 100644 index 000000000..c0960fdc6 --- /dev/null +++ b/ccip/devenv/ledgerbind/inputs_prod.go @@ -0,0 +1,64 @@ +//go:build prodledger + +package ledgerbind + +import ( + "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/chainlink/chainlinkapi" + latestmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" + "github.com/smartcontractkit/chainlink-canton/contracts" + "github.com/smartcontractkit/go-daml/pkg/types" +) + +func rawInstanceAddressBinding(addr contracts.RawInstanceAddress) chainlinkapi.RawInstanceAddress { + return chainlinkapi.RawInstanceAddress{Unpack: types.TEXT(addr)} +} + +func NewCCVSendInput(ccvAddress contracts.RawInstanceAddress, ccvCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) CCVSendInput { + return CCVSendInput{ + CcvAddress: rawInstanceAddressBinding(ccvAddress), + CcvCid: ccvCid, + CcvExtraContext: AdaptChoiceContext(ctx), + } +} + +func NewCCVExtraArg(ccvAddress contracts.RawInstanceAddress, ccvArgs string) CCVExtraArg { + return CCVExtraArg{ + CcvAddress: rawInstanceAddressBinding(ccvAddress), + CcvArgs: types.TEXT(ccvArgs), + } +} + +func NewExecutorInput(executorCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) ExecutorInput { + return ExecutorInput{ + ExecutorCid: executorCid, + ExecutorExtraContext: AdaptChoiceContext(ctx), + } +} + +func NewTokenTransferInput(senderInputCids []types.CONTRACT_ID, tokenPoolCid types.CONTRACT_ID, ctx latestmeta.ChoiceContext) TokenTransferInput { + return TokenTransferInput{ + SenderInputCids: senderInputCids, + TokenPoolCid: tokenPoolCid, + PoolExtraContext: AdaptChoiceContext(ctx), + } +} + +func NewReceiverCCVInput(ccvCid types.CONTRACT_ID, verifierResults types.TEXT, ctx latestmeta.ChoiceContext) ReceiverCCVInput { + return ReceiverCCVInput{ + CcvCid: ccvCid, + VerifierResults: verifierResults, + CcvExtraContext: AdaptChoiceContext(ctx), + } +} + +func NewReceiverTokenTransferInput( + tokenPoolCid types.CONTRACT_ID, + tokenReceiverParty types.PARTY, + ctx latestmeta.ChoiceContext, +) ReceiverTokenTransferInput { + return ReceiverTokenTransferInput{ + TokenPoolCid: tokenPoolCid, + TokenReceiverParty: tokenReceiverParty, + PoolExtraContext: AdaptChoiceContext(ctx), + } +} diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index 05e451178..6861730e6 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -20,12 +20,11 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" "github.com/smartcontractkit/chainlink-canton/contracts" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" @@ -112,7 +111,7 @@ func (c *Chain) findPerPartyRouterByParty( partyId string, preferredInstanceID contracts.InstanceID, ) (contracts.InstanceAddress, string, bool, error) { - templateID := contracts.TemplateIDFromBinding(ccipruntime.PerPartyRouter{}).ToLedgerIdentifier() + templateID := contracts.TemplateIDFromBinding(ledgerbind.PerPartyRouter{}).ToLedgerIdentifier() activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) if err != nil { return contracts.InstanceAddress{}, "", false, err @@ -192,7 +191,7 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ccipsender.CCIPSender{}.GetTemplateID(), + ledgerbind.CCIPSender{}.GetTemplateID(), senderAddress, ); err == nil { c.senderAddress = senderAddress @@ -213,7 +212,7 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ccipsender.CCIPSender{}.GetTemplateID(), + ledgerbind.CCIPSender{}.GetTemplateID(), senderAddress, ); findErr == nil { c.senderAddress = senderAddress @@ -239,7 +238,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ccipreceiver.CCIPReceiver{}.GetTemplateID(), + ledgerbind.CCIPReceiver{}.GetTemplateID(), receiverAddress, ); err == nil { c.receiverAddress = receiverAddress @@ -269,7 +268,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ccipreceiver.CCIPReceiver{}.GetTemplateID(), + ledgerbind.CCIPReceiver{}.GetTemplateID(), receiverAddress, ); findErr == nil { c.receiverAddress = receiverAddress @@ -331,12 +330,12 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get CCIP execute disclosure: %w", err) } - executeArgs := ccipreceiver.Execute{ - Context: ccipExecuteDisclosure.ChoiceContext, + executeArgs := ledgerbind.ReceiverExecute{ + Context: ledgerbind.AdaptChoiceContext(ccipExecuteDisclosure.ChoiceContext), RouterCid: types.CONTRACT_ID(routerCid), EncodedMessage: types.TEXT(encodedMessageHex), TokenTransfer: nil, - CcvInputs: make([]ccipreceiver.CCVInput, len(verifiers)), + CcvInputs: make([]ledgerbind.ReceiverCCVInput, len(verifiers)), } disclosedContracts := ccipExecuteDisclosure.DisclosedContracts @@ -351,11 +350,11 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get CCV execute disclosure for verifier %s: %w", verifier.String(), err) } - executeArgs.CcvInputs[i] = ccipreceiver.CCVInput{ - CcvCid: types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), - VerifierResults: types.TEXT(hex.EncodeToString(vr)), - Context: ccvExecuteDisclosure.ChoiceContext, - } + executeArgs.CcvInputs[i] = ledgerbind.NewReceiverCCVInput( + types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), + types.TEXT(hex.EncodeToString(vr)), + ccvExecuteDisclosure.ChoiceContext, + ) disclosedContracts = append(disclosedContracts, ccvExecuteDisclosure.DisclosedContracts...) } @@ -372,11 +371,12 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get token pool execute disclosure: %w", err) } - executeArgs.TokenTransfer = &ccipreceiver.TokenTransferInput{ - TokenPoolCid: types.CONTRACT_ID(tokenPoolDisclosure.ContractId), - TokenReceiverParty: types.PARTY(executingParty), - Context: tokenPoolDisclosure.ChoiceContext, - } + tokenTransfer := ledgerbind.NewReceiverTokenTransferInput( + types.CONTRACT_ID(tokenPoolDisclosure.ContractId), + types.PARTY(executingParty), + tokenPoolDisclosure.ChoiceContext, + ) + executeArgs.TokenTransfer = &tokenTransfer disclosedContracts = append(disclosedContracts, tokenPoolDisclosure.DisclosedContracts...) } @@ -387,7 +387,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes Str("Receiver", hex.EncodeToString(message.Receiver)). Msg("Executing message...") - executeReport, err := operations.ExecuteOperation(c.e.OperationsBundle, receiver.Execute, c.chain, contract.ChoiceInput[ccipreceiver.Execute]{ + executeReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipReceiverExecuteOperation, c.chain, contract.ChoiceInput[ledgerbind.ReceiverExecute]{ InstanceAddress: receiverAddress, ParticipantIndex: clientIdx, Args: executeArgs, @@ -449,7 +449,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes } // Get ExecutionStateChangedEvent from events - expectedTemplateID := events.ExecutionStateChanged{}.GetTemplateID() + expectedTemplateID := ledgerbind.ExecutionStateChanged{}.GetTemplateID() for _, event := range update.GetTransaction().GetEvents() { //nolint:nestif // need to check if all of these are nil if createdEvent := event.GetCreated(); createdEvent != nil { @@ -475,7 +475,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes // parseExecutionStateChangedEvent parses a common.ExecutionStateChanged event from a Daml CreatedEvent and converts it to cciptestinterfaces.ExecutionStateChangedEvent. func parseExecutionStateChangedEvent(event *apiv2.CreatedEvent) (cciptestinterfaces.ExecutionStateChangedEvent, error) { - executionStateChanged, err := bindings.UnmarshalCreatedEvent[events.ExecutionStateChanged](event) + executionStateChanged, err := bindings.UnmarshalCreatedEvent[ledgerbind.ExecutionStateChanged](event) if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to unmarshal ExecutionStateChanged event: %w", err) } @@ -500,13 +500,13 @@ func parseExecutionStateChangedEvent(event *apiv2.CreatedEvent) (cciptestinterfa // Execution state var executionState cciptestinterfaces.MessageExecutionState switch executionStateChanged.Event.State { - case ccipapi.MessageExecutionStateUNTOUCHED: + case ledgerbind.MessageExecutionStateUNTOUCHED: executionState = cciptestinterfaces.ExecutionStateUntouched - case ccipapi.MessageExecutionStateIN_PROGRESS: + case ledgerbind.MessageExecutionStateIN_PROGRESS: executionState = cciptestinterfaces.ExecutionStateInProgress - case ccipapi.MessageExecutionStateSUCCESS: + case ledgerbind.MessageExecutionStateSUCCESS: executionState = cciptestinterfaces.ExecutionStateSuccess - case ccipapi.MessageExecutionStateFAILURE: + case ledgerbind.MessageExecutionStateFAILURE: executionState = cciptestinterfaces.ExecutionStateFailure default: return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("unknown execution state %q", executionStateChanged.Event.State) @@ -556,7 +556,7 @@ func (c *Chain) findExistingExecutionState( } participant := c.chain.Participants[0] - templateID := contracts.TemplateIDFromBinding(events.ExecutionStateChanged{}).ToLedgerIdentifier() + templateID := contracts.TemplateIDFromBinding(ledgerbind.ExecutionStateChanged{}).ToLedgerIdentifier() activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, false, fmt.Errorf("findExistingExecutionState: list active ExecutionStateChanged contracts: %w", err) diff --git a/ccip/devenv/participant.go b/ccip/devenv/participant.go index abea6ed7b..c180383b7 100644 --- a/ccip/devenv/participant.go +++ b/ccip/devenv/participant.go @@ -42,3 +42,10 @@ func (c *Chain) clientParticipantIndex() int { return OwnerParticipantIndex } + +// isRemote reports whether this chain targets live testnet infrastructure with +// only a client party. Remote callers lack FeeQuoter ACS visibility; limits +// must come from EDS disclosures or on-chain validation instead. +func (c *Chain) isRemote() bool { + return len(c.chain.Participants) <= 1 +} diff --git a/ccip/devenv/send_op_devenv.go b/ccip/devenv/send_op_devenv.go new file mode 100644 index 000000000..a5bd2fb44 --- /dev/null +++ b/ccip/devenv/send_op_devenv.go @@ -0,0 +1,7 @@ +//go:build !prodledger + +package devenv + +import senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" + +var ccipSenderSendOperation = senderop.Send diff --git a/ccip/devenv/send_op_prod.go b/ccip/devenv/send_op_prod.go new file mode 100644 index 000000000..b968b9b09 --- /dev/null +++ b/ccip/devenv/send_op_prod.go @@ -0,0 +1,24 @@ +//go:build prodledger + +package devenv + +import ( + ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var prodSenderEncoder = ccipsender.NewContract("", "CCIP.CCIPSender", "CCIPSender").Encoder() + +var ccipSenderSendOperation = contract.NewExercise(contract.ExerciseParams[ccipsender.Send]{ + Name: "canton/ccip/sender/send", + Version: sender.Version, + Description: "Calls the Send choice on a CCIP Sender contract", + ContractType: sender.ContractType, + Validate: func(input ccipsender.Send) error { + return nil + }, + Template: ccipsender.CCIPSender{}, + Method: ccipsender.CCIPSender{}.Send, + EncodeMethod: prodSenderEncoder.Send, +}) diff --git a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go index da0a4a118..ae37721a3 100644 --- a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go +++ b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go @@ -4,19 +4,17 @@ import ( "math/big" "testing" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/proxy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/versioned_verifier_resolver" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" ccldf "github.com/smartcontractkit/chainlink-ccv/build/devenv/cldf" "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/stretchr/testify/require" + cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // register Canton ImplFactory devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" "github.com/smartcontractkit/chainlink-canton/testhelpers" @@ -54,13 +52,6 @@ func TestEVM2Canton_Basic(t *testing.T) { common.DefaultCommitteeVerifierQualifier, "source committee verifier", ) - executorAddress := devenvtests.GetContractAddress( - t, ds, srcSelector, - datastore.ContractType(sequences.ExecutorProxyType), - proxy.Deploy.Version(), - common.DefaultExecutorQualifier, - "source executor", - ) t.Run("message transfer", func(t *testing.T) { subtestCtx := ccv.Plog.WithContext(t.Context()) @@ -70,7 +61,7 @@ func TestEVM2Canton_Basic(t *testing.T) { sendMessageResult, err := boot.EVM.SendMessage(subtestCtx, dstSelector, cciptestinterfaces.MessageFields{ Receiver: receiver, Data: []byte("Hello message transfer from EVM!"), - }, devenvtests.EVMToCantonMessageOptions(200_000, executorAddress, ccvAddr), 3) + }, devenvtests.EVMToCantonMessageOptions(200_000, cantondevenv.EVMToCantonFinalityConfig, ccvAddr), 3) require.NoError(t, err) require.NotNil(t, sendMessageResult.Message) @@ -119,18 +110,7 @@ func TestEVM2Canton_Basic(t *testing.T) { Amount: lane.TransferAmount, TokenAddress: srcToken, }, - }, cciptestinterfaces.MessageOptions{ - ExecutionGasLimit: lane.ExecutionGasLimit, - FinalityConfig: lane.FinalityConfig, - Executor: executorAddress, - CCVs: []protocol.CCV{ - { - CCVAddress: ccvAddr, - Args: []byte{}, - ArgsLen: 0, - }, - }, - }, 3) + }, devenvtests.EVMToCantonMessageOptions(lane.ExecutionGasLimit, lane.FinalityConfig, ccvAddr), 3) require.NoError(t, err) require.NotNil(t, sendMessageResult.Message) require.NotNil(t, sendMessageResult.Message.TokenTransfer) diff --git a/ccip/devenv/tests/env.go b/ccip/devenv/tests/env.go index ed2a2b019..aff6c84f6 100644 --- a/ccip/devenv/tests/env.go +++ b/ccip/devenv/tests/env.go @@ -55,7 +55,7 @@ func (e CCIPEnv) ConfigPath() string { case EnvDevenv: return "env-canton-evm-out.toml" case EnvProdTestnet: - return "env-prod-testnet-out.toml" + return "env-prod-testnet.ci.toml" default: return "" } diff --git a/ccip/devenv/tests/helpers.go b/ccip/devenv/tests/helpers.go index 257c6e10d..dae5a6552 100644 --- a/ccip/devenv/tests/helpers.go +++ b/ccip/devenv/tests/helpers.go @@ -72,12 +72,17 @@ func ConfirmSendTimeout(t *testing.T, env CCIPEnv) time.Duration { return timeout } -// EVMToCantonMessageOptions returns standard message options for EVM→Canton sends with FTF. -func EVMToCantonMessageOptions(gasLimit uint32, executor, ccvAddr protocol.UnknownAddress) cciptestinterfaces.MessageOptions { +// EVMToCantonMessageOptions returns message options for EVM→Canton sends. +// Execution on Canton is manual; the EVM Executor is skipped via NO_EXECUTION_ADDRESS. +func EVMToCantonMessageOptions( + gasLimit uint32, + finality protocol.Finality, + ccvAddr protocol.UnknownAddress, +) cciptestinterfaces.MessageOptions { return cciptestinterfaces.MessageOptions{ ExecutionGasLimit: gasLimit, - FinalityConfig: cantondevenv.EVMToCantonFinalityConfig, - Executor: executor, + FinalityConfig: finality, + Executor: cantondevenv.EVMToCantonNoExecutionExecutor, CCVs: []protocol.CCV{ {CCVAddress: ccvAddr, Args: []byte{}, ArgsLen: 0}, }, diff --git a/ccip/devenv/tests/load/gun_evm2canton_test.go b/ccip/devenv/tests/load/gun_evm2canton_test.go index 7d57f5d1f..3508c94c9 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_test.go @@ -32,13 +32,13 @@ func TestEVM2Canton_Load(t *testing.T) { t.Logf("EVM→Canton load: source=%d dest=%d receiver=%x", boot.EVM.ChainSelector(), cantonDest.Chain.ChainSelector(), cantonDest.Receiver) - ccvAddr, executorAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) + ccvAddr, _ := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( boot.EVM, []Destination{cantonDest}, ccvAddr, - executorAddr, + nil, LoadGunOptions{ ConfirmSend: EVMSourceConfirmSend(boot), ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index 24f608228..7ffa1d791 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -63,13 +63,13 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { t.Logf("warning: EVM sender balance may be insufficient for full run") } - ccvAddr, executorAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) + ccvAddr, _ := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( boot.EVM, []Destination{cantonDest}, ccvAddr, - executorAddr, + nil, LoadGunOptions{ ConfirmSend: EVMSourceConfirmSend(boot), ConfirmExecTimeout: devenvtests.ConfirmExecTimeout(t), diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index e404a3329..d0df585d2 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" + cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" canton_committee_verifier "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/executor" ) @@ -384,11 +385,11 @@ func cantonLoadDestination(chain cciptestinterfaces.CCIP17, receiver protocol.Un return Destination{ Chain: chain, Receiver: receiver, - buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, executorAddr protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { + buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, _ protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { return cciptestinterfaces.MessageFields{ Receiver: receiver, Data: fmt.Appendf(nil, "evm2canton load n=%d dest=%d", callNum, destSelector), - }, devenvtests.EVMToCantonMessageOptions(200_000, executorAddr, ccvAddr), nil + }, devenvtests.EVMToCantonMessageOptions(200_000, cantondevenv.EVMToCantonFinalityConfig, ccvAddr), nil }, } } @@ -400,7 +401,7 @@ func cantonTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protoc Chain: chain, Receiver: receiver, TokenLane: &laneCopy, - buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, executorAddr protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { + buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, _ protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { return cciptestinterfaces.MessageFields{ Receiver: receiver, Data: fmt.Appendf(nil, "evm2canton token load n=%d dest=%d", callNum, destSelector), @@ -408,14 +409,7 @@ func cantonTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protoc Amount: lane.TransferAmount, TokenAddress: lane.SrcToken, }, - }, cciptestinterfaces.MessageOptions{ - ExecutionGasLimit: lane.ExecutionGasLimit, - FinalityConfig: lane.FinalityConfig, - Executor: executorAddr, - CCVs: []protocol.CCV{ - {CCVAddress: ccvAddr, Args: []byte{}, ArgsLen: 0}, - }, - }, nil + }, devenvtests.EVMToCantonMessageOptions(lane.ExecutionGasLimit, lane.FinalityConfig, ccvAddr), nil }, } } From 8dbabfccf62dd3002f6a85286be6fc9598aa7e59 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 19:15:45 -0300 Subject: [PATCH 16/26] fix(load): fix prod-testnet receiver --- ccip/devenv/env-prod-testnet.ci.toml | 2 +- ccip/devenv/manual_execution.go | 24 ++++++++++++++++++- ccip/devenv/tests/helpers.go | 26 +++++++++++++++++++-- examples/cli/cmd/canton.go | 35 ++++++++++++++++++++++++++++ testhelpers/addresses.go | 11 ++++++++- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/ccip/devenv/env-prod-testnet.ci.toml b/ccip/devenv/env-prod-testnet.ci.toml index 6f5b5bf35..b024ad59e 100644 --- a/ccip/devenv/env-prod-testnet.ci.toml +++ b/ccip/devenv/env-prod-testnet.ci.toml @@ -7,7 +7,7 @@ indexer_endpoints = [ [cldf] addresses = [ '[{"address":"0x181Ac7dC295f1C8C87342d07CFaBA90bC477DB5d","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"OnRamp","version":"2.0.0"},{"address":"0xc6A246A9AcdAaE651708706494720F79C3E5d0A1","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"OffRamp","version":"2.0.0"},{"address":"0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59","chainSelector":16015286601757825753,"labels":[],"qualifier":"0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59-Router","type":"Router","version":"1.2.0"},{"address":"0x8632C3025FAFdD85A299211FD5838b5fBE2df816","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"FeeQuoter","version":"2.0.0"},{"address":"0x8f3ee3c77D2B27c32306a89D367654F959Db223D","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"CommitteeVerifierResolver","version":"2.0.0"},{"address":"0x66f9E0738a4a6fe54aE62DEd00Ca1F72bDecc092","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"ExecutorProxy","version":"2.0.0"},{"address":"0x78874Df86F55b207d5584093C17B59220E8c6B15","chainSelector":16015286601757825753,"labels":[],"qualifier":"default","type":"Executor","version":"2.0.0"},{"address":"0x097D90c9d3E0B50Ca60e1ae45F6A81010f9FB534","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"WETH9","version":"1.0.0"},{"address":"0x779877A7B0D9E8603169DdbD7836e478b4624789","chainSelector":16015286601757825753,"labels":[],"qualifier":"","type":"LinkToken","version":"1.0.0"},{"address":"0x5185b41F1588FC8C541360709C992794925D484C","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"BurnMintTokenPool","version":"2.0.0"},{"address":"0xeEe6675b20fE5950eb51361b93021D076289F612","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"BurnMintERC20WithDrip","version":"1.5.0"},{"address":"0xA0a507CE0709D3D40F71166c730a860aa29f3491","chainSelector":16015286601757825753,"labels":[],"qualifier":"TEST","type":"AdvancedPoolHooks","version":"2.0.0"}]', - '[{"address":"0x03519eac48d545c4d0ecdc3e3022e443d9e878867827eecb37d5e5a60ae0c989","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OnRamp","version":"2.0.0"},{"address":"0xd03375cb15a7179bfbfa7cfb843250a6b26e4ddc7a4c30e7bc1777cf3afbf580","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OffRamp","version":"2.0.0"},{"address":"0x18a00775f6781bfe3032d1b9ceeee225416fb36143218969818df860cb1e29c6","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"FeeQuoter","version":"2.0.0"},{"address":"0x45f673fb23aa33fdaa07e7ae7ea0d37218bcff8e63575a5582a2356cbfaa8883","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonGlobalConfig","version":"2.0.0"},{"address":"0x665cfb57b33b2b74383e99af880ff7d967ad85ef5966ac912f9a3cd4faaee5f9","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonPerPartyRouterFactory","version":"2.0.0"},{"address":"0xe8df5a00cc82df74bae0ca2a032e4d5a7fb5424c1194c4036caabe5fd9f40f81","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"TokenAdminRegistry","version":"2.0.0"},{"address":"0xd4678dbaa95efbf0631162b4fbbae08bf42bbde19038692fec94b6f6b5528fec","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"RMNRemote","version":"2.0.0"},{"address":"0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe","chainSelector":9268731218649498074,"labels":[],"qualifier":"default","type":"CommitteeVerifier","version":"2.0.0"},{"address":"0x4b19a247edd769630a136840ef703bd55ca3eda382317ce4384f2aadd47ddbaa","chainSelector":9268731218649498074,"labels":[],"qualifier":"default","type":"Executor","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"BurnMintTokenPool","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"CantonBurnMintTokenPool","version":"2.0.0"},{"address":"39c6212f2291365bed155b2533abbd7ddf340498ccbbb358cee07b83106986ad","chainSelector":9268731218649498074,"labels":["instrument-admin:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551","instrument-id:link-token","ccip-owner:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"Token","version":"2.0.0"},{"address":"0x576182aab988a0804a1aa13081902c076ed6108c1162a04b3e971e871a608527","chainSelector":9268731218649498074,"labels":["linkregistry@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"","type":"LinkRegistry","version":"2.0.0"}]', + '[{"address":"0x03519eac48d545c4d0ecdc3e3022e443d9e878867827eecb37d5e5a60ae0c989","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OnRamp","version":"2.0.0"},{"address":"0xd03375cb15a7179bfbfa7cfb843250a6b26e4ddc7a4c30e7bc1777cf3afbf580","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"OffRamp","version":"2.0.0"},{"address":"0x18a00775f6781bfe3032d1b9ceeee225416fb36143218969818df860cb1e29c6","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"FeeQuoter","version":"2.0.0"},{"address":"0x45f673fb23aa33fdaa07e7ae7ea0d37218bcff8e63575a5582a2356cbfaa8883","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonGlobalConfig","version":"2.0.0"},{"address":"0x665cfb57b33b2b74383e99af880ff7d967ad85ef5966ac912f9a3cd4faaee5f9","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"CantonPerPartyRouterFactory","version":"2.0.0"},{"address":"0xe8df5a00cc82df74bae0ca2a032e4d5a7fb5424c1194c4036caabe5fd9f40f81","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"TokenAdminRegistry","version":"2.0.0"},{"address":"0xd4678dbaa95efbf0631162b4fbbae08bf42bbde19038692fec94b6f6b5528fec","chainSelector":9268731218649498074,"labels":[],"qualifier":"","type":"RMNRemote","version":"2.0.0"},{"address":"0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe","chainSelector":9268731218649498074,"labels":["committeeverifier-tqkny@ccvOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"default","type":"CommitteeVerifier","version":"2.0.0"},{"address":"0x4b19a247edd769630a136840ef703bd55ca3eda382317ce4384f2aadd47ddbaa","chainSelector":9268731218649498074,"labels":[],"qualifier":"default","type":"Executor","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"BurnMintTokenPool","version":"2.0.0"},{"address":"0x6ac67a53d53ac425440550d27afeb1da16f6d41c224dcd1ed8e9ab1ae20f7ace","chainSelector":9268731218649498074,"labels":["burnminttokenpool-LINK@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"CantonBurnMintTokenPool","version":"2.0.0"},{"address":"39c6212f2291365bed155b2533abbd7ddf340498ccbbb358cee07b83106986ad","chainSelector":9268731218649498074,"labels":["instrument-admin:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551","instrument-id:link-token","ccip-owner:ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"LINK","type":"Token","version":"2.0.0"},{"address":"0x576182aab988a0804a1aa13081902c076ed6108c1162a04b3e971e871a608527","chainSelector":9268731218649498074,"labels":["linkregistry@ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551"],"qualifier":"","type":"LinkRegistry","version":"2.0.0"}]', ] env_metadata = "{\"metadata\":{\"cantonConfigs\":{\"eds_url\":\"https://eds.testnet.ccip.chain.link\"},\"offchainConfigs\":{\"indexers\":{\"indexer-1\":{\"verifiers\":[{\"issuerAddresses\":[\"0x8f3ee3c77D2B27c32306a89D367654F959Db223D\",\"0xec1e288bcf8bbf034ac2d31b67f9b15a3f1f828d086c5b9d8fc2866129cd02fe\"],\"name\":\"default-verifier\"}]}}}}}" diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index 6861730e6..272980518 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -24,6 +24,7 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" + chainlinkapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/chainlink/chainlinkapi" "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" "github.com/smartcontractkit/chainlink-canton/contracts" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" @@ -245,6 +246,11 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return receiverAddress, nil } + requiredCCVs, err := receiverRequiredCCVsFromEnv() + if err != nil { + return contracts.InstanceAddress{}, err + } + finalityConfig, err := encodeReceiverFinalityConfig(receiverFinality) if err != nil { return contracts.InstanceAddress{}, fmt.Errorf("failed to encode receiver finality config: %w", err) @@ -256,7 +262,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti Template: ccipreceiver.CCIPReceiver{ InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), - RequiredCCVs: nil, + RequiredCCVs: requiredCCVs, OptionalCCVs: nil, OptionalThreshold: 0, ReceiverFinalityConfig: finalityConfig, @@ -281,6 +287,22 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return receiverAddress, nil } +func receiverRequiredCCVsFromEnv() ([]chainlinkapi.RawInstanceAddress, error) { + raw := strings.TrimSpace(os.Getenv("CANTON_RECEIVER_REQUIRED_CCV")) + if raw == "" { + return nil, nil + } + + parsed, err := contracts.RawInstanceAddressFromString(raw) + if err != nil { + return nil, fmt.Errorf("parse CANTON_RECEIVER_REQUIRED_CCV: %w", err) + } + + return []chainlinkapi.RawInstanceAddress{ + {Unpack: types.TEXT(parsed.String())}, + }, nil +} + // ManuallyExecuteMessage implements cciptestinterfaces.CCIP17. func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Message, gasLimit uint64, verifiers []protocol.UnknownAddress, verifierResults [][]byte) (cciptestinterfaces.ExecutionStateChangedEvent, error) { participant, clientIdx, err := c.ClientParticipant() diff --git a/ccip/devenv/tests/helpers.go b/ccip/devenv/tests/helpers.go index dae5a6552..5ca694114 100644 --- a/ccip/devenv/tests/helpers.go +++ b/ccip/devenv/tests/helpers.go @@ -15,6 +15,7 @@ import ( chainsel "github.com/smartcontractkit/chain-selectors" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" + devenvcommon "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/tcapi" "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -23,6 +24,7 @@ import ( "github.com/stretchr/testify/require" cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" + "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) @@ -204,10 +206,30 @@ func (b E2EBootstrap) SetupCantonTokenSend(t *testing.T, ctx context.Context, la } } -// SetupCantonReceive deploys the client party's PerPartyRouter before inbound messages -// are executed on Canton (e.g. EVM→Canton). +// SetupCantonReceive sets CANTON_RECEIVER_REQUIRED_CCV when unset, then deploys the client +// party's PerPartyRouter before inbound messages are executed on Canton (e.g. EVM→Canton). func (b E2EBootstrap) SetupCantonReceive(t *testing.T, ctx context.Context) { t.Helper() + + if strings.TrimSpace(os.Getenv("CANTON_RECEIVER_REQUIRED_CCV")) == "" { + ds, err := b.Lib.DataStore() + require.NoError(t, err) + + cantonChain := GetChainFromMap(t, blockchain.TypeCanton, b.Cfg, b.ChainMap) + _, raw, err := testhelpers.ResolveAddressFromDatastore( + ds, + cantonChain.ChainSelector(), + committee_verifier.ContractType, + committee_verifier.Version, + devenvcommon.DefaultCommitteeVerifierQualifier, + ) + if err != nil && b.Env.IsRemote() { + require.FailNow(t, err.Error()+"; set CANTON_RECEIVER_REQUIRED_CCV to the Canton committee verifier raw address (instanceId@owner)") + } + require.NoError(t, err) + require.NoError(t, os.Setenv("CANTON_RECEIVER_REQUIRED_CCV", raw.String())) + } + require.NoError(t, b.Canton.SetupReceive(ctx)) } diff --git a/examples/cli/cmd/canton.go b/examples/cli/cmd/canton.go index d76d7cd8f..5ff4303ad 100644 --- a/examples/cli/cmd/canton.go +++ b/examples/cli/cmd/canton.go @@ -67,6 +67,7 @@ func NewCantonCmd(g *Globals) *cobra.Command { c.AddCommand(newCantonSendMessageCmd(g)) c.AddCommand(newCantonSendTokenCmd(g)) c.AddCommand(newCantonExecuteCmd(g)) + c.AddCommand(newCantonSyncReceiverCCVCmd(g)) c.AddCommand(newCantonListEventsCmd(g)) c.AddCommand(newCantonListHoldingsCmd(g)) c.AddCommand(newCantonCreateTransferCmd(g)) @@ -403,6 +404,40 @@ func newCantonExecuteCmd(g *Globals) *cobra.Command { return c } +func newCantonSyncReceiverCCVCmd(g *Globals) *cobra.Command { + var ( + requiredCCV string + finalityName string + ) + c := &cobra.Command{ + Use: "sync-receiver-ccv", + Short: "Deploy or update CCIPReceiver required CCVs (run once before inbound load/e2e on prod)", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + b, err := g.Resolve(ctx) + if err != nil { + return err + } + fin, err := finality.Parse(finalityName) + if err != nil { + return err + } + raw, err := contracts.RawInstanceAddressFromString(requiredCCV) + if err != nil { + return fmt.Errorf("parse --required-ccv: %w", err) + } + + _, err = cantonops.GetOrCreateReceiver(ctx, b.Participant, fin.Receiver, raw) + return err + }, + } + c.Flags().StringVar(&requiredCCV, "required-ccv", "", "Canton committee verifier raw address (instanceId@owner)") + c.Flags().StringVar(&finalityName, "finality", "1", "receiver finality profile: finality|safe|1-65535") + _ = c.MarkFlagRequired("required-ccv") + + return c +} + func cantonExecute(ctx context.Context, b *clients.Bundle, vr protocol.VerifierResult, fin finality.Parsed) error { withToken := vr.Message.TokenTransfer != nil diff --git a/testhelpers/addresses.go b/testhelpers/addresses.go index 14ef0e431..0a4221a01 100644 --- a/testhelpers/addresses.go +++ b/testhelpers/addresses.go @@ -1,6 +1,8 @@ package testhelpers import ( + "fmt" + "github.com/Masterminds/semver/v3" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -24,7 +26,14 @@ func ResolveAddressFromDatastore( if err != nil { return datastore.AddressRef{}, "", err } - rawInstanceAddress, err := contracts.RawInstanceAddressFromString(addressRef.Labels.List()[0]) + labels := addressRef.Labels.List() + if len(labels) == 0 { + return datastore.AddressRef{}, "", fmt.Errorf( + "address ref %s/%s has no labels (raw instance address missing)", + contractType, qualifier, + ) + } + rawInstanceAddress, err := contracts.RawInstanceAddressFromString(labels[0]) if err != nil { return datastore.AddressRef{}, "", err } From 220fa1a002cd3d5fe2c7cb46a87a2b150929fc2b Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 19:54:34 -0300 Subject: [PATCH 17/26] fix lint issues --- ccip/devenv/cldf_auth_test.go | 6 +- ccip/devenv/ledgerbind/exports_devenv.go | 4 +- ccip/devenv/ledgerbind/inputs_devenv.go | 3 +- ccip/devenv/manual_execution.go | 14 ++-- ccip/devenv/tests/e2e/evm2canton_e2e_test.go | 1 - ccip/devenv/tests/env_test.go | 6 +- .../canton_prod_testnet_connection_test.go | 71 ------------------- ccip/devenv/tests/load/gun_evm2canton_test.go | 2 +- .../tests/load/gun_evm2canton_token_test.go | 2 +- ccip/devenv/tests/load/load_helpers.go | 31 +++----- ccip/devenv/tests/token_lane.go | 1 + ccip/devenv/verifier_observation.go | 13 ++-- ccip/devenv/verifier_observation_test.go | 4 ++ examples/cli/cmd/canton.go | 1 + 14 files changed, 45 insertions(+), 114 deletions(-) delete mode 100644 ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go diff --git a/ccip/devenv/cldf_auth_test.go b/ccip/devenv/cldf_auth_test.go index 29600a4e2..465c450eb 100644 --- a/ccip/devenv/cldf_auth_test.go +++ b/ccip/devenv/cldf_auth_test.go @@ -36,7 +36,7 @@ func clearCantonEnv(t *testing.T) { func TestEnvTrim(t *testing.T) { clearCantonEnv(t) - require.Equal(t, "", envTrim("CANTON_PARTY_ID")) + require.Empty(t, envTrim("CANTON_PARTY_ID")) t.Setenv("CANTON_PARTY_ID", "party-1") require.Equal(t, "party-1", envTrim("CANTON_PARTY_ID")) @@ -130,7 +130,7 @@ func TestResolveUserID(t *testing.T) { func TestResolvePartyID(t *testing.T) { clearCantonEnv(t) - require.Equal(t, "", resolvePartyID()) + require.Empty(t, resolvePartyID()) t.Setenv("CANTON_PARTY_ID", "party::1220abc") require.Equal(t, "party::1220abc", resolvePartyID()) @@ -163,6 +163,8 @@ func TestJWTSubject(t *testing.T) { } func TestUserIDFromToken(t *testing.T) { + t.Parallel() + userID, err := userIDFromToken(context.Background(), testAuthProvider{token: validJWT}) require.NoError(t, err) require.Equal(t, "1234567890", userID) diff --git a/ccip/devenv/ledgerbind/exports_devenv.go b/ccip/devenv/ledgerbind/exports_devenv.go index 75cf3bf66..5018a6011 100644 --- a/ccip/devenv/ledgerbind/exports_devenv.go +++ b/ccip/devenv/ledgerbind/exports_devenv.go @@ -3,9 +3,9 @@ package ledgerbind import ( - rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec" + rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/clientapi" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" @@ -14,7 +14,7 @@ import ( ) type ( - CreateRouter = rt.CreateRouter + CreateRouter = rt.CreateRouter PerPartyRouter = rt.PerPartyRouter FinalityConfig = ccipcodec.FinalityConfig diff --git a/ccip/devenv/ledgerbind/inputs_devenv.go b/ccip/devenv/ledgerbind/inputs_devenv.go index 8bdbd8d7b..0ab24b04e 100644 --- a/ccip/devenv/ledgerbind/inputs_devenv.go +++ b/ccip/devenv/ledgerbind/inputs_devenv.go @@ -3,10 +3,11 @@ package ledgerbind import ( + "github.com/smartcontractkit/go-daml/pkg/types" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/chainlink/chainlinkapi" latestmeta "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" "github.com/smartcontractkit/chainlink-canton/contracts" - "github.com/smartcontractkit/go-daml/pkg/types" ) func rawInstanceAddressBinding(addr contracts.RawInstanceAddress) chainlinkapi.RawInstanceAddress { diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index 272980518..f037e4d8f 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -38,6 +38,7 @@ func instanceIDFromEnv(key, defaultID string) contracts.InstanceID { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return contracts.InstanceID(v) } + return contracts.InstanceID(defaultID) } @@ -86,11 +87,12 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, participant canton.Par ) if err != nil { if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { - return contracts.InstanceAddress{}, fmt.Errorf("create per-party router: %w (find after failure: %v)", err, findErr) + return contracts.InstanceAddress{}, fmt.Errorf("create per-party router: %w (find after failure: %w)", err, findErr) } else if ok { c.routerAddress = found return found, nil } + return contracts.InstanceAddress{}, fmt.Errorf("create per-party router: %w", err) } @@ -101,8 +103,8 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, participant canton.Par if !ok { return contracts.InstanceAddress{}, fmt.Errorf("per-party router not found after create for party %s", partyId) } - c.routerAddress = found + return found, nil } @@ -161,6 +163,7 @@ func perPartyRouterFieldsFromCreated(created *apiv2.CreatedEvent) (instanceId, p partyOwner = field.GetValue().GetParty() } } + return instanceId, partyOwner, instanceId != "" && partyOwner != "" } @@ -175,6 +178,7 @@ func (c *Chain) findPerPartyRouterCidByParty(ctx context.Context, participant ca if !ok { return "", fmt.Errorf("no active PerPartyRouter found for party %s", partyId) } + return cid, nil } @@ -219,10 +223,11 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici c.senderAddress = senderAddress return senderAddress, nil } + return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy ccip sender contract: %w", err) } - c.senderAddress = senderAddress + return senderAddress, nil } @@ -280,10 +285,11 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti c.receiverAddress = receiverAddress return receiverAddress, nil } + return contracts.InstanceAddress{}, fmt.Errorf("failed to deploy receiver contract: %w", err) } - c.receiverAddress = receiverAddress + return receiverAddress, nil } diff --git a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go index ae37721a3..ee8da5ff4 100644 --- a/ccip/devenv/tests/e2e/evm2canton_e2e_test.go +++ b/ccip/devenv/tests/e2e/evm2canton_e2e_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/require" cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" - _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // register Canton ImplFactory devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) diff --git a/ccip/devenv/tests/env_test.go b/ccip/devenv/tests/env_test.go index 190a50ac3..1f72e840b 100644 --- a/ccip/devenv/tests/env_test.go +++ b/ccip/devenv/tests/env_test.go @@ -10,11 +10,11 @@ func TestResolveConfigPath(t *testing.T) { t.Setenv(envConfigFile, "") require.Equal(t, "env-canton-evm-out.toml", ResolveConfigPath(EnvDevenv)) - require.Equal(t, "env-prod-testnet-out.toml", ResolveConfigPath(EnvProdTestnet)) - - t.Setenv(envConfigFile, "env-prod-testnet.ci.toml") require.Equal(t, "env-prod-testnet.ci.toml", ResolveConfigPath(EnvProdTestnet)) + t.Setenv(envConfigFile, "env-prod-testnet.local.toml") + require.Equal(t, "env-prod-testnet.local.toml", ResolveConfigPath(EnvProdTestnet)) + t.Setenv(envConfigFile, "ccip/devenv/custom.toml") require.Equal(t, "env-canton-evm-out.toml", ResolveConfigPath(EnvDevenv)) require.Equal(t, "custom.toml", ResolveConfigPath(EnvProdTestnet)) diff --git a/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go b/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go deleted file mode 100644 index 30542fc2c..000000000 --- a/ccip/devenv/tests/integration/canton_prod_testnet_connection_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package integration - -import ( - "flag" - "os" - "path/filepath" - "testing" - - ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" - _ "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" // register EVM ImplFactory - "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" - "github.com/stretchr/testify/require" - - _ "github.com/smartcontractkit/chainlink-canton/ccip/devenv" // register canton impl factory - cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" - devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" - "github.com/smartcontractkit/chainlink-canton/testhelpers" -) - -func TestMain(m *testing.M) { - flag.Parse() - os.Exit(m.Run()) -} - -func TestIntegration_CantonProdTestnet_Connection(t *testing.T) { - if testing.Short() { - t.Skip("skipping prod testnet connection smoke test in short mode") - } - if os.Getenv("CANTON_GRPC_URL") == "" { - t.Skip("CANTON_GRPC_URL unset: not configured for real Canton testnet") - } - - env := devenvtests.ParseEnvFromFlag(t) - configPath := filepath.Join("..", "..", devenvtests.ResolveConfigPath(env)) - in, err := ccv.LoadOutput[ccv.Cfg](configPath) - require.NoError(t, err) - - var cantonBlockchain *blockchain.Input - for _, bc := range in.Blockchains { - if bc.Type == blockchain.TypeCanton && bc.ChainID == "TestNet" { - cantonBlockchain = bc - break - } - } - require.NotNil(t, cantonBlockchain, "need Canton TestNet blockchain in %s", configPath) - - ctx := t.Context() - chainBC, _, err := cantondevenv.NewCLDF(ctx, cantonBlockchain) - require.NoError(t, err) - - chain, ok := chainBC.(*canton.Chain) - require.True(t, ok, "expected *canton.Chain, got %T", chainBC) - require.NotEmpty(t, chain.Participants) - - participant := chain.Participants[0] - require.NotEmpty(t, participant.PartyID, "participant should be connected with a party ID") - t.Logf("connected participant user_id=%s party_id=%s grpc=%s", participant.UserID, participant.PartyID, participant.Endpoints.GRPCLedgerAPIURL) - - holdings, err := testhelpers.ListHoldingsForInstrument( - ctx, - participant, - nil, - testhelpers.WithHoldingOwner(participant.PartyID), - ) - require.NoError(t, err) - t.Logf("listed %d holdings for party %s", len(holdings), participant.PartyID) - for _, holding := range holdings { - t.Logf("holding contract_id=%s instrument=%s amount=%s", holding.ContractID, holding.View.InstrumentId.Id, holding.Amount) - } -} diff --git a/ccip/devenv/tests/load/gun_evm2canton_test.go b/ccip/devenv/tests/load/gun_evm2canton_test.go index 3508c94c9..8ba4c6496 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_test.go @@ -32,7 +32,7 @@ func TestEVM2Canton_Load(t *testing.T) { t.Logf("EVM→Canton load: source=%d dest=%d receiver=%x", boot.EVM.ChainSelector(), cantonDest.Chain.ChainSelector(), cantonDest.Receiver) - ccvAddr, _ := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) + ccvAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( boot.EVM, diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index 7ffa1d791..b02e2c9b3 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -63,7 +63,7 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { t.Logf("warning: EVM sender balance may be insufficient for full run") } - ccvAddr, _ := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) + ccvAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( boot.EVM, diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index d0df585d2..bcba15092 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -9,8 +9,6 @@ import ( "time" chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/proxy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/versioned_verifier_resolver" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" @@ -23,8 +21,8 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/wasp" "github.com/stretchr/testify/require" - devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" cantondevenv "github.com/smartcontractkit/chainlink-canton/ccip/devenv" + devenvtests "github.com/smartcontractkit/chainlink-canton/ccip/devenv/tests" canton_committee_verifier "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/executor" ) @@ -403,13 +401,13 @@ func cantonTokenLoadDestination(chain cciptestinterfaces.CCIP17, receiver protoc TokenLane: &laneCopy, buildMessage: func(_ cciptestinterfaces.CCIP17, callNum int64, ccvAddr, _ protocol.UnknownAddress) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) { return cciptestinterfaces.MessageFields{ - Receiver: receiver, - Data: fmt.Appendf(nil, "evm2canton token load n=%d dest=%d", callNum, destSelector), - TokenAmount: cciptestinterfaces.TokenAmount{ - Amount: lane.TransferAmount, - TokenAddress: lane.SrcToken, - }, - }, devenvtests.EVMToCantonMessageOptions(lane.ExecutionGasLimit, lane.FinalityConfig, ccvAddr), nil + Receiver: receiver, + Data: fmt.Appendf(nil, "evm2canton token load n=%d dest=%d", callNum, destSelector), + TokenAmount: cciptestinterfaces.TokenAmount{ + Amount: lane.TransferAmount, + TokenAddress: lane.SrcToken, + }, + }, devenvtests.EVMToCantonMessageOptions(lane.ExecutionGasLimit, lane.FinalityConfig, ccvAddr), nil }, } } @@ -438,26 +436,17 @@ func resolveCantonSourceAddrs(t *testing.T, lib ccv.Lib, cantonSelector uint64) return ccvAddr, executorAddr } -func resolveEVMSourceAddrs(t *testing.T, lib ccv.Lib, evmSelector uint64) (protocol.UnknownAddress, protocol.UnknownAddress) { +func resolveEVMSourceAddrs(t *testing.T, lib ccv.Lib, evmSelector uint64) protocol.UnknownAddress { t.Helper() ds, err := lib.DataStore() require.NoError(t, err) - ccvAddr := devenvtests.GetContractAddress( + return devenvtests.GetContractAddress( t, ds, evmSelector, datastore.ContractType(versioned_verifier_resolver.CommitteeVerifierResolverType), versioned_verifier_resolver.Version.String(), common.DefaultCommitteeVerifierQualifier, "source committee verifier", ) - executorAddr := devenvtests.GetContractAddress( - t, ds, evmSelector, - datastore.ContractType(sequences.ExecutorProxyType), - proxy.Deploy.Version(), - common.DefaultExecutorQualifier, - "source executor", - ) - - return ccvAddr, executorAddr } diff --git a/ccip/devenv/tests/token_lane.go b/ccip/devenv/tests/token_lane.go index 4180ec6e9..a6cce3d23 100644 --- a/ccip/devenv/tests/token_lane.go +++ b/ccip/devenv/tests/token_lane.go @@ -123,6 +123,7 @@ func ResolveTokenLane( t.Fatalf("no token transfer config on chain %d matches pool %s (have %s)", srcSelector, poolRefString(dir.PoolRef), poolRefsString(cfgs)) } + return resolveTokenLaneFromDatastore(t, cldfEnv, dir, srcSelector, destSelectors) } diff --git a/ccip/devenv/verifier_observation.go b/ccip/devenv/verifier_observation.go index dc495ce6f..76b72960c 100644 --- a/ccip/devenv/verifier_observation.go +++ b/ccip/devenv/verifier_observation.go @@ -41,13 +41,12 @@ func VerifierObservationFromLib(lib ccv.Lib) (VerifierObservation, error) { IndexerMonitor: indexerMonitor, } - aggregatorClients, err := lib.AllAggregators() - if err != nil { - return obs, nil - } - aggregatorClient, ok := aggregatorClients[devenvcommon.DefaultCommitteeVerifierQualifier] - if ok && aggregatorClient != nil { - obs.AggregatorClient = aggregatorClient + aggregatorClients, aggErr := lib.AllAggregators() + if aggErr == nil { + aggregatorClient, ok := aggregatorClients[devenvcommon.DefaultCommitteeVerifierQualifier] + if ok && aggregatorClient != nil { + obs.AggregatorClient = aggregatorClient + } } return obs, nil diff --git a/ccip/devenv/verifier_observation_test.go b/ccip/devenv/verifier_observation_test.go index f8fd6412c..810d8f5cf 100644 --- a/ccip/devenv/verifier_observation_test.go +++ b/ccip/devenv/verifier_observation_test.go @@ -8,6 +8,8 @@ import ( ) func TestVerifierObservation_wired_indexerOnly(t *testing.T) { + t.Parallel() + obs := VerifierObservation{ IndexerMonitor: &ccv.IndexerMonitor{}, } @@ -15,6 +17,8 @@ func TestVerifierObservation_wired_indexerOnly(t *testing.T) { } func TestVerifierObservation_wired_indexerNil(t *testing.T) { + t.Parallel() + obs := VerifierObservation{ AggregatorClient: &ccv.AggregatorClient{}, } diff --git a/examples/cli/cmd/canton.go b/examples/cli/cmd/canton.go index 5ff4303ad..93c0dacc0 100644 --- a/examples/cli/cmd/canton.go +++ b/examples/cli/cmd/canton.go @@ -428,6 +428,7 @@ func newCantonSyncReceiverCCVCmd(g *Globals) *cobra.Command { } _, err = cantonops.GetOrCreateReceiver(ctx, b.Participant, fin.Receiver, raw) + return err }, } From 709048ac142cc1f9b37e056549167c8a319b320b Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 20:08:04 -0300 Subject: [PATCH 18/26] feat(load): enable load connectivity to proxy - tailscale --- .github/actions/ccip-load-test/action.yml | 19 +++++++++++++++++++ .github/workflows/ccip-load-tests.yml | 2 ++ ccip/devenv/env-prod-testnet.ci.toml | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index afec3fc88..a67e69998 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -48,6 +48,10 @@ inputs: description: "CANTON_PARTY_ID" required: false default: "u_d53a15c42af6::1220c250c23c55120f7c758bccc5cbc739629015ab921594e1c29656981f985bffa7" + user_id: + description: "CANTON_USER_ID" + required: false + default: "0oau1l03xrG9NQw9z5d7" grpc_url: description: "CANTON_GRPC_URL" required: false @@ -64,6 +68,12 @@ inputs: CCIP_PROD_TESTNET_PRIVATE_KEY: description: "GitHub secret CCIP_PROD_TESTNET_PRIVATE_KEY" required: false + TS_OAUTH_CLIENT_ID_CANTON: + description: "GitHub secret TS_OAUTH_CLIENT_ID_CANTON (prod-testnet Tailscale)" + required: false + TS_AUDIENCE_CANTON: + description: "GitHub secret TS_AUDIENCE_CANTON (prod-testnet Tailscale)" + required: false ccv-iam-role: description: "AWS IAM role for CCV ECR authentication (devenv only)" required: false @@ -87,6 +97,14 @@ runs: jd-registry: ${{ inputs.jd-registry }} jd-image: ${{ inputs.jd-image }} + - name: Connect to Tailscale (prod-testnet) + if: inputs.ccip_env == 'prod-testnet' + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + with: + oauth-client-id: ${{ inputs.TS_OAUTH_CLIENT_ID_CANTON }} + audience: ${{ inputs.TS_AUDIENCE_CANTON }} + tags: tag:bcy-validators-gha + - name: Install Go (prod-testnet) if: inputs.ccip_env == 'prod-testnet' uses: actions/setup-go@v6 @@ -130,6 +148,7 @@ runs: CANTON_CLIENT_ID: ${{ inputs.CANTON_OKTA_CLIENT_ID_TESTNET }} CANTON_CLIENT_SECRET: ${{ inputs.CANTON_OKTA_CLIENT_SECRET_TESTNET }} CANTON_PARTY_ID: ${{ inputs.party_id }} + CANTON_USER_ID: ${{ inputs.user_id }} CANTON_GRPC_URL: ${{ inputs.grpc_url }} CANTON_LOAD_MESSAGE_RATE: ${{ inputs.message_rate }} CANTON_LOAD_DURATION: ${{ inputs.load_duration }} diff --git a/.github/workflows/ccip-load-tests.yml b/.github/workflows/ccip-load-tests.yml index 18a1e00b0..e63f88a47 100644 --- a/.github/workflows/ccip-load-tests.yml +++ b/.github/workflows/ccip-load-tests.yml @@ -127,6 +127,8 @@ jobs: CANTON_OKTA_CLIENT_ID_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_ID_TESTNET }} CANTON_OKTA_CLIENT_SECRET_TESTNET: ${{ secrets.CANTON_OKTA_CLIENT_SECRET_TESTNET }} CCIP_PROD_TESTNET_PRIVATE_KEY: ${{ secrets.CCIP_PROD_TESTNET_PRIVATE_KEY }} + TS_OAUTH_CLIENT_ID_CANTON: ${{ secrets.TS_OAUTH_CLIENT_ID_CANTON }} + TS_AUDIENCE_CANTON: ${{ secrets.TS_AUDIENCE_CANTON }} ccv-iam-role: ${{ secrets.CCV_IAM_ROLE }} jd-registry: ${{ secrets.JD_REGISTRY }} jd-image: ${{ secrets.JD_IMAGE }} diff --git a/ccip/devenv/env-prod-testnet.ci.toml b/ccip/devenv/env-prod-testnet.ci.toml index b024ad59e..46d47c6f5 100644 --- a/ccip/devenv/env-prod-testnet.ci.toml +++ b/ccip/devenv/env-prod-testnet.ci.toml @@ -21,7 +21,7 @@ out.chain_id = "11155111" out.family = "evm" out.container_name = "ethereum-testnet-sepolia" out.nodes = [ - { http_url = "https://rpcs.cldev.sh/16015286601757825753", internal_http_url = "https://rpcs.cldev.sh/16015286601757825753", internal_ws_url = "wss://rpcs.cldev.sh/16015286601757825753", ws_url = "wss://rpcs.cldev.sh/16015286601757825753" }, + { http_url = "https://rpc-proxy-main-prod.tailec47a7.ts.net/gha-canton/16015286601757825753", internal_http_url = "https://rpc-proxy-main-prod.tailec47a7.ts.net/gha-canton/16015286601757825753", internal_ws_url = "wss://rpc-proxy-main-prod.tailec47a7.ts.net/gha-canton/16015286601757825753", ws_url = "wss://rpc-proxy-main-prod.tailec47a7.ts.net/gha-canton/16015286601757825753" }, ] [[blockchains]] From 9c58046c2d294dd369a26898ad3111f2273b9618 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 20:35:35 -0300 Subject: [PATCH 19/26] fix(router): use v1_0_0 instead of ledger for router too --- ccip/devenv/create_router_op_devenv.go | 7 +++++ ccip/devenv/create_router_op_prod.go | 33 +++++++++++++++++++ ccip/devenv/deploy_receiver_op_devenv.go | 7 +++++ ccip/devenv/deploy_receiver_op_prod.go | 29 +++++++++++++++++ ccip/devenv/deploy_sender_op_devenv.go | 7 +++++ ccip/devenv/deploy_sender_op_prod.go | 29 +++++++++++++++++ ccip/devenv/ledgerbind/exports_devenv.go | 3 ++ ccip/devenv/ledgerbind/exports_prod.go | 3 ++ ccip/devenv/manual_execution.go | 40 ++++++++++-------------- 9 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 ccip/devenv/create_router_op_devenv.go create mode 100644 ccip/devenv/create_router_op_prod.go create mode 100644 ccip/devenv/deploy_receiver_op_devenv.go create mode 100644 ccip/devenv/deploy_receiver_op_prod.go create mode 100644 ccip/devenv/deploy_sender_op_devenv.go create mode 100644 ccip/devenv/deploy_sender_op_prod.go diff --git a/ccip/devenv/create_router_op_devenv.go b/ccip/devenv/create_router_op_devenv.go new file mode 100644 index 000000000..5214d9457 --- /dev/null +++ b/ccip/devenv/create_router_op_devenv.go @@ -0,0 +1,7 @@ +//go:build !prodledger + +package devenv + +import pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" + +var createRouterOperation = pprof.CreateRouter diff --git a/ccip/devenv/create_router_op_prod.go b/ccip/devenv/create_router_op_prod.go new file mode 100644 index 000000000..10f79aa39 --- /dev/null +++ b/ccip/devenv/create_router_op_prod.go @@ -0,0 +1,33 @@ +//go:build prodledger + +package devenv + +import ( + "errors" + + rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/ccipruntime" + pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var prodCreateRouterEncoder = rt.NewContract("", "CCIP.PerPartyRouter", "PerPartyRouterFactory").Encoder() + +var createRouterOperation = contract.NewExercise(contract.ExerciseParams[rt.CreateRouter]{ + Name: "canton/ccip/per_party_router_factory/create_router", + Version: pprof.Version, + Description: "Creates a new PerPartyRouter using the PerPartyRouterFactory", + ContractType: pprof.ContractType, + Validate: func(input rt.CreateRouter) error { + if input.InstanceId == "" { + return errors.New("instance ID cannot be empty") + } + if input.PartyOwner == "" { + return errors.New("router owner cannot be empty") + } + + return nil + }, + Template: rt.PerPartyRouterFactory{}, + Method: rt.PerPartyRouterFactory{}.CreateRouter, + EncodeMethod: prodCreateRouterEncoder.CreateRouter, +}) diff --git a/ccip/devenv/deploy_receiver_op_devenv.go b/ccip/devenv/deploy_receiver_op_devenv.go new file mode 100644 index 000000000..66ea91cca --- /dev/null +++ b/ccip/devenv/deploy_receiver_op_devenv.go @@ -0,0 +1,7 @@ +//go:build !prodledger + +package devenv + +import receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + +var ccipReceiverDeployOperation = receiverop.Deploy diff --git a/ccip/devenv/deploy_receiver_op_prod.go b/ccip/devenv/deploy_receiver_op_prod.go new file mode 100644 index 000000000..e49031c7e --- /dev/null +++ b/ccip/devenv/deploy_receiver_op_prod.go @@ -0,0 +1,29 @@ +//go:build prodledger + +package devenv + +import ( + "errors" + + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + v1receiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/contracts" + receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var ccipReceiverDeployOperation = contract.NewDeploy(contract.DeployParams[v1receiver.CCIPReceiver]{ + Name: "canton/ccip/receiver/deploy", + TypeAndVersion: deployment.NewTypeAndVersion(receiverop.ContractType, *receiverop.Version), + Description: "Deploys a CCIP Receiver contract on Canton", + Validate: func(template v1receiver.CCIPReceiver) error { + if template.Owner == "" { + return errors.New("owner cannot be empty") + } + + return nil + }, + PackageName: string(contracts.CCIPReceiver), + Prefix: "ccipreceiver", +}) diff --git a/ccip/devenv/deploy_sender_op_devenv.go b/ccip/devenv/deploy_sender_op_devenv.go new file mode 100644 index 000000000..c4d839027 --- /dev/null +++ b/ccip/devenv/deploy_sender_op_devenv.go @@ -0,0 +1,7 @@ +//go:build !prodledger + +package devenv + +import senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" + +var ccipSenderDeployOperation = senderop.Deploy diff --git a/ccip/devenv/deploy_sender_op_prod.go b/ccip/devenv/deploy_sender_op_prod.go new file mode 100644 index 000000000..e7416a8df --- /dev/null +++ b/ccip/devenv/deploy_sender_op_prod.go @@ -0,0 +1,29 @@ +//go:build prodledger + +package devenv + +import ( + "errors" + + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + v1sender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/contracts" + senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var ccipSenderDeployOperation = contract.NewDeploy(contract.DeployParams[v1sender.CCIPSender]{ + Name: "canton/ccip/sender/deploy", + TypeAndVersion: deployment.NewTypeAndVersion(senderop.ContractType, *senderop.Version), + Description: "Deploys a CCIP Sender contract on Canton", + Validate: func(template v1sender.CCIPSender) error { + if template.Owner == "" { + return errors.New("owner cannot be empty") + } + + return nil + }, + PackageName: string(contracts.CCIPSender), + Prefix: "ccipsender", +}) diff --git a/ccip/devenv/ledgerbind/exports_devenv.go b/ccip/devenv/ledgerbind/exports_devenv.go index 5018a6011..3df4ed9dd 100644 --- a/ccip/devenv/ledgerbind/exports_devenv.go +++ b/ccip/devenv/ledgerbind/exports_devenv.go @@ -11,12 +11,15 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/events" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" + chainlinkapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/chainlink/chainlinkapi" ) type ( CreateRouter = rt.CreateRouter PerPartyRouter = rt.PerPartyRouter + RawInstanceAddress = chainlinkapi.RawInstanceAddress + FinalityConfig = ccipcodec.FinalityConfig ExecutionStateChanged = events.ExecutionStateChanged diff --git a/ccip/devenv/ledgerbind/exports_prod.go b/ccip/devenv/ledgerbind/exports_prod.go index d7159d50e..4194a613e 100644 --- a/ccip/devenv/ledgerbind/exports_prod.go +++ b/ccip/devenv/ledgerbind/exports_prod.go @@ -7,12 +7,15 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/core" ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" + chainlinkapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/chainlink/chainlinkapi" ) type ( CreateRouter = rt.CreateRouter PerPartyRouter = rt.PerPartyRouter + RawInstanceAddress = chainlinkapi.RawInstanceAddress + FinalityConfig = core.FinalityConfig ExecutionStateChanged = core.ExecutionStateChanged diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index f037e4d8f..f970d3d1f 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -20,16 +20,8 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec" - "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" - ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" - ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" - chainlinkapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/chainlink/chainlinkapi" "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" "github.com/smartcontractkit/chainlink-canton/contracts" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" "github.com/smartcontractkit/chainlink-canton/testhelpers" ) @@ -71,19 +63,19 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, participant canton.Par _, err = operations.ExecuteOperation( c.e.OperationsBundle, - per_party_router_factory.CreateRouter, + createRouterOperation, c.chain, - contract.ChoiceInput[ccipruntime.CreateRouter]{ + contract.ChoiceInput[ledgerbind.CreateRouter]{ InstanceAddress: perPartyRouterFactoryDisclosure.Address.InstanceAddress(), ContractID: perPartyRouterFactoryDisclosure.ContractId, ParticipantIndex: c.clientParticipantIndex(), DisclosedContracts: contract.DisclosedContractsFromProto(perPartyRouterFactoryDisclosure.DisclosedContracts), - Args: ccipruntime.CreateRouter{ + Args: ledgerbind.CreateRouter{ PartyOwner: types.PARTY(partyId), InstanceId: types.TEXT(routerInstanceID.String()), }, }, - operations.WithForceExecute[contract.ChoiceInput[ccipruntime.CreateRouter], canton.Chain](), + operations.WithForceExecute[contract.ChoiceInput[ledgerbind.CreateRouter], canton.Chain](), ) if err != nil { if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { @@ -203,10 +195,10 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici return senderAddress, nil } - _, err := operations.ExecuteOperation(c.e.OperationsBundle, sender.Deploy, c.chain, contract.DeployInput[ccipsender.CCIPSender]{ + _, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipSenderDeployOperation, c.chain, contract.DeployInput[ledgerbind.CCIPSender]{ Qualifier: nil, ParticipantIndex: c.clientParticipantIndex(), - Template: ccipsender.CCIPSender{ + Template: ledgerbind.CCIPSender{ InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), }, @@ -261,10 +253,10 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return contracts.InstanceAddress{}, fmt.Errorf("failed to encode receiver finality config: %w", err) } - _, err = operations.ExecuteOperation(c.e.OperationsBundle, receiver.Deploy, c.chain, contract.DeployInput[ccipreceiver.CCIPReceiver]{ + _, err = operations.ExecuteOperation(c.e.OperationsBundle, ccipReceiverDeployOperation, c.chain, contract.DeployInput[ledgerbind.CCIPReceiver]{ Qualifier: nil, ParticipantIndex: c.clientParticipantIndex(), - Template: ccipreceiver.CCIPReceiver{ + Template: ledgerbind.CCIPReceiver{ InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), RequiredCCVs: requiredCCVs, @@ -293,7 +285,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return receiverAddress, nil } -func receiverRequiredCCVsFromEnv() ([]chainlinkapi.RawInstanceAddress, error) { +func receiverRequiredCCVsFromEnv() ([]ledgerbind.RawInstanceAddress, error) { raw := strings.TrimSpace(os.Getenv("CANTON_RECEIVER_REQUIRED_CCV")) if raw == "" { return nil, nil @@ -304,7 +296,7 @@ func receiverRequiredCCVsFromEnv() ([]chainlinkapi.RawInstanceAddress, error) { return nil, fmt.Errorf("parse CANTON_RECEIVER_REQUIRED_CCV: %w", err) } - return []chainlinkapi.RawInstanceAddress{ + return []ledgerbind.RawInstanceAddress{ {Unpack: types.TEXT(parsed.String())}, }, nil } @@ -660,18 +652,18 @@ func (c *Chain) fetchVerifierResult(ctx context.Context, messageID protocol.Byte }, nil } -func encodeReceiverFinalityConfig(finality int64) (ccipcodec.FinalityConfig, error) { +func encodeReceiverFinalityConfig(finality int64) (ledgerbind.FinalityConfig, error) { switch { case finality < 0: - return ccipcodec.FinalityConfig{}, fmt.Errorf("invalid finality %d: must be non-negative", finality) + return ledgerbind.FinalityConfig{}, fmt.Errorf("invalid finality %d: must be non-negative", finality) case finality == 0: - return ccipcodec.FinalityConfig{WaitForFinality: &types.UNIT{}}, nil + return ledgerbind.FinalityConfig{WaitForFinality: &types.UNIT{}}, nil case finality == 0x00010000: - return ccipcodec.FinalityConfig{WaitForSafe: &types.UNIT{}}, nil + return ledgerbind.FinalityConfig{WaitForSafe: &types.UNIT{}}, nil case finality > 0xFFFF: - return ccipcodec.FinalityConfig{}, fmt.Errorf("invalid finality %d: max supported block depth is 65535", finality) + return ledgerbind.FinalityConfig{}, fmt.Errorf("invalid finality %d: max supported block depth is 65535", finality) default: - return ccipcodec.FinalityConfig{BlockDepth: new(types.INT64(finality))}, nil + return ledgerbind.FinalityConfig{BlockDepth: new(types.INT64(finality))}, nil } } From 12de4ae36fe44fa3013c504654f41b00c290e497 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 20:46:53 -0300 Subject: [PATCH 20/26] fix router --- .github/actions/ccip-load-test/action.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index a67e69998..f76342395 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -56,6 +56,18 @@ inputs: description: "CANTON_GRPC_URL" required: false default: "testnet.cv1.bcy-v.metalhosts.com:443" + router_instance_id: + description: "CANTON_ROUTER_INSTANCE_ID (prod-testnet)" + required: false + default: "gha-load-router" + sender_instance_id: + description: "CANTON_SENDER_INSTANCE_ID (prod-testnet)" + required: false + default: "gha-load-sender" + receiver_instance_id: + description: "CANTON_RECEIVER_INSTANCE_ID (prod-testnet)" + required: false + default: "gha-load-receiver" CANTON_OKTA_AUTHORIZER_TESTNET: description: "GitHub secret CANTON_OKTA_AUTHORIZER_TESTNET" required: false @@ -150,6 +162,9 @@ runs: CANTON_PARTY_ID: ${{ inputs.party_id }} CANTON_USER_ID: ${{ inputs.user_id }} CANTON_GRPC_URL: ${{ inputs.grpc_url }} + CANTON_ROUTER_INSTANCE_ID: ${{ inputs.router_instance_id }} + CANTON_SENDER_INSTANCE_ID: ${{ inputs.sender_instance_id }} + CANTON_RECEIVER_INSTANCE_ID: ${{ inputs.receiver_instance_id }} CANTON_LOAD_MESSAGE_RATE: ${{ inputs.message_rate }} CANTON_LOAD_DURATION: ${{ inputs.load_duration }} CANTON_CONFIRM_EXEC_TIMEOUT: ${{ inputs.confirm_exec_timeout }} From d2998a983aa0185c83bee6ec342827e02b1b7975 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 8 Jul 2026 22:05:19 -0300 Subject: [PATCH 21/26] feat(load): pre-approve EVM tokens before load tests, not before every send --- ccip/devenv/tests/helpers.go | 66 +++++++++++++++++++ .../tests/load/gun_evm2canton_token_test.go | 15 +++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/ccip/devenv/tests/helpers.go b/ccip/devenv/tests/helpers.go index 5ca694114..6fa15e406 100644 --- a/ccip/devenv/tests/helpers.go +++ b/ccip/devenv/tests/helpers.go @@ -11,14 +11,18 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" gethcrypto "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" + routeroperations "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router" ccv "github.com/smartcontractkit/chainlink-ccv/build/devenv" "github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces" devenvcommon "github.com/smartcontractkit/chainlink-ccv/build/devenv/common" "github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/tcapi" "github.com/smartcontractkit/chainlink-ccv/protocol" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/erc20" "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/stretchr/testify/require" @@ -206,6 +210,68 @@ func (b E2EBootstrap) SetupCantonTokenSend(t *testing.T, ctx context.Context, la } } +// SetupEVMTokenSend pre-approves the CCIP router to spend transfer tokens for N sends. +// The CCV EVM impl approves only the per-message amount on each send; without this +// upfront approval, load tests would pay one extra approve tx per message. +func (b E2EBootstrap) SetupEVMTokenSend(t *testing.T, ctx context.Context, lane TokenLane, sends int) { + t.Helper() + + require.Positive(t, sends) + require.NotNil(t, lane.TransferAmount) + require.False(t, lane.SrcToken.IsEmpty(), "SetupEVMTokenSend requires an EVM source token") + + requiredAllowance := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(sends))) + + opsEnv, err := b.Lib.CLDFEnvironment() + require.NoError(t, err) + + selector := b.EVM.ChainSelector() + chain, ok := opsEnv.BlockChains.EVMChains()[selector] + require.True(t, ok, "EVM chain %d not in CLDF environment", selector) + + ds, err := b.Lib.DataStore() + require.NoError(t, err) + + routerVersion := semver.MustParse(routeroperations.Deploy.Version()) + routerRefs := ds.Addresses().Filter( + datastore.AddressRefByChainSelector(selector), + datastore.AddressRefByType(datastore.ContractType(routeroperations.ContractType)), + datastore.AddressRefByVersion(routerVersion), + ) + require.Len(t, routerRefs, 1, "expected exactly one CCIP router for chain %d", selector) + + routerAddr := common.HexToAddress(routerRefs[0].Address) + tokenAddr := common.BytesToAddress(lane.SrcToken.Bytes()) + owner := chain.DeployerKey.From + + tkn, err := erc20.NewERC20(tokenAddr, chain.Client) + require.NoError(t, err) + + allowance, err := tkn.Allowance(&bind.CallOpts{Context: ctx}, owner, routerAddr) + require.NoError(t, err) + if allowance.Cmp(requiredAllowance) >= 0 { + t.Logf("EVM router allowance sufficient: have=%s need=%s (token=%s router=%s)", + allowance.String(), requiredAllowance.String(), tokenAddr.Hex(), routerAddr.Hex()) + return + } + + t.Logf("approving CCIP router %s for %s transfer tokens (%d sends × %s)", + routerAddr.Hex(), requiredAllowance.String(), sends, lane.TransferAmount.String()) + + auth := *chain.DeployerKey + auth.Context = ctx + auth.Value = nil // approve is non-payable; clear stale msg.Value from shared opts + + tx, err := tkn.Approve(&auth, routerAddr, requiredAllowance) + require.NoError(t, err) + + _, err = chain.Confirm(tx) + require.NoError(t, err) + + t.Logf("Approved router %s for transfer token %s amount=%s tx=%s", + routerAddr.Hex(), tokenAddr.Hex(), requiredAllowance.String(), tx.Hash().Hex()) +} + // SetupCantonReceive sets CANTON_RECEIVER_REQUIRED_CCV when unset, then deploys the client // party's PerPartyRouter before inbound messages are executed on Canton (e.g. EVM→Canton). func (b E2EBootstrap) SetupCantonReceive(t *testing.T, ctx context.Context) { diff --git a/ccip/devenv/tests/load/gun_evm2canton_token_test.go b/ccip/devenv/tests/load/gun_evm2canton_token_test.go index b02e2c9b3..4663768aa 100644 --- a/ccip/devenv/tests/load/gun_evm2canton_token_test.go +++ b/ccip/devenv/tests/load/gun_evm2canton_token_test.go @@ -17,8 +17,8 @@ import ( // // Devenv: requires a running devenv and env-canton-evm-out.toml; EVM sender is pre-funded. // -// Prod-testnet: requires PRIVATE_KEY wallet with TEST tokens and Sepolia ETH on Sepolia, -// plus a pre-funded Canton party for execution fees. +// Prod-testnet: requires PRIVATE_KEY wallet with TEST tokens and Sepolia ETH on Sepolia +// (one upfront router approve plus send fees), plus a pre-funded Canton party for execution fees. // //nolint:paralleltest // single-flight exec on Canton dest; shares env with e2e. func TestEVM2Canton_TokenLoad(t *testing.T) { @@ -47,22 +47,23 @@ func TestEVM2Canton_TokenLoad(t *testing.T) { cantonDest := cantonTokenLoadDestination(boot.Canton, receiver, lane) sched := loadSchedule(t) - estimatedMessages := estimateMessages(sched) + sendBudget := sequentialSendsForLoad(sched) evmSender := boot.ResolveEVMReceiver(t) senderBalance, err := boot.EVM.GetTokenBalance(ctx, evmSender, lane.SrcToken) require.NoError(t, err) - requiredBalance := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(estimatedMessages))) - t.Logf("EVM sender token balance=%s requiredForRun=%s (estimatedMessages=%d; devenv pre-funds sender)", - senderBalance.String(), requiredBalance.String(), estimatedMessages) + requiredBalance := new(big.Int).Mul(lane.TransferAmount, big.NewInt(int64(sendBudget))) + t.Logf("EVM sender token balance=%s requiredForRun=%s (sendBudget=%d; devenv pre-funds sender)", + senderBalance.String(), requiredBalance.String(), sendBudget) if boot.Env.IsRemote() { require.GreaterOrEqual(t, senderBalance.Cmp(requiredBalance), 0, "EVM sender token balance %s is below required %s; fund PRIVATE_KEY wallet with TEST tokens on Sepolia", senderBalance.String(), requiredBalance.String()) - t.Log("prod-testnet: ensure PRIVATE_KEY wallet has Sepolia ETH for send and possible router approve tx") } else if senderBalance.Cmp(requiredBalance) < 0 { t.Logf("warning: EVM sender balance may be insufficient for full run") } + boot.SetupEVMTokenSend(t, ctx, lane, sendBudget) + ccvAddr := resolveEVMSourceAddrs(t, boot.Lib, boot.EVM.ChainSelector()) gun, err := NewCCIPLoadGun( From 180e85d0310227fe74b81334f25eb77869cbec6a Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 9 Jul 2026 00:23:44 -0300 Subject: [PATCH 22/26] refactor(load): move bindings affected code to a single layer --- ccip/devenv/README.md | 2 +- ccip/devenv/create_router_op_devenv.go | 7 -- ccip/devenv/create_router_op_prod.go | 33 ------ ccip/devenv/deploy_receiver_op_devenv.go | 7 -- ccip/devenv/deploy_receiver_op_prod.go | 29 ----- ccip/devenv/deploy_sender_op_devenv.go | 7 -- ccip/devenv/deploy_sender_op_prod.go | 29 ----- ccip/devenv/execute_op_devenv.go | 7 -- ccip/devenv/execute_op_prod.go | 24 ----- ccip/devenv/fee_quoter_limits.go | 30 +++--- ccip/devenv/impl.go | 42 ++++---- ccip/devenv/ledgertarget/README.md | 7 ++ .../context_adapt_devenv.go | 2 +- .../context_adapt_prod.go | 2 +- .../{ledgerbind => ledgertarget}/doc.go | 4 +- .../exports_devenv.go | 2 +- .../exports_prod.go | 10 +- .../inputs_devenv.go | 2 +- .../inputs_prod.go | 2 +- ccip/devenv/ledgertarget/ops_devenv.go | 17 +++ ccip/devenv/ledgertarget/ops_prod.go | 100 ++++++++++++++++++ ccip/devenv/manual_execution.go | 70 ++++++------ ccip/devenv/send_op_devenv.go | 7 -- ccip/devenv/send_op_prod.go | 24 ----- 24 files changed, 208 insertions(+), 258 deletions(-) delete mode 100644 ccip/devenv/create_router_op_devenv.go delete mode 100644 ccip/devenv/create_router_op_prod.go delete mode 100644 ccip/devenv/deploy_receiver_op_devenv.go delete mode 100644 ccip/devenv/deploy_receiver_op_prod.go delete mode 100644 ccip/devenv/deploy_sender_op_devenv.go delete mode 100644 ccip/devenv/deploy_sender_op_prod.go delete mode 100644 ccip/devenv/execute_op_devenv.go delete mode 100644 ccip/devenv/execute_op_prod.go create mode 100644 ccip/devenv/ledgertarget/README.md rename ccip/devenv/{ledgerbind => ledgertarget}/context_adapt_devenv.go (97%) rename ccip/devenv/{ledgerbind => ledgertarget}/context_adapt_prod.go (98%) rename ccip/devenv/{ledgerbind => ledgertarget}/doc.go (67%) rename ccip/devenv/{ledgerbind => ledgertarget}/exports_devenv.go (99%) rename ccip/devenv/{ledgerbind => ledgertarget}/exports_prod.go (90%) rename ccip/devenv/{ledgerbind => ledgertarget}/inputs_devenv.go (99%) rename ccip/devenv/{ledgerbind => ledgertarget}/inputs_prod.go (99%) create mode 100644 ccip/devenv/ledgertarget/ops_devenv.go create mode 100644 ccip/devenv/ledgertarget/ops_prod.go delete mode 100644 ccip/devenv/send_op_devenv.go delete mode 100644 ccip/devenv/send_op_prod.go diff --git a/ccip/devenv/README.md b/ccip/devenv/README.md index ab1592192..42a884b67 100644 --- a/ccip/devenv/README.md +++ b/ccip/devenv/README.md @@ -80,7 +80,7 @@ Build prod-targeting tests with `-tags=prodledger` so devenv code resolves the c | devenv (default) | _(none)_ | `bindings/generated/latest` | | prod-testnet / mainnet | `-tags=prodledger` | `bindings/generated/v1_0_0` | -Implementation lives in [`ccip/devenv/ledgerbind/`](./ledgerbind/). +Implementation lives in [`ccip/devenv/ledgertarget/`](./ledgertarget/). ## Load tests diff --git a/ccip/devenv/create_router_op_devenv.go b/ccip/devenv/create_router_op_devenv.go deleted file mode 100644 index 5214d9457..000000000 --- a/ccip/devenv/create_router_op_devenv.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !prodledger - -package devenv - -import pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" - -var createRouterOperation = pprof.CreateRouter diff --git a/ccip/devenv/create_router_op_prod.go b/ccip/devenv/create_router_op_prod.go deleted file mode 100644 index 10f79aa39..000000000 --- a/ccip/devenv/create_router_op_prod.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build prodledger - -package devenv - -import ( - "errors" - - rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/ccipruntime" - pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" - "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" -) - -var prodCreateRouterEncoder = rt.NewContract("", "CCIP.PerPartyRouter", "PerPartyRouterFactory").Encoder() - -var createRouterOperation = contract.NewExercise(contract.ExerciseParams[rt.CreateRouter]{ - Name: "canton/ccip/per_party_router_factory/create_router", - Version: pprof.Version, - Description: "Creates a new PerPartyRouter using the PerPartyRouterFactory", - ContractType: pprof.ContractType, - Validate: func(input rt.CreateRouter) error { - if input.InstanceId == "" { - return errors.New("instance ID cannot be empty") - } - if input.PartyOwner == "" { - return errors.New("router owner cannot be empty") - } - - return nil - }, - Template: rt.PerPartyRouterFactory{}, - Method: rt.PerPartyRouterFactory{}.CreateRouter, - EncodeMethod: prodCreateRouterEncoder.CreateRouter, -}) diff --git a/ccip/devenv/deploy_receiver_op_devenv.go b/ccip/devenv/deploy_receiver_op_devenv.go deleted file mode 100644 index 66ea91cca..000000000 --- a/ccip/devenv/deploy_receiver_op_devenv.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !prodledger - -package devenv - -import receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" - -var ccipReceiverDeployOperation = receiverop.Deploy diff --git a/ccip/devenv/deploy_receiver_op_prod.go b/ccip/devenv/deploy_receiver_op_prod.go deleted file mode 100644 index e49031c7e..000000000 --- a/ccip/devenv/deploy_receiver_op_prod.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build prodledger - -package devenv - -import ( - "errors" - - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - - v1receiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" - "github.com/smartcontractkit/chainlink-canton/contracts" - receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" - "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" -) - -var ccipReceiverDeployOperation = contract.NewDeploy(contract.DeployParams[v1receiver.CCIPReceiver]{ - Name: "canton/ccip/receiver/deploy", - TypeAndVersion: deployment.NewTypeAndVersion(receiverop.ContractType, *receiverop.Version), - Description: "Deploys a CCIP Receiver contract on Canton", - Validate: func(template v1receiver.CCIPReceiver) error { - if template.Owner == "" { - return errors.New("owner cannot be empty") - } - - return nil - }, - PackageName: string(contracts.CCIPReceiver), - Prefix: "ccipreceiver", -}) diff --git a/ccip/devenv/deploy_sender_op_devenv.go b/ccip/devenv/deploy_sender_op_devenv.go deleted file mode 100644 index c4d839027..000000000 --- a/ccip/devenv/deploy_sender_op_devenv.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !prodledger - -package devenv - -import senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" - -var ccipSenderDeployOperation = senderop.Deploy diff --git a/ccip/devenv/deploy_sender_op_prod.go b/ccip/devenv/deploy_sender_op_prod.go deleted file mode 100644 index e7416a8df..000000000 --- a/ccip/devenv/deploy_sender_op_prod.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build prodledger - -package devenv - -import ( - "errors" - - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - - v1sender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" - "github.com/smartcontractkit/chainlink-canton/contracts" - senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" - "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" -) - -var ccipSenderDeployOperation = contract.NewDeploy(contract.DeployParams[v1sender.CCIPSender]{ - Name: "canton/ccip/sender/deploy", - TypeAndVersion: deployment.NewTypeAndVersion(senderop.ContractType, *senderop.Version), - Description: "Deploys a CCIP Sender contract on Canton", - Validate: func(template v1sender.CCIPSender) error { - if template.Owner == "" { - return errors.New("owner cannot be empty") - } - - return nil - }, - PackageName: string(contracts.CCIPSender), - Prefix: "ccipsender", -}) diff --git a/ccip/devenv/execute_op_devenv.go b/ccip/devenv/execute_op_devenv.go deleted file mode 100644 index 767ad836a..000000000 --- a/ccip/devenv/execute_op_devenv.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !prodledger - -package devenv - -import receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" - -var ccipReceiverExecuteOperation = receiverop.Execute diff --git a/ccip/devenv/execute_op_prod.go b/ccip/devenv/execute_op_prod.go deleted file mode 100644 index 9e00c8287..000000000 --- a/ccip/devenv/execute_op_prod.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build prodledger - -package devenv - -import ( - ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" - "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" -) - -var prodReceiverEncoder = ccipreceiver.NewContract("", "CCIP.CCIPReceiver", "CCIPReceiver").Encoder() - -var ccipReceiverExecuteOperation = contract.NewExercise(contract.ExerciseParams[ccipreceiver.Execute]{ - Name: "canton/ccip/receiver/execute", - Version: receiver.Version, - Description: "Calls the Execute choice on a CCIP Receiver contract", - ContractType: receiver.ContractType, - Validate: func(input ccipreceiver.Execute) error { - return nil - }, - Template: ccipreceiver.CCIPReceiver{}, - Method: ccipreceiver.CCIPReceiver{}.Execute, - EncodeMethod: prodReceiverEncoder.Execute, -}) diff --git a/ccip/devenv/fee_quoter_limits.go b/ccip/devenv/fee_quoter_limits.go index f4c56d624..e65482286 100644 --- a/ccip/devenv/fee_quoter_limits.go +++ b/ccip/devenv/fee_quoter_limits.go @@ -11,7 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/go-daml/pkg/service/ledger" - "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgertarget" "github.com/smartcontractkit/chainlink-canton/contracts" feequoterop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/fee_quoter" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" @@ -49,12 +49,12 @@ func (c *Chain) GetMaxPerMsgGasLimit(ctx context.Context, remoteChainSelector ui return uint32(cfg.MaxPerMsgGasLimit), nil } -func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uint64) (ledgerbind.FeeQuoterDestChainConfig, error) { +func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uint64) (ledgertarget.FeeQuoterDestChainConfig, error) { if c.e == nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("canton chain environment is nil") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("canton chain environment is nil") } if len(c.chain.Participants) == 0 { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("no canton participants configured") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("no canton participants configured") } feeQuoterRef, err := c.e.DataStore.Addresses().Get(datastore.NewAddressRefKey( @@ -64,7 +64,7 @@ func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uin "", )) if err != nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("resolve FeeQuoter address: %w", err) + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("resolve FeeQuoter address: %w", err) } participant := c.chain.Participants[0] @@ -75,22 +75,22 @@ func (c *Chain) feeQuoterDestConfig(ctx context.Context, remoteChainSelector uin ctx, participant.LedgerServices.State, []string{party}, - ledgerbind.FeeQuoter{}.GetTemplateID(), + ledgertarget.FeeQuoter{}.GetTemplateID(), feeQuoterAddress, ) if err != nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("find active FeeQuoter contract: %w", err) + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("find active FeeQuoter contract: %w", err) } createArgs := activeContract.GetCreatedEvent().GetCreateArguments() if createArgs == nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("FeeQuoter create arguments missing") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("FeeQuoter create arguments missing") } return destChainConfigFromFeeQuoterCreateArgs(createArgs, remoteChainSelector) } -func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChainSelector uint64) (ledgerbind.FeeQuoterDestChainConfig, error) { +func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChainSelector uint64) (ledgertarget.FeeQuoterDestChainConfig, error) { selectorKey := strconv.FormatUint(remoteChainSelector, 10) for _, field := range createArgs.GetFields() { if field.GetLabel() != "destChainConfigs" { @@ -98,7 +98,7 @@ func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChai } genMap := field.GetValue().GetGenMap() if genMap == nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs is not a GenMap") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs is not a GenMap") } for _, entry := range genMap.GetEntries() { key := strings.TrimSuffix(entry.GetKey().GetNumeric(), ".") @@ -107,18 +107,18 @@ func destChainConfigFromFeeQuoterCreateArgs(createArgs *apiv2.Record, remoteChai } record := entry.GetValue().GetRecord() if record == nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("dest chain config value is not a record") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("dest chain config value is not a record") } - var cfg ledgerbind.FeeQuoterDestChainConfig + var cfg ledgertarget.FeeQuoterDestChainConfig if err := ledger.RecordToStruct(record, &cfg); err != nil { - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("parse dest chain config record: %w", err) + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("parse dest chain config record: %w", err) } return cfg, nil } - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("no FeeQuoter dest config for chain selector %d", remoteChainSelector) + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("no FeeQuoter dest config for chain selector %d", remoteChainSelector) } - return ledgerbind.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs field not found on FeeQuoter") + return ledgertarget.FeeQuoterDestChainConfig{}, fmt.Errorf("destChainConfigs field not found on FeeQuoter") } diff --git a/ccip/devenv/impl.go b/ccip/devenv/impl.go index 65d4ca6da..5637f9c0e 100644 --- a/ccip/devenv/impl.go +++ b/ccip/devenv/impl.go @@ -47,7 +47,7 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_transfer_instruction_v1" - "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgertarget" "github.com/smartcontractkit/chainlink-canton/contracts" cantonchangesets "github.com/smartcontractkit/chainlink-canton/deployment/changesets" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" @@ -1280,26 +1280,26 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("find active contract ID for router at address %s: %w", c.routerAddress, err) } var disclosedContracts []*apiv2.DisclosedContract - sendArgs := ledgerbind.Send{ + sendArgs := ledgertarget.Send{ RouterCid: types.CONTRACT_ID(routerCid), DestinationChainSelector: types.NUMERIC(strconv.FormatUint(dest, 10)), - Message: ledgerbind.Canton2AnyMessage{ + Message: ledgertarget.Canton2AnyMessage{ Receiver: types.TEXT(hex.EncodeToString(fields.Receiver)), Payload: types.TEXT(hex.EncodeToString(fields.Data)), - FeeToken: ledgerbind.AdaptInstrumentId(c.feeTokenInstrument), - ExtraArgs: ledgerbind.ExtraArgs{ - V3: &ledgerbind.GenericExtraArgsV3{ + FeeToken: ledgertarget.AdaptInstrumentId(c.feeTokenInstrument), + ExtraArgs: ledgertarget.ExtraArgs{ + V3: &ledgertarget.GenericExtraArgsV3{ GasLimit: types.INT64(opts.ExecutionGasLimit), - Executor: ledgerbind.ExecutorExtraArg{ - ExecutorUseDefault: &ledgerbind.ExecutorUseDefault{}, + Executor: ledgertarget.ExecutorExtraArg{ + ExecutorUseDefault: &ledgertarget.ExecutorUseDefault{}, }, }, }, }, - FeeTokenInput: ledgerbind.FeeTokenInput{ + FeeTokenInput: ledgertarget.FeeTokenInput{ SenderInputCids: []types.CONTRACT_ID{types.CONTRACT_ID(c.nextFeeCID)}, FeeTokenTransferFactory: types.CONTRACT_ID(feeTransferFactorycid), - FeeTokenExtraArgs: ledgerbind.AdaptExtraArgs(splice_api_token_metadata_v1.ExtraArgs{ + FeeTokenExtraArgs: ledgertarget.AdaptExtraArgs(splice_api_token_metadata_v1.ExtraArgs{ Context: splice_api_token_metadata_v1.ChoiceContext{Values: testhelpers.ExtractChoiceContextValues(feeTransferFactoryChoiceContextValue)}, Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, }), @@ -1326,14 +1326,14 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get Token Pool Send disclosure for token pool at address %s: %w", tokenPoolAddress.String(), err) } - sendArgs.Message.TokenTransfer = &ledgerbind.TokenTransfer{ - Token: ledgerbind.AdaptInstrumentId(splice_api_token_holding_v1.InstrumentId{ + sendArgs.Message.TokenTransfer = &ledgertarget.TokenTransfer{ + Token: ledgertarget.AdaptInstrumentId(splice_api_token_holding_v1.InstrumentId{ Admin: types.PARTY(outgoingMessage.TokenTransfer.Token.Admin), Id: types.TEXT(outgoingMessage.TokenTransfer.Token.Id), }), Amount: types.NUMERIC(outgoingMessage.TokenTransfer.Amount), } - tokenTransferInput := ledgerbind.NewTokenTransferInput( + tokenTransferInput := ledgertarget.NewTokenTransferInput( []types.CONTRACT_ID{types.CONTRACT_ID(c.nextTransferCID)}, types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), tokenPoolSendDisclosure.ChoiceContext, @@ -1348,7 +1348,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get CCIP Send disclosure: %w", err) } - sendArgs.Context = ledgerbind.AdaptChoiceContext(ccipSendDisclosure.ChoiceContext) + sendArgs.Context = ledgertarget.AdaptChoiceContext(ccipSendDisclosure.ChoiceContext) sendArgs.FeeTokenInput.FeeTokenConfigCid = types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid) disclosedContracts = append(disclosedContracts, ccipSendDisclosure.DisclosedContracts...) @@ -1367,12 +1367,12 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get CCV Send disclosure for CCV %q: %w", v, err) } - sendArgs.CcvSendInputs = append(sendArgs.CcvSendInputs, ledgerbind.NewCCVSendInput( + sendArgs.CcvSendInputs = append(sendArgs.CcvSendInputs, ledgertarget.NewCCVSendInput( ccvSendDisclosure.Address, types.CONTRACT_ID(ccvSendDisclosure.ContractId), ccvSendDisclosure.ChoiceContext, )) - sendArgs.Message.ExtraArgs.V3.Ccvs = append(sendArgs.Message.ExtraArgs.V3.Ccvs, ledgerbind.NewCCVExtraArg( + sendArgs.Message.ExtraArgs.V3.Ccvs = append(sendArgs.Message.ExtraArgs.V3.Ccvs, ledgertarget.NewCCVExtraArg( ccvSendDisclosure.Address, "", )) @@ -1393,7 +1393,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint if err != nil { return cciptestinterfaces.MessageSentEvent{}, fmt.Errorf("failed to get Executor Send disclosure for Executor %q: %w", *ccipSendDisclosure.Executor, err) } - executorInput := ledgerbind.NewExecutorInput( + executorInput := ledgertarget.NewExecutorInput( types.CONTRACT_ID(executorSendDisclosure.ContractId), executorSendDisclosure.ChoiceContext, ) @@ -1408,7 +1408,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint disclosedContracts = testhelpers.DeduplicateDisclosedContracts(disclosedContracts...) // Call CCIPSend - ccipSendReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipSenderSendOperation, c.chain, contract.ChoiceInput[ledgerbind.Send]{ + ccipSendReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ledgertarget.SenderSendOperation, c.chain, contract.ChoiceInput[ledgertarget.Send]{ InstanceAddress: c.senderAddress, ParticipantIndex: clientIdx, Args: sendArgs, @@ -1430,7 +1430,7 @@ func (c *Chain) SendMessage(ctx context.Context, dest uint64, fields cciptestint { IdentifierFilter: &apiv2.CumulativeFilter_TemplateFilter{ TemplateFilter: &apiv2.TemplateFilter{ - TemplateId: contracts.TemplateIDFromBinding(ledgerbind.CCIPMessageSent{}).ToLedgerIdentifier(), + TemplateId: contracts.TemplateIDFromBinding(ledgertarget.CCIPMessageSent{}).ToLedgerIdentifier(), IncludeCreatedEventBlob: true, }, }, @@ -1513,7 +1513,7 @@ type ccipMessageSentFromSendUpdate struct { // before this send; returned seqNo is either previousSeq+1 or the sequence from the encoded payload // when that path is present. func parseFirstCCIPMessageSentFromLedgerEvents(events []*apiv2.Event, previousSeq uint64) (ccipMessageSentFromSendUpdate, error) { - messageSentTemplateID := contracts.TemplateIDFromBinding(ledgerbind.CCIPMessageSent{}) + messageSentTemplateID := contracts.TemplateIDFromBinding(ledgertarget.CCIPMessageSent{}) // Find CCIPMessageSent event in the events var created *apiv2.CreatedEvent @@ -1534,7 +1534,7 @@ func parseFirstCCIPMessageSentFromLedgerEvents(events []*apiv2.Event, previousSe return ccipMessageSentFromSendUpdate{}, fmt.Errorf("no CCIPMessageSent event found in sender transaction") } - parsed, err := bindings.UnmarshalCreatedEvent[ledgerbind.CCIPMessageSent](created) + parsed, err := bindings.UnmarshalCreatedEvent[ledgertarget.CCIPMessageSent](created) if err != nil { return ccipMessageSentFromSendUpdate{}, fmt.Errorf("unmarshal CCIPMessageSent created event: %w", err) } diff --git a/ccip/devenv/ledgertarget/README.md b/ccip/devenv/ledgertarget/README.md new file mode 100644 index 000000000..e20fa6a25 --- /dev/null +++ b/ccip/devenv/ledgertarget/README.md @@ -0,0 +1,7 @@ +# ledgertarget + +Compile-time adapter for Canton ledger layout differences. + +Local devenv uses `bindings/generated/latest` (current DAML). Prod testnet/mainnet still runs `v1_0_0` contracts with different module paths and field names. + +Go build tags select the target: default for devenv, `-tags=prodledger` for prod. All tag-split bindings and operations live here so the rest of `devenv` stays tag-free. diff --git a/ccip/devenv/ledgerbind/context_adapt_devenv.go b/ccip/devenv/ledgertarget/context_adapt_devenv.go similarity index 97% rename from ccip/devenv/ledgerbind/context_adapt_devenv.go rename to ccip/devenv/ledgertarget/context_adapt_devenv.go index a9097625d..a4df7c085 100644 --- a/ccip/devenv/ledgerbind/context_adapt_devenv.go +++ b/ccip/devenv/ledgertarget/context_adapt_devenv.go @@ -1,6 +1,6 @@ //go:build !prodledger -package ledgerbind +package ledgertarget import ( latestholding "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" diff --git a/ccip/devenv/ledgerbind/context_adapt_prod.go b/ccip/devenv/ledgertarget/context_adapt_prod.go similarity index 98% rename from ccip/devenv/ledgerbind/context_adapt_prod.go rename to ccip/devenv/ledgertarget/context_adapt_prod.go index 9440c58cc..2632a5b5f 100644 --- a/ccip/devenv/ledgerbind/context_adapt_prod.go +++ b/ccip/devenv/ledgertarget/context_adapt_prod.go @@ -1,6 +1,6 @@ //go:build prodledger -package ledgerbind +package ledgertarget import ( "encoding/json" diff --git a/ccip/devenv/ledgerbind/doc.go b/ccip/devenv/ledgertarget/doc.go similarity index 67% rename from ccip/devenv/ledgerbind/doc.go rename to ccip/devenv/ledgertarget/doc.go index a1be143c4..50449f968 100644 --- a/ccip/devenv/ledgerbind/doc.go +++ b/ccip/devenv/ledgertarget/doc.go @@ -1,7 +1,7 @@ -// Package ledgerbind selects CCIP ledger bindings at compile time. +// Package ledgertarget selects CCIP ledger bindings and operations at compile time. // // Devenv (default): bindings/generated/latest — current dev DAML layout. // // Prod / deployed ledger: go build -tags=prodledger // Uses bindings/generated/v1_0_0 — template IDs matching Canton TestNet/mainnet today. -package ledgerbind +package ledgertarget diff --git a/ccip/devenv/ledgerbind/exports_devenv.go b/ccip/devenv/ledgertarget/exports_devenv.go similarity index 99% rename from ccip/devenv/ledgerbind/exports_devenv.go rename to ccip/devenv/ledgertarget/exports_devenv.go index 3df4ed9dd..f7b49cffd 100644 --- a/ccip/devenv/ledgerbind/exports_devenv.go +++ b/ccip/devenv/ledgertarget/exports_devenv.go @@ -1,6 +1,6 @@ //go:build !prodledger -package ledgerbind +package ledgertarget import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipapi" diff --git a/ccip/devenv/ledgerbind/exports_prod.go b/ccip/devenv/ledgertarget/exports_prod.go similarity index 90% rename from ccip/devenv/ledgerbind/exports_prod.go rename to ccip/devenv/ledgertarget/exports_prod.go index 4194a613e..4e1c82559 100644 --- a/ccip/devenv/ledgerbind/exports_prod.go +++ b/ccip/devenv/ledgertarget/exports_prod.go @@ -1,6 +1,6 @@ //go:build prodledger -package ledgerbind +package ledgertarget import ( rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/ccipruntime" @@ -11,7 +11,7 @@ import ( ) type ( - CreateRouter = rt.CreateRouter + CreateRouter = rt.CreateRouter PerPartyRouter = rt.PerPartyRouter RawInstanceAddress = chainlinkapi.RawInstanceAddress @@ -44,9 +44,9 @@ type ( CCVSendInput = ccipsender.CCVSendInput ExecutorInput = ccipsender.ExecutorInput - ReceiverExecute = ccipreceiver.Execute - ReceiverCCVInput = ccipreceiver.CCVInput - ReceiverTokenTransferInput = ccipreceiver.TokenTransferInput + ReceiverExecute = ccipreceiver.Execute + ReceiverCCVInput = ccipreceiver.CCVInput + ReceiverTokenTransferInput = ccipreceiver.TokenTransferInput ) const ( diff --git a/ccip/devenv/ledgerbind/inputs_devenv.go b/ccip/devenv/ledgertarget/inputs_devenv.go similarity index 99% rename from ccip/devenv/ledgerbind/inputs_devenv.go rename to ccip/devenv/ledgertarget/inputs_devenv.go index 0ab24b04e..5912659fd 100644 --- a/ccip/devenv/ledgerbind/inputs_devenv.go +++ b/ccip/devenv/ledgertarget/inputs_devenv.go @@ -1,6 +1,6 @@ //go:build !prodledger -package ledgerbind +package ledgertarget import ( "github.com/smartcontractkit/go-daml/pkg/types" diff --git a/ccip/devenv/ledgerbind/inputs_prod.go b/ccip/devenv/ledgertarget/inputs_prod.go similarity index 99% rename from ccip/devenv/ledgerbind/inputs_prod.go rename to ccip/devenv/ledgertarget/inputs_prod.go index c0960fdc6..c645d77ff 100644 --- a/ccip/devenv/ledgerbind/inputs_prod.go +++ b/ccip/devenv/ledgertarget/inputs_prod.go @@ -1,6 +1,6 @@ //go:build prodledger -package ledgerbind +package ledgertarget import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/chainlink/chainlinkapi" diff --git a/ccip/devenv/ledgertarget/ops_devenv.go b/ccip/devenv/ledgertarget/ops_devenv.go new file mode 100644 index 000000000..be76f3ea6 --- /dev/null +++ b/ccip/devenv/ledgertarget/ops_devenv.go @@ -0,0 +1,17 @@ +//go:build !prodledger + +package ledgertarget + +import ( + pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" + receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" +) + +var ( + CreateRouterOperation = pprof.CreateRouter + ReceiverDeployOperation = receiverop.Deploy + SenderDeployOperation = senderop.Deploy + ReceiverExecuteOperation = receiverop.Execute + SenderSendOperation = senderop.Send +) diff --git a/ccip/devenv/ledgertarget/ops_prod.go b/ccip/devenv/ledgertarget/ops_prod.go new file mode 100644 index 000000000..3ba81c363 --- /dev/null +++ b/ccip/devenv/ledgertarget/ops_prod.go @@ -0,0 +1,100 @@ +//go:build prodledger + +package ledgertarget + +import ( + "errors" + + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + rt "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/ccipruntime" + ccipreceiver "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/receiver" + ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/contracts" + pprof "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory" + receiverop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/receiver" + senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" +) + +var prodCreateRouterEncoder = rt.NewContract("", "CCIP.PerPartyRouter", "PerPartyRouterFactory").Encoder() + +var CreateRouterOperation = contract.NewExercise(contract.ExerciseParams[rt.CreateRouter]{ + Name: "canton/ccip/per_party_router_factory/create_router", + Version: pprof.Version, + Description: "Creates a new PerPartyRouter using the PerPartyRouterFactory", + ContractType: pprof.ContractType, + Validate: func(input rt.CreateRouter) error { + if input.InstanceId == "" { + return errors.New("instance ID cannot be empty") + } + if input.PartyOwner == "" { + return errors.New("router owner cannot be empty") + } + + return nil + }, + Template: rt.PerPartyRouterFactory{}, + Method: rt.PerPartyRouterFactory{}.CreateRouter, + EncodeMethod: prodCreateRouterEncoder.CreateRouter, +}) + +var ReceiverDeployOperation = contract.NewDeploy(contract.DeployParams[ccipreceiver.CCIPReceiver]{ + Name: "canton/ccip/receiver/deploy", + TypeAndVersion: deployment.NewTypeAndVersion(receiverop.ContractType, *receiverop.Version), + Description: "Deploys a CCIP Receiver contract on Canton", + Validate: func(template ccipreceiver.CCIPReceiver) error { + if template.Owner == "" { + return errors.New("owner cannot be empty") + } + + return nil + }, + PackageName: string(contracts.CCIPReceiver), + Prefix: "ccipreceiver", +}) + +var SenderDeployOperation = contract.NewDeploy(contract.DeployParams[ccipsender.CCIPSender]{ + Name: "canton/ccip/sender/deploy", + TypeAndVersion: deployment.NewTypeAndVersion(senderop.ContractType, *senderop.Version), + Description: "Deploys a CCIP Sender contract on Canton", + Validate: func(template ccipsender.CCIPSender) error { + if template.Owner == "" { + return errors.New("owner cannot be empty") + } + + return nil + }, + PackageName: string(contracts.CCIPSender), + Prefix: "ccipsender", +}) + +var prodReceiverEncoder = ccipreceiver.NewContract("", "CCIP.CCIPReceiver", "CCIPReceiver").Encoder() + +var ReceiverExecuteOperation = contract.NewExercise(contract.ExerciseParams[ccipreceiver.Execute]{ + Name: "canton/ccip/receiver/execute", + Version: receiverop.Version, + Description: "Calls the Execute choice on a CCIP Receiver contract", + ContractType: receiverop.ContractType, + Validate: func(input ccipreceiver.Execute) error { + return nil + }, + Template: ccipreceiver.CCIPReceiver{}, + Method: ccipreceiver.CCIPReceiver{}.Execute, + EncodeMethod: prodReceiverEncoder.Execute, +}) + +var prodSenderEncoder = ccipsender.NewContract("", "CCIP.CCIPSender", "CCIPSender").Encoder() + +var SenderSendOperation = contract.NewExercise(contract.ExerciseParams[ccipsender.Send]{ + Name: "canton/ccip/sender/send", + Version: senderop.Version, + Description: "Calls the Send choice on a CCIP Sender contract", + ContractType: senderop.ContractType, + Validate: func(input ccipsender.Send) error { + return nil + }, + Template: ccipsender.CCIPSender{}, + Method: ccipsender.CCIPSender{}.Send, + EncodeMethod: prodSenderEncoder.Send, +}) diff --git a/ccip/devenv/manual_execution.go b/ccip/devenv/manual_execution.go index f970d3d1f..83ac88012 100644 --- a/ccip/devenv/manual_execution.go +++ b/ccip/devenv/manual_execution.go @@ -20,7 +20,7 @@ import ( "github.com/smartcontractkit/go-daml/pkg/types" "github.com/smartcontractkit/chainlink-canton/bindings" - "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgerbind" + "github.com/smartcontractkit/chainlink-canton/ccip/devenv/ledgertarget" "github.com/smartcontractkit/chainlink-canton/contracts" "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" "github.com/smartcontractkit/chainlink-canton/testhelpers" @@ -63,19 +63,19 @@ func (c *Chain) DeployPerPartyRouter(ctx context.Context, participant canton.Par _, err = operations.ExecuteOperation( c.e.OperationsBundle, - createRouterOperation, + ledgertarget.CreateRouterOperation, c.chain, - contract.ChoiceInput[ledgerbind.CreateRouter]{ + contract.ChoiceInput[ledgertarget.CreateRouter]{ InstanceAddress: perPartyRouterFactoryDisclosure.Address.InstanceAddress(), ContractID: perPartyRouterFactoryDisclosure.ContractId, ParticipantIndex: c.clientParticipantIndex(), DisclosedContracts: contract.DisclosedContractsFromProto(perPartyRouterFactoryDisclosure.DisclosedContracts), - Args: ledgerbind.CreateRouter{ + Args: ledgertarget.CreateRouter{ PartyOwner: types.PARTY(partyId), InstanceId: types.TEXT(routerInstanceID.String()), }, }, - operations.WithForceExecute[contract.ChoiceInput[ledgerbind.CreateRouter], canton.Chain](), + operations.WithForceExecute[contract.ChoiceInput[ledgertarget.CreateRouter], canton.Chain](), ) if err != nil { if found, _, ok, findErr := c.findPerPartyRouterByParty(ctx, participant, partyId, routerInstanceID); findErr != nil { @@ -106,7 +106,7 @@ func (c *Chain) findPerPartyRouterByParty( partyId string, preferredInstanceID contracts.InstanceID, ) (contracts.InstanceAddress, string, bool, error) { - templateID := contracts.TemplateIDFromBinding(ledgerbind.PerPartyRouter{}).ToLedgerIdentifier() + templateID := contracts.TemplateIDFromBinding(ledgertarget.PerPartyRouter{}).ToLedgerIdentifier() activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) if err != nil { return contracts.InstanceAddress{}, "", false, err @@ -188,17 +188,17 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ledgerbind.CCIPSender{}.GetTemplateID(), + ledgertarget.CCIPSender{}.GetTemplateID(), senderAddress, ); err == nil { c.senderAddress = senderAddress return senderAddress, nil } - _, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipSenderDeployOperation, c.chain, contract.DeployInput[ledgerbind.CCIPSender]{ + _, err := operations.ExecuteOperation(c.e.OperationsBundle, ledgertarget.SenderDeployOperation, c.chain, contract.DeployInput[ledgertarget.CCIPSender]{ Qualifier: nil, ParticipantIndex: c.clientParticipantIndex(), - Template: ledgerbind.CCIPSender{ + Template: ledgertarget.CCIPSender{ InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), }, @@ -209,7 +209,7 @@ func (c *Chain) DeployCCIPSender(ctx context.Context, participant canton.Partici ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ledgerbind.CCIPSender{}.GetTemplateID(), + ledgertarget.CCIPSender{}.GetTemplateID(), senderAddress, ); findErr == nil { c.senderAddress = senderAddress @@ -236,7 +236,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ledgerbind.CCIPReceiver{}.GetTemplateID(), + ledgertarget.CCIPReceiver{}.GetTemplateID(), receiverAddress, ); err == nil { c.receiverAddress = receiverAddress @@ -253,10 +253,10 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return contracts.InstanceAddress{}, fmt.Errorf("failed to encode receiver finality config: %w", err) } - _, err = operations.ExecuteOperation(c.e.OperationsBundle, ccipReceiverDeployOperation, c.chain, contract.DeployInput[ledgerbind.CCIPReceiver]{ + _, err = operations.ExecuteOperation(c.e.OperationsBundle, ledgertarget.ReceiverDeployOperation, c.chain, contract.DeployInput[ledgertarget.CCIPReceiver]{ Qualifier: nil, ParticipantIndex: c.clientParticipantIndex(), - Template: ledgerbind.CCIPReceiver{ + Template: ledgertarget.CCIPReceiver{ InstanceId: types.TEXT(instanceID), Owner: types.PARTY(partyId), RequiredCCVs: requiredCCVs, @@ -271,7 +271,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti ctx, participant.LedgerServices.State, contract.LedgerQueryParties(participant), - ledgerbind.CCIPReceiver{}.GetTemplateID(), + ledgertarget.CCIPReceiver{}.GetTemplateID(), receiverAddress, ); findErr == nil { c.receiverAddress = receiverAddress @@ -285,7 +285,7 @@ func (c *Chain) DeployCCIPReceiver(ctx context.Context, participant canton.Parti return receiverAddress, nil } -func receiverRequiredCCVsFromEnv() ([]ledgerbind.RawInstanceAddress, error) { +func receiverRequiredCCVsFromEnv() ([]ledgertarget.RawInstanceAddress, error) { raw := strings.TrimSpace(os.Getenv("CANTON_RECEIVER_REQUIRED_CCV")) if raw == "" { return nil, nil @@ -296,7 +296,7 @@ func receiverRequiredCCVsFromEnv() ([]ledgerbind.RawInstanceAddress, error) { return nil, fmt.Errorf("parse CANTON_RECEIVER_REQUIRED_CCV: %w", err) } - return []ledgerbind.RawInstanceAddress{ + return []ledgertarget.RawInstanceAddress{ {Unpack: types.TEXT(parsed.String())}, }, nil } @@ -350,12 +350,12 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get CCIP execute disclosure: %w", err) } - executeArgs := ledgerbind.ReceiverExecute{ - Context: ledgerbind.AdaptChoiceContext(ccipExecuteDisclosure.ChoiceContext), + executeArgs := ledgertarget.ReceiverExecute{ + Context: ledgertarget.AdaptChoiceContext(ccipExecuteDisclosure.ChoiceContext), RouterCid: types.CONTRACT_ID(routerCid), EncodedMessage: types.TEXT(encodedMessageHex), TokenTransfer: nil, - CcvInputs: make([]ledgerbind.ReceiverCCVInput, len(verifiers)), + CcvInputs: make([]ledgertarget.ReceiverCCVInput, len(verifiers)), } disclosedContracts := ccipExecuteDisclosure.DisclosedContracts @@ -370,7 +370,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get CCV execute disclosure for verifier %s: %w", verifier.String(), err) } - executeArgs.CcvInputs[i] = ledgerbind.NewReceiverCCVInput( + executeArgs.CcvInputs[i] = ledgertarget.NewReceiverCCVInput( types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), types.TEXT(hex.EncodeToString(vr)), ccvExecuteDisclosure.ChoiceContext, @@ -391,7 +391,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to get token pool execute disclosure: %w", err) } - tokenTransfer := ledgerbind.NewReceiverTokenTransferInput( + tokenTransfer := ledgertarget.NewReceiverTokenTransferInput( types.CONTRACT_ID(tokenPoolDisclosure.ContractId), types.PARTY(executingParty), tokenPoolDisclosure.ChoiceContext, @@ -407,7 +407,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes Str("Receiver", hex.EncodeToString(message.Receiver)). Msg("Executing message...") - executeReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ccipReceiverExecuteOperation, c.chain, contract.ChoiceInput[ledgerbind.ReceiverExecute]{ + executeReport, err := operations.ExecuteOperation(c.e.OperationsBundle, ledgertarget.ReceiverExecuteOperation, c.chain, contract.ChoiceInput[ledgertarget.ReceiverExecute]{ InstanceAddress: receiverAddress, ParticipantIndex: clientIdx, Args: executeArgs, @@ -469,7 +469,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes } // Get ExecutionStateChangedEvent from events - expectedTemplateID := ledgerbind.ExecutionStateChanged{}.GetTemplateID() + expectedTemplateID := ledgertarget.ExecutionStateChanged{}.GetTemplateID() for _, event := range update.GetTransaction().GetEvents() { //nolint:nestif // need to check if all of these are nil if createdEvent := event.GetCreated(); createdEvent != nil { @@ -495,7 +495,7 @@ func (c *Chain) ManuallyExecuteMessage(ctx context.Context, message protocol.Mes // parseExecutionStateChangedEvent parses a common.ExecutionStateChanged event from a Daml CreatedEvent and converts it to cciptestinterfaces.ExecutionStateChangedEvent. func parseExecutionStateChangedEvent(event *apiv2.CreatedEvent) (cciptestinterfaces.ExecutionStateChangedEvent, error) { - executionStateChanged, err := bindings.UnmarshalCreatedEvent[ledgerbind.ExecutionStateChanged](event) + executionStateChanged, err := bindings.UnmarshalCreatedEvent[ledgertarget.ExecutionStateChanged](event) if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("failed to unmarshal ExecutionStateChanged event: %w", err) } @@ -520,13 +520,13 @@ func parseExecutionStateChangedEvent(event *apiv2.CreatedEvent) (cciptestinterfa // Execution state var executionState cciptestinterfaces.MessageExecutionState switch executionStateChanged.Event.State { - case ledgerbind.MessageExecutionStateUNTOUCHED: + case ledgertarget.MessageExecutionStateUNTOUCHED: executionState = cciptestinterfaces.ExecutionStateUntouched - case ledgerbind.MessageExecutionStateIN_PROGRESS: + case ledgertarget.MessageExecutionStateIN_PROGRESS: executionState = cciptestinterfaces.ExecutionStateInProgress - case ledgerbind.MessageExecutionStateSUCCESS: + case ledgertarget.MessageExecutionStateSUCCESS: executionState = cciptestinterfaces.ExecutionStateSuccess - case ledgerbind.MessageExecutionStateFAILURE: + case ledgertarget.MessageExecutionStateFAILURE: executionState = cciptestinterfaces.ExecutionStateFailure default: return cciptestinterfaces.ExecutionStateChangedEvent{}, fmt.Errorf("unknown execution state %q", executionStateChanged.Event.State) @@ -576,7 +576,7 @@ func (c *Chain) findExistingExecutionState( } participant := c.chain.Participants[0] - templateID := contracts.TemplateIDFromBinding(ledgerbind.ExecutionStateChanged{}).ToLedgerIdentifier() + templateID := contracts.TemplateIDFromBinding(ledgertarget.ExecutionStateChanged{}).ToLedgerIdentifier() activeContracts, err := testhelpers.ListActiveContractsByTemplateId(ctx, participant, templateID) if err != nil { return cciptestinterfaces.ExecutionStateChangedEvent{}, false, fmt.Errorf("findExistingExecutionState: list active ExecutionStateChanged contracts: %w", err) @@ -652,18 +652,18 @@ func (c *Chain) fetchVerifierResult(ctx context.Context, messageID protocol.Byte }, nil } -func encodeReceiverFinalityConfig(finality int64) (ledgerbind.FinalityConfig, error) { +func encodeReceiverFinalityConfig(finality int64) (ledgertarget.FinalityConfig, error) { switch { case finality < 0: - return ledgerbind.FinalityConfig{}, fmt.Errorf("invalid finality %d: must be non-negative", finality) + return ledgertarget.FinalityConfig{}, fmt.Errorf("invalid finality %d: must be non-negative", finality) case finality == 0: - return ledgerbind.FinalityConfig{WaitForFinality: &types.UNIT{}}, nil + return ledgertarget.FinalityConfig{WaitForFinality: &types.UNIT{}}, nil case finality == 0x00010000: - return ledgerbind.FinalityConfig{WaitForSafe: &types.UNIT{}}, nil + return ledgertarget.FinalityConfig{WaitForSafe: &types.UNIT{}}, nil case finality > 0xFFFF: - return ledgerbind.FinalityConfig{}, fmt.Errorf("invalid finality %d: max supported block depth is 65535", finality) + return ledgertarget.FinalityConfig{}, fmt.Errorf("invalid finality %d: max supported block depth is 65535", finality) default: - return ledgerbind.FinalityConfig{BlockDepth: new(types.INT64(finality))}, nil + return ledgertarget.FinalityConfig{BlockDepth: new(types.INT64(finality))}, nil } } diff --git a/ccip/devenv/send_op_devenv.go b/ccip/devenv/send_op_devenv.go deleted file mode 100644 index a5bd2fb44..000000000 --- a/ccip/devenv/send_op_devenv.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !prodledger - -package devenv - -import senderop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" - -var ccipSenderSendOperation = senderop.Send diff --git a/ccip/devenv/send_op_prod.go b/ccip/devenv/send_op_prod.go deleted file mode 100644 index b968b9b09..000000000 --- a/ccip/devenv/send_op_prod.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build prodledger - -package devenv - -import ( - ccipsender "github.com/smartcontractkit/chainlink-canton/bindings/generated/v1_0_0/ccip/sender" - "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/sender" - "github.com/smartcontractkit/chainlink-canton/deployment/utils/operations/contract" -) - -var prodSenderEncoder = ccipsender.NewContract("", "CCIP.CCIPSender", "CCIPSender").Encoder() - -var ccipSenderSendOperation = contract.NewExercise(contract.ExerciseParams[ccipsender.Send]{ - Name: "canton/ccip/sender/send", - Version: sender.Version, - Description: "Calls the Send choice on a CCIP Sender contract", - ContractType: sender.ContractType, - Validate: func(input ccipsender.Send) error { - return nil - }, - Template: ccipsender.CCIPSender{}, - Method: ccipsender.CCIPSender{}.Send, - EncodeMethod: prodSenderEncoder.Send, -}) From 59687a57f767d4a4798ae55c61382eb9d520e597 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 9 Jul 2026 00:37:29 -0300 Subject: [PATCH 23/26] fix(load): remove unecessary action params | use better defaults --- .github/actions/ccip-load-test/action.yml | 8 ++--- .github/actions/setup-ccip-devenv/action.yml | 7 +--- .github/workflows/ccip-load-tests.yml | 38 ++++++++------------ ccip/devenv/README.md | 9 +++-- ccip/devenv/tests/load/load_helpers.go | 2 +- go.mod | 2 +- 6 files changed, 23 insertions(+), 43 deletions(-) diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index f76342395..59f2f50c5 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -15,7 +15,7 @@ inputs: message_rate: description: "CANTON_LOAD_MESSAGE_RATE" required: false - default: "1/45s" + default: "1/10s" load_duration: description: "CANTON_LOAD_DURATION" required: false @@ -32,10 +32,6 @@ inputs: description: "Checkout path for chainlink-canton (`.` for PR root, `chainlink-canton` for nested devenv checkout)" required: false default: . - canton_ref: - description: "chainlink-canton git ref for devenv setup (empty = workflow SHA)" - required: false - default: "" skip_exec_confirm: description: "CANTON_LOAD_SKIP_EXEC_CONFIRM" required: false @@ -103,7 +99,6 @@ runs: if: inputs.ccip_env == 'devenv' uses: ./.github/actions/setup-ccip-devenv with: - canton-ref: ${{ inputs.canton_ref }} canton-path: ${{ inputs.canton_path }} ccv-iam-role: ${{ inputs.ccv-iam-role }} jd-registry: ${{ inputs.jd-registry }} @@ -138,6 +133,7 @@ runs: env: CANTON_LOAD_MESSAGE_RATE: ${{ inputs.message_rate }} CANTON_LOAD_DURATION: ${{ inputs.load_duration }} + CANTON_LOAD_SKIP_EXEC_CONFIRM: ${{ inputs.skip_exec_confirm }} run: | case "${{ inputs.direction }}" in canton2evm) TEST_RUN='^TestCanton2EVM_Load$' ;; diff --git a/.github/actions/setup-ccip-devenv/action.yml b/.github/actions/setup-ccip-devenv/action.yml index 092424590..7ff0f953d 100644 --- a/.github/actions/setup-ccip-devenv/action.yml +++ b/.github/actions/setup-ccip-devenv/action.yml @@ -12,11 +12,6 @@ inputs: # that generates their configs. A mismatch across config-format changes (e.g. #1193 # bootstrap monitoring.Config) makes the verifier container exit on startup. default: f56568515e8ad749f71e437342fa170f4ec12a0b - canton-ref: - description: >- - chainlink-canton git ref. Leave empty to use the ref that triggered the workflow. - required: false - default: '' ccv-iam-role: description: AWS IAM role ARN for CCV ECR authentication required: true @@ -58,7 +53,7 @@ runs: with: repository: smartcontractkit/chainlink-canton path: ${{ inputs.canton-path }} - ref: ${{ inputs.canton-ref != '' && inputs.canton-ref || github.sha }} + ref: github.sha - name: Check out chainlink-ccv uses: actions/checkout@v6 diff --git a/.github/workflows/ccip-load-tests.yml b/.github/workflows/ccip-load-tests.yml index e63f88a47..12694a86d 100644 --- a/.github/workflows/ccip-load-tests.yml +++ b/.github/workflows/ccip-load-tests.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: ccip_env: - description: 'Target environment' + description: "Target environment" required: true default: devenv type: choice @@ -12,7 +12,7 @@ on: - devenv - prod-testnet direction: - description: 'Load test direction' + description: "Load test direction" required: true default: canton2evm type: choice @@ -22,39 +22,34 @@ on: - canton2evm-token - evm2canton-token message_rate: - description: 'CANTON_LOAD_MESSAGE_RATE (e.g. 1/1s, 1/45s)' + description: "CANTON_LOAD_MESSAGE_RATE (e.g. 1/45s)" required: false - default: '1/1s' + default: "1/10s" type: string load_duration: - description: 'CANTON_LOAD_DURATION (Go duration, e.g. 90s, 2m)' + description: "CANTON_LOAD_DURATION (e.g. 90s, 2m)" required: false - default: '90s' + default: "90s" type: string test_timeout: - description: 'go test -timeout value (must exceed load duration + teardown)' + description: "go test -timeout value (must exceed load duration + teardown)" required: false - default: '40m' - type: string - canton_ref: - description: 'chainlink-canton git ref (empty = workflow ref; devenv only)' - required: false - default: '' + default: "40m" type: string config_file: - description: 'CCIP_CONFIG_FILE basename under ccip/devenv (prod-testnet only)' + description: "CCIP_CONFIG_FILE basename under ccip/devenv (prod-testnet only)" required: false default: env-prod-testnet.ci.toml type: string skip_exec_confirm: - description: 'CANTON_LOAD_SKIP_EXEC_CONFIRM (prod-testnet only)' + description: "CANTON_LOAD_SKIP_EXEC_CONFIRM" required: false - default: 'true' + default: "false" type: string confirm_exec_timeout: - description: 'CANTON_CONFIRM_EXEC_TIMEOUT (prod-testnet only)' + description: "CANTON_CONFIRM_EXEC_TIMEOUT (prod-testnet only)" required: false - default: '10m' + default: "10m" type: string concurrency: @@ -78,11 +73,7 @@ jobs: run: | if [ "${{ inputs.ccip_env }}" = "prod-testnet" ]; then echo "canton_path=." >> "$GITHUB_OUTPUT" - if [ "${{ inputs.message_rate }}" = "1/1s" ]; then - echo "message_rate=1/45s" >> "$GITHUB_OUTPUT" - else - echo "message_rate=${{ inputs.message_rate }}" >> "$GITHUB_OUTPUT" - fi + echo "message_rate=${{ inputs.message_rate }}" >> "$GITHUB_OUTPUT" if [ "${{ inputs.load_duration }}" = "90s" ]; then echo "load_duration=2m" >> "$GITHUB_OUTPUT" else @@ -120,7 +111,6 @@ jobs: test_timeout: ${{ steps.prod_defaults.outputs.test_timeout }} config_file: ${{ inputs.config_file }} canton_path: ${{ steps.prod_defaults.outputs.canton_path }} - canton_ref: ${{ inputs.canton_ref }} skip_exec_confirm: ${{ inputs.ccip_env == 'prod-testnet' && steps.prod_defaults.outputs.skip_exec_confirm || inputs.skip_exec_confirm }} confirm_exec_timeout: ${{ inputs.confirm_exec_timeout }} CANTON_OKTA_AUTHORIZER_TESTNET: ${{ secrets.CANTON_OKTA_AUTHORIZER_TESTNET }} diff --git a/ccip/devenv/README.md b/ccip/devenv/README.md index 42a884b67..92a25692f 100644 --- a/ccip/devenv/README.md +++ b/ccip/devenv/README.md @@ -92,11 +92,11 @@ Sequential Canton→EVM messages round-robined across every EVM destination in t **Devenv** (requires `make start-devenv` so `ccip/devenv/env-canton-evm-out.toml` exists): pre-mints fee holdings and calls `SetupSend` once before WASP starts. Full send + EVM exec confirmation per message. -Schedule is configured via env vars (defaults are `1/1s` for 90s): +Schedule is configured via env vars (defaults are `1/10s` for 90s): | Env var | Form | Default | Meaning | |---|---|---|---| -| `CANTON_LOAD_MESSAGE_RATE` | `/` (e.g. `1/1s`, `1/20s`, `10/5m`) | `1/1s` | rate per rate-limit window | +| `CANTON_LOAD_MESSAGE_RATE` | `/` (e.g. `1/10s`, `1/20s`, `10/5m`) | `1/10s` | rate per rate-limit window | | `CANTON_LOAD_DURATION` | Go duration (e.g. `90s`, `10m`, `1h`) | `90s` | total runtime | Example — 1 message every 20 seconds for 10 minutes: @@ -406,13 +406,12 @@ Load tests use the composite action (`.github/actions/ccip-load-test`) from **CC |---|---|---|---| | `ccip_env` | `devenv` | select `prod-testnet` | `-ccip-env` | | `direction` | `canton2evm` | same | test `-run` regex | -| `message_rate` | `1/1s` | `1/45s` (when left at devenv default) | `CANTON_LOAD_MESSAGE_RATE` | +| `message_rate` | `1/10s` | `1/10s` | `CANTON_LOAD_MESSAGE_RATE` | | `load_duration` | `90s` | `2m` (when left at devenv default) | `CANTON_LOAD_DURATION` | | `test_timeout` | `40m` | `30m` / `45m` for evm2canton or evm2canton-token | `go test -timeout` | | `config_file` | — | `env-prod-testnet.ci.toml` | `CCIP_CONFIG_FILE` | -| `skip_exec_confirm` | — | `true` for canton2evm only; `false` for token + evm2canton | `CANTON_LOAD_SKIP_EXEC_CONFIRM` | +| `skip_exec_confirm` | `false` | `true` for canton2evm only; `false` for token + evm2canton | `CANTON_LOAD_SKIP_EXEC_CONFIRM` | | `confirm_exec_timeout` | — | `10m` | `CANTON_CONFIRM_EXEC_TIMEOUT` | -| `canton_ref` | workflow ref | devenv only | chainlink-canton checkout | **Devenv** spins up Docker via `setup-ccip-devenv` (same as CCIP E2E). **Prod-testnet** hits live Canton TestNet + Sepolia with no local devenv. diff --git a/ccip/devenv/tests/load/load_helpers.go b/ccip/devenv/tests/load/load_helpers.go index bcba15092..b60aaae90 100644 --- a/ccip/devenv/tests/load/load_helpers.go +++ b/ccip/devenv/tests/load/load_helpers.go @@ -32,7 +32,7 @@ const ( envLoadDuration = "CANTON_LOAD_DURATION" envLoadSkipExecConfirm = "CANTON_LOAD_SKIP_EXEC_CONFIRM" envLoadCallTimeout = "CANTON_LOAD_CALL_TIMEOUT" - defaultMessageRate = "1/1s" + defaultMessageRate = "1/10s" defaultLoadDuration = 90 * time.Second defaultLoadCallPadding = 2 * time.Minute defaultSendOnlyCallBudget = 5 * time.Minute diff --git a/go.mod b/go.mod index 5026e49f0..4de71c785 100644 --- a/go.mod +++ b/go.mod @@ -435,7 +435,7 @@ require ( github.com/smartcontractkit/chainlink-common/keystore v1.0.2 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260427132147-1ef18876ae9b // indirect - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd // indirect + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect From 968e3e266de8d22b10235b1e4c9e572e21d92c08 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 9 Jul 2026 14:57:38 -0300 Subject: [PATCH 24/26] fix action --- .github/actions/setup-ccip-devenv/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-ccip-devenv/action.yml b/.github/actions/setup-ccip-devenv/action.yml index 7ff0f953d..fe15a300e 100644 --- a/.github/actions/setup-ccip-devenv/action.yml +++ b/.github/actions/setup-ccip-devenv/action.yml @@ -36,7 +36,7 @@ inputs: free-disk-space: description: Run the free-disk-space action before builds required: false - default: 'true' + default: "true" runs: using: composite @@ -53,7 +53,7 @@ runs: with: repository: smartcontractkit/chainlink-canton path: ${{ inputs.canton-path }} - ref: github.sha + ref: ${{ github.sha }} - name: Check out chainlink-ccv uses: actions/checkout@v6 From 1a5bdb44193744b9a02546e38c803275367c8252 Mon Sep 17 00:00:00 2001 From: Rodrigo Soares <38868277+rodrigombsoares@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:56:28 -0300 Subject: [PATCH 25/26] Potential fix for pull request finding 'CodeQL / Code injection' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/actions/ccip-load-test/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/ccip-load-test/action.yml b/.github/actions/ccip-load-test/action.yml index a8fbe0acc..d8a2b20c8 100644 --- a/.github/actions/ccip-load-test/action.yml +++ b/.github/actions/ccip-load-test/action.yml @@ -167,6 +167,7 @@ runs: CANTON_CONFIRM_EXEC_TIMEOUT: ${{ inputs.confirm_exec_timeout }} CANTON_LOAD_SKIP_EXEC_CONFIRM: ${{ inputs.skip_exec_confirm }} PRIVATE_KEY: ${{ inputs.CCIP_PROD_TESTNET_PRIVATE_KEY }} + TEST_TIMEOUT: ${{ inputs.test_timeout }} run: | case "${{ inputs.direction }}" in canton2evm) TEST_RUN='^TestCanton2EVM_Load$' ;; @@ -175,7 +176,7 @@ runs: evm2canton-token) TEST_RUN='^TestEVM2Canton_TokenLoad$' ;; *) echo "unknown direction: ${{ inputs.direction }}" >&2; exit 1 ;; esac - go test -tags=prodledger -timeout "${{ inputs.test_timeout }}" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" + go test -tags=prodledger -timeout "$TEST_TIMEOUT" -v -count 1 -ccip-env=prod-testnet -run "$TEST_RUN" - name: Upload devenv logs if: always() && inputs.ccip_env == 'devenv' From 11f349cb1d34d3c1c0b78d59f64bd304336d9d6a Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 16 Jul 2026 18:00:46 -0300 Subject: [PATCH 26/26] fix merge issue --- .github/actions/setup-ccip-devenv/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-ccip-devenv/action.yml b/.github/actions/setup-ccip-devenv/action.yml index fe15a300e..4e4d9faec 100644 --- a/.github/actions/setup-ccip-devenv/action.yml +++ b/.github/actions/setup-ccip-devenv/action.yml @@ -11,7 +11,7 @@ inputs: # builds the verifier/devenv Docker images, while go.mod pins the `ccv up` tooling # that generates their configs. A mismatch across config-format changes (e.g. #1193 # bootstrap monitoring.Config) makes the verifier container exit on startup. - default: f56568515e8ad749f71e437342fa170f4ec12a0b + default: f20ea93af3005f17859ca1d7e6529e0121a0430c ccv-iam-role: description: AWS IAM role ARN for CCV ECR authentication required: true