Skip to content

Instantly share code, notes, and snippets.

@sigma
Last active January 21, 2025 23:35
Show Gist options
  • Save sigma/ca3c38ef69c07b2b7bf1091530c2bd25 to your computer and use it in GitHub Desktop.
Save sigma/ca3c38ef69c07b2b7bf1091530c2bd25 to your computer and use it in GitHub Desktop.
package interop_smoke_test
import (
"bytes"
"testing"
"github.com/ethereum-optimism/optimism/devnet"
"github.com/ethereum-optimism/optimism/devnet/constraints"
"github.com/ethereum-optimism/optimism/devnet/contracts"
"github.com/ethereum-optimism/optimism/testlib/constants"
"github.com/ethereum-optimism/optimism/testlib/errors"
"github.com/ethereum-optimism/optimism/testlib/systest"
"github.com/ethereum-optimism/optimism/testlib/types"
"github.com/stretchr/testify/require"
)
var (
testUserMarker = &struct{}{}
)
// These are System validators.
// The idea is that we should have a structurally-separated way of:
// - checking that the test pre-conditions are met
// - some day automatically remediating gaps (e.g. the framework would recognize
// that something needs to be adjusted, and know how to do it). We probably
// would need some stronger error typing and explicit remediation handlers in
// order to get there.
//
// If the validators are general-purpose enough, they should probably land in a
// library.
type InteropValidator func(t systest.T, sys devnet.InteropSystem) systest.T
func interopSetSizeValidator(n int) InteropValidator {
return func(t systest.T, sys devnet.InteropSystem) systest.T {
if size := sys.InteropSet(t).Size(); size < n {
t.Skipf("expected %d chains, got %d", n, size)
}
return t
}
}
// This validator has a side-effect: it sets the user on the context. That's a
// generally useful mechanism to identify concrete pieces of data to be used in
// the test: not only the pre-condition is met, but we identify a concrete value
// that meets it.
func userFundsValidator(chainIdx uint64, minFunds types.Balance, userMarker interface{}) InteropValidator {
return func(t systest.T, sys devnet.InteropSystem) systest.T {
chain := sys.InteropSet(t).Chain(chainIdx)
user, err := chain.User(t.Context(), constraints.WithFunds(minFunds))
if err != nil {
t.Skipf("No available user with funds: %v", err)
}
return t.WithContext(t.Context().WithValue(userMarker, user))
}
}
// This is a System test.
// We express the need for a particular type of system, then pass it over to the test body.
// The actual test body will run only if all pre-conditions are met (therefore
// transforming a test failures issue into a test *coverage* issue).
// We could also imagine bypassing the preconditions checking in order to check
// their soundness, chaos-monkey style: if the tests passes anyway, that's
// anecdotical evidence that the precondition is too strict.
// Conversely, if we change the precondition parameters and the test behaves the
// same, then that's anecdotical evidence that the precondition is too weak.
func TestInteropL2ToL2(t *testing.T) {
systest.InteropSystemTest(t,
func(t systest.T, sys devnet.InteropSystem) {
ctx := t.Context()
interopSet := sys.InteropSet(t)
// we can safely get those chains due to the size validator
chain0 := interopSet.Chain(0)
chain1 := interopSet.Chain(1)
// we can safely get the user due to the funds validator
funds := 0.5 * constants.ETH
user := ctx.Value(testUserMarker).(types.Address)
// This implies the existence of a higher-level contract interface
// for each of the contracts we're using. We could imagine building
// that higher-level interface on top of some generated bindings for
// example.
scw0 := ResolveContract[contracts.SuperchainWETH](chain0, sys.ContractAddress(constants.SuperchainWETH))
tb0 := ResolveContract[contracts.SuperchainTokenBridge](chain0, sys.ContractAddress(constants.SuperchainTokenBridge))
xdm1 := ResolveContract[contracts.L2ToL2CrossDomainMessenger](chain1, sys.ContractAddress(constants.L2ToL2CrossDomainMessenger))
// WrapETH is an example of such a higher-level interface function.
// Generally we can imagine having synchronous (simulated) Calls,
// and asynchronous Sends (borrowing the "cast" terminology), that
// we can Wait() on.
require.NoError(t, scw0.WrapETH(user, funds).Send(ctx).Wait())
balance := scw0.BalanceOf(user).Call(ctx)
require.GreaterOrEqual(t, balance, funds)
// In some cases, we are interested in reasoning about the
// observable state changes that a particular function call
// generates.
//
// An idea for this would be to have a "state" type that can be
// populated using some callback (we can add an optional list of
// callbacks to Wait()).
//
// If the callbacks are general-purpose enough, they should probably
// land in a library.
cb := &BridgeCallback{emitter: xdm1.Address(), chainID: chain1.ID()}
tb0.SendERC20(user, scw0, funds, chain1.ID()).Send(ctx).Wait(cb.ExtractData)
// The state type can also be leveraged to compute parameters for
// other functions. Seems like a decent building block for
// higher-level functions (e.g. "synthetic" contract functions or
// something).
//
// In particular in the interop area, where messages are gonna move
// back and forth across chains, that seems like a generally useful
// patterm.
err := xdm1.RelayMessage(cb.L2ToL2Params(), cb.payload).Send(ctx).Wait()
require.NoError(t, err)
// Replaying the relay message a 2nd time should fail. Probably
// should have a higher-level helper to check for common error
// patterns.
err = xdm1.RelayMessage(cb.L2ToL2Params(), cb.payload).Send(ctx).Wait()
require.ErrorAs(t, err, &errors.ExecutionRevertedError{})
},
// Validate the pre-conditions. Should maybe use an option pattern for
// the arguments here?
interopSetSizeValidator(2),
userFundsValidator(0, 1*constants.ETH, testUserMarker),
)
}
type BridgeCallback struct {
emitter types.Address
chainID uint64
blockNumber uint64
logIndex uint64
timestamp uint64
payload []byte
}
func (b *BridgeCallback) L2ToL2Params() *contracts.L2ToL2CrossDomainMessengerParams {
return &contracts.L2ToL2CrossDomainMessengerParams{
Messenger: b.emitter,
BlockNumber: b.blockNumber,
LogIndex: b.logIndex,
Timestamp: b.timestamp,
ChainID: b.chainID,
}
}
func (b *BridgeCallback) ExtractData(t types.TransactionView) error {
logs := t.LogsFor(b.emitter)
require.Len(t, logs, 1)
log := logs[0]
b.blockNumber = log.BlockNumber()
b.logIndex = log.LogIndex()
b.timestamp = log.BlockTimestamp()
payload := bytes.NewBufferString("0x")
for _, topic := range log.Topics {
payload.WriteString(topic.Hex())
}
payload.WriteString(log.Data.Hex())
b.payload = payload.Bytes()
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment