Commit f39a124b authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-e2e: Always run subtests on the parent executor. (#9593)

* op-e2e: Always run subtests on the parent executor.

* op-e2e: Fix TestMixedWithdrawalValidity by not cancelling its own context.

Moves timeouts to be handled in helpers to reduce complexity of the test.

* op-e2e: Fix TestMixedWithdrawalValidity for fault proofs

* op-e2e: Evaluate test options for subtests

Ensures that tests are skipped correctly when using conditional options like UsesCannon even if not specified in the parent case.

Add InitParallel back to cannon tests so they can execute in parallel.
parent 6ae7288f
...@@ -66,6 +66,25 @@ func WithPollInterval(pollInterval time.Duration) Option { ...@@ -66,6 +66,25 @@ func WithPollInterval(pollInterval time.Duration) Option {
} }
} }
// findMonorepoRoot finds the relative path to the monorepo root
// Different tests might be nested in subdirectories of the op-e2e dir.
func findMonorepoRoot(t *testing.T) string {
path := "./"
// Only search up 5 directories
// Avoids infinite recursion if the root isn't found for some reason
for i := 0; i < 5; i++ {
_, err := os.Stat(path + "op-e2e")
if errors.Is(err, os.ErrNotExist) {
path = path + "../"
continue
}
require.NoErrorf(t, err, "Failed to stat %v even though it existed", path)
return path
}
t.Fatalf("Could not find monorepo root, trying up to %v", path)
return ""
}
func applyCannonConfig( func applyCannonConfig(
c *config.Config, c *config.Config,
t *testing.T, t *testing.T,
...@@ -75,9 +94,10 @@ func applyCannonConfig( ...@@ -75,9 +94,10 @@ func applyCannonConfig(
) { ) {
require := require.New(t) require := require.New(t)
c.CannonL2 = l2Endpoint c.CannonL2 = l2Endpoint
c.CannonBin = "../../cannon/bin/cannon" root := findMonorepoRoot(t)
c.CannonServer = "../../op-program/bin/op-program" c.CannonBin = root + "cannon/bin/cannon"
c.CannonAbsolutePreState = "../../op-program/bin/prestate.json" c.CannonServer = root + "op-program/bin/op-program"
c.CannonAbsolutePreState = root + "op-program/bin/prestate.json"
c.CannonSnapshotFreq = 10_000_000 c.CannonSnapshotFreq = 10_000_000
genesisBytes, err := json.Marshal(l2Genesis) genesisBytes, err := json.Marshal(l2Genesis)
......
...@@ -13,13 +13,14 @@ import ( ...@@ -13,13 +13,14 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
) )
// ForOutputRootPublished waits until there is an output published for an L2 block number larger than the supplied l2BlockNumber // ForOutputRootPublished waits until there is an output published for an L2 block number larger than the supplied l2BlockNumber
// This function polls and can block for a very long time if used on mainnet. // This function polls and can block for a very long time if used on mainnet.
// This returns the block number to use for proof generation. // This returns the block number to use for proof generation.
func ForOutputRootPublished(ctx context.Context, client *ethclient.Client, l2OutputOracleAddr common.Address, l2BlockNumber *big.Int) (uint64, error) { func ForOutputRootPublished(ctx context.Context, client *ethclient.Client, l2OutputOracleAddr common.Address, l2BlockNumber *big.Int) (uint64, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
l2BlockNumber = new(big.Int).Set(l2BlockNumber) // Don't clobber caller owned l2BlockNumber l2BlockNumber = new(big.Int).Set(l2BlockNumber) // Don't clobber caller owned l2BlockNumber
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
...@@ -76,6 +77,8 @@ func ForFinalizationPeriod(ctx context.Context, client *ethclient.Client, l1Prov ...@@ -76,6 +77,8 @@ func ForFinalizationPeriod(ctx context.Context, client *ethclient.Client, l1Prov
// ForGamePublished waits until a game is published on L1 for the given l2BlockNumber. // ForGamePublished waits until a game is published on L1 for the given l2BlockNumber.
func ForGamePublished(ctx context.Context, client *ethclient.Client, optimismPortalAddr common.Address, disputeGameFactoryAddr common.Address, l2BlockNumber *big.Int) (uint64, error) { func ForGamePublished(ctx context.Context, client *ethclient.Client, optimismPortalAddr common.Address, disputeGameFactoryAddr common.Address, l2BlockNumber *big.Int) (uint64, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
l2BlockNumber = new(big.Int).Set(l2BlockNumber) // Don't clobber caller owned l2BlockNumber l2BlockNumber = new(big.Int).Set(l2BlockNumber) // Don't clobber caller owned l2BlockNumber
optimismPortal2Contract, err := bindingspreview.NewOptimismPortal2Caller(optimismPortalAddr, client) optimismPortal2Contract, err := bindingspreview.NewOptimismPortal2Caller(optimismPortalAddr, client)
...@@ -108,6 +111,8 @@ func ForGamePublished(ctx context.Context, client *ethclient.Client, optimismPor ...@@ -108,6 +111,8 @@ func ForGamePublished(ctx context.Context, client *ethclient.Client, optimismPor
// ForWithdrawalCheck waits until the withdrawal check in the portal succeeds. // ForWithdrawalCheck waits until the withdrawal check in the portal succeeds.
func ForWithdrawalCheck(ctx context.Context, client *ethclient.Client, withdrawal crossdomain.Withdrawal, optimismPortalAddr common.Address) error { func ForWithdrawalCheck(ctx context.Context, client *ethclient.Client, withdrawal crossdomain.Withdrawal, optimismPortalAddr common.Address) error {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
portal, err := bindingspreview.NewOptimismPortal2Caller(optimismPortalAddr, client) portal, err := bindingspreview.NewOptimismPortal2Caller(optimismPortalAddr, client)
if err != nil { if err != nil {
...@@ -115,14 +120,12 @@ func ForWithdrawalCheck(ctx context.Context, client *ethclient.Client, withdrawa ...@@ -115,14 +120,12 @@ func ForWithdrawalCheck(ctx context.Context, client *ethclient.Client, withdrawa
} }
return For(ctx, time.Second, func() (bool, error) { return For(ctx, time.Second, func() (bool, error) {
log.Warn("checking withdrawal!")
wdHash, err := withdrawal.Hash() wdHash, err := withdrawal.Hash()
if err != nil { if err != nil {
return false, fmt.Errorf("hash withdrawal: %w", err) return false, fmt.Errorf("hash withdrawal: %w", err)
} }
err = portal.CheckWithdrawal(opts, wdHash) err = portal.CheckWithdrawal(opts, wdHash)
log.Warn("checking withdrawal", "hash", wdHash, "err", err)
return err == nil, nil return err == nil, nil
}) })
} }
...@@ -101,6 +101,7 @@ func TestOutputCannon_ChallengeAllZeroClaim(t *testing.T) { ...@@ -101,6 +101,7 @@ func TestOutputCannon_ChallengeAllZeroClaim(t *testing.T) {
} }
func TestOutputCannon_PublishCannonRootClaim(t *testing.T) { func TestOutputCannon_PublishCannonRootClaim(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
tests := []struct { tests := []struct {
disputeL2BlockNumber uint64 disputeL2BlockNumber uint64
}{ }{
...@@ -129,6 +130,7 @@ func TestOutputCannon_PublishCannonRootClaim(t *testing.T) { ...@@ -129,6 +130,7 @@ func TestOutputCannon_PublishCannonRootClaim(t *testing.T) {
} }
func TestOutputCannonDisputeGame(t *testing.T) { func TestOutputCannonDisputeGame(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
tests := []struct { tests := []struct {
name string name string
defendClaimDepth types.Depth defendClaimDepth types.Depth
...@@ -254,6 +256,7 @@ func TestOutputCannonStepWithLargePreimage(t *testing.T) { ...@@ -254,6 +256,7 @@ func TestOutputCannonStepWithLargePreimage(t *testing.T) {
} }
func TestOutputCannonStepWithPreimage(t *testing.T) { func TestOutputCannonStepWithPreimage(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
testPreimageStep := func(t *testing.T, preimageType cannon.PreimageOpt, preloadPreimage bool) { testPreimageStep := func(t *testing.T, preimageType cannon.PreimageOpt, preloadPreimage bool) {
op_e2e.InitParallel(t, op_e2e.UsesCannon) op_e2e.InitParallel(t, op_e2e.UsesCannon)
...@@ -296,6 +299,7 @@ func TestOutputCannonStepWithPreimage(t *testing.T) { ...@@ -296,6 +299,7 @@ func TestOutputCannonStepWithPreimage(t *testing.T) {
func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) { func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) {
t.Skip("TODO: Fix flaky test") t.Skip("TODO: Fix flaky test")
op_e2e.InitParallel(t, op_e2e.UsesCannon)
testPreimageStep := func(t *testing.T, preloadPreimage bool) { testPreimageStep := func(t *testing.T, preloadPreimage bool) {
op_e2e.InitParallel(t, op_e2e.UsesCannon) op_e2e.InitParallel(t, op_e2e.UsesCannon)
...@@ -337,6 +341,7 @@ func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) { ...@@ -337,6 +341,7 @@ func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) {
} }
func TestOutputCannonProposedOutputRootValid(t *testing.T) { func TestOutputCannonProposedOutputRootValid(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
// honestStepsFail attempts to perform both an attack and defend step using the correct trace. // honestStepsFail attempts to perform both an attack and defend step using the correct trace.
honestStepsFail := func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, parentClaimIdx int64) { honestStepsFail := func(ctx context.Context, game *disputegame.OutputCannonGameHelper, correctTrace *disputegame.OutputHonestHelper, parentClaimIdx int64) {
// Attack step should fail // Attack step should fail
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
) )
func TestPrecompiles(t *testing.T) { func TestPrecompiles(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
// precompile test vectors copied from go-ethereum // precompile test vectors copied from go-ethereum
tests := []struct { tests := []struct {
name string name string
......
...@@ -4,65 +4,61 @@ import ( ...@@ -4,65 +4,61 @@ import (
"crypto/md5" "crypto/md5"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
) )
var enableParallelTesting bool = os.Getenv("OP_E2E_DISABLE_PARALLEL") != "true" var enableParallelTesting bool = os.Getenv("OP_E2E_DISABLE_PARALLEL") != "true"
type testopts struct { func InitParallel(t e2eutils.TestingBase, args ...func(t e2eutils.TestingBase)) {
executor uint64
}
func InitParallel(t e2eutils.TestingBase, args ...func(t e2eutils.TestingBase, opts *testopts)) {
t.Helper() t.Helper()
if enableParallelTesting { if enableParallelTesting {
t.Parallel() t.Parallel()
} }
for _, arg := range args {
arg(t)
}
autoAllocateExecutor(t)
}
// isSubTest determines if the test is a sub-test or top level test.
// It does this by checking if the test name contains /
// This is not a particularly great way check, but appears to be the only option currently.
func isSubTest(t e2eutils.TestingBase) bool {
return strings.Contains(t.Name(), "/")
}
func autoAllocateExecutor(t e2eutils.TestingBase) {
if isSubTest(t) {
// Always run subtests, they only start on the same executor as their parent.
return
}
info := getExecutorInfo(t) info := getExecutorInfo(t)
tName := t.Name() tName := t.Name()
tHash := md5.Sum([]byte(tName)) tHash := md5.Sum([]byte(tName))
executor := uint64(tHash[0]) % info.total executor := uint64(tHash[0]) % info.total
opts := &testopts{ checkExecutor(t, info, executor)
executor: executor,
}
for _, arg := range args {
arg(t, opts)
}
checkExecutor(t, info, opts.executor)
} }
func UsesCannon(t e2eutils.TestingBase, opts *testopts) { func UsesCannon(t e2eutils.TestingBase) {
if os.Getenv("OP_E2E_CANNON_ENABLED") == "false" { if os.Getenv("OP_E2E_CANNON_ENABLED") == "false" {
t.Skip("Skipping cannon test") t.Skip("Skipping cannon test")
} }
} }
func SkipOnFPAC(t e2eutils.TestingBase, opts *testopts) { func SkipOnFPAC(t e2eutils.TestingBase) {
if e2eutils.UseFPAC() { if e2eutils.UseFPAC() {
t.Skip("Skipping test for FPAC") t.Skip("Skipping test for FPAC")
} }
} }
func SkipOnNotFPAC(t e2eutils.TestingBase, opts *testopts) { func SkipOnNotFPAC(t e2eutils.TestingBase) {
if !e2eutils.UseFPAC() { if !e2eutils.UseFPAC() {
t.Skip("Skipping test for non-FPAC") t.Skip("Skipping test for non-FPAC")
} }
} }
// UseExecutor allows manually splitting tests between circleci executors
//
// Tests default to run on the first executor but can be moved to the second with:
// InitParallel(t, UseExecutor(1))
// Any tests assigned to an executor greater than the number available automatically use the last executor.
// Executor indexes start from 0
func UseExecutor(assignedIdx uint64) func(t e2eutils.TestingBase, opts *testopts) {
return func(t e2eutils.TestingBase, opts *testopts) {
opts.executor = assignedIdx
}
}
type executorInfo struct { type executorInfo struct {
total uint64 total uint64
idx uint64 idx uint64
......
...@@ -15,6 +15,8 @@ import ( ...@@ -15,6 +15,8 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/withdrawals" "github.com/ethereum-optimism/optimism/op-node/withdrawals"
...@@ -546,7 +548,6 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -546,7 +548,6 @@ func TestMixedWithdrawalValidity(t *testing.T) {
transactor.ExpectedL2Nonce = transactor.ExpectedL2Nonce + 1 transactor.ExpectedL2Nonce = transactor.ExpectedL2Nonce + 1
// Wait for the finalization period, then we can finalize this withdrawal. // Wait for the finalization period, then we can finalize this withdrawal.
ctx, withdrawalCancel := context.WithTimeout(context.Background(), 60*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.NotEqual(t, cfg.L1Deployments.L2OutputOracleProxy, common.Address{}) require.NotEqual(t, cfg.L1Deployments.L2OutputOracleProxy, common.Address{})
var blockNumber uint64 var blockNumber uint64
if e2eutils.UseFPAC() { if e2eutils.UseFPAC() {
...@@ -554,12 +555,9 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -554,12 +555,9 @@ func TestMixedWithdrawalValidity(t *testing.T) {
} else { } else {
blockNumber, err = wait.ForOutputRootPublished(ctx, l1Client, cfg.L1Deployments.L2OutputOracleProxy, receipt.BlockNumber) blockNumber, err = wait.ForOutputRootPublished(ctx, l1Client, cfg.L1Deployments.L2OutputOracleProxy, receipt.BlockNumber)
} }
withdrawalCancel()
require.Nil(t, err) require.Nil(t, err)
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
header, err = l2Verif.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) header, err = l2Verif.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber))
txCancel()
require.Nil(t, err) require.Nil(t, err)
rpcClient, err := rpc.Dial(sys.EthInstances["verifier"].WSEndpoint()) rpcClient, err := rpc.Dial(sys.EthInstances["verifier"].WSEndpoint())
...@@ -658,14 +656,19 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -658,14 +656,19 @@ func TestMixedWithdrawalValidity(t *testing.T) {
} else { } else {
require.NoError(t, err) require.NoError(t, err)
if e2eutils.UseFPAC() {
// Start a challenger to resolve claims and games once the clock expires
factoryHelper := disputegame.NewFactoryHelper(t, ctx, sys)
factoryHelper.StartChallenger(ctx, "Challenger",
challenger.WithCannon(t, sys.RollupConfig, sys.L2GenesisCfg, sys.RollupEndpoint("sequencer"), sys.NodeEndpoint("sequencer")),
challenger.WithPrivKey(sys.Cfg.Secrets.Mallory))
}
receipt, err = wait.ForReceiptOK(ctx, l1Client, tx.Hash()) receipt, err = wait.ForReceiptOK(ctx, l1Client, tx.Hash())
require.Nil(t, err, "finalize withdrawal") require.NoError(t, err, "finalize withdrawal")
// Verify balance after withdrawal // Verify balance after withdrawal
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
header, err = l1Client.HeaderByNumber(ctx, receipt.BlockNumber) header, err = l1Client.HeaderByNumber(ctx, receipt.BlockNumber)
txCancel() require.NoError(t, err)
require.Nil(t, err)
// Ensure that withdrawal - gas fees are added to the L1 balance // Ensure that withdrawal - gas fees are added to the L1 balance
// Fun fact, the fee is greater than the withdrawal amount // Fun fact, the fee is greater than the withdrawal amount
...@@ -676,17 +679,17 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -676,17 +679,17 @@ func TestMixedWithdrawalValidity(t *testing.T) {
// Ensure that our withdrawal was proved successfully // Ensure that our withdrawal was proved successfully
_, err := wait.ForReceiptOK(ctx, l1Client, tx.Hash()) _, err := wait.ForReceiptOK(ctx, l1Client, tx.Hash())
require.Nil(t, err, "prove withdrawal") require.NoError(t, err, "prove withdrawal")
// Wait for finalization and then create the Finalized Withdrawal Transaction // Wait for finalization and then create the Finalized Withdrawal Transaction
ctx, withdrawalCancel := context.WithTimeout(context.Background(), 60*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) ctx, withdrawalCancel := context.WithTimeout(context.Background(), 60*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
defer withdrawalCancel() defer withdrawalCancel()
if e2eutils.UseFPAC() { if e2eutils.UseFPAC() {
err = wait.ForWithdrawalCheck(ctx, l1Client, withdrawal, cfg.L1Deployments.OptimismPortalProxy) err = wait.ForWithdrawalCheck(ctx, l1Client, withdrawal, cfg.L1Deployments.OptimismPortalProxy)
require.Nil(t, err) require.NoError(t, err)
} else { } else {
err = wait.ForFinalizationPeriod(ctx, l1Client, header.Number, cfg.L1Deployments.L2OutputOracleProxy) err = wait.ForFinalizationPeriod(ctx, l1Client, header.Number, cfg.L1Deployments.L2OutputOracleProxy)
require.Nil(t, err) require.NoError(t, err)
} }
// Finalize withdrawal // Finalize withdrawal
...@@ -700,39 +703,27 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -700,39 +703,27 @@ func TestMixedWithdrawalValidity(t *testing.T) {
// At the end, assert our account balance/nonce states. // At the end, assert our account balance/nonce states.
// Obtain the L2 sequencer account balance // Obtain the L2 sequencer account balance
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL1Balance, err := l1Client.BalanceAt(ctx, transactor.Account.L1Opts.From, nil) endL1Balance, err := l1Client.BalanceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// Obtain the L1 account nonce // Obtain the L1 account nonce
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL1Nonce, err := l1Client.NonceAt(ctx, transactor.Account.L1Opts.From, nil) endL1Nonce, err := l1Client.NonceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// Obtain the L2 sequencer account balance // Obtain the L2 sequencer account balance
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL2SeqBalance, err := l2Seq.BalanceAt(ctx, transactor.Account.L1Opts.From, nil) endL2SeqBalance, err := l2Seq.BalanceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// Obtain the L2 sequencer account nonce // Obtain the L2 sequencer account nonce
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL2SeqNonce, err := l2Seq.NonceAt(ctx, transactor.Account.L1Opts.From, nil) endL2SeqNonce, err := l2Seq.NonceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// Obtain the L2 verifier account balance // Obtain the L2 verifier account balance
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL2VerifBalance, err := l2Verif.BalanceAt(ctx, transactor.Account.L1Opts.From, nil) endL2VerifBalance, err := l2Verif.BalanceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// Obtain the L2 verifier account nonce // Obtain the L2 verifier account nonce
ctx, txCancel = context.WithTimeout(context.Background(), txTimeoutDuration)
endL2VerifNonce, err := l2Verif.NonceAt(ctx, transactor.Account.L1Opts.From, nil) endL2VerifNonce, err := l2Verif.NonceAt(ctx, transactor.Account.L1Opts.From, nil)
txCancel()
require.NoError(t, err) require.NoError(t, err)
// TODO: Check L1 balance as well here. We avoided this due to time constraints as it seems L1 fees // TODO: Check L1 balance as well here. We avoided this due to time constraints as it seems L1 fees
......
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