diff --git a/.github/workflows/espresso-devnet-tests.yaml b/.github/workflows/espresso-devnet-tests.yaml index 6f929ba63ae..f9bbade3a0d 100644 --- a/.github/workflows/espresso-devnet-tests.yaml +++ b/.github/workflows/espresso-devnet-tests.yaml @@ -21,7 +21,7 @@ jobs: tests: "TestSmokeWithoutTEE|TestBatcherRestart" tee: false - group: 2 - tests: "TestWithdrawal" + tests: "TestWithdrawal|TestBatcherSwitching" tee: false - group: 3 tests: "TestSmokeWithTEE|TestForcedTransaction" diff --git a/README_ESPRESSO.md b/README_ESPRESSO.md index 1c927d0a869..f3a543fcf7f 100644 --- a/README_ESPRESSO.md +++ b/README_ESPRESSO.md @@ -456,3 +456,4 @@ We are working on a set of scripts to handle the migration from a Celo Testnet t Some relevant documents: * [Documentation of configuration parameters](docs/README_ESPRESSO_DEPLOY_CONFIG.md) * [Celo Testnet Migration Guide](docs/CELO_TESTNET_MIGRATION.md) (WIP) + diff --git a/espresso/devnet-tests/batcher_switching_test.go b/espresso/devnet-tests/batcher_switching_test.go new file mode 100644 index 00000000000..74aa6a76528 --- /dev/null +++ b/espresso/devnet-tests/batcher_switching_test.go @@ -0,0 +1,87 @@ +package devnet_tests + +import ( + "context" + "testing" + + "github.com/ethereum-optimism/optimism/op-batcher/bindings" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" +) + +// TestBatcherSwitching tests that the batcher can be switched from the TEE-enabled +// batcher to a fallback non-TEE batcher using the BatchAuthenticator contract. +// +// This is the devnet equivalent of TestBatcherSwitching from the E2E tests. +// The test runs two batchers in parallel: +// - op-batcher: The primary batcher with Espresso enabled (initially active) +// - op-batcher-fallback: The fallback batcher without Espresso (initially stopped) +func TestBatcherSwitching(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize devnet with NON_TEE profile (starts both batchers) + d := NewDevnet(ctx, t) + require.NoError(t, d.Up(NON_TEE)) + defer func() { + require.NoError(t, d.Down()) + }() + + // Send initial transaction to verify everything has started up ok + require.NoError(t, d.RunSimpleL2Burn()) + + // Get rollup config to access BatchAuthenticator address + config, err := d.RollupConfig(ctx) + require.NoError(t, err) + + // Get L1 chain ID for transaction signing + l1ChainID, err := d.L1.ChainID(ctx) + require.NoError(t, err) + + // Create transactor options using the deployer key (owner of BatchAuthenticator) + deployerOpts, err := bind.NewKeyedTransactorWithChainID(d.secrets.Deployer, l1ChainID) + require.NoError(t, err) + + // Bind to BatchAuthenticator contract + batchAuthenticator, err := bindings.NewBatchAuthenticator(config.BatchAuthenticatorAddress, d.L1) + require.NoError(t, err) + + // Check current active batcher state before switching + activeIsTee, err := batchAuthenticator.ActiveIsTee(&bind.CallOpts{}) + require.NoError(t, err) + t.Logf("Before switch: activeIsTee = %v", activeIsTee) + + // Stop the primary "TEE" batcher (op-batcher with Espresso enabled) + require.NoError(t, d.StopBatcherSubmitting("op-batcher")) + t.Logf("Stopped op-batcher batch submission") + + // Switch active batcher via BatchAuthenticator contract + tx, err := batchAuthenticator.SwitchBatcher(deployerOpts) + require.NoError(t, err) + t.Logf("Submitted switchBatcher transaction: %s", tx.Hash().Hex()) + + // Wait for transaction receipt + receipt, err := wait.ForReceiptOK(ctx, d.L1, tx.Hash()) + require.NoError(t, err) + t.Logf("SwitchBatcher transaction confirmed in block %d", receipt.BlockNumber.Uint64()) + + // Verify the switch happened + activeIsTeeAfter, err := batchAuthenticator.ActiveIsTee(&bind.CallOpts{}) + require.NoError(t, err) + require.NotEqual(t, activeIsTee, activeIsTeeAfter, "activeIsTee should have toggled") + t.Logf("After switch: activeIsTee = %v", activeIsTeeAfter) + + // Start the fallback batcher + require.NoError(t, d.StartBatcherSubmitting("op-batcher-fallback")) + t.Logf("Started op-batcher-fallback batch submission") + + // Verify everything still works with the fallback batcher + require.NoError(t, d.RunSimpleL2Burn()) + t.Logf("Transaction verified with fallback batcher") + + // Submit another transaction and verify system continues to work + d.SleepRecoveryDuration() + require.NoError(t, d.RunSimpleL2Burn()) + t.Logf("System continues to work after batcher switch") +} diff --git a/espresso/devnet-tests/devnet_tools.go b/espresso/devnet-tests/devnet_tools.go index 5bae43e440d..6fe44c3e521 100644 --- a/espresso/devnet-tests/devnet_tools.go +++ b/espresso/devnet-tests/devnet_tools.go @@ -229,6 +229,36 @@ func (d *Devnet) ServiceRestart(service string) error { return nil } +// callBatcherRPC calls a batcher RPC method on a running batcher service +func (d *Devnet) callBatcherRPC(service, method string) error { + cmd := exec.CommandContext( + d.ctx, + "docker", "compose", "exec", "-T", service, + "sh", "-c", + fmt.Sprintf("wget -q -O- --header='Content-Type: application/json' --post-data='{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":[],\"id\":1}' http://localhost:8545", method), + ) + buf := new(bytes.Buffer) + cmd.Stdout = buf + cmd.Stderr = buf + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to call %s (%w): %s", method, err, buf.String()) + } + log.Info("RPC call successful", "service", service, "method", method, "response", buf.String()) + return nil +} + +// StartBatcherSubmitting starts batch submission on a running batcher service +func (d *Devnet) StartBatcherSubmitting(service string) error { + log.Info("starting batch submission", "service", service) + return d.callBatcherRPC(service, "admin_startBatcher") +} + +// StopBatcherSubmitting stops batch submission on a running batcher service +func (d *Devnet) StopBatcherSubmitting(service string) error { + log.Info("stopping batch submission", "service", service) + return d.callBatcherRPC(service, "admin_stopBatcher") +} + func (d *Devnet) RollupConfig(ctx context.Context) (*rollup.Config, error) { return d.L2SeqRollup.RollupConfig(ctx) } diff --git a/espresso/docker-compose.yml b/espresso/docker-compose.yml index f06e1f3f4d9..f9d210fafcc 100644 --- a/espresso/docker-compose.yml +++ b/espresso/docker-compose.yml @@ -391,6 +391,45 @@ services: - --altda.put-timeout=30s - --altda.get-timeout=30s - --data-availability-type=calldata + - --rpc.enable-admin + + op-batcher-fallback: + profiles: ["default"] + build: + context: ../ + dockerfile: espresso/docker/op-stack/Dockerfile + target: op-batcher-target + image: op-batcher:espresso + depends_on: + l1-geth: + condition: service_healthy + op-geth-sequencer: + condition: service_started + op-node-sequencer: + condition: service_started + l2-genesis: + condition: service_completed_successfully + environment: + L1_RPC: http://l1-geth:${L1_HTTP_PORT} + OP_BATCHER_L1_ETH_RPC: http://l1-geth:${L1_HTTP_PORT} + OP_BATCHER_L2_ETH_RPC: http://op-geth-sequencer:${OP_HTTP_PORT} + OP_BATCHER_ROLLUP_RPC: http://op-node-sequencer:${ROLLUP_PORT} + OP_BATCHER_MAX_CHANNEL_DURATION: ${MAX_CHANNEL_DURATION:-32} + OP_BATCHER_MAX_PENDING_TX: ${MAX_PENDING_TX:-32} + OP_BATCHER_STOPPED: "true" + volumes: + - ../packages/contracts-bedrock/lib/superchain-registry/ops/testdata/monorepo:/config + command: + - op-batcher + - --espresso.enabled=false + - --private-key=7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 + - --stopped=true + - --throttle-threshold=0 + - --max-channel-duration=2 + - --target-num-frames=1 + - --max-pending-tx=32 + - --data-availability-type=calldata + - --rpc.enable-admin op-batcher-tee: profiles: ["tee"] diff --git a/espresso/scripts/prepare-allocs.sh b/espresso/scripts/prepare-allocs.sh index 87803180c6d..887cb777ac9 100755 --- a/espresso/scripts/prepare-allocs.sh +++ b/espresso/scripts/prepare-allocs.sh @@ -77,9 +77,12 @@ op-deployer init --l1-chain-id "${L1_CHAIN_ID}" \ dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .chains.[0].espressoEnabled -t bool -v true -# Configure Espresso batchers for devnet. We reuse the operator address for both -# the non-TEE and TEE batchers to ensure they are non-zero and consistent. -dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .chains.[0].nonTeeBatcher -v "${OPERATOR_ADDRESS}" +# Configure Espresso batchers for devnet. We reuse the operator address for the +# TEE batcher, but use a separate address for the non-TEE fallback batcher. +# We use Anvil test account #3 for the fallback batcher (already prefunded by Anvil): +# Private key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a +# Address: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 +dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .chains.[0].nonTeeBatcher -v "0x90F79bf6EB2c4f870365E785982E1f101E93b906" dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .chains.[0].teeBatcher -v "${OPERATOR_ADDRESS}" dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .l1ContractsLocator -v "${ARTIFACTS_DIR}" dasel put -f "${DEPLOYER_DIR}/intent.toml" -s .l2ContractsLocator -v "${ARTIFACTS_DIR}" diff --git a/justfile b/justfile index 553fd13b55b..631d4a8ca9f 100644 --- a/justfile +++ b/justfile @@ -26,6 +26,9 @@ devnet-forced-transaction-test: build-devnet devnet-withdraw-test: build-devnet U_ID={{uid}} GID={{gid}} go test -timeout 30m -p 1 -count 1 -v -run TestWithdrawal ./espresso/devnet-tests/... +devnet-batcher-switching-test: build-devnet + U_ID={{uid}} GID={{gid}} go test -timeout 30m -p 1 -count 1 -v -run TestBatcherSwitching ./espresso/devnet-tests/... + build-devnet: compile-contracts rm -Rf espresso/deployment (cd op-deployer && just)