diff --git a/deployment/ccip/changeset/testhelpers/test_assertions.go b/deployment/ccip/changeset/testhelpers/test_assertions.go index 647ceec4ea2..446bee0d126 100644 --- a/deployment/ccip/changeset/testhelpers/test_assertions.go +++ b/deployment/ccip/changeset/testhelpers/test_assertions.go @@ -558,6 +558,9 @@ func ConfirmCommitWithExpectedSeqNumRangeSol( for { select { + case <-time.After(10 * time.Second): + t.Logf("Waiting for CommitReportAccepted event on chain selector %d from source selector %d expected seq nr range %s", + dest.Selector, srcSelector, expectedSeqNumRange.String()) case commitEvent := <-sink: // if merkle root is zero, it only contains price updates if commitEvent.Report == nil { @@ -774,6 +777,9 @@ func ConfirmExecWithSeqNrsSol( for { select { + case <-time.After(10 * time.Second): + t.Logf("Waiting for ExecutionStateChanged event on chain %d (offramp %s) from chain %d with expected sequence numbers %+v", + dest.Selector, offrampAddress.String(), srcSelector, expectedSeqNrs) case execEvent := <-sink: // TODO: share with EVM _, found := seqNrsToWatch[execEvent.SequenceNumber] diff --git a/integration-tests/smoke/ccip/canonical/adapters/evm.go b/integration-tests/smoke/ccip/canonical/adapters/evm.go new file mode 100644 index 00000000000..09f619a79b8 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/adapters/evm.go @@ -0,0 +1,129 @@ +package adapters + +import ( + "context" + crand "crypto/rand" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + "github.com/ethereum/go-ethereum/common" + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + "github.com/smartcontractkit/chainlink/deployment/ccip/shared/stateview/evm" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/types" +) + +var _ types.Adapter = &evmAdapter{} + +type evmAdapter struct { + chain cldf.Chain + chainState evm.CCIPChainState +} + +// ChainSelector implements types.Adapter. +func (a *evmAdapter) ChainSelector() uint64 { + return a.chain.Selector +} + +// GetExtraArgs implements types.Adapter. +// TODO: apply opts. +func (a *evmAdapter) GetExtraArgs(_ []byte, sourceFamily string, opts ...types.ExtraArgOpt) ([]byte, error) { + switch sourceFamily { + case chain_selectors.FamilyEVM: + return []byte{}, nil // default extra args are empty for EVM + case chain_selectors.FamilySolana: + return []byte{}, nil // default extra args are empty for Solana + default: + return nil, fmt.Errorf("unsupported source family: %s", sourceFamily) + } +} + +// CCIPReceiver implements types.Adapter. +func (a *evmAdapter) CCIPReceiver() []byte { + return common.LeftPadBytes(a.chainState.Receiver.Address().Bytes(), 32) +} + +// ValidateCommit implements types.Adapter. +func (a *evmAdapter) ValidateCommit( + t *testing.T, + sourceSelector uint64, + seqNumRange ccipocr3.SeqNumRange) { + testhelpers.ConfirmCommitWithExpectedSeqNumRange( + t, + sourceSelector, + a.chain, + a.chainState.OffRamp, + nil, + seqNumRange, + false, + ) +} + +// ValidateExec implements types.Adapter. +func (a *evmAdapter) ValidateExec( + t *testing.T, + sourceSelector uint64, + seqNrs []uint64) { + testhelpers.ConfirmExecWithSeqNrs( + t, + sourceSelector, + a.chain, + a.chainState.OffRamp, + nil, + seqNrs, + ) +} + +// BuildMessage implements types.Adapter. +func (a *evmAdapter) BuildMessage(components types.MessageComponents) (any, error) { + var tokenAmounts []router.ClientEVMTokenAmount + for _, tokenAmount := range components.TokenAmounts { + tokenAmounts = append(tokenAmounts, router.ClientEVMTokenAmount{ + Token: common.HexToAddress(tokenAmount.Token), + Amount: tokenAmount.Amount, + }) + } + + msg := router.ClientEVM2AnyMessage{ + Receiver: components.Receiver, + Data: components.Data, + TokenAmounts: tokenAmounts, + FeeToken: common.HexToAddress(components.FeeToken), + ExtraArgs: components.ExtraArgs, + } + + return msg, nil +} + +// NativeFeeToken implements types.Adapter. +func (a *evmAdapter) NativeFeeToken() string { + return common.HexToAddress("0x0").Hex() +} + +// RandomReceiver implements types.Adapter. +func (a *evmAdapter) RandomReceiver() []byte { + b := make([]byte, 20) + _, _ = crand.Read(b) // Assignment for errcheck. Only used in tests so we can ignore. + // return a random address as a left-padded 32 byte array + addr := common.LeftPadBytes(b, 32) + + return addr +} + +func NewEVMAdapter(chain cldf.Chain, chainState evm.CCIPChainState) types.Adapter { + return &evmAdapter{chain: chain, chainState: chainState} +} + +func (a *evmAdapter) ChainFamily() string { + return chain_selectors.FamilyEVM +} + +func (a *evmAdapter) GetInboundNonce(ctx context.Context, sender []byte, srcSel uint64) (uint64, error) { + return a.chainState.NonceManager.GetInboundNonce(&bind.CallOpts{ + Context: ctx, + }, srcSel, sender) +} diff --git a/integration-tests/smoke/ccip/canonical/adapters/svm.go b/integration-tests/smoke/ccip/canonical/adapters/svm.go new file mode 100644 index 00000000000..0095d151548 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/adapters/svm.go @@ -0,0 +1,176 @@ +package adapters + +import ( + "context" + crand "crypto/rand" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/message_hasher" + solconfig "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + solccip "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip" + solcommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + solanastate "github.com/smartcontractkit/chainlink/deployment/ccip/shared/stateview/solana" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" +) + +var _ types.Adapter = &svmAdapter{} + +type svmAdapter struct { + chain cldf.SolChain + chainState solanastate.CCIPChainState +} + +// ChainSelector implements types.Adapter. +func (s *svmAdapter) ChainSelector() uint64 { + return s.chain.Selector +} + +// GetExtraArgs implements types.Adapter. +// TODO: apply opts. +func (s *svmAdapter) GetExtraArgs(receiver []byte, sourceFamily string, opts ...types.ExtraArgOpt) ([]byte, error) { + receiverProgram := solana.PublicKeyFromBytes(receiver) + receiverTargetAccountPDA, _, _ := solana.FindProgramAddress([][]byte{[]byte("counter")}, receiverProgram) + receiverExternalExecutionConfigPDA, _, _ := solana.FindProgramAddress([][]byte{[]byte("external_execution_config")}, receiverProgram) + + accounts := [][32]byte{ + receiverExternalExecutionConfigPDA, + receiverTargetAccountPDA, + solana.SystemProgramID, + } + + switch sourceFamily { + case chain_selectors.FamilyEVM: + extraArgs, err := ccipevm.SerializeClientSVMExtraArgsV1(message_hasher.ClientSVMExtraArgsV1{ + AccountIsWritableBitmap: solccip.GenerateBitMapForIndexes([]int{0, 1}), + Accounts: accounts, + ComputeUnits: 80_000, + }) + if err != nil { + return nil, fmt.Errorf("failed to serialize extra args: %w", err) + } + return extraArgs, nil + default: + // TODO: add support for other families + return nil, fmt.Errorf("unsupported source family: %s", sourceFamily) + } +} + +// CCIPReceiver implements types.Adapter. +func (s *svmAdapter) CCIPReceiver() []byte { + return s.chainState.Receiver.Bytes() +} + +func NewSVMAdapter(chain cldf.SolChain, chainState solanastate.CCIPChainState) types.Adapter { + return &svmAdapter{ + chain: chain, + chainState: chainState, + } +} + +// BuildMessage implements types.Adapter. +func (s *svmAdapter) BuildMessage(components types.MessageComponents) (any, error) { + feeToken := solana.PublicKey{} + if len(components.FeeToken) > 0 { + var err error + feeToken, err = solana.PublicKeyFromBase58(components.FeeToken) + if err != nil { + return nil, fmt.Errorf("invalid format for fee token: %w", err) + } + } + + var tokenAmounts []ccip_router.SVMTokenAmount + if len(components.TokenAmounts) > 0 { + tokenAmounts = make([]ccip_router.SVMTokenAmount, len(components.TokenAmounts)) + for i, amount := range components.TokenAmounts { + token, err := solana.PublicKeyFromBase58(amount.Token) + if err != nil { + return nil, fmt.Errorf("invalid format for token: %w", err) + } + tokenAmounts[i] = ccip_router.SVMTokenAmount{ + Token: token, + Amount: amount.Amount.Uint64(), + } + } + } + + msg := ccip_router.SVM2AnyMessage{ + Receiver: components.Receiver, + TokenAmounts: tokenAmounts, + Data: components.Data, + FeeToken: feeToken, + ExtraArgs: components.ExtraArgs, + } + + return msg, nil +} + +// ChainFamily implements types.Adapter. +func (s *svmAdapter) ChainFamily() string { + return chain_selectors.FamilySolana +} + +// GetInboundNonce implements types.Adapter. +func (s *svmAdapter) GetInboundNonce(ctx context.Context, sender []byte, srcSel uint64) (uint64, error) { + client := s.chain.Client + // TODO: solcommon.FindNoncePDA expected the sender to be a solana pubkey + chainSelectorLE := solcommon.Uint64ToLE(s.chain.Selector) + noncePDA, _, err := solana.FindProgramAddress([][]byte{[]byte("nonce"), chainSelectorLE, sender}, s.chainState.Router) + if err != nil { + return 0, fmt.Errorf("failed to find nonce PDA: %w", err) + } + var nonceCounterAccount ccip_router.Nonce + // we ignore the error because the account might not exist yet + _ = solcommon.GetAccountDataBorshInto(ctx, client, noncePDA, solconfig.DefaultCommitment, &nonceCounterAccount) + latestNonce := nonceCounterAccount.Counter + return latestNonce, nil +} + +// NativeFeeToken implements types.Adapter. +func (s *svmAdapter) NativeFeeToken() string { + return solana.PublicKey{}.String() +} + +// RandomReceiver implements types.Adapter. +func (s *svmAdapter) RandomReceiver() []byte { + b := make([]byte, 20) + _, _ = crand.Read(b) // Assignment for errcheck. Only used in tests so we can ignore. + // return a random address as a left-padded 32 byte array + addr := common.LeftPadBytes(b, 32) + + return addr +} + +// ValidateCommit implements types.Adapter. +func (s *svmAdapter) ValidateCommit(t *testing.T, sourceSelector uint64, seqNumRange ccipocr3.SeqNumRange) { + testhelpers.ConfirmCommitWithExpectedSeqNumRangeSol( + t, + sourceSelector, + s.chain, + s.chainState.OffRamp, + 0, // startSlot + seqNumRange, + true, + ) +} + +// ValidateExec implements types.Adapter. +func (s *svmAdapter) ValidateExec(t *testing.T, sourceSelector uint64, seqNrs []uint64) { + testhelpers.ConfirmExecWithSeqNrsSol( + t, + sourceSelector, + s.chain, + s.chainState.OffRamp, + 0, // startSlot + seqNrs, + ) +} diff --git a/integration-tests/smoke/ccip/canonical/execctxs/twochains.go b/integration-tests/smoke/ccip/canonical/execctxs/twochains.go new file mode 100644 index 00000000000..4f7ffc0d0d9 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/execctxs/twochains.go @@ -0,0 +1,151 @@ +package execctxs + +import ( + "maps" + "slices" + "testing" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + "github.com/smartcontractkit/chainlink/deployment/ccip/shared/stateview" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/adapters" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/types" + testsetups "github.com/smartcontractkit/chainlink/integration-tests/testsetups/ccip" + "github.com/stretchr/testify/require" +) + +func AllOneToOneExecContextNames() []string { + return []string{ + "evm_2_evm", + "evm_2_solana", + "solana_2_evm", + } +} + +// AllOneToOneExecContexts returns a list of all one-source-to-one-dest execution contexts for testing. +func AllOneToOneExecContexts(t *testing.T) []types.ExecContext { + return []types.ExecContext{ + NewEVM2EVMCtx(t), + NewEVM2SolanaCtx(t), + NewSolana2EVM(t), + } +} + +var _ types.ExecContext = &execCtx{} + +type execCtx struct { + name string + env cldf.Environment + state stateview.CCIPOnChainState + sources []types.Adapter + dest types.Adapter +} + +func (e *execCtx) Name() string { + return e.name +} + +// ReplayLogs implements types.ExecContext. +func (e *execCtx) ReplayLogs(t *testing.T, selectorToBlockMap map[uint64]uint64) { + testhelpers.ReplayLogs(t, e.env.Offchain, selectorToBlockMap) +} + +// Env implements types.ExecContext. +func (e *execCtx) Env() cldf.Environment { + return e.env +} + +// OnchainState implements types.ExecContext. +func (e *execCtx) OnchainState() stateview.CCIPOnChainState { + return e.state +} + +func (e *execCtx) Sources() []types.Adapter { + return e.sources +} + +func (e *execCtx) Dest() types.Adapter { + return e.dest +} + +func NewEVM2EVMCtx(t *testing.T) types.ExecContext { + e, _, _ := testsetups.NewIntegrationEnvironment(t) + + state, err := stateview.LoadOnchainState(e.Env) + require.NoError(t, err) + + allChainSelectors := slices.Collect(maps.Keys(e.Env.Chains)) + require.Len(t, allChainSelectors, 2) + sourceChain := allChainSelectors[0] + destChain := allChainSelectors[1] + + // connect a single lane, source to dest + testhelpers.AddLaneWithDefaultPricesAndFeeQuoterConfig(t, &e, state, sourceChain, destChain, false) + + return &execCtx{ + name: "evm_2_evm", + env: e.Env, + state: state, + sources: []types.Adapter{ + adapters.NewEVMAdapter(e.Env.Chains[sourceChain], state.Chains[sourceChain]), + }, + dest: adapters.NewEVMAdapter(e.Env.Chains[destChain], state.Chains[destChain]), + } +} + +func NewEVM2SolanaCtx(t *testing.T) types.ExecContext { + e, _, _ := testsetups.NewIntegrationEnvironment(t, testhelpers.WithSolChains(1)) + + // TODO: do this as part of setup + t.Logf("deploying solana ccip receiver") + testhelpers.DeploySolanaCcipReceiver(t, e.Env) + + state, err := stateview.LoadOnchainState(e.Env) + require.NoError(t, err) + + allChainSelectors := slices.Collect(maps.Keys(e.Env.Chains)) + allSolChainSelectors := slices.Collect(maps.Keys(e.Env.SolChains)) + sourceChain := allChainSelectors[0] + destChain := allSolChainSelectors[0] + + t.Logf("sourceChain: %d, destChain: %d", sourceChain, destChain) + + // connect a single lane, source to dest + testhelpers.AddLaneWithDefaultPricesAndFeeQuoterConfig(t, &e, state, sourceChain, destChain, false) + + return &execCtx{ + name: "evm_2_solana", + env: e.Env, + state: state, + sources: []types.Adapter{ + adapters.NewEVMAdapter(e.Env.Chains[sourceChain], state.Chains[sourceChain]), + }, + dest: adapters.NewSVMAdapter(e.Env.SolChains[destChain], state.SolChains[destChain]), + } +} + +func NewSolana2EVM(t *testing.T) types.ExecContext { + e, _, _ := testsetups.NewIntegrationEnvironment(t, testhelpers.WithSolChains(1)) + + state, err := stateview.LoadOnchainState(e.Env) + require.NoError(t, err) + + allChainSelectors := slices.Collect(maps.Keys(e.Env.Chains)) + allSolChainSelectors := slices.Collect(maps.Keys(e.Env.SolChains)) + require.Len(t, allChainSelectors, 2) + sourceChain := allSolChainSelectors[0] + destChain := allChainSelectors[1] + + // connect a single lane, source to dest + testhelpers.AddLaneWithDefaultPricesAndFeeQuoterConfig(t, &e, state, sourceChain, destChain, false) + + return &execCtx{ + name: "solana_2_evm", + env: e.Env, + state: state, + sources: []types.Adapter{ + adapters.NewSVMAdapter(e.Env.SolChains[sourceChain], state.SolChains[sourceChain]), + }, + dest: adapters.NewEVMAdapter(e.Env.Chains[destChain], state.Chains[destChain]), + } +} diff --git a/integration-tests/smoke/ccip/canonical/messaging_test.go b/integration-tests/smoke/ccip/canonical/messaging_test.go new file mode 100644 index 00000000000..9f4b7b3cad7 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/messaging_test.go @@ -0,0 +1,63 @@ +package canonical + +import ( + "fmt" + "testing" + + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/execctxs" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/scenarios" +) + +func Test_MessagingToEOA_EVM2EVM(t *testing.T) { + // the “where” + exec := execctxs.NewEVM2EVMCtx(t) + // the “what” + scenario := scenarios.NewMessagingToEOAScenario(t, scenarios.ValidationTypeExec) + + scenario.Run(t, exec) +} + +func Test_MessagingToReceiver_EVM2Solana(t *testing.T) { + exec := execctxs.NewEVM2SolanaCtx(t) + scenario := scenarios.NewMessagingToCCIPReceiverScenario(t, scenarios.ValidationTypeExec) + + scenario.Run(t, exec) +} + +func Test_MessagingToReceiver_Solana2EVM(t *testing.T) { + exec := execctxs.NewSolana2EVM(t) + scenario := scenarios.NewMessagingToCCIPReceiverScenario(t, scenarios.ValidationTypeExec) + + scenario.Run(t, exec) +} + +func Test_MessagingToReceiver_Matrix(t *testing.T) { + execContexts := execctxs.AllOneToOneExecContexts(t) + for _, execCtx := range execContexts { + t.Run(fmt.Sprintf("messaging_%s", execCtx.Name()), func(t *testing.T) { + scenario := scenarios.NewMessagingToCCIPReceiverScenario(t, scenarios.ValidationTypeExec) + scenario.Run(t, execCtx) + }) + } +} + +func Test_MessagingToReceiver_Matrix_ListTests(t *testing.T) { + execCtxNames := execctxs.AllOneToOneExecContextNames() + for _, execCtx := range execCtxNames { + t.Run(fmt.Sprintf("messaging_%s", execCtx), func(t *testing.T) { + // do nothing, just to list the tests. + }) + } +} + +func Test_AllMessagingTests_ListTests(t *testing.T) { + scenarioNames := scenarios.AllMessagingScenariosNames() + execCtxNames := execctxs.AllOneToOneExecContextNames() + for _, execCtxName := range execCtxNames { + for _, scenarioName := range scenarioNames { + t.Run(fmt.Sprintf("%s_%s", scenarioName, execCtxName), func(t *testing.T) { + // do nothing, just to list the tests. + }) + } + } +} diff --git a/integration-tests/smoke/ccip/canonical/scenarios/messaging.go b/integration-tests/smoke/ccip/canonical/scenarios/messaging.go new file mode 100644 index 00000000000..9701b873644 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/scenarios/messaging.go @@ -0,0 +1,194 @@ +package scenarios + +import ( + "testing" + "time" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + "github.com/smartcontractkit/chainlink/integration-tests/smoke/ccip/canonical/types" + "github.com/stretchr/testify/require" +) + +func AllMessagingScenariosNames() []string { + return []string{ + "messaging_to_ccip_receiver", + "messaging_to_eoa", + } +} + +func AllMessagingScenarios(t *testing.T) []types.Scenario { + return []types.Scenario{ + NewMessagingToCCIPReceiverScenario(t, ValidationTypeExec), + NewMessagingToEOAScenario(t, ValidationTypeExec), + } +} + +// ValidationType is the type of validation to perform in the scenario. +type ValidationType int + +const ( + ValidationTypeNone ValidationType = iota + ValidationTypeCommitOnly + ValidationTypeExec +) + +func NewMessagingToCCIPReceiverScenario(t *testing.T, validationType ValidationType) types.Scenario { + return &messagingToCCIPReceiverScenario{ + name: "messaging_to_ccip_receiver", + validationType: validationType, + } +} + +type messagingToCCIPReceiverScenario struct { + name string + validationType ValidationType +} + +func (s *messagingToCCIPReceiverScenario) Name() string { + return s.name +} + +func (s *messagingToCCIPReceiverScenario) Run(t *testing.T, ctx types.ExecContext) { + // sanity checks + require.GreaterOrEqual(t, len(ctx.Sources()), 1) + require.NotNil(t, ctx.Dest()) + + // determine the source and chain families so we can appropriately build the message + // note that we select the first source, this scenario doesn't support multiple sources + // anyways. + source := ctx.Sources()[0] + dest := ctx.Dest() + + receiver := dest.CCIPReceiver() + nativeFeeToken := source.NativeFeeToken() + + extraArgs, err := dest.GetExtraArgs(receiver, source.ChainFamily()) + require.NoError(t, err) + + components := types.MessageComponents{ + Receiver: receiver, + Data: []byte("hello ccip receiver testing scenario"), + FeeToken: nativeFeeToken, + ExtraArgs: extraArgs, + TokenAmounts: []types.TokenAmount{}, + } + + t.Logf("receiver length: %d, receiver: %x", len(receiver), receiver) + + msg, err := source.BuildMessage(components) + require.NoError(t, err) + + t.Logf("source: %d, dest: %d", source.ChainSelector(), dest.ChainSelector()) + + // send the message on the source chain to the destination chain. + sendEvent := testhelpers.TestSendRequest( + t, + ctx.Env(), + ctx.OnchainState(), + source.ChainSelector(), + dest.ChainSelector(), + false, + msg, + ) + + time.Sleep(30 * time.Second) + ctx.ReplayLogs(t, map[uint64]uint64{ + source.ChainSelector(): 0, + }) + + switch s.validationType { + case ValidationTypeNone: + return + case ValidationTypeCommitOnly: + dest.ValidateCommit(t, source.ChainSelector(), ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(sendEvent.SequenceNumber), + ccipocr3.SeqNum(sendEvent.SequenceNumber), + }) + case ValidationTypeExec: + dest.ValidateCommit(t, source.ChainSelector(), ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(sendEvent.SequenceNumber), + ccipocr3.SeqNum(sendEvent.SequenceNumber), + }) + dest.ValidateExec(t, source.ChainSelector(), []uint64{sendEvent.SequenceNumber}) + } +} + +func NewMessagingToEOAScenario(t *testing.T, validationType ValidationType) types.Scenario { + return &messagingToEOAScenario{ + name: "messaging_to_eoa", + validationType: validationType, + } +} + +type messagingToEOAScenario struct { + name string + validationType ValidationType +} + +func (s *messagingToEOAScenario) Name() string { + return s.name +} + +func (s *messagingToEOAScenario) Run(t *testing.T, ctx types.ExecContext) { + // sanity checks + require.GreaterOrEqual(t, len(ctx.Sources()), 1) + require.NotNil(t, ctx.Dest()) + // Solana doesn't support sending messages to EOAs + require.NotEqual(t, chain_selectors.FamilySolana, ctx.Dest().ChainFamily()) + // Aptos doesn't support sending messages to EOAs + require.NotEqual(t, chain_selectors.FamilyAptos, ctx.Dest().ChainFamily()) + + // determine the source and chain families so we can appropriately build the message + // note that we select the first source, this scenario doesn't support multiple sources + // anyways. + source := ctx.Sources()[0] + dest := ctx.Dest() + + receiver := dest.RandomReceiver() + nativeFeeToken := source.NativeFeeToken() + + components := types.MessageComponents{ + Receiver: receiver, + Data: []byte("hello eoa testing scenario"), + FeeToken: nativeFeeToken, + ExtraArgs: []byte{}, + TokenAmounts: []types.TokenAmount{}, + } + + msg, err := source.BuildMessage(components) + require.NoError(t, err) + + // send the message on the source chain to the destination chain. + sendEvent := testhelpers.TestSendRequest( + t, + ctx.Env(), + ctx.OnchainState(), + source.ChainSelector(), + dest.ChainSelector(), + false, + msg, + ) + + time.Sleep(30 * time.Second) + ctx.ReplayLogs(t, map[uint64]uint64{ + source.ChainSelector(): 0, + }) + + switch s.validationType { + case ValidationTypeNone: + return + case ValidationTypeCommitOnly: + dest.ValidateCommit(t, source.ChainSelector(), ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(sendEvent.SequenceNumber), + ccipocr3.SeqNum(sendEvent.SequenceNumber), + }) + case ValidationTypeExec: + dest.ValidateCommit(t, source.ChainSelector(), ccipocr3.SeqNumRange{ + ccipocr3.SeqNum(sendEvent.SequenceNumber), + ccipocr3.SeqNum(sendEvent.SequenceNumber), + }) + dest.ValidateExec(t, source.ChainSelector(), []uint64{sendEvent.SequenceNumber}) + } +} diff --git a/integration-tests/smoke/ccip/canonical/types/types.go b/integration-tests/smoke/ccip/canonical/types/types.go new file mode 100644 index 00000000000..486b1747df6 --- /dev/null +++ b/integration-tests/smoke/ccip/canonical/types/types.go @@ -0,0 +1,137 @@ +package types + +import ( + "context" + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/shared/stateview" +) + +type TokenAmount struct { + // Will be encoded in the source-native format, so EIP-55 for Ethereum, + // base58 for Solana, etc. + Token string + Amount *big.Int +} + +// MessageComponents is a struct that contains the makeup for a general CCIP message +// irrespective of the chain family it originates from. +type MessageComponents struct { + // Receiver is the receiver on the destination chain. + // Must be appropriately dest-chain-family encoded, so abi.encode for Ethereum, + // 32 bytes for Solana, etc. + Receiver []byte + // Data is the data to be sent to the destination chain. + Data []byte + // Will be encoded in the source-native format, so EIP-55 for Ethereum, + // base58 for Solana, etc. + FeeToken string + // ExtraArgs are the message extra args which tune message semantics and behavior. + // For example, out of order execution can be specified here. + ExtraArgs []byte + // TokenAmounts are the tokens and their respective amounts to be sent to the + // destination chain. + // Note that the tokens must be "approved" to the router for the message send to work. + TokenAmounts []TokenAmount +} + +// ExtraArgOpt is a generic representation of an extra arg that can be applied +// to any kind of ccip message. +// We use this to make it possible to specify extra args in a chain-agnostic way. +type ExtraArgOpt struct { + Name string + Value any +} + +func NewOutOfOrderExtraArg(outOfOrder bool) ExtraArgOpt { + return ExtraArgOpt{ + Name: "outOfOrderExecutionEnabled", + Value: outOfOrder, + } +} + +func NewGasLimitExtraArg(gasLimit *big.Int) ExtraArgOpt { + return ExtraArgOpt{ + Name: "gasLimit|computeUnits", + Value: gasLimit, + } +} + +// Adapter is our interface for interacting with a specific chain, scoped to a family. +// An adapter instance is an instance of a concrete chain. +// So if there are e.g 3 source chains that are EVM and a dest that is Solana, +// we would have 3 EVM adapters and 1 Solana adapter. +type Adapter interface { + // ChainSelector returns the selector of the chain for the given adapter. + ChainSelector() uint64 + + // ChainFamily returns the family of the chain for the given adapter. + ChainFamily() string + + // BuildMessage builds a message from the given components, + // with the overall message type being ChainFamily2Any, where + // ChainFamily is the family of the adapter. + // As a concrete example, for EVM, the message type is router.ClientEVM2AnyMessage, + // and for Solana, the message type is ccip_router.SVM2AnyMessage. + BuildMessage(components MessageComponents) (any, error) + + // RandomReceiver returns a random receiver for the given chain family. + RandomReceiver() []byte + + // CCIPReceiver returns a CCIP receiver for the given chain family. + CCIPReceiver() []byte + + // NativeFeeToken returns the native fee token for the given chain family. + NativeFeeToken() string + + // GetExtraArgs returns the default extra args for sending messages to this + // chain family from the given source family. + // Therefore the extra args are source-family encoded, so abi.encode for EVM, + // borsch for Solana, etc. + GetExtraArgs(receiver []byte, sourceFamily string, opts ...ExtraArgOpt) ([]byte, error) + + // GetInboundNonce returns the inbound nonce for the given sender and source chain selector. + // For chains that don't have the concept of nonces, this will always return 0. + GetInboundNonce(ctx context.Context, sender []byte, srcSel uint64) (uint64, error) + + // ValidateCommit validates that the message specified by the given send event was committed. + ValidateCommit(t *testing.T, sourceSelector uint64, seqNumRange ccipocr3.SeqNumRange) + + // ValidateExec validates that the message specified by the given send event was executed. + ValidateExec(t *testing.T, sourceSelector uint64, seqNrs []uint64) +} + +// Scenario is our interface for running a specific test case. +// Scenarios include logic to trigger a test case and also validate the outcome. +type Scenario interface { + // Name is a human-readable name of this test scenario. + Name() string + // Run executes the scenario in the given context. + Run(t *testing.T, ctx ExecContext) +} + +// ExecContext is our interface for representing the execution context of a test. +// All scenarios must be executed within the context of an ExecContext. +type ExecContext interface { + // Name is a human-readable name of this exec context. + Name() string + + // Env returns the environment in which the test is being executed. + Env() cldf.Environment + + // OnchainState returns the onchain state of the environment. + OnchainState() stateview.CCIPOnChainState + + // Sources returns the source chain adapters for the test. + Sources() []Adapter + + // Dest returns the destination chain adapter for the test. + Dest() Adapter + + // ReplayLogs replays logs for the given blocks using the provided offchain client. + // By default, it will assert on errors. Use WithAssertOnError(false) to change this behavior. + ReplayLogs(t *testing.T, selectorToBlockMap map[uint64]uint64) +}