Skip to content

Commit 5a9adc8

Browse files
authored
kt-devnet: minimal interop test: execute message (ethereum-optimism#14838)
* devnet-sdk: Plugin op-service ethclient * Add low level client access * Use e2e wait util * execute message * typo * newline * use sha256 precompile * comments * Polish * comment * Log block time * Reduce logs * fix block number
1 parent a26a0f3 commit 5a9adc8

File tree

9 files changed

+222
-18
lines changed

9 files changed

+222
-18
lines changed

devnet-sdk/constraints/constraints_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math/big"
66
"testing"
77

8+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
89
"github.com/ethereum-optimism/optimism/devnet-sdk/system"
910
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
1011
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -39,6 +40,10 @@ func (m mockWallet) InitiateMessage(chainID types.ChainID, target common.Address
3940
panic("not implemented")
4041
}
4142

43+
func (m mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] {
44+
panic("not implemented")
45+
}
46+
4247
func (m mockWallet) Nonce() uint64 {
4348
return 0
4449
}

devnet-sdk/system/interfaces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"math/big"
66

7+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
78
"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
89
"github.com/ethereum-optimism/optimism/devnet-sdk/interfaces"
910
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
@@ -74,6 +75,7 @@ type Wallet interface {
7475
Address() types.Address
7576
SendETH(to types.Address, amount types.Balance) types.WriteInvocation[any]
7677
InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any]
78+
ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any]
7779
Balance() types.Balance
7880
Nonce() uint64
7981

@@ -103,6 +105,12 @@ type Transaction interface {
103105
TransactionData
104106
}
105107

108+
type Receipt interface {
109+
BlockNumber() *big.Int
110+
Logs() []*coreTypes.Log
111+
TxHash() common.Hash
112+
}
113+
106114
// RawTransaction is an optional interface that can be implemented by a Transaction
107115
// to provide access to the raw transaction data.
108116
// It is currently necessary to perform processing operations (signing, sending)

devnet-sdk/system/tx.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,22 @@ func (t *EthTx) Type() uint8 {
198198
func (t *EthTx) Raw() *types.Transaction {
199199
return t.tx
200200
}
201+
202+
// EthReceipt is the default implementation of Receipt that wraps types.Receipt
203+
type EthReceipt struct {
204+
blockNumber *big.Int
205+
logs []*types.Log
206+
txHash common.Hash
207+
}
208+
209+
func (t *EthReceipt) BlockNumber() *big.Int {
210+
return t.blockNumber
211+
}
212+
213+
func (t *EthReceipt) Logs() []*types.Log {
214+
return t.logs
215+
}
216+
217+
func (t *EthReceipt) TxHash() common.Hash {
218+
return t.txHash
219+
}

devnet-sdk/system/txbuilder_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"math/big"
77
"testing"
88

9+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
910
"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
1011
"github.com/ethereum-optimism/optimism/devnet-sdk/interfaces"
1112
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
@@ -59,6 +60,11 @@ func (m *mockWallet) InitiateMessage(chainID types.ChainID, target common.Addres
5960
return args.Get(0).(types.WriteInvocation[any])
6061
}
6162

63+
func (m *mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] {
64+
args := m.Called(identifier, sentMessage)
65+
return args.Get(0).(types.WriteInvocation[any])
66+
}
67+
6268
func (m *mockWallet) Balance() types.Balance {
6369
args := m.Called()
6470
return args.Get(0).(types.Balance)

devnet-sdk/system/wallet.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math/big"
88
"strings"
99

10+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
1011
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants"
1112
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
1213
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
@@ -114,6 +115,16 @@ func (w *wallet) InitiateMessage(chainID types.ChainID, target common.Address, m
114115
}
115116
}
116117

118+
func (w *wallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] {
119+
return &executeMessageImpl{
120+
chain: w.chain,
121+
processor: w,
122+
from: w.address,
123+
identifier: identifier,
124+
sentMessage: sentMessage,
125+
}
126+
}
127+
117128
type initiateMessageImpl struct {
118129
chain internalChain
119130
processor TransactionProcessor
@@ -169,6 +180,58 @@ func (i *initiateMessageImpl) Send(ctx context.Context) types.InvocationResult {
169180
}
170181
}
171182

183+
type executeMessageImpl struct {
184+
chain internalChain
185+
processor TransactionProcessor
186+
from types.Address
187+
188+
identifier bindings.Identifier
189+
sentMessage []byte
190+
}
191+
192+
func (i *executeMessageImpl) Call(ctx context.Context) (any, error) {
193+
builder := NewTxBuilder(ctx, i.chain)
194+
messenger, err := i.chain.ContractsRegistry().L2ToL2CrossDomainMessenger(constants.L2ToL2CrossDomainMessenger)
195+
if err != nil {
196+
return nil, fmt.Errorf("failed to init transaction: %w", err)
197+
}
198+
data, err := messenger.ABI().Pack("relayMessage", i.identifier, i.sentMessage)
199+
if err != nil {
200+
return nil, fmt.Errorf("failed to build calldata: %w", err)
201+
}
202+
tx, err := builder.BuildTx(
203+
WithFrom(i.from),
204+
WithTo(constants.L2ToL2CrossDomainMessenger),
205+
WithValue(big.NewInt(0)),
206+
WithData(data),
207+
)
208+
if err != nil {
209+
return nil, fmt.Errorf("failed to build transaction: %w", err)
210+
}
211+
tx, err = i.processor.Sign(tx)
212+
if err != nil {
213+
return nil, fmt.Errorf("failed to sign transaction: %w", err)
214+
}
215+
return tx, nil
216+
}
217+
218+
func (i *executeMessageImpl) Send(ctx context.Context) types.InvocationResult {
219+
result, err := i.Call(ctx)
220+
if err != nil {
221+
return &sendResult{chain: i.chain, tx: nil, err: err}
222+
}
223+
tx, ok := result.(Transaction)
224+
if !ok {
225+
return &sendResult{chain: i.chain, tx: nil, err: fmt.Errorf("unexpected return type")}
226+
}
227+
err = i.processor.Send(ctx, tx)
228+
return &sendResult{
229+
chain: i.chain,
230+
tx: tx,
231+
err: err,
232+
}
233+
}
234+
172235
func (w *wallet) Nonce() uint64 {
173236
client, err := w.chain.Client()
174237
if err != nil {
@@ -293,9 +356,10 @@ func (i *sendImpl) Send(ctx context.Context) types.InvocationResult {
293356
}
294357

295358
type sendResult struct {
296-
chain internalChain
297-
tx Transaction
298-
err error
359+
chain internalChain
360+
tx Transaction
361+
receipt Receipt
362+
err error
299363
}
300364

301365
func (r *sendResult) Error() error {
@@ -320,11 +384,15 @@ func (r *sendResult) Wait() error {
320384
if err != nil {
321385
return fmt.Errorf("failed waiting for transaction confirmation: %w", err)
322386
}
323-
387+
r.receipt = &EthReceipt{blockNumber: receipt.BlockNumber, logs: receipt.Logs, txHash: receipt.TxHash}
324388
if receipt.Status == 0 {
325389
return fmt.Errorf("transaction failed")
326390
}
327391
}
328392

329393
return nil
330394
}
395+
396+
func (r *sendResult) Info() any {
397+
return r.receipt
398+
}

devnet-sdk/testing/testlib/validators/validators_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"math/big"
77
"testing"
88

9+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
910
"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
1011
"github.com/ethereum-optimism/optimism/devnet-sdk/interfaces"
1112
"github.com/ethereum-optimism/optimism/devnet-sdk/system"
@@ -328,6 +329,10 @@ func (m mockWallet) InitiateMessage(chainID types.ChainID, target common.Address
328329
panic("not implemented")
329330
}
330331

332+
func (m mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] {
333+
panic("not implemented")
334+
}
335+
331336
func (m mockWallet) Nonce() uint64 {
332337
return 0
333338
}

devnet-sdk/types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type WriteInvocation[T any] interface {
2424
type InvocationResult interface {
2525
Error() error
2626
Wait() error
27+
Info() any
2728
}
2829

2930
type Key = *ecdsa.PrivateKey
Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,133 @@
11
package interop
22

33
import (
4+
"encoding/hex"
45
"math/big"
56
"testing"
67

8+
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings"
79
"github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants"
810
"github.com/ethereum-optimism/optimism/devnet-sdk/system"
911
"github.com/ethereum-optimism/optimism/devnet-sdk/testing/systest"
1012
"github.com/ethereum-optimism/optimism/devnet-sdk/testing/testlib/validators"
1113
sdktypes "github.com/ethereum-optimism/optimism/devnet-sdk/types"
14+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
1215
"github.com/ethereum-optimism/optimism/op-service/testlog"
1316
"github.com/ethereum/go-ethereum/common"
17+
"github.com/ethereum/go-ethereum/core/vm"
1418
"github.com/ethereum/go-ethereum/log"
1519
"github.com/stretchr/testify/require"
1620
)
1721

18-
func initiateMessageScenario(sourceChainIdx, destChainIdx uint64, walletGetter validators.WalletGetter) systest.InteropSystemTestFunc {
22+
func messagePassingScenario(lowLevelSystemGetter validators.LowLevelSystemGetter, sourceChainIdx, destChainIdx uint64, sourceWalletGetter, destWalletGetter validators.WalletGetter) systest.InteropSystemTestFunc {
1923
return func(t systest.T, sys system.InteropSystem) {
2024
ctx := t.Context()
25+
llsys := lowLevelSystemGetter(ctx)
2126

2227
logger := testlog.Logger(t, log.LevelInfo)
23-
logger = logger.With("test", "TestInitiateMessage", "devnet", sys.Identifier())
28+
logger = logger.With("test", "TestMessagePassing", "devnet", sys.Identifier())
2429

2530
chainA := sys.L2s()[sourceChainIdx]
26-
chainB := sys.L2s()[destChainIdx]
31+
chainB := llsys.L2s()[destChainIdx]
2732

28-
logger = logger.With("sourceChain", chainA.ID(), "destChain", chainB.ID())
33+
logger.Info("chain info", "sourceChain", chainA.ID(), "destChain", chainB.ID())
2934

30-
// userA is funded at chainA and want to send message to chainB
31-
userA := walletGetter(ctx)
35+
// userA is funded at chainA and want to initialize message at chain A
36+
userA := sourceWalletGetter(ctx)
37+
// userB is funded at chainB and want to execute message to chainB
38+
userB := destWalletGetter(ctx)
39+
40+
sha256PrecompileAddr := common.BytesToAddress([]byte{0x2})
41+
dummyMessage := []byte("l33t message")
3242

3343
// Initiate message
34-
dummyAddress := common.Address{0x13, 0x37}
35-
dummyMessage := []byte{0x13, 0x33, 0x33, 0x37}
36-
logger.Info("Initiate message", "address", dummyAddress, "message", dummyMessage)
37-
require.NoError(t, userA.InitiateMessage(chainB.ID(), dummyAddress, dummyMessage).Send(ctx).Wait())
44+
logger.Info("Initiate message", "address", sha256PrecompileAddr, "message", dummyMessage)
45+
initResult := userA.InitiateMessage(chainB.ID(), sha256PrecompileAddr, dummyMessage).Send(ctx)
46+
require.NoError(t, initResult.Wait())
47+
48+
initReceipt, ok := initResult.Info().(system.Receipt)
49+
require.True(t, ok)
50+
logger.Info("Initiate message", "txHash", initReceipt.TxHash().Hex())
51+
logs := initReceipt.Logs()
52+
// We are directly calling sendMessage, so we expect single log for SentMessage event
53+
require.Equal(t, 1, len(logs), "expected single log")
54+
log := logs[0]
55+
56+
// Build sentMessage for message execution
57+
blockNumber := initReceipt.BlockNumber()
58+
blockA, err := chainA.Node().BlockByNumber(ctx, blockNumber)
59+
require.NoError(t, err)
60+
blockTimeA := big.NewInt(int64(blockA.Time()))
61+
logger.Info("Initiate message was included at", "timestamp", blockTimeA.String())
62+
63+
sentMessage := []byte{}
64+
for _, topic := range log.Topics {
65+
sentMessage = append(sentMessage, topic.Bytes()...)
66+
}
67+
sentMessage = append(sentMessage, log.Data...)
68+
69+
// Build identifier for message execution
70+
logIndex := big.NewInt(int64(log.Index))
71+
identifier := bindings.Identifier{
72+
Origin: constants.L2ToL2CrossDomainMessenger,
73+
BlockNumber: blockNumber,
74+
LogIndex: logIndex,
75+
Timestamp: blockTimeA,
76+
ChainId: chainA.ID(),
77+
}
78+
79+
// Execute message
80+
logger.Info("Execute message", "address", sha256PrecompileAddr, "message", dummyMessage)
81+
execResult := userB.ExecuteMessage(identifier, sentMessage).Send(ctx)
82+
require.NoError(t, execResult.Wait())
83+
84+
execReceipt, ok := execResult.Info().(system.Receipt)
85+
require.True(t, ok)
86+
87+
execTxHash := execReceipt.TxHash()
88+
logger.Info("Execute message", "txHash", execTxHash.Hex())
89+
90+
blockNumberB := execReceipt.BlockNumber()
91+
blockB, err := chainB.Node().BlockByNumber(ctx, blockNumberB)
92+
require.NoError(t, err)
93+
blockTimeB := big.NewInt(int64(blockB.Time()))
94+
logger.Info("Execute message was included at", "timestamp", blockTimeB.String())
95+
96+
// Validation that message has passed and got executed successfully
97+
gethClient, err := chainB.GethClient()
98+
require.NoError(t, err)
99+
100+
trace, err := wait.DebugTraceTx(ctx, gethClient, execTxHash)
101+
require.NoError(t, err)
102+
103+
precompile := vm.PrecompiledContractsHomestead[sha256PrecompileAddr]
104+
expected, err := precompile.Run(dummyMessage)
105+
require.NoError(t, err)
106+
logger.Info("sha256 computed offchain", "value", hex.EncodeToString(expected))
107+
108+
// length of sha256 image is 32
109+
output := trace.CallTrace.Output
110+
require.GreaterOrEqual(t, len(output), 32)
111+
actual := []byte(output[len(output)-32:])
112+
logger.Info("sha256 computed onchain", "value", hex.EncodeToString(actual))
113+
114+
require.Equal(t, expected, actual)
38115
}
39116
}
40117

41-
func TestInteropSystemInitiateMessage(t *testing.T) {
118+
// TestMessagePassing checks the basic functionality of message passing two interoperable chains.
119+
// Scenario: Source chain initiates message to make destination chain execute sha256 precompile.
120+
func TestMessagePassing(t *testing.T) {
42121
sourceChainIdx := uint64(0)
43122
destChainIdx := uint64(1)
44-
walletGetter, fundsValidator := validators.AcquireL2WalletWithFunds(sourceChainIdx, sdktypes.NewBalance(big.NewInt(1.0*constants.ETH)))
123+
sourceWalletGetter, sourcefundsValidator := validators.AcquireL2WalletWithFunds(sourceChainIdx, sdktypes.NewBalance(big.NewInt(1.0*constants.ETH)))
124+
destWalletGetter, destfundsValiator := validators.AcquireL2WalletWithFunds(destChainIdx, sdktypes.NewBalance(big.NewInt(1.0*constants.ETH)))
125+
lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem()
45126

46127
systest.InteropSystemTest(t,
47-
initiateMessageScenario(sourceChainIdx, destChainIdx, walletGetter),
48-
fundsValidator,
128+
messagePassingScenario(lowLevelSystemGetter, sourceChainIdx, destChainIdx, sourceWalletGetter, destWalletGetter),
129+
sourcefundsValidator,
130+
destfundsValiator,
131+
lowLevelSystemValidator,
49132
)
50133
}

0 commit comments

Comments
 (0)