github.com/ava-labs/subnet-evm@v0.6.4/tests/warp/warp_test.go (about)

     1  // Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  // Implements solidity tests.
     5  package warp
     6  
     7  import (
     8  	"context"
     9  	"crypto/ecdsa"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"math/big"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	ginkgo "github.com/onsi/ginkgo/v2"
    20  
    21  	"github.com/onsi/gomega"
    22  
    23  	"github.com/stretchr/testify/require"
    24  
    25  	"github.com/ethereum/go-ethereum/common"
    26  	"github.com/ethereum/go-ethereum/crypto"
    27  	"github.com/ethereum/go-ethereum/log"
    28  
    29  	"github.com/ava-labs/avalanchego/api/info"
    30  	"github.com/ava-labs/avalanchego/ids"
    31  	"github.com/ava-labs/avalanchego/snow/validators"
    32  	"github.com/ava-labs/avalanchego/tests/fixture/e2e"
    33  	"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
    34  	"github.com/ava-labs/avalanchego/utils/constants"
    35  	"github.com/ava-labs/avalanchego/vms/platformvm"
    36  	avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp"
    37  	"github.com/ava-labs/avalanchego/vms/platformvm/warp/payload"
    38  
    39  	"github.com/ava-labs/subnet-evm/cmd/simulator/key"
    40  	"github.com/ava-labs/subnet-evm/cmd/simulator/load"
    41  	"github.com/ava-labs/subnet-evm/cmd/simulator/metrics"
    42  	"github.com/ava-labs/subnet-evm/cmd/simulator/txs"
    43  	"github.com/ava-labs/subnet-evm/core/types"
    44  	"github.com/ava-labs/subnet-evm/ethclient"
    45  	"github.com/ava-labs/subnet-evm/interfaces"
    46  	"github.com/ava-labs/subnet-evm/params"
    47  	"github.com/ava-labs/subnet-evm/precompile/contracts/warp"
    48  	"github.com/ava-labs/subnet-evm/predicate"
    49  	"github.com/ava-labs/subnet-evm/tests"
    50  	"github.com/ava-labs/subnet-evm/tests/utils"
    51  	warpBackend "github.com/ava-labs/subnet-evm/warp"
    52  	"github.com/ava-labs/subnet-evm/warp/aggregator"
    53  )
    54  
    55  const (
    56  	subnetAName = "warp-subnet-a"
    57  	subnetBName = "warp-subnet-b"
    58  )
    59  
    60  var (
    61  	flagVars *e2e.FlagVars
    62  
    63  	repoRootPath = tests.GetRepoRootPath("tests/warp")
    64  
    65  	genesisPath = filepath.Join(repoRootPath, "tests/precompile/genesis/warp.json")
    66  
    67  	subnetA, subnetB, cChainSubnetDetails *Subnet
    68  
    69  	testPayload = []byte{1, 2, 3}
    70  )
    71  
    72  func init() {
    73  	// Configures flags used to configure tmpnet (via SynchronizedBeforeSuite)
    74  	flagVars = e2e.RegisterFlags()
    75  }
    76  
    77  // Subnet provides the basic details of a created subnet
    78  type Subnet struct {
    79  	// SubnetID is the txID of the transaction that created the subnet
    80  	SubnetID ids.ID
    81  	// For simplicity assume a single blockchain per subnet
    82  	BlockchainID ids.ID
    83  	// Key funded in the genesis of the blockchain
    84  	PreFundedKey *ecdsa.PrivateKey
    85  	// ValidatorURIs are the base URIs for each participant of the Subnet
    86  	ValidatorURIs []string
    87  }
    88  
    89  func TestE2E(t *testing.T) {
    90  	gomega.RegisterFailHandler(ginkgo.Fail)
    91  	ginkgo.RunSpecs(t, "subnet-evm warp e2e test")
    92  }
    93  
    94  var _ = ginkgo.SynchronizedBeforeSuite(func() []byte {
    95  	// Run only once in the first ginkgo process
    96  
    97  	nodes := utils.NewTmpnetNodes(tmpnet.DefaultNodeCount)
    98  
    99  	env := e2e.NewTestEnvironment(
   100  		flagVars,
   101  		utils.NewTmpnetNetwork(
   102  			"subnet-evm-warp-e2e",
   103  			nodes,
   104  			tmpnet.FlagsMap{},
   105  			utils.NewTmpnetSubnet(subnetAName, genesisPath, utils.DefaultChainConfig, nodes...),
   106  			utils.NewTmpnetSubnet(subnetBName, genesisPath, utils.DefaultChainConfig, nodes...),
   107  		),
   108  	)
   109  
   110  	return env.Marshal()
   111  }, func(envBytes []byte) {
   112  	// Run in every ginkgo process
   113  
   114  	require := require.New(ginkgo.GinkgoT())
   115  
   116  	// Initialize the local test environment from the global state
   117  	if len(envBytes) > 0 {
   118  		e2e.InitSharedTestEnvironment(envBytes)
   119  	}
   120  
   121  	network := e2e.Env.GetNetwork()
   122  
   123  	// By default all nodes are validating all subnets
   124  	validatorURIs := make([]string, len(network.Nodes))
   125  	for i, node := range network.Nodes {
   126  		validatorURIs[i] = node.URI
   127  	}
   128  
   129  	tmpnetSubnetA := network.GetSubnet(subnetAName)
   130  	require.NotNil(tmpnetSubnetA)
   131  	subnetA = &Subnet{
   132  		SubnetID:      tmpnetSubnetA.SubnetID,
   133  		BlockchainID:  tmpnetSubnetA.Chains[0].ChainID,
   134  		PreFundedKey:  tmpnetSubnetA.Chains[0].PreFundedKey.ToECDSA(),
   135  		ValidatorURIs: validatorURIs,
   136  	}
   137  
   138  	tmpnetSubnetB := network.GetSubnet(subnetBName)
   139  	require.NotNil(tmpnetSubnetB)
   140  	subnetB = &Subnet{
   141  		SubnetID:      tmpnetSubnetB.SubnetID,
   142  		BlockchainID:  tmpnetSubnetB.Chains[0].ChainID,
   143  		PreFundedKey:  tmpnetSubnetB.Chains[0].PreFundedKey.ToECDSA(),
   144  		ValidatorURIs: validatorURIs,
   145  	}
   146  
   147  	infoClient := info.NewClient(network.Nodes[0].URI)
   148  	cChainBlockchainID, err := infoClient.GetBlockchainID(e2e.DefaultContext(), "C")
   149  	require.NoError(err)
   150  
   151  	cChainSubnetDetails = &Subnet{
   152  		SubnetID:      constants.PrimaryNetworkID,
   153  		BlockchainID:  cChainBlockchainID,
   154  		PreFundedKey:  tmpnet.HardhatKey.ToECDSA(),
   155  		ValidatorURIs: validatorURIs,
   156  	}
   157  })
   158  
   159  var _ = ginkgo.Describe("[Warp]", func() {
   160  	testFunc := func(sendingSubnet *Subnet, receivingSubnet *Subnet) {
   161  		w := newWarpTest(e2e.DefaultContext(), sendingSubnet, receivingSubnet)
   162  
   163  		log.Info("Sending message from A to B")
   164  		w.sendMessageFromSendingSubnet()
   165  
   166  		log.Info("Aggregating signatures via API")
   167  		w.aggregateSignaturesViaAPI()
   168  
   169  		log.Info("Aggregating signatures via p2p aggregator")
   170  		w.aggregateSignatures()
   171  
   172  		log.Info("Delivering addressed call payload to receiving subnet")
   173  		w.deliverAddressedCallToReceivingSubnet()
   174  
   175  		log.Info("Delivering block hash payload to receiving subnet")
   176  		w.deliverBlockHashPayload()
   177  
   178  		log.Info("Executing HardHat test")
   179  		w.executeHardHatTest()
   180  
   181  		log.Info("Executing warp load test")
   182  		w.warpLoad()
   183  	}
   184  	ginkgo.It("SubnetA -> SubnetB", func() { testFunc(subnetA, subnetB) })
   185  	ginkgo.It("SubnetA -> SubnetA", func() { testFunc(subnetA, subnetA) })
   186  	ginkgo.It("SubnetA -> C-Chain", func() { testFunc(subnetA, cChainSubnetDetails) })
   187  	ginkgo.It("C-Chain -> SubnetA", func() { testFunc(cChainSubnetDetails, subnetA) })
   188  	ginkgo.It("C-Chain -> C-Chain", func() { testFunc(cChainSubnetDetails, cChainSubnetDetails) })
   189  })
   190  
   191  type warpTest struct {
   192  	// network-wide fields set in the constructor
   193  	networkID uint32
   194  
   195  	// sendingSubnet fields set in the constructor
   196  	sendingSubnet              *Subnet
   197  	sendingSubnetURIs          []string
   198  	sendingSubnetClients       []ethclient.Client
   199  	sendingSubnetFundedKey     *ecdsa.PrivateKey
   200  	sendingSubnetFundedAddress common.Address
   201  	sendingSubnetChainID       *big.Int
   202  	sendingSubnetSigner        types.Signer
   203  
   204  	// receivingSubnet fields set in the constructor
   205  	receivingSubnet              *Subnet
   206  	receivingSubnetURIs          []string
   207  	receivingSubnetClients       []ethclient.Client
   208  	receivingSubnetFundedKey     *ecdsa.PrivateKey
   209  	receivingSubnetFundedAddress common.Address
   210  	receivingSubnetChainID       *big.Int
   211  	receivingSubnetSigner        types.Signer
   212  
   213  	// Fields set throughout test execution
   214  	blockID                     ids.ID
   215  	blockPayload                *payload.Hash
   216  	blockPayloadUnsignedMessage *avalancheWarp.UnsignedMessage
   217  	blockPayloadSignedMessage   *avalancheWarp.Message
   218  
   219  	addressedCallUnsignedMessage *avalancheWarp.UnsignedMessage
   220  	addressedCallSignedMessage   *avalancheWarp.Message
   221  }
   222  
   223  func newWarpTest(ctx context.Context, sendingSubnet *Subnet, receivingSubnet *Subnet) *warpTest {
   224  	require := require.New(ginkgo.GinkgoT())
   225  
   226  	sendingSubnetFundedKey := sendingSubnet.PreFundedKey
   227  	receivingSubnetFundedKey := receivingSubnet.PreFundedKey
   228  
   229  	warpTest := &warpTest{
   230  		sendingSubnet:                sendingSubnet,
   231  		sendingSubnetURIs:            sendingSubnet.ValidatorURIs,
   232  		receivingSubnet:              receivingSubnet,
   233  		receivingSubnetURIs:          receivingSubnet.ValidatorURIs,
   234  		sendingSubnetFundedKey:       sendingSubnetFundedKey,
   235  		sendingSubnetFundedAddress:   crypto.PubkeyToAddress(sendingSubnetFundedKey.PublicKey),
   236  		receivingSubnetFundedKey:     receivingSubnetFundedKey,
   237  		receivingSubnetFundedAddress: crypto.PubkeyToAddress(receivingSubnetFundedKey.PublicKey),
   238  	}
   239  	infoClient := info.NewClient(sendingSubnet.ValidatorURIs[0])
   240  	networkID, err := infoClient.GetNetworkID(ctx)
   241  	require.NoError(err)
   242  	warpTest.networkID = networkID
   243  
   244  	warpTest.initClients()
   245  
   246  	sendingClient := warpTest.sendingSubnetClients[0]
   247  	sendingSubnetChainID, err := sendingClient.ChainID(ctx)
   248  	require.NoError(err)
   249  	warpTest.sendingSubnetChainID = sendingSubnetChainID
   250  	warpTest.sendingSubnetSigner = types.LatestSignerForChainID(sendingSubnetChainID)
   251  
   252  	receivingClient := warpTest.receivingSubnetClients[0]
   253  	receivingChainID, err := receivingClient.ChainID(ctx)
   254  	require.NoError(err)
   255  	// Issue transactions to activate ProposerVM on the receiving chain
   256  	require.NoError(utils.IssueTxsToActivateProposerVMFork(ctx, receivingChainID, receivingSubnetFundedKey, receivingClient))
   257  	warpTest.receivingSubnetChainID = receivingChainID
   258  	warpTest.receivingSubnetSigner = types.LatestSignerForChainID(receivingChainID)
   259  
   260  	return warpTest
   261  }
   262  
   263  func (w *warpTest) initClients() {
   264  	require := require.New(ginkgo.GinkgoT())
   265  
   266  	w.sendingSubnetClients = make([]ethclient.Client, 0, len(w.sendingSubnetClients))
   267  	for _, uri := range w.sendingSubnet.ValidatorURIs {
   268  		wsURI := toWebsocketURI(uri, w.sendingSubnet.BlockchainID.String())
   269  		log.Info("Creating ethclient for blockchain A", "blockchainID", w.sendingSubnet.BlockchainID)
   270  		client, err := ethclient.Dial(wsURI)
   271  		require.NoError(err)
   272  		w.sendingSubnetClients = append(w.sendingSubnetClients, client)
   273  	}
   274  
   275  	w.receivingSubnetClients = make([]ethclient.Client, 0, len(w.receivingSubnetClients))
   276  	for _, uri := range w.receivingSubnet.ValidatorURIs {
   277  		wsURI := toWebsocketURI(uri, w.receivingSubnet.BlockchainID.String())
   278  		log.Info("Creating ethclient for blockchain B", "blockchainID", w.receivingSubnet.BlockchainID)
   279  		client, err := ethclient.Dial(wsURI)
   280  		require.NoError(err)
   281  		w.receivingSubnetClients = append(w.receivingSubnetClients, client)
   282  	}
   283  }
   284  
   285  func (w *warpTest) getBlockHashAndNumberFromTxReceipt(ctx context.Context, client ethclient.Client, tx *types.Transaction) (common.Hash, uint64) {
   286  	// This uses the Subnet-EVM client to fetch a block from Coreth (when testing the C-Chain), so we use this
   287  	// workaround to get the correct block hash. Note the client recalculates the block hash locally, which results
   288  	// in a different block hash due to small differences in the block format.
   289  	require := require.New(ginkgo.GinkgoT())
   290  	for {
   291  		require.NoError(ctx.Err())
   292  		receipt, err := client.TransactionReceipt(ctx, tx.Hash())
   293  		if err == nil {
   294  			return receipt.BlockHash, receipt.BlockNumber.Uint64()
   295  		}
   296  	}
   297  }
   298  
   299  func (w *warpTest) sendMessageFromSendingSubnet() {
   300  	ctx := e2e.DefaultContext()
   301  	require := require.New(ginkgo.GinkgoT())
   302  
   303  	client := w.sendingSubnetClients[0]
   304  	log.Info("Subscribing to new heads")
   305  	newHeads := make(chan *types.Header, 10)
   306  	sub, err := client.SubscribeNewHead(ctx, newHeads)
   307  	require.NoError(err)
   308  	defer sub.Unsubscribe()
   309  
   310  	startingNonce, err := client.NonceAt(ctx, w.sendingSubnetFundedAddress, nil)
   311  	require.NoError(err)
   312  
   313  	packedInput, err := warp.PackSendWarpMessage(testPayload)
   314  	require.NoError(err)
   315  	tx := types.NewTx(&types.DynamicFeeTx{
   316  		ChainID:   w.sendingSubnetChainID,
   317  		Nonce:     startingNonce,
   318  		To:        &warp.Module.Address,
   319  		Gas:       200_000,
   320  		GasFeeCap: big.NewInt(225 * params.GWei),
   321  		GasTipCap: big.NewInt(params.GWei),
   322  		Value:     common.Big0,
   323  		Data:      packedInput,
   324  	})
   325  	signedTx, err := types.SignTx(tx, w.sendingSubnetSigner, w.sendingSubnetFundedKey)
   326  	require.NoError(err)
   327  	log.Info("Sending sendWarpMessage transaction", "txHash", signedTx.Hash())
   328  	err = client.SendTransaction(ctx, signedTx)
   329  	require.NoError(err)
   330  
   331  	log.Info("Waiting for new block confirmation")
   332  	<-newHeads
   333  	receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
   334  	defer cancel()
   335  	blockHash, blockNumber := w.getBlockHashAndNumberFromTxReceipt(receiptCtx, client, signedTx)
   336  
   337  	log.Info("Constructing warp block hash unsigned message", "blockHash", blockHash)
   338  	w.blockID = ids.ID(blockHash) // Set blockID to construct a warp message containing a block hash payload later
   339  	w.blockPayload, err = payload.NewHash(w.blockID)
   340  	require.NoError(err)
   341  	w.blockPayloadUnsignedMessage, err = avalancheWarp.NewUnsignedMessage(w.networkID, w.sendingSubnet.BlockchainID, w.blockPayload.Bytes())
   342  	require.NoError(err)
   343  
   344  	log.Info("Fetching relevant warp logs from the newly produced block")
   345  	logs, err := client.FilterLogs(ctx, interfaces.FilterQuery{
   346  		BlockHash: &blockHash,
   347  		Addresses: []common.Address{warp.Module.Address},
   348  	})
   349  	require.NoError(err)
   350  	require.Len(logs, 1)
   351  
   352  	// Check for relevant warp log from subscription and ensure that it matches
   353  	// the log extracted from the last block.
   354  	txLog := logs[0]
   355  	log.Info("Parsing logData as unsigned warp message")
   356  	unsignedMsg, err := warp.UnpackSendWarpEventDataToMessage(txLog.Data)
   357  	require.NoError(err)
   358  
   359  	// Set local variables for the duration of the test
   360  	w.addressedCallUnsignedMessage = unsignedMsg
   361  	log.Info("Parsed unsignedWarpMsg", "unsignedWarpMessageID", w.addressedCallUnsignedMessage.ID(), "unsignedWarpMessage", w.addressedCallUnsignedMessage)
   362  
   363  	// Loop over each client on chain A to ensure they all have time to accept the block.
   364  	// Note: if we did not confirm this here, the next stage could be racy since it assumes every node
   365  	// has accepted the block.
   366  	for i, client := range w.sendingSubnetClients {
   367  		// Loop until each node has advanced to >= the height of the block that emitted the warp log
   368  		for {
   369  			block, err := client.BlockByNumber(ctx, nil)
   370  			require.NoError(err)
   371  			if block.NumberU64() >= blockNumber {
   372  				log.Info("client accepted the block containing SendWarpMessage", "client", i, "height", block.NumberU64())
   373  				break
   374  			}
   375  		}
   376  	}
   377  }
   378  
   379  func (w *warpTest) aggregateSignaturesViaAPI() {
   380  	require := require.New(ginkgo.GinkgoT())
   381  	ctx := e2e.DefaultContext()
   382  
   383  	warpAPIs := make(map[ids.NodeID]warpBackend.Client, len(w.sendingSubnetURIs))
   384  	for _, uri := range w.sendingSubnetURIs {
   385  		client, err := warpBackend.NewClient(uri, w.sendingSubnet.BlockchainID.String())
   386  		require.NoError(err)
   387  
   388  		infoClient := info.NewClient(uri)
   389  		nodeID, _, err := infoClient.GetNodeID(ctx)
   390  		require.NoError(err)
   391  		warpAPIs[nodeID] = client
   392  	}
   393  
   394  	pChainClient := platformvm.NewClient(w.sendingSubnetURIs[0])
   395  	pChainHeight, err := pChainClient.GetHeight(ctx)
   396  	require.NoError(err)
   397  	// If the source subnet is the Primary Network, then we only need to aggregate signatures from the receiving
   398  	// subnet's validator set instead of the entire Primary Network.
   399  	// If the destination turns out to be the Primary Network as well, then this is a no-op.
   400  	var validators map[ids.NodeID]*validators.GetValidatorOutput
   401  	if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID {
   402  		validators, err = pChainClient.GetValidatorsAt(ctx, w.receivingSubnet.SubnetID, pChainHeight)
   403  	} else {
   404  		validators, err = pChainClient.GetValidatorsAt(ctx, w.sendingSubnet.SubnetID, pChainHeight)
   405  	}
   406  	require.NoError(err)
   407  	require.NotZero(len(validators))
   408  
   409  	totalWeight := uint64(0)
   410  	warpValidators := make([]*avalancheWarp.Validator, 0, len(validators))
   411  	for nodeID, validator := range validators {
   412  		warpValidators = append(warpValidators, &avalancheWarp.Validator{
   413  			PublicKey: validator.PublicKey,
   414  			Weight:    validator.Weight,
   415  			NodeIDs:   []ids.NodeID{nodeID},
   416  		})
   417  		totalWeight += validator.Weight
   418  	}
   419  
   420  	log.Info("Aggregating signatures from validator set", "numValidators", len(warpValidators), "totalWeight", totalWeight)
   421  	apiSignatureGetter := warpBackend.NewAPIFetcher(warpAPIs)
   422  	signatureResult, err := aggregator.New(apiSignatureGetter, warpValidators, totalWeight).AggregateSignatures(ctx, w.addressedCallUnsignedMessage, 100)
   423  	require.NoError(err)
   424  	require.Equal(signatureResult.SignatureWeight, signatureResult.TotalWeight)
   425  	require.Equal(signatureResult.SignatureWeight, totalWeight)
   426  
   427  	w.addressedCallSignedMessage = signatureResult.Message
   428  
   429  	signatureResult, err = aggregator.New(apiSignatureGetter, warpValidators, totalWeight).AggregateSignatures(ctx, w.blockPayloadUnsignedMessage, 100)
   430  	require.NoError(err)
   431  	require.Equal(signatureResult.SignatureWeight, signatureResult.TotalWeight)
   432  	require.Equal(signatureResult.SignatureWeight, totalWeight)
   433  	w.blockPayloadSignedMessage = signatureResult.Message
   434  
   435  	log.Info("Aggregated signatures for warp messages", "addressedCallMessage", common.Bytes2Hex(w.addressedCallSignedMessage.Bytes()), "blockPayloadMessage", common.Bytes2Hex(w.blockPayloadSignedMessage.Bytes()))
   436  }
   437  
   438  func (w *warpTest) aggregateSignatures() {
   439  	require := require.New(ginkgo.GinkgoT())
   440  	ctx := e2e.DefaultContext()
   441  
   442  	// Verify that the signature aggregation matches the results of manually constructing the warp message
   443  	client, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String())
   444  	require.NoError(err)
   445  
   446  	log.Info("Fetching addressed call aggregate signature via p2p API")
   447  	subnetIDStr := ""
   448  	if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID {
   449  		subnetIDStr = w.receivingSubnet.SubnetID.String()
   450  	}
   451  	signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, w.addressedCallSignedMessage.ID(), warp.WarpQuorumDenominator, subnetIDStr)
   452  	require.NoError(err)
   453  	require.Equal(w.addressedCallSignedMessage.Bytes(), signedWarpMessageBytes)
   454  
   455  	log.Info("Fetching block payload aggregate signature via p2p API")
   456  	signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, w.blockID, warp.WarpQuorumDenominator, subnetIDStr)
   457  	require.NoError(err)
   458  	require.Equal(w.blockPayloadSignedMessage.Bytes(), signedWarpBlockBytes)
   459  }
   460  
   461  func (w *warpTest) deliverAddressedCallToReceivingSubnet() {
   462  	require := require.New(ginkgo.GinkgoT())
   463  	ctx := e2e.DefaultContext()
   464  
   465  	client := w.receivingSubnetClients[0]
   466  	log.Info("Subscribing to new heads")
   467  	newHeads := make(chan *types.Header, 10)
   468  	sub, err := client.SubscribeNewHead(ctx, newHeads)
   469  	require.NoError(err)
   470  	defer sub.Unsubscribe()
   471  
   472  	nonce, err := client.NonceAt(ctx, w.receivingSubnetFundedAddress, nil)
   473  	require.NoError(err)
   474  
   475  	packedInput, err := warp.PackGetVerifiedWarpMessage(0)
   476  	require.NoError(err)
   477  	tx := predicate.NewPredicateTx(
   478  		w.receivingSubnetChainID,
   479  		nonce,
   480  		&warp.Module.Address,
   481  		5_000_000,
   482  		big.NewInt(225*params.GWei),
   483  		big.NewInt(params.GWei),
   484  		common.Big0,
   485  		packedInput,
   486  		types.AccessList{},
   487  		warp.ContractAddress,
   488  		w.addressedCallSignedMessage.Bytes(),
   489  	)
   490  	signedTx, err := types.SignTx(tx, w.receivingSubnetSigner, w.receivingSubnetFundedKey)
   491  	require.NoError(err)
   492  	txBytes, err := signedTx.MarshalBinary()
   493  	require.NoError(err)
   494  	log.Info("Sending getVerifiedWarpMessage transaction", "txHash", signedTx.Hash(), "txBytes", common.Bytes2Hex(txBytes))
   495  	require.NoError(client.SendTransaction(ctx, signedTx))
   496  
   497  	log.Info("Waiting for new block confirmation")
   498  	<-newHeads
   499  	receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
   500  	defer cancel()
   501  	blockHash, _ := w.getBlockHashAndNumberFromTxReceipt(receiptCtx, client, signedTx)
   502  
   503  	log.Info("Fetching relevant warp logs and receipts from new block")
   504  	logs, err := client.FilterLogs(ctx, interfaces.FilterQuery{
   505  		BlockHash: &blockHash,
   506  		Addresses: []common.Address{warp.Module.Address},
   507  	})
   508  	require.NoError(err)
   509  	require.Len(logs, 0)
   510  	receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
   511  	require.NoError(err)
   512  	require.Equal(receipt.Status, types.ReceiptStatusSuccessful)
   513  }
   514  
   515  func (w *warpTest) deliverBlockHashPayload() {
   516  	require := require.New(ginkgo.GinkgoT())
   517  	ctx := e2e.DefaultContext()
   518  
   519  	client := w.receivingSubnetClients[0]
   520  	log.Info("Subscribing to new heads")
   521  	newHeads := make(chan *types.Header, 10)
   522  	sub, err := client.SubscribeNewHead(ctx, newHeads)
   523  	require.NoError(err)
   524  	defer sub.Unsubscribe()
   525  
   526  	nonce, err := client.NonceAt(ctx, w.receivingSubnetFundedAddress, nil)
   527  	require.NoError(err)
   528  
   529  	packedInput, err := warp.PackGetVerifiedWarpBlockHash(0)
   530  	require.NoError(err)
   531  	tx := predicate.NewPredicateTx(
   532  		w.receivingSubnetChainID,
   533  		nonce,
   534  		&warp.Module.Address,
   535  		5_000_000,
   536  		big.NewInt(225*params.GWei),
   537  		big.NewInt(params.GWei),
   538  		common.Big0,
   539  		packedInput,
   540  		types.AccessList{},
   541  		warp.ContractAddress,
   542  		w.blockPayloadSignedMessage.Bytes(),
   543  	)
   544  	signedTx, err := types.SignTx(tx, w.receivingSubnetSigner, w.receivingSubnetFundedKey)
   545  	require.NoError(err)
   546  	txBytes, err := signedTx.MarshalBinary()
   547  	require.NoError(err)
   548  	log.Info("Sending getVerifiedWarpBlockHash transaction", "txHash", signedTx.Hash(), "txBytes", common.Bytes2Hex(txBytes))
   549  	err = client.SendTransaction(ctx, signedTx)
   550  	require.NoError(err)
   551  
   552  	log.Info("Waiting for new block confirmation")
   553  	<-newHeads
   554  	receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
   555  	defer cancel()
   556  	blockHash, _ := w.getBlockHashAndNumberFromTxReceipt(receiptCtx, client, signedTx)
   557  	log.Info("Fetching relevant warp logs and receipts from new block")
   558  	logs, err := client.FilterLogs(ctx, interfaces.FilterQuery{
   559  		BlockHash: &blockHash,
   560  		Addresses: []common.Address{warp.Module.Address},
   561  	})
   562  	require.NoError(err)
   563  	require.Len(logs, 0)
   564  	receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
   565  	require.NoError(err)
   566  	require.Equal(receipt.Status, types.ReceiptStatusSuccessful)
   567  }
   568  
   569  func (w *warpTest) executeHardHatTest() {
   570  	require := require.New(ginkgo.GinkgoT())
   571  	ctx := e2e.DefaultContext()
   572  
   573  	client := w.sendingSubnetClients[0]
   574  	log.Info("Subscribing to new heads")
   575  	newHeads := make(chan *types.Header, 10)
   576  	sub, err := client.SubscribeNewHead(ctx, newHeads)
   577  	require.NoError(err)
   578  	defer sub.Unsubscribe()
   579  
   580  	chainID, err := client.ChainID(ctx)
   581  	require.NoError(err)
   582  
   583  	rpcURI := toRPCURI(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String())
   584  
   585  	os.Setenv("SENDER_ADDRESS", crypto.PubkeyToAddress(w.sendingSubnetFundedKey.PublicKey).Hex())
   586  	os.Setenv("SOURCE_CHAIN_ID", "0x"+w.sendingSubnet.BlockchainID.Hex())
   587  	os.Setenv("PAYLOAD", "0x"+common.Bytes2Hex(testPayload))
   588  	os.Setenv("EXPECTED_UNSIGNED_MESSAGE", "0x"+hex.EncodeToString(w.addressedCallUnsignedMessage.Bytes()))
   589  	os.Setenv("CHAIN_ID", fmt.Sprintf("%d", chainID.Uint64()))
   590  
   591  	cmdPath := filepath.Join(repoRootPath, "contracts")
   592  	// test path is relative to the cmd path
   593  	testPath := "./test/warp.ts"
   594  	utils.RunHardhatTestsCustomURI(ctx, rpcURI, cmdPath, testPath)
   595  }
   596  
   597  func (w *warpTest) warpLoad() {
   598  	require := require.New(ginkgo.GinkgoT())
   599  	ctx := e2e.DefaultContext()
   600  
   601  	var (
   602  		numWorkers           = len(w.sendingSubnetClients)
   603  		txsPerWorker  uint64 = 10
   604  		batchSize     uint64 = 10
   605  		sendingClient        = w.sendingSubnetClients[0]
   606  	)
   607  
   608  	chainAKeys, chainAPrivateKeys := generateKeys(w.sendingSubnetFundedKey, numWorkers)
   609  	chainBKeys, chainBPrivateKeys := generateKeys(w.receivingSubnetFundedKey, numWorkers)
   610  
   611  	loadMetrics := metrics.NewDefaultMetrics()
   612  
   613  	log.Info("Distributing funds on sending subnet", "numKeys", len(chainAKeys))
   614  	chainAKeys, err := load.DistributeFunds(ctx, sendingClient, chainAKeys, len(chainAKeys), new(big.Int).Mul(big.NewInt(100), big.NewInt(params.Ether)), loadMetrics)
   615  	require.NoError(err)
   616  
   617  	log.Info("Distributing funds on receiving subnet", "numKeys", len(chainBKeys))
   618  	_, err = load.DistributeFunds(ctx, w.receivingSubnetClients[0], chainBKeys, len(chainBKeys), new(big.Int).Mul(big.NewInt(100), big.NewInt(params.Ether)), loadMetrics)
   619  	require.NoError(err)
   620  
   621  	log.Info("Creating workers for each subnet...")
   622  	chainAWorkers := make([]txs.Worker[*types.Transaction], 0, len(chainAKeys))
   623  	for i := range chainAKeys {
   624  		chainAWorkers = append(chainAWorkers, load.NewTxReceiptWorker(ctx, w.sendingSubnetClients[i]))
   625  	}
   626  	chainBWorkers := make([]txs.Worker[*types.Transaction], 0, len(chainBKeys))
   627  	for i := range chainBKeys {
   628  		chainBWorkers = append(chainBWorkers, load.NewTxReceiptWorker(ctx, w.receivingSubnetClients[i]))
   629  	}
   630  
   631  	log.Info("Subscribing to warp send events on sending subnet")
   632  	logs := make(chan types.Log, numWorkers*int(txsPerWorker))
   633  	sub, err := sendingClient.SubscribeFilterLogs(ctx, interfaces.FilterQuery{
   634  		Addresses: []common.Address{warp.Module.Address},
   635  	}, logs)
   636  	require.NoError(err)
   637  	defer func() {
   638  		sub.Unsubscribe()
   639  		err := <-sub.Err()
   640  		require.NoError(err)
   641  	}()
   642  
   643  	log.Info("Generating tx sequence to send warp messages...")
   644  	warpSendSequences, err := txs.GenerateTxSequences(ctx, func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) {
   645  		data, err := warp.PackSendWarpMessage([]byte(fmt.Sprintf("Jets %d-%d Dolphins", key.X.Int64(), nonce)))
   646  		if err != nil {
   647  			return nil, err
   648  		}
   649  		tx := types.NewTx(&types.DynamicFeeTx{
   650  			ChainID:   w.sendingSubnetChainID,
   651  			Nonce:     nonce,
   652  			To:        &warp.Module.Address,
   653  			Gas:       200_000,
   654  			GasFeeCap: big.NewInt(225 * params.GWei),
   655  			GasTipCap: big.NewInt(params.GWei),
   656  			Value:     common.Big0,
   657  			Data:      data,
   658  		})
   659  		return types.SignTx(tx, w.sendingSubnetSigner, key)
   660  	}, w.sendingSubnetClients[0], chainAPrivateKeys, txsPerWorker, false)
   661  	require.NoError(err)
   662  	log.Info("Executing warp send loader...")
   663  	warpSendLoader := load.New(chainAWorkers, warpSendSequences, batchSize, loadMetrics)
   664  	// TODO: execute send and receive loaders concurrently.
   665  	require.NoError(warpSendLoader.Execute(ctx))
   666  	require.NoError(warpSendLoader.ConfirmReachedTip(ctx))
   667  
   668  	warpClient, err := warpBackend.NewClient(w.sendingSubnetURIs[0], w.sendingSubnet.BlockchainID.String())
   669  	require.NoError(err)
   670  	subnetIDStr := ""
   671  	if w.sendingSubnet.SubnetID == constants.PrimaryNetworkID {
   672  		subnetIDStr = w.receivingSubnet.SubnetID.String()
   673  	}
   674  
   675  	log.Info("Executing warp delivery sequences...")
   676  	warpDeliverSequences, err := txs.GenerateTxSequences(ctx, func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) {
   677  		// Wait for the next warp send log
   678  		warpLog := <-logs
   679  
   680  		unsignedMessage, err := warp.UnpackSendWarpEventDataToMessage(warpLog.Data)
   681  		if err != nil {
   682  			return nil, err
   683  		}
   684  		log.Info("Fetching addressed call aggregate signature via p2p API")
   685  
   686  		signedWarpMessageBytes, err := warpClient.GetMessageAggregateSignature(ctx, unsignedMessage.ID(), warp.WarpDefaultQuorumNumerator, subnetIDStr)
   687  		if err != nil {
   688  			return nil, err
   689  		}
   690  
   691  		packedInput, err := warp.PackGetVerifiedWarpMessage(0)
   692  		if err != nil {
   693  			return nil, err
   694  		}
   695  		tx := predicate.NewPredicateTx(
   696  			w.receivingSubnetChainID,
   697  			nonce,
   698  			&warp.Module.Address,
   699  			5_000_000,
   700  			big.NewInt(225*params.GWei),
   701  			big.NewInt(params.GWei),
   702  			common.Big0,
   703  			packedInput,
   704  			types.AccessList{},
   705  			warp.ContractAddress,
   706  			signedWarpMessageBytes,
   707  		)
   708  		return types.SignTx(tx, w.receivingSubnetSigner, key)
   709  	}, w.receivingSubnetClients[0], chainBPrivateKeys, txsPerWorker, true)
   710  	require.NoError(err)
   711  
   712  	log.Info("Executing warp delivery...")
   713  	warpDeliverLoader := load.New(chainBWorkers, warpDeliverSequences, batchSize, loadMetrics)
   714  	require.NoError(warpDeliverLoader.Execute(ctx))
   715  	require.NoError(warpSendLoader.ConfirmReachedTip(ctx))
   716  	log.Info("Completed warp delivery successfully.")
   717  }
   718  
   719  func generateKeys(preFundedKey *ecdsa.PrivateKey, numWorkers int) ([]*key.Key, []*ecdsa.PrivateKey) {
   720  	keys := []*key.Key{
   721  		key.CreateKey(preFundedKey),
   722  	}
   723  	privateKeys := []*ecdsa.PrivateKey{
   724  		preFundedKey,
   725  	}
   726  	for i := 1; i < numWorkers; i++ {
   727  		newKey, err := key.Generate()
   728  		require.NoError(ginkgo.GinkgoT(), err)
   729  		keys = append(keys, newKey)
   730  		privateKeys = append(privateKeys, newKey.PrivKey)
   731  	}
   732  	return keys, privateKeys
   733  }
   734  
   735  func toWebsocketURI(uri string, blockchainID string) string {
   736  	return fmt.Sprintf("ws://%s/ext/bc/%s/ws", strings.TrimPrefix(uri, "http://"), blockchainID)
   737  }
   738  
   739  func toRPCURI(uri string, blockchainID string) string {
   740  	return fmt.Sprintf("%s/ext/bc/%s/rpc", uri, blockchainID)
   741  }