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/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..ed2a2b019 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,47 @@ 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. +// 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) + } + + 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..190a50ac3 --- /dev/null +++ b/ccip/devenv/tests/env_test.go @@ -0,0 +1,36 @@ +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, "env-canton-evm-out.toml", ResolveConfigPath(EnvDevenv)) + require.Equal(t, "custom.toml", ResolveConfigPath(EnvProdTestnet)) +} + +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..0bc1f2b2b 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,60 @@ 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) + boot.SetupCantonTokenSend(t, ctx, lane, sequentialSendsForLoad(sched)) - 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..e404a3329 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,25 @@ 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 + + // 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 { @@ -76,7 +79,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 +99,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 +172,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 +198,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 +217,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,29 +251,39 @@ 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 } +// 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 { @@ -254,36 +297,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 +330,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 +344,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 +386,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 +405,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 +467,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, - ) - } -}