Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
159 changes: 154 additions & 5 deletions system_tests/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ type NodeBuilder struct {
withProdConfirmPeriodBlocks bool
delayBufferThreshold uint64
withL1ClientWrapper bool
executionClientMode ExecutionClientMode

// Created nodes
L1 *TestClient
Expand Down Expand Up @@ -459,6 +460,11 @@ func (b *NodeBuilder) WithExtraArchs(targets []string) *NodeBuilder {
return b
}

func (b *NodeBuilder) WithExecutionClientMode(mode ExecutionClientMode) *NodeBuilder {
b.executionClientMode = mode
return b
}

// WithDelayBuffer sets the delay-buffer threshold, which is the number of blocks the batch-poster
// is allowed to delay a batch with a delayed message.
// Setting the threshold to zero disabled the delay buffer (default behaviour).
Expand Down Expand Up @@ -888,11 +894,79 @@ func (b *NodeBuilder) BuildL2(t *testing.T) func() {
t, b.L2Info, b.dataDir, b.chainConfig, b.arbOSInit, nil, b.l2StackConfig, b.execConfig)

execConfigFetcher := NewCommonConfigFetcher(b.execConfig)
execNode, err := gethexec.CreateExecutionNode(b.ctx, b.L2.Stack, chainDb, blockchain, nil, execConfigFetcher, big.NewInt(1337), 0)
Require(t, err)
b.L2.ExecutionConfigFetcher = execConfigFetcher

// Create execution client based on mode
var execNode nethexec.FullExecutionClient
fatalErrChan := make(chan error, 10)

switch b.executionClientMode {
case ExecutionClientModeInternal, 0: // 0 for default/unset
// Original behavior - internal geth
execNode, err = gethexec.CreateExecutionNode(b.ctx, b.L2.Stack, chainDb, blockchain, nil, execConfigFetcher, big.NewInt(1337), 0)
Require(t, err)

case ExecutionClientModeExternal:
// External Nethermind
nethermindUrl := os.Getenv("PR_NETH_RPC_CLIENT_URL")
if nethermindUrl == "" {
nethermindUrl = "http://localhost:20545"
}
nethermindWsUrl := os.Getenv("PR_NETH_WS_URL")
if nethermindWsUrl == "" {
nethermindWsUrl = "ws://localhost:28551"
}
nethermindExecClient, err := nethexec.NewNethermindExecutionClient(nethermindUrl, nethermindWsUrl)
Require(t, err)

// Initialize Nethermind with genesis - CREATE INIT MESSAGE
serializedChainConfig, err := json.Marshal(b.chainConfig)
Require(t, err)
initMsg := &arbostypes.ParsedInitMessage{
ChainId: b.chainConfig.ChainID,
InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee,
ChainConfig: b.chainConfig,
SerializedChainConfig: serializedChainConfig,
}
result := nethermindExecClient.DigestInitMessage(b.ctx, initMsg.InitialL1BaseFee, initMsg.SerializedChainConfig)
if result == nil {
Fatal(t, "DigestInitMessage returned nil for external execution client")
}

execNode = nethermindExecClient

case ExecutionClientModeComparison:
// Both - create comparison client
gethExec, err := gethexec.CreateExecutionNode(b.ctx, b.L2.Stack, chainDb, blockchain, nil, execConfigFetcher, big.NewInt(1337), 0)
Require(t, err)

nethermindUrl := os.Getenv("PR_NETH_RPC_CLIENT_URL")
if nethermindUrl == "" {
nethermindUrl = "http://localhost:20545"
}
nethermindWsUrl := os.Getenv("PR_NETH_WS_URL")
if nethermindWsUrl == "" {
nethermindWsUrl = "ws://localhost:28551"
}
nethExec, err := nethexec.NewNethermindExecutionClient(nethermindUrl, nethermindWsUrl)
Require(t, err)

// Initialize Nethermind with genesis - CREATE INIT MESSAGE
serializedChainConfig, err := json.Marshal(b.chainConfig)
Require(t, err)
initMsg := &arbostypes.ParsedInitMessage{
ChainId: b.chainConfig.ChainID,
InitialL1BaseFee: arbostypes.DefaultInitialL1BaseFee,
ChainConfig: b.chainConfig,
SerializedChainConfig: serializedChainConfig,
}
result := nethExec.DigestInitMessage(b.ctx, initMsg.InitialL1BaseFee, initMsg.SerializedChainConfig)
if result == nil {
Fatal(t, "DigestInitMessage returned nil for external execution client")
}

execNode = nethexec.NewCompareExecutionClient(gethExec, nethExec, fatalErrChan)
}

locator, err := server_common.NewMachineLocator(b.valnodeConfig.Wasm.RootPath)
Require(t, err)
consensusConfigFetcher := NewCommonConfigFetcher(b.nodeConfig)
Expand All @@ -909,7 +983,21 @@ func (b *NodeBuilder) BuildL2(t *testing.T) func() {
err = b.L2.ConsensusNode.Start(b.ctx)
Require(t, err)

b.L2.Client = ClientForStack(t, b.L2.Stack)
// Set client based on execution mode
switch b.executionClientMode {
case ExecutionClientModeExternal:
// For external execution client, connect directly to Nethermind for RPC calls
nethRpcUrl := os.Getenv("PR_NETH_RPC_CLIENT_URL")
if nethRpcUrl == "" {
nethRpcUrl = "http://localhost:20545"
}
externalRpcClient, err := rpc.Dial(nethRpcUrl)
Require(t, err)
b.L2.Client = ethclient.NewClient(externalRpcClient)
default:
// For internal and comparison modes, use internal geth client
b.L2.Client = ClientForStack(t, b.L2.Stack)
}

if b.takeOwnership {
debugAuth := b.L2Info.GetDefaultTransactOpts("Owner", b.ctx)
Expand All @@ -927,7 +1015,11 @@ func (b *NodeBuilder) BuildL2(t *testing.T) func() {

StartWatchChanErr(t, b.ctx, fatalErrChan, b.L2.ConsensusNode)

b.L2.ExecNode = getExecNode(t, b.L2.ConsensusNode)
// Only set ExecNode if it's internal geth
if b.executionClientMode == ExecutionClientModeInternal || b.executionClientMode == 0 {
b.L2.ExecNode = getExecNode(t, b.L2.ConsensusNode)
}

b.L2.cleanup = func() { b.L2.ConsensusNode.StopAndWait() }
return func() {
b.L2.cleanup()
Expand Down Expand Up @@ -2365,3 +2457,60 @@ func populateMachineDir(t *testing.T, cr *github.ConsensusRelease) string {
Require(t, err)
return machineDir
}

// BuildReplicaWithExecutionMode builds a replica node with the specified execution client mode
// Returns the replica's test client and cleanup function
func BuildReplicaWithExecutionMode(t *testing.T, builder *NodeBuilder, executionClientMode ExecutionClientMode) (*TestClient, func()) {
replicaConfig := arbnode.ConfigDefaultL1NonSequencerTest()
replicaParams := &SecondNodeParams{
nodeConfig: replicaConfig,
useExecutionClientOnly: true,
executionClientMode: executionClientMode,
}
replica, cleanup := builder.Build2ndNode(t, replicaParams)

// Wait for replica to initialize
time.Sleep(time.Second * 2)

return replica, cleanup
}

// WaitForReplicaSync waits for replica to catch up to primary's block number
// Returns an error if replica fails to sync within the timeout
func WaitForReplicaSync(ctx context.Context, t *testing.T, primaryClient, replicaClient *ethclient.Client, maxAttempts int) {
primaryBlock, err := primaryClient.BlockNumber(ctx)
Require(t, err)

for i := 0; i < maxAttempts; i++ {
replicaBlock, err := replicaClient.BlockNumber(ctx)
Require(t, err)
if replicaBlock >= primaryBlock {
return
}
time.Sleep(time.Millisecond * 100)
}

// Final check and fail if not synced
replicaBlock, err := replicaClient.BlockNumber(ctx)
Require(t, err)
if replicaBlock < primaryBlock {
Fatal(t, "Replica at block", replicaBlock, "failed to catch up to primary at block", primaryBlock)
}
}

// ReplicaTestFunc is a test function that takes an execution client mode parameter
type ReplicaTestFunc func(t *testing.T, executionClientMode ExecutionClientMode)

// CreateReplicaTestVariants generates Internal, External, and Comparison test functions
// from a single test implementation
func CreateReplicaTestVariants(testFunc ReplicaTestFunc) (internal, external, comparison func(*testing.T)) {
return func(t *testing.T) {
testFunc(t, ExecutionClientModeInternal)
},
func(t *testing.T) {
testFunc(t, ExecutionClientModeExternal)
},
func(t *testing.T) {
testFunc(t, ExecutionClientModeComparison)
}
}
71 changes: 66 additions & 5 deletions system_tests/contract_tx_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2021-2022, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package arbtest

import (
Expand All @@ -18,29 +17,74 @@ import (
"github.com/offchainlabs/nitro/arbos"
"github.com/offchainlabs/nitro/arbos/arbostypes"
"github.com/offchainlabs/nitro/cmd/chaininfo"
"github.com/offchainlabs/nitro/statetransfer"
"github.com/offchainlabs/nitro/util/arbmath"
)

func TestContractTxDeploy(t *testing.T) {
func testContractTxDeploy(t *testing.T, executionClientMode ExecutionClientMode) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Create builder but don't build yet
builder := NewNodeBuilder(ctx).DefaultConfig(t, false)
builder.takeOwnership = false

// Fund the test account at GENESIS - before building
from := common.HexToAddress("0x123412341234")

// Add to genesis allocation in L2Info
// This ensures BOTH geth and Nethermind have the account from block 0
if builder.L2Info.ArbInitData.Accounts == nil {
builder.L2Info.ArbInitData.Accounts = []statetransfer.AccountInitializationInfo{}
}

builder.L2Info.ArbInitData.Accounts = append(builder.L2Info.ArbInitData.Accounts,
statetransfer.AccountInitializationInfo{
Addr: from,
EthBalance: big.NewInt(1e18),
Nonce: 0,
ContractInfo: nil,
})

// Also add to the L2Info.Accounts map for test helpers to use
// Use SetFullAccountInfo which handles the atomic.Uint64 correctly
builder.L2Info.SetFullAccountInfo("TestAccount", &AccountInfo{
Address: from,
PrivateKey: nil, // No private key needed for ArbitrumContractTx
})

// NOW set execution mode and build
builder = builder.WithExecutionClientMode(executionClientMode)

cleanup := builder.Build(t)
defer cleanup()

from := common.HexToAddress("0x123412341234")
builder.L2.TransferBalanceTo(t, "Faucet", from, big.NewInt(1e18), builder.L2Info)
// Wait for initialization to complete
if executionClientMode == ExecutionClientModeComparison || executionClientMode == ExecutionClientModeExternal {
time.Sleep(time.Second * 2)
}

// Verify account was funded at genesis in both clients
balance, err := builder.L2.Client.BalanceAt(ctx, from, nil)
Require(t, err)
if balance.Cmp(big.NewInt(1e18)) != 0 {
Fatal(t, "Test account not funded at genesis, got balance:", balance)
}
t.Log("Verified account funded at genesis with balance:", balance)

// NO TransferBalance call - account is already funded!

for stateNonce := uint64(0); stateNonce < 2; stateNonce++ {
msgCount, err := builder.L2.ConsensusNode.TxStreamer.GetMessageCount()
Require(t, err)

var delayedMessagesRead uint64
if msgCount > 0 {
lastMessage, err := builder.L2.ConsensusNode.TxStreamer.GetMessage(msgCount - 1)
Require(t, err)
delayedMessagesRead = lastMessage.DelayedMessagesRead
}

// Deploys a single 0xFE (INVALID) byte as a smart contract
deployCode := []byte{
0x60, 0xFE, // PUSH1 0xFE
Expand All @@ -50,9 +94,11 @@ func TestContractTxDeploy(t *testing.T) {
0x60, 0x00, // PUSH1 0
0xF3, // RETURN
}

var requestId common.Hash
// #nosec G115
requestId[0] = uint8(stateNonce)

contractTx := &types.ArbitrumContractTx{
ChainId: chaininfo.ArbitrumDevTestChainConfig().ChainID,
RequestId: requestId,
Expand All @@ -63,6 +109,7 @@ func TestContractTxDeploy(t *testing.T) {
Value: big.NewInt(0),
Data: deployCode,
}

l2Msg := []byte{arbos.L2MessageKind_ContractTx}
l2Msg = append(l2Msg, arbmath.Uint64ToU256Bytes(contractTx.Gas)...)
l2Msg = append(l2Msg, arbmath.U256Bytes(contractTx.GasFeeCap)...)
Expand Down Expand Up @@ -92,6 +139,7 @@ func TestContractTxDeploy(t *testing.T) {

txHash := types.NewTx(contractTx).Hash()
t.Log("made contract tx", contractTx, "with hash", txHash)

receipt, err := WaitForTx(ctx, builder.L2.Client, txHash, time.Second*10)
Require(t, err)
if receipt.Status != types.ReceiptStatusSuccessful {
Expand All @@ -103,12 +151,25 @@ func TestContractTxDeploy(t *testing.T) {
Fatal(t, "expected address", from, "nonce", stateNonce, "to deploy to", expectedAddr, "but got", receipt.ContractAddress)
}
t.Log("deployed contract", receipt.ContractAddress, "from address", from, "with nonce", stateNonce)
stateNonce++

code, err := builder.L2.Client.CodeAt(ctx, receipt.ContractAddress, nil)
Require(t, err)
if !bytes.Equal(code, []byte{0xFE}) {
Fatal(t, "expected contract", receipt.ContractAddress, "code of 0xFE but got", hex.EncodeToString(code))
}

stateNonce++
}
}

func TestContractTxDeployInternal(t *testing.T) {
testContractTxDeploy(t, ExecutionClientModeInternal)
}

func TestContractTxDeployExternal(t *testing.T) {
testContractTxDeploy(t, ExecutionClientModeExternal)
}

func TestContractTxDeployComparison(t *testing.T) {
testContractTxDeploy(t, ExecutionClientModeComparison)
}