Commit 1ca85526 authored by Axel Kingsley's avatar Axel Kingsley Committed by GitHub

Interop: Expanded E2E Tests (#12659)

* Expand Interop E2E Testing

* fix test ; address comment
parent b46bffed
......@@ -3,22 +3,24 @@ package interop
import (
"context"
"math/big"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-chain-ops/interopgen"
"github.com/ethereum-optimism/optimism/op-e2e/system/helpers"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
gethTypes "github.com/ethereum/go-ethereum/core/types"
)
// TestInteropTrivial tests a simple interop scenario
// Chains A and B exist, but no messages are sent between them
// and in fact no event-logs are emitted by either chain at all.
// A transaction is sent from Alice to Bob on Chain A.
// The balance of Bob on Chain A is checked before and after the tx.
// The balance of Bob on Chain B is checked after the tx.
func TestInteropTrivial(t *testing.T) {
// setupAndRun is a helper function that sets up a SuperSystem
// which contains two L2 Chains, and two users on each chain.
func setupAndRun(t *testing.T, fn func(*testing.T, SuperSystem)) {
recipe := interopgen.InteropDevRecipe{
L1ChainID: 900100,
L2ChainIDs: []uint64{900200, 900201},
......@@ -32,66 +34,163 @@ func TestInteropTrivial(t *testing.T) {
// create a super system from the recipe
// and get the L2 IDs for use in the test
s2 := NewSuperSystem(t, &recipe, worldResources)
ids := s2.L2IDs()
// chainA is the first L2 chain
chainA := ids[0]
// chainB is the second L2 chain
chainB := ids[1]
// create two users on all L2 chains
s2.AddUser("Alice")
s2.AddUser("Bob")
bobAddr := s2.Address(chainA, "Bob")
// check the balance of Bob
clientA := s2.L2GethClient(chainA)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err := clientA.BalanceAt(ctx, bobAddr, nil)
require.NoError(t, err)
expectedBalance, _ := big.NewInt(0).SetString("10000000000000000000000000", 10)
require.Equal(t, expectedBalance, bobBalance)
// send a tx from Alice to Bob
s2.SendL2Tx(
chainA,
"Alice",
func(l2Opts *helpers.TxOpts) {
l2Opts.ToAddr = &bobAddr
l2Opts.Value = big.NewInt(1000000)
l2Opts.GasFeeCap = big.NewInt(1_000_000_000)
l2Opts.GasTipCap = big.NewInt(1_000_000_000)
},
)
// check the balance of Bob after the tx
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err = clientA.BalanceAt(ctx, bobAddr, nil)
require.NoError(t, err)
expectedBalance, _ = big.NewInt(0).SetString("10000000000000000001000000", 10)
require.Equal(t, expectedBalance, bobBalance)
// check that the balance of Bob on ChainB hasn't changed
bobAddrB := s2.Address(chainB, "Bob")
clientB := s2.L2GethClient(chainB)
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err = clientB.BalanceAt(ctx, bobAddrB, nil)
require.NoError(t, err)
expectedBalance, _ = big.NewInt(0).SetString("10000000000000000000000000", 10)
require.Equal(t, expectedBalance, bobBalance)
s2.DeployEmitterContract(chainA, "Alice")
s2.DeployEmitterContract(chainB, "Alice")
for i := 0; i < 1; i++ {
s2.EmitData(chainA, "Alice", "0x1234567890abcdef")
s2.EmitData(chainB, "Alice", "0x1234567890abcdef")
}
// run the test
fn(t, s2)
}
time.Sleep(60 * time.Second)
// TestInterop_IsolatedChains tests a simple interop scenario
// Chains A and B exist, but no messages are sent between them
// a transaction is sent from Alice to Bob on Chain A,
// and only Chain A is affected.
func TestInterop_IsolatedChains(t *testing.T) {
test := func(t *testing.T, s2 SuperSystem) {
ids := s2.L2IDs()
chainA := ids[0]
chainB := ids[1]
// check the balance of Bob
bobAddr := s2.Address(chainA, "Bob")
clientA := s2.L2GethClient(chainA)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err := clientA.BalanceAt(ctx, bobAddr, nil)
require.NoError(t, err)
expectedBalance, _ := big.NewInt(0).SetString("10000000000000000000000000", 10)
require.Equal(t, expectedBalance, bobBalance)
// send a tx from Alice to Bob
s2.SendL2Tx(
chainA,
"Alice",
func(l2Opts *helpers.TxOpts) {
l2Opts.ToAddr = &bobAddr
l2Opts.Value = big.NewInt(1000000)
l2Opts.GasFeeCap = big.NewInt(1_000_000_000)
l2Opts.GasTipCap = big.NewInt(1_000_000_000)
},
)
// check the balance of Bob after the tx
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err = clientA.BalanceAt(ctx, bobAddr, nil)
require.NoError(t, err)
expectedBalance, _ = big.NewInt(0).SetString("10000000000000000001000000", 10)
require.Equal(t, expectedBalance, bobBalance)
// check that the balance of Bob on ChainB hasn't changed
bobAddrB := s2.Address(chainB, "Bob")
clientB := s2.L2GethClient(chainB)
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bobBalance, err = clientB.BalanceAt(ctx, bobAddrB, nil)
require.NoError(t, err)
expectedBalance, _ = big.NewInt(0).SetString("10000000000000000000000000", 10)
require.Equal(t, expectedBalance, bobBalance)
}
setupAndRun(t, test)
}
// TestInteropTrivial_EmitLogs tests a simple interop scenario
// Chains A and B exist, but no messages are sent between them.
// A contract is deployed on each chain, and logs are emitted repeatedly.
func TestInteropTrivial_EmitLogs(t *testing.T) {
test := func(t *testing.T, s2 SuperSystem) {
ids := s2.L2IDs()
chainA := ids[0]
chainB := ids[1]
EmitterA := s2.DeployEmitterContract(chainA, "Alice")
EmitterB := s2.DeployEmitterContract(chainB, "Alice")
payload1 := "SUPER JACKPOT!"
numEmits := 10
// emit logs on both chains in parallel
var emitParallel sync.WaitGroup
emitOn := func(chainID string) {
for i := 0; i < numEmits; i++ {
s2.EmitData(chainID, "Alice", payload1)
}
emitParallel.Done()
}
emitParallel.Add(2)
go emitOn(chainA)
go emitOn(chainB)
emitParallel.Wait()
clientA := s2.L2GethClient(chainA)
clientB := s2.L2GethClient(chainB)
// check that the logs are emitted on chain A
qA := ethereum.FilterQuery{
Addresses: []common.Address{EmitterA},
}
logsA, err := clientA.FilterLogs(context.Background(), qA)
require.NoError(t, err)
require.Len(t, logsA, numEmits)
// check that the logs are emitted on chain B
qB := ethereum.FilterQuery{
Addresses: []common.Address{EmitterB},
}
logsB, err := clientB.FilterLogs(context.Background(), qB)
require.NoError(t, err)
require.Len(t, logsB, numEmits)
// wait for cross-safety to settle
// I've tried 30s but not all logs are cross-safe by then
time.Sleep(60 * time.Second)
supervisor := s2.SupervisorClient()
// requireMessage checks the safety level of a log against the supervisor
// it also checks that the error is as expected
requireMessage := func(chainID string, log gethTypes.Log, expectedSafety types.SafetyLevel, expectedError error) {
client := s2.L2GethClient(chainID)
// construct the expected hash of the log's payload
// (topics concatenated with data)
msgPayload := make([]byte, 0)
for _, topic := range log.Topics {
msgPayload = append(msgPayload, topic.Bytes()...)
}
msgPayload = append(msgPayload, log.Data...)
expectedHash := common.BytesToHash(crypto.Keccak256(msgPayload))
// convert payload hash to log hash
logHash := types.PayloadHashToLogHash(expectedHash, log.Address)
// get block for the log (for timestamp)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
block, err := client.BlockByHash(ctx, log.BlockHash)
require.NoError(t, err)
// make an identifier out of the sample log
identifier := types.Identifier{
Origin: log.Address,
BlockNumber: log.BlockNumber,
LogIndex: uint64(log.Index),
Timestamp: block.Time(),
ChainID: types.ChainIDFromBig(s2.ChainID(chainID)),
}
safety, error := supervisor.CheckMessage(context.Background(),
identifier,
logHash,
)
require.ErrorIs(t, error, expectedError)
// the supervisor could progress the safety level more quickly than we expect,
// which is why we check for a minimum safety level
require.True(t, safety.AtLeastAsSafe(expectedSafety), "log: %v should be at least %s, but is %s", log, expectedSafety.String(), safety.String())
}
// all logs should be cross-safe
for _, log := range logsA {
requireMessage(chainA, log, types.CrossSafe, nil)
}
for _, log := range logsB {
requireMessage(chainB, log, types.CrossSafe, nil)
}
}
setupAndRun(t, test)
}
......@@ -7,6 +7,7 @@ import (
"os"
"path"
"path/filepath"
"sort"
"testing"
"time"
......@@ -79,8 +80,10 @@ type SuperSystem interface {
L2GethClient(network string) *ethclient.Client
// get the secret for a network and role
L2OperatorKey(network string, role devkeys.ChainOperatorRole) ecdsa.PrivateKey
// get the list of network IDs
// get the list of network IDs as key-strings
L2IDs() []string
// get the chain ID for a network
ChainID(network string) *big.Int
// register a username to an account on all L2s
AddUser(username string)
// get the user key for a user on an L2
......@@ -415,6 +418,10 @@ func (s *interopE2ESystem) newL2(id string, l2Out *interopgen.L2Output) l2Set {
}
}
func (s *interopE2ESystem) ChainID(network string) *big.Int {
return s.l2s[network].chainID
}
// prepareSupervisor creates a new supervisor for the system
func (s *interopE2ESystem) prepareSupervisor() *supervisor.SupervisorService {
// Be verbose with op-supervisor, it's in early test phase
......@@ -604,6 +611,7 @@ func (s *interopE2ESystem) L2IDs() []string {
for id := range s.l2s {
ids = append(ids, id)
}
sort.Strings(ids)
return ids
}
......
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
"github.com/ethereum/go-ethereum/common"
)
type SupervisorClient struct {
......@@ -19,9 +20,7 @@ func NewSupervisorClient(client client.RPC) *SupervisorClient {
}
}
func (cl *SupervisorClient) Stop(
ctx context.Context,
) error {
func (cl *SupervisorClient) Stop(ctx context.Context) error {
var result error
err := cl.client.CallContext(
ctx,
......@@ -33,9 +32,7 @@ func (cl *SupervisorClient) Stop(
return result
}
func (cl *SupervisorClient) Start(
ctx context.Context,
) error {
func (cl *SupervisorClient) Start(ctx context.Context) error {
var result error
err := cl.client.CallContext(
ctx,
......@@ -47,10 +44,7 @@ func (cl *SupervisorClient) Start(
return result
}
func (cl *SupervisorClient) AddL2RPC(
ctx context.Context,
rpc string,
) error {
func (cl *SupervisorClient) AddL2RPC(ctx context.Context, rpc string) error {
var result error
err := cl.client.CallContext(
ctx,
......@@ -63,6 +57,25 @@ func (cl *SupervisorClient) AddL2RPC(
return result
}
func (cl *SupervisorClient) CheckMessage(ctx context.Context, identifier types.Identifier, logHash common.Hash) (types.SafetyLevel, error) {
var result types.SafetyLevel
err := cl.client.CallContext(
ctx,
&result,
"supervisor_checkMessage",
identifier,
logHash)
if err != nil {
return types.Invalid, fmt.Errorf("failed to check message (chain %s), (block %v), (index %v), (logHash %s): %w",
identifier.ChainID,
identifier.BlockNumber,
identifier.LogIndex,
logHash,
err)
}
return result, nil
}
func (cl *SupervisorClient) UnsafeView(ctx context.Context, chainID types.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) {
var result types.ReferenceView
err := cl.client.CallContext(
......@@ -93,26 +106,51 @@ func (cl *SupervisorClient) SafeView(ctx context.Context, chainID types.ChainID,
func (cl *SupervisorClient) Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error) {
var result eth.BlockID
err := cl.client.CallContext(ctx, &result, "supervisor_finalized", chainID)
err := cl.client.CallContext(
ctx,
&result,
"supervisor_finalized",
chainID)
return result, err
}
func (cl *SupervisorClient) DerivedFrom(ctx context.Context, chainID types.ChainID, derived eth.BlockID) (eth.BlockRef, error) {
var result eth.BlockRef
err := cl.client.CallContext(ctx, &result, "supervisor_derivedFrom", chainID, derived)
err := cl.client.CallContext(
ctx,
&result,
"supervisor_derivedFrom",
chainID,
derived)
return result, err
}
func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID types.ChainID, head eth.BlockRef) error {
return cl.client.CallContext(ctx, nil, "supervisor_updateLocalUnsafe", chainID, head)
return cl.client.CallContext(
ctx,
nil,
"supervisor_updateLocalUnsafe",
chainID,
head)
}
func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID types.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.BlockRef) error {
return cl.client.CallContext(ctx, nil, "supervisor_updateLocalSafe", chainID, derivedFrom, lastDerived)
return cl.client.CallContext(
ctx,
nil,
"supervisor_updateLocalSafe",
chainID,
derivedFrom,
lastDerived)
}
func (cl *SupervisorClient) UpdateFinalizedL1(ctx context.Context, chainID types.ChainID, finalizedL1 eth.L1BlockRef) error {
return cl.client.CallContext(ctx, nil, "supervisor_updateFinalizedL1", chainID, finalizedL1)
return cl.client.CallContext(
ctx,
nil,
"supervisor_updateFinalizedL1",
chainID,
finalizedL1)
}
func (cl *SupervisorClient) Close() {
......
......@@ -308,14 +308,14 @@ func (su *SupervisorBackend) DependencySet() depset.DependencySet {
// Query methods
// ----------------------------
func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, payloadHash common.Hash) (types.SafetyLevel, error) {
func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, logHash common.Hash) (types.SafetyLevel, error) {
su.mu.RLock()
defer su.mu.RUnlock()
chainID := identifier.ChainID
blockNum := identifier.BlockNumber
logIdx := identifier.LogIndex
_, err := su.chainDBs.Check(chainID, blockNum, uint32(logIdx), payloadHash)
_, err := su.chainDBs.Check(chainID, blockNum, uint32(logIdx), logHash)
if errors.Is(err, types.ErrFuture) {
return types.LocalUnsafe, nil
}
......
......@@ -358,7 +358,7 @@ func (db *ChainsDB) NextDerivedFrom(chain types.ChainID, derivedFrom eth.BlockID
// Safest returns the strongest safety level that can be guaranteed for the given log entry.
// it assumes the log entry has already been checked and is valid, this function only checks safety levels.
// Cross-safety levels are all considered to be more safe than any form of local-safety.
// Safety levels are assumed to graduate from LocalUnsafe to LocalSafe to CrossUnsafe to CrossSafe, with Finalized as the strongest.
func (db *ChainsDB) Safest(chainID types.ChainID, blockNum uint64, index uint32) (safest types.SafetyLevel, err error) {
db.mu.RLock()
defer db.mu.RUnlock()
......
......@@ -14,7 +14,6 @@ import (
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
const (
......@@ -74,7 +73,7 @@ func (i *CrossL2Inbox) DecodeExecutingMessageLog(l *ethTypes.Log) (types.Executi
if err != nil {
return types.ExecutingMessage{}, fmt.Errorf("failed to convert chain ID %v to uint32: %w", identifier.ChainId, err)
}
hash := payloadHashToLogHash(msgHash, identifier.Origin)
hash := types.PayloadHashToLogHash(msgHash, identifier.Origin)
return types.ExecutingMessage{
Chain: types.ChainIndex(chainID), // TODO(#11105): translate chain ID to chain index
Hash: hash,
......@@ -117,17 +116,3 @@ func identifierFromBytes(identifierBytes io.Reader) (contractIdentifier, error)
ChainId: chainID,
}, nil
}
// payloadHashToLogHash converts the payload hash to the log hash
// it is the concatenation of the log's address and the hash of the log's payload,
// which is then hashed again. This is the hash that is stored in the log storage.
// The logHash can then be used to traverse from the executing message
// to the log the referenced initiating message.
// TODO(#12424): this function is duplicated between contracts and backend/source/log_processor.go
// to avoid a circular dependency. It should be reorganized to avoid this duplication.
func payloadHashToLogHash(payloadHash common.Hash, addr common.Address) common.Hash {
msg := make([]byte, 0, 2*common.HashLength)
msg = append(msg, addr.Bytes()...)
msg = append(msg, payloadHash.Bytes()...)
return crypto.Keccak256Hash(msg)
}
......@@ -32,7 +32,7 @@ func TestDecodeExecutingMessageEvent(t *testing.T) {
Timestamp: new(big.Int).SetUint64(expected.Timestamp),
LogIndex: new(big.Int).SetUint64(uint64(expected.LogIdx)),
}
expected.Hash = payloadHashToLogHash(payloadHash, contractIdent.Origin)
expected.Hash = types.PayloadHashToLogHash(payloadHash, contractIdent.Origin)
abi := snapshots.LoadCrossL2InboxABI()
validData, err := abi.Events[eventExecutingMessage].Inputs.Pack(payloadHash, contractIdent)
require.NoError(t, err)
......
......@@ -78,7 +78,7 @@ func (p *logProcessor) ProcessLogs(_ context.Context, block eth.BlockRef, rcpts
// and because they represent paired data.
func logToLogHash(l *ethTypes.Log) common.Hash {
payloadHash := crypto.Keccak256(logToMessagePayload(l))
return payloadHashToLogHash(common.Hash(payloadHash), l.Address)
return types.PayloadHashToLogHash(common.Hash(payloadHash), l.Address)
}
// logToMessagePayload is the data that is hashed to get the logHash
......@@ -92,15 +92,3 @@ func logToMessagePayload(l *ethTypes.Log) []byte {
msg = append(msg, l.Data...)
return msg
}
// payloadHashToLogHash converts the payload hash to the log hash
// it is the concatenation of the log's address and the hash of the log's payload,
// which is then hashed. This is the hash that is stored in the log storage.
// The logHash can then be used to traverse from the executing message
// to the log the referenced initiating message.
func payloadHashToLogHash(payloadHash common.Hash, addr common.Address) common.Hash {
msg := make([]byte, 0, 2*common.HashLength)
msg = append(msg, addr.Bytes()...)
msg = append(msg, payloadHash.Bytes()...)
return crypto.Keccak256Hash(msg)
}
......@@ -12,6 +12,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
......@@ -251,3 +252,15 @@ func BlockSealFromRef(ref eth.BlockRef) BlockSeal {
Timestamp: ref.Time,
}
}
// PayloadHashToLogHash converts the payload hash to the log hash
// it is the concatenation of the log's address and the hash of the log's payload,
// which is then hashed again. This is the hash that is stored in the log storage.
// The logHash can then be used to traverse from the executing message
// to the log the referenced initiating message.
func PayloadHashToLogHash(payloadHash common.Hash, addr common.Address) common.Hash {
msg := make([]byte, 0, 2*common.HashLength)
msg = append(msg, addr.Bytes()...)
msg = append(msg, payloadHash.Bytes()...)
return crypto.Keccak256Hash(msg)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment