Last active
January 21, 2025 23:35
-
-
Save sigma/ca3c38ef69c07b2b7bf1091530c2bd25 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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