Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/docker-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ jobs:
toolchain: stable
override: true

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
Expand Down Expand Up @@ -344,7 +339,7 @@ jobs:

build-op-batcher-tee:
needs: prepare-deployment
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-8core
permissions:
contents: read
packages: write
Expand Down
45 changes: 0 additions & 45 deletions README_ESPRESSO.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,51 +355,6 @@ In order to refresh this AMI one needs to:
2. Copy the script `espresso/scrips/enclave-prepare-ami.sh` in the EC2 instance (e.g. using scp) and run it.
3. [Export the AMI instance](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/tkv-create-ami-from-instance.html).

## Demo to Celo
For convenience some scripts have been added to make it easier to showcase the
results, and monitor the progress of the docker compose file. The primary
script concerns evaluating `optimism_syncStatus` and displaying the results.

This script requires the commands `tmux`, and `watch` to be installed and
in the `PATH`. Check to see if you have them, and if you don't, be sure to
install them using whatever method you deem necessary in order to run the
script.

After that has been done you should be able to spin up the simple script
using the following command:
```console
./espresso/scripts/demo_tmux_get_sync_status.sh
```

This will launch a `tmux` session setup with a script to automatically
query and display the `optimism_syncStatus` result for the `sequencer`,
`verifier`, and `caff-node`.

It assumes that the `docker-file.yml` is being run with the default values
and will attempt to connect to them as needed.

If you're not used to `tmux` you should be able to disconnect from the session
using `<C-b> d`. This only detaches from the session, the session will still
exist and be running in the background. You can kill the session using the
following command:
```console
tmux kill-session
```

Or you can reattach to it using this command instead:
```console
tmux attach
```

If you want to target different RPC endpoints for optimism, if you're not
running the local demo, and want to target the remote, you can always
specify environment variables before running the script:
```console
OP_RPC_SEQUENCER=http://sequencer.example.com:4545 \
OP_RPC_VERIFIER=http://verifier.example.com:4545 \
OP_RPC_CAFF=http://caff.example.com:4545 \
./espresso/scripts/demo_tmux_get_sync_status.sh
```

## Celo Deployment

Expand Down
2 changes: 2 additions & 0 deletions espresso/.env
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ BATCH_AUTHENTICATOR_OWNER_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751

# cast wallet address --mnemonic "test test ... junk" --hd-path "m/44'/60'/0'/0/1"
PROPOSER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
# cast wallet private-key --mnemonic "test test ... junk" --hd-path "m/44'/60'/0'/0/1"
PROPOSER_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

# cast wallet address --mnemonic "test test ... junk" --hd-path "m/44'/60'/0'/0/5"
NON_TEE_BATCHER_ADDRESS=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
Expand Down
92 changes: 73 additions & 19 deletions espresso/devnet-tests/challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-e2e/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/stretchr/testify/require"
)

// TestChallengeGame verifies that the succinct proposer creates dispute games
// and that games can be queried from the DisputeGameFactory contract.
// The succinct proposer needs finalized L2 blocks before creating games.
func TestChallengeGame(t *testing.T) {
t.Skip("Temporarily skipped: Re-enable once Succinct Integration is investigated.")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand All @@ -20,33 +24,83 @@ func TestChallengeGame(t *testing.T) {
require.NoError(t, d.Down())
}()

// Wait for the proposer to make a claim.
// Verify devnet is running and generate some L2 activity
require.NoError(t, d.RunSimpleL2Burn())

// Wait a bit for blocks to be produced and batched
t.Log("Waiting for blocks to be produced and batched...")
time.Sleep(10 * time.Second)

// Wait for the succinct proposer to create a dispute game
// The proposer creates games when safe L2 head >= anchor + proposal_interval (3 blocks)
t.Log("Waiting for succinct-proposer to create a dispute game...")
var games []ChallengeGame
maxGameWait := 2 * time.Minute
gameWaitStart := time.Now()

for len(games) == 0 {
var err error
t.Logf("waiting for a challenge game")
if time.Since(gameWaitStart) > maxGameWait {
t.Fatalf("timeout waiting for dispute game to be created (waited %v)", maxGameWait)
}

t.Logf("waiting for a challenge game to be created by succinct-proposer...")
time.Sleep(5 * time.Second)

var err error
games, err = d.ListChallengeGames()
require.NoError(t, err)
if err != nil {
t.Logf("error listing games (will retry): %v", err)
}
}
t.Logf("game created: %v", games[0])
require.Equal(t, uint64(1), games[0].Claims)

// Attack the first claimed state.
t.Logf("attacking game")
require.NoError(t, d.OpChallenger("move", "--attack", "--game-address", games[0].Address.Hex()))
t.Logf("game created: index=%d, address=%s, claims=%d",
games[0].Index, games[0].Address.Hex(), games[0].Claims)

// Verify the game has at least 1 claim (the root claim from proposer)
require.GreaterOrEqual(t, games[0].Claims, uint64(1), "Game should have at least 1 claim")

// Bind the dispute game contract and log its initial status.
disputeGame, err := bindings.NewFaultDisputeGame(games[0].Address, d.L1)
require.NoError(t, err)
statusRaw, err := disputeGame.Status(&bind.CallOpts{})
require.NoError(t, err)
gameStatus, err := types.GameStatusFromUint8(statusRaw)
require.NoError(t, err)
t.Logf("dispute game initial status: %s (%d)", gameStatus.String(), statusRaw)
require.Equal(t, types.GameStatusInProgress, gameStatus, "Dispute game should start InProgress")

// Observe the dispute game for a limited time to see if it resolves.
maxObservation := 15 * time.Minute
pollInterval := 10 * time.Second
waitStart := time.Now()
finalStatus := gameStatus
finalStatusRaw := statusRaw

t.Logf("Observing dispute game %s for up to %s to see if it resolves...", games[0].Address.Hex(), maxObservation)

// Check that the proposer correctly responds.
CLAIMS_NUMBER := uint64(3) // First claim by the proposer + attack + response
for {
updatedGames, err := d.ListChallengeGames()
for time.Since(waitStart) < maxObservation {
statusRaw, err := disputeGame.Status(&bind.CallOpts{})
require.NoError(t, err)
if updatedGames[0].Claims == CLAIMS_NUMBER {
require.Equal(t, updatedGames[0].OutputRoot, games[0].OutputRoot)
status, err := types.GameStatusFromUint8(statusRaw)
require.NoError(t, err)

finalStatus = status
finalStatusRaw = statusRaw

if status != types.GameStatusInProgress {
t.Logf("dispute game resolved during observation window: %s (%d)", status.String(), statusRaw)
require.Equal(t, types.GameStatusDefenderWon, status, "Expected honest proposer/defender to win succinct dispute game")
break
}

t.Logf("waiting for a response")
time.Sleep(time.Second)
time.Sleep(pollInterval)
}

t.Logf("dispute game observed final status after %s: %s (%d)", time.Since(waitStart), finalStatus.String(), finalStatusRaw)
require.Equal(t, finalStatus, types.GameStatusDefenderWon,
"succinct dispute game final status must be DefenderWon, got %s (%d)",
finalStatus.String(), finalStatusRaw,
)

t.Logf("TestChallengeGame passed: dispute game successfully created by succinct-proposer")
}
74 changes: 63 additions & 11 deletions espresso/devnet-tests/devnet_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,29 +593,81 @@ func ParseChallengeGame(s string) (ChallengeGame, error) {
}

func (d *Devnet) ListChallengeGames() ([]ChallengeGame, error) {
output, err := d.OpChallengerOutput("list-games")
// Succinct only supports contract-based query
games, err := d.ListChallengeGamesFromContract()
if err == nil && len(games) > 0 {
return games, nil
}

return nil, fmt.Errorf("failed to list challenge games: %w", err)
}

// ListChallengeGamesFromContract queries games directly from the DisputeGameFactory contract
// Only supports OPSuccinctFaultDisputeGame (game type 42)
func (d *Devnet) ListChallengeGamesFromContract() ([]ChallengeGame, error) {
ctx := d.ctx

// Get SystemConfig to find DisputeGameFactory address
systemConfig, _, err := d.SystemConfig(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get system config: %w", err)
}

factoryAddr, err := systemConfig.DisputeGameFactory(&bind.CallOpts{})
if err != nil {
return nil, fmt.Errorf("failed to get dispute game factory address: %w", err)
}

// Bind to DisputeGameFactory
factory, err := bindings.NewDisputeGameFactory(factoryAddr, d.L1)
if err != nil {
return nil, fmt.Errorf("failed to bind to dispute game factory: %w", err)
}

// Get game count
gameCount, err := factory.GameCount(&bind.CallOpts{})
if err != nil {
return nil, fmt.Errorf("failed to get game count: %w", err)
}

var games []ChallengeGame
for i, line := range strings.Split(output, "\n") {
if i == 0 {
// Ignore header.
for i := uint64(0); i < gameCount.Uint64(); i++ {
// Get game at index
gameInfo, err := factory.GameAtIndex(&bind.CallOpts{}, new(big.Int).SetUint64(i))
if err != nil {
log.Warn("failed to get game at index", "index", i, "error", err)
continue
}

// Only include game type 42 (OPSuccinctFaultDisputeGame)
if gameInfo.GameType != 42 {
continue
}
line = strings.TrimSpace(line)
if len(line) == 0 {
// Ignore empty lines (e.g. trailing newline)

gameProxy := gameInfo.Proxy

// Get root claim from the game contract
// OPSuccinctFaultDisputeGame only has root claim, no claim tree
disputeGame, err := bindings.NewFaultDisputeGame(gameProxy, d.L1)
if err != nil {
log.Warn("failed to bind to dispute game", "address", gameProxy, "error", err)
continue
}

game, err := ParseChallengeGame(line)
rootClaim, err := disputeGame.RootClaim(&bind.CallOpts{})
if err != nil {
return nil, fmt.Errorf("game %v is invalid: %w", i, err)
log.Warn("failed to get root claim", "address", gameProxy, "error", err)
continue
}
games = append(games, game)

games = append(games, ChallengeGame{
Index: i,
Address: gameProxy,
OutputRoot: rootClaim[:],
Claims: 1, // OPSuccinctFaultDisputeGame only has root claim
})
}

return games, nil
}

Expand Down
1 change: 0 additions & 1 deletion espresso/devnet-tests/withdraw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ func finalizeWithdrawal(d *Devnet, ctx context.Context, t *testing.T, userAddres

func TestWithdrawal(t *testing.T) {
t.Skip("Temporarily skipped: Re-enable once Succinct Integration is investigated.")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand Down
Loading
Loading