Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build/devenv/cciptestinterfaces/extra_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ type MessageV3TokenArgs []byte
// MessageV3TokenReceiver is a type to indicate how to use the MessageV3Destination interface.
type MessageV3TokenReceiver []byte

// V3DestinationLoadDefaults supplies dest chains load test MessageOptions
// so source side guns avoid family specific details.
type V3DestinationLoadDefaults interface {
MessageV3Destination
V3LoadMessageOptions() MessageOptions
}

// MessageV3Destination is an interface for any chain that can receive a V3 message.
// We use an interface rather than a struct because the V3 message structure is chain agnostic.
type MessageV3Destination interface {
Expand Down
85 changes: 75 additions & 10 deletions build/devenv/tests/e2e/gun.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,12 @@ func (m *EVMTXGun) Call(_ *wasp.Generator) *wasp.Response {
return &wasp.Response{Error: fmt.Errorf("failed to select dest selector: %w", err).Error(), Failed: true}
}

fields, opts, err := m.selectMessageProfile(srcSelector, destSelector)
dest, err := m.resolveDestLoadInfo(destSelector)
if err != nil {
return &wasp.Response{Error: fmt.Errorf("failed to resolve destination: %w", err).Error(), Failed: true}
}

fields, opts, err := m.selectMessageProfile(srcSelector, dest)
if err != nil {
return &wasp.Response{Error: fmt.Errorf("failed to select message profile: %w", err).Error(), Failed: true}
}
Expand Down Expand Up @@ -202,20 +207,21 @@ func (m *EVMTXGun) Call(_ *wasp.Generator) *wasp.Response {
return &wasp.Response{Error: "impl is not ChainAsSource", Failed: true}
}

extraArgs, err := evm.SerializeEVMExtraArgs(3, opts)
// encoding is decided based on dest
extraArgs, err := m.buildExtraArgs(srcSelector, dest, opts)
if err != nil {
return &wasp.Response{Error: fmt.Errorf("failed to serialize extra args: %w", err).Error(), Failed: true}
return &wasp.Response{Error: fmt.Errorf("failed to build extra args: %w", err).Error(), Failed: true}
}

srcMessage, err := chainAsSource.BuildChainMessage(ctx, fields, extraArgs)
if err != nil {
return &wasp.Response{Error: fmt.Errorf("failed to build message: %w", err).Error(), Failed: true}
}

// WETH fees need msgValue=0; DisableTokenAmountValidation sets msgValue=fee and reverts.
sentEvent, _, err := chainAsSource.SendChainMessage(ctx, destSelector, srcMessage, evm.SendOptions{
Nonce: &currentNonce,
Sender: sender,
DisableTokenAmountValidation: true,
Nonce: &currentNonce,
Sender: sender,
})
Comment thread
Farber98 marked this conversation as resolved.
if err != nil {
return &wasp.Response{Error: fmt.Errorf("failed to send message: %w", err).Error(), Failed: true}
Expand Down Expand Up @@ -256,17 +262,76 @@ func (m *EVMTXGun) SelectDestSelector(excludeSelector uint64) (uint64, error) {
return load.GetSelectorByRatio(choices)
}

func (m *EVMTXGun) selectMessageProfile(srcSelector, destSelector uint64) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) {
// destLoadInfo determines destination routing: receiver and whether to use EVM or AltVM extra-args.
type destLoadInfo struct {
selector uint64
receiver protocol.UnknownAddress
hasMockReceiver bool
}

// resolveDestLoadInfo resolves receiver + encoding shape.
func (m *EVMTXGun) resolveDestLoadInfo(destSelector uint64) (destLoadInfo, error) {
mockReceiverRef, err := m.e.DataStore.Addresses().Get(
datastore.NewAddressRefKey(
destSelector,
datastore.ContractType(mock_receiver_v2.ContractType),
semver.MustParse(mock_receiver_v2.Deploy.Version()),
devenvcommon.DefaultReceiverQualifier))
if err == nil {
return destLoadInfo{
selector: destSelector,
receiver: protocol.UnknownAddress(common.HexToAddress(mockReceiverRef.Address).Bytes()),
hasMockReceiver: true,
}, nil
}

destImpl, ok := m.impl[destSelector]
if !ok {
return destLoadInfo{}, fmt.Errorf("destination chain %d not found in impls", destSelector)
}
receiver, err := destImpl.GetEOAReceiverAddress()
if err != nil {
return cciptestinterfaces.MessageFields{}, cciptestinterfaces.MessageOptions{}, fmt.Errorf("could not find mock receiver address in datastore: %w", err)
return destLoadInfo{}, fmt.Errorf("could not get EOA receiver for dest %d: %w", destSelector, err)
}
return destLoadInfo{
selector: destSelector,
receiver: receiver,
hasMockReceiver: false,
}, nil
}

// buildExtraArgs picks encoding based on destination shape.
// V3 destinations without mock_receiver use BuildV3ExtraArgs; execution limits come
// from the destination via V3DestinationLoadDefaults when the profile leaves them unset.
func (m *EVMTXGun) buildExtraArgs(srcSelector uint64, dest destLoadInfo, opts cciptestinterfaces.MessageOptions) (cciptestinterfaces.GenericExtraArgs, error) {
if dest.hasMockReceiver {
return evm.SerializeEVMExtraArgs(3, opts)
}

v3Src, ok := m.impl[srcSelector].(cciptestinterfaces.MessageV3Source)
if !ok {
return nil, fmt.Errorf("source chain %d does not implement MessageV3Source", srcSelector)
}
v3Dest, ok := m.impl[dest.selector].(cciptestinterfaces.MessageV3Destination)
if !ok {
return nil, fmt.Errorf("destination chain %d does not implement MessageV3Destination", dest.selector)
}

v3Opts := opts
if v3Opts.ExecutionGasLimit == 0 {
if def, ok := m.impl[dest.selector].(cciptestinterfaces.V3DestinationLoadDefaults); ok {
v3Opts.ExecutionGasLimit = def.V3LoadMessageOptions().ExecutionGasLimit
}
}

// Receiver is in MessageFields; token params nil for data only load
return v3Src.BuildV3ExtraArgs(v3Opts, v3Dest, nil, nil, nil)
}

// selectMessageProfile builds message options for load sends and applies defaults when no profile is configured.
func (m *EVMTXGun) selectMessageProfile(srcSelector uint64, dest destLoadInfo) (cciptestinterfaces.MessageFields, cciptestinterfaces.MessageOptions, error) {
receiver := dest.receiver

wethContract, err := m.e.DataStore.Addresses().Get(
datastore.NewAddressRefKey(
srcSelector,
Expand Down Expand Up @@ -294,7 +359,7 @@ func (m *EVMTXGun) selectMessageProfile(srcSelector, destSelector uint64) (ccipt
}
if m.testConfig == nil || m.testConfig.Messages == nil {
return cciptestinterfaces.MessageFields{
Receiver: protocol.UnknownAddress(common.HexToAddress(mockReceiverRef.Address).Bytes()),
Receiver: receiver,
Data: []byte{},
FeeToken: protocol.UnknownAddress(common.HexToAddress(wethContract.Address).Bytes()),
}, cciptestinterfaces.MessageOptions{
Expand All @@ -314,7 +379,7 @@ func (m *EVMTXGun) selectMessageProfile(srcSelector, destSelector uint64) (ccipt
return cciptestinterfaces.MessageFields{}, cciptestinterfaces.MessageOptions{}, fmt.Errorf("failed to get message profile: %w", err)
}
fields := cciptestinterfaces.MessageFields{
Receiver: protocol.UnknownAddress(common.HexToAddress(mockReceiverRef.Address).Bytes()),
Receiver: receiver,
Data: []byte{},
FeeToken: protocol.UnknownAddress(common.HexToAddress(wethContract.Address).Bytes()),
}
Expand Down
109 changes: 109 additions & 0 deletions build/devenv/tests/e2e/load_prep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package e2e

import (
"context"
"math/big"
"strconv"
"testing"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/weth"
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/weth9"
)

const requiredWETHBalanceForLoad = 1e18

// EnsureWETHBalanceAndApproval prepares WETH and router approval for CCIP load senders on chain.Users.
func EnsureWETHBalanceAndApproval(ctx context.Context, t *testing.T, logger zerolog.Logger, e *deployment.Environment, chain cldfevm.Chain) {
t.Helper()

requiredWETH := big.NewInt(requiredWETHBalanceForLoad)

logger.Info().Str("chain", strconv.FormatUint(chain.Selector, 10)).Msg("Ensuring WETH balance and approval")
wethContract, err := e.DataStore.Addresses().Get(
datastore.NewAddressRefKey(
chain.Selector,
datastore.ContractType(weth.ContractType),
semver.MustParse(weth.Deploy.Version()),
""))
require.NoErrorf(t, err, "failed to resolve WETH contract for chain %d", chain.Selector)

wethInstance, err := weth9.NewWETH9(common.HexToAddress(wethContract.Address), chain.Client)
require.NoErrorf(t, err, "failed to bind WETH contract %s for chain %d", wethContract.Address, chain.Selector)

routerInstance, err := e.DataStore.Addresses().Get(datastore.NewAddressRefKey(
chain.Selector,
datastore.ContractType(router.ContractType),
semver.MustParse(router.Deploy.Version()),
""))
require.NoErrorf(t, err, "failed to resolve router contract for chain %d", chain.Selector)

routerAddr := common.HexToAddress(routerInstance.Address)
funded := make(map[common.Address]struct{}, len(chain.Users)+1)

for _, user := range chain.Users {
ensureUserWETHBalanceAndApproval(ctx, t, logger, chain, wethInstance, routerAddr, user, requiredWETH)
funded[user.From] = struct{}{}
}

// SendChainMessage pre-checks DeployerKey, not the round-robin sender.
if _, ok := funded[chain.DeployerKey.From]; !ok {
ensureUserWETHBalanceAndApproval(ctx, t, logger, chain, wethInstance, routerAddr, chain.DeployerKey, requiredWETH)
}
}

// ensureUserWETHBalanceAndApproval makes one signing key ready to pay CCIP load fees in WETH.
func ensureUserWETHBalanceAndApproval(
ctx context.Context,
t *testing.T,
logger zerolog.Logger,
chain cldfevm.Chain,
wethInstance *weth9.WETH9,
routerAddr common.Address,
user *bind.TransactOpts,
requiredWETH *big.Int,
) {
t.Helper()

oldValue := user.Value
defer func() { user.Value = oldValue }()

logger.Info().Str("user", user.From.String()).Msg("User address")
balance, err := chain.Client.BalanceAt(ctx, user.From, nil)
require.NoErrorf(t, err, "failed to read native balance for user %s on chain %d", user.From.String(), chain.Selector)
logger.Info().Str("balance", balance.String()).Msg("User native balance before deposit")

wethBalance, err := wethInstance.BalanceOf(&bind.CallOpts{Context: ctx}, user.From)
require.NoErrorf(t, err, "failed to read WETH balance for user %s on chain %d", user.From.String(), chain.Selector)
logger.Info().
Str("wethBalance", wethBalance.String()).
Str("requiredWETH", requiredWETH.String()).
Msg("User WETH balance before deposit")

if wethBalance.Cmp(requiredWETH) < 0 {
depositAmount := new(big.Int).Sub(requiredWETH, wethBalance)
user.Value = depositAmount
tx1, err := wethInstance.Deposit(user)
require.NoErrorf(t, err, "failed to deposit WETH for user %s on chain %d", user.From.String(), chain.Selector)
_, err = chain.Confirm(tx1)
require.NoErrorf(t, err, "failed to confirm WETH deposit tx %s for user %s on chain %d", tx1.Hash().Hex(), user.From.String(), chain.Selector)
logger.Info().Str("depositAmount", depositAmount.String()).Msg("Deposited WETH")
}
Comment thread
Farber98 marked this conversation as resolved.

// Approve is non-payable; shared TransactOpts may carry stale msg.Value.
user.Value = nil
tx, err := wethInstance.Approve(user, routerAddr, requiredWETH)
require.NoErrorf(t, err, "failed to approve router %s for user %s on chain %d", routerAddr.Hex(), user.From.String(), chain.Selector)
Comment thread
Farber98 marked this conversation as resolved.
_, err = chain.Confirm(tx)
require.NoErrorf(t, err, "failed to confirm approve tx %s for user %s on chain %d", tx.Hash().Hex(), user.From.String(), chain.Selector)
Comment thread
Farber98 marked this conversation as resolved.
logger.Info().Str("approvedAmount", requiredWETH.String()).Msg("Approved WETH for router")
}
83 changes: 12 additions & 71 deletions build/devenv/tests/e2e/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,15 @@ import (
"testing"
"time"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/common"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"

chain_selectors "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/weth"
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/load"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/tests/e2e/metrics"
cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/weth9"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/chaos"
"github.com/smartcontractkit/chainlink-testing-framework/framework/rpc"
Expand All @@ -38,7 +32,6 @@ import (

const (
postTestVerificationDelay = 30 * time.Second
requiredWETHBalance = 1e18
)

type ChaosTestCase struct {
Expand All @@ -56,56 +49,6 @@ type GasTestCase struct {
waitBetweenTests time.Duration
}

func ensureWETHBalanceAndApproval(ctx context.Context, t *testing.T, logger zerolog.Logger, e *deployment.Environment, chain cldfevm.Chain, requiredWETH *big.Int) {
logger.Info().Str("chain", strconv.FormatUint(chain.Selector, 10)).Msg("Ensuring WETH balance and approval")
wethContract, err := e.DataStore.Addresses().Get(
datastore.NewAddressRefKey(
chain.Selector,
datastore.ContractType(weth.ContractType),
semver.MustParse(weth.Deploy.Version()),
""))
require.NoError(t, err)

wethInstance, err := weth9.NewWETH9(common.HexToAddress(wethContract.Address), chain.Client)
require.NoError(t, err)

routerInstance, err := e.DataStore.Addresses().Get(datastore.NewAddressRefKey(
chain.Selector,
datastore.ContractType(router.ContractType),
semver.MustParse(router.Deploy.Version()),
""))
require.NoError(t, err)

for _, user := range chain.Users {
logger.Info().Str("user address", user.From.String()).Msg("User address")
balance, err := chain.Client.BalanceAt(ctx, user.From, nil)
require.NoError(t, err)
logger.Info().Str("balance", balance.String()).Msg("Deployer balance before deposit")

wethBalance, err := wethInstance.BalanceOf(nil, user.From)
require.NoError(t, err)
logger.Info().Str("wethBalance", wethBalance.String()).Str("requiredWETH", requiredWETH.String()).Msg("Deployer WETH balance before deposit")

if wethBalance.Cmp(requiredWETH) < 0 {
depositAmount := new(big.Int).Sub(requiredWETH, wethBalance)
oldValue := user.Value
user.Value = depositAmount
tx1, err := wethInstance.Deposit(user)
require.NoError(t, err)
_, err = chain.Confirm(tx1)
require.NoError(t, err)
user.Value = oldValue
logger.Info().Str("depositAmount", depositAmount.String()).Msg("Deposited WETH")
}

tx, err := wethInstance.Approve(user, common.HexToAddress(routerInstance.Address), requiredWETH)
require.NoError(t, err)
_, err = chain.Confirm(tx)
require.NoError(t, err)
logger.Info().Str("approvedAmount", requiredWETH.String()).Msg("Approved WETH for router")
}
}

func gasControlFunc(t *testing.T, r *rpc.RPCClient, blockPace time.Duration) {
startGasPrice := big.NewInt(2e9)
// ramp
Expand Down Expand Up @@ -210,7 +153,7 @@ func TestE2ELoad(t *testing.T) {
}

// Ensure we have at least 1 WETH and approve router to spend it
ensureWETHBalanceAndApproval(ctx, t, *l, e, srcChain, big.NewInt(requiredWETHBalance))
EnsureWETHBalanceAndApproval(ctx, t, *l, e, srcChain)

t.Run("clean", func(t *testing.T) {
// just a clean load test to measure performance
Expand Down Expand Up @@ -638,30 +581,28 @@ func TestStaging(t *testing.T) {
err = verifyTestConfig(e, testConfig)
require.NoError(t, err)

var wg sync.WaitGroup
// require must run on the test goroutine: fund each source chain once before wasp workers start.
fundedSourceChains := make(map[uint64]struct{})
for _, testProfile := range testConfig.TestProfiles {
Comment thread
Farber98 marked this conversation as resolved.
if !testProfile.Enabled {
continue
}
for _, chainInfo := range testProfile.ChainsAsSource {
wg.Add(1)
go func(chainInfo load.ChainProfileConfig) {
defer wg.Done()
chainSelector, err := strconv.ParseUint(chainInfo.Selector, 10, 64)
if err != nil {
t.Logf("failed to parse chain selector: %v", err)
return
}
chain := e.BlockChains.EVMChains()[chainSelector]
ensureWETHBalanceAndApproval(ctx, t, *l, e, chain, big.NewInt(requiredWETHBalance))
}(chainInfo)
chainSelector, err := strconv.ParseUint(chainInfo.Selector, 10, 64)
require.NoError(t, err)
if _, done := fundedSourceChains[chainSelector]; done {
continue
}
fundedSourceChains[chainSelector] = struct{}{}
chain := e.BlockChains.EVMChains()[chainSelector]
EnsureWETHBalanceAndApproval(ctx, t, *l, e, chain)
}
}
wg.Wait()

// Wait for old txns and nonces to settled before we start the load test
time.Sleep(30 * time.Second)

var wg sync.WaitGroup
for idx, testProfile := range testConfig.TestProfiles {
if !testProfile.Enabled {
continue
Expand Down
Loading