Skip to content

Commit f357150

Browse files
Test a challenge game in the docker devnet (#228)
* Add test for devnet challenge game * Fixes * Make sure batcher key used by default in the devnet tests is the same one registered in the inbox contract * Remove check in batcher that prevents it from sending transactions to Espresso immediately * Ensures that the deployment files are deleted before building a new devnet. Update README_ESPRESSO.md to remind running docker as a non root user. * Run devnet tests on CI again. * Ensure deployment files are not written by the root user. * Ignore rotate batcher key and change batch inbox owmer tests. * Clean way of setting UID and GID. * Ignore devnet tests for now so that we can merge. * Add fallback values for UID and GID. * Pinpoint forge version in CI as the linter is complaining. * Add comment regarding the number of claims. * Add comment to function TaggedWriter.Write. --------- Co-authored-by: Philippe Camacho <[email protected]>
1 parent ef5cefa commit f357150

File tree

14 files changed

+326
-51
lines changed

14 files changed

+326
-51
lines changed

.github/workflows/docker-images.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
- name: Install Foundry
4545
uses: foundry-rs/foundry-toolchain@v1
4646
with:
47-
version: nightly
47+
version: nightly-654c8f01721e43dbc8a53c7a3b022548cb82b2f9 # same as for the nix environment
4848

4949
- name: Install dasel
5050
run: |

.github/workflows/espresso-devnet-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
docker compose build
5252
5353
# - name: Run Devnet tests
54-
# run: go test -timeout 30m -p 1 -count 1 -v ./espresso/devnet-tests/...
54+
# run: go test -timeout 30m -p 1 -count 1 -v ./espresso/devnet-tests/...
5555

5656
- name: Save Nix cache
5757
uses: nix-community/cache-nix-action/save@v6

README_ESPRESSO.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Provide Docker with the PAT.
3333
> echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
3434
```
3535

36+
Run docker as a non root user:
37+
```console
38+
> sudo add group docker
39+
> sudo usermod -aG docker $USER
40+
```
41+
3642
### Run the tests
3743

3844
Run the Espresso smoke tests:

espresso/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf
3434
# cast wallet address --private-key $OPERATOR_PRIVATE_KEY
3535
OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
3636

37+
# cast wallet address --mnemonic "test test ... junk" --hd-path "m/44'/60'/0'/0/1"
38+
PROPOSER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
39+
3740
L1_CHAIN_ID=11155111
3841
L2_CHAIN_ID=22266222
3942

espresso/devnet-tests/batcher_restart_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func TestBatcherRestart(t *testing.T) {
1313
defer cancel()
1414

1515
d := NewDevnet(ctx, t)
16-
require.NoError(t, d.Up(testing.Verbose()))
16+
require.NoError(t, d.Up())
1717
defer func() {
1818
require.NoError(t, d.Down())
1919
}()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package devnet_tests
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestChallengeGame(t *testing.T) {
12+
ctx, cancel := context.WithCancel(context.Background())
13+
defer cancel()
14+
15+
d := NewDevnet(ctx, t)
16+
require.NoError(t, d.Up())
17+
defer func() {
18+
require.NoError(t, d.Down())
19+
}()
20+
21+
// Wait for the proposer to make a claim.
22+
var games []ChallengeGame
23+
for len(games) == 0 {
24+
var err error
25+
t.Logf("waiting for a challenge game")
26+
time.Sleep(5 * time.Second)
27+
games, err = d.ListChallengeGames()
28+
require.NoError(t, err)
29+
}
30+
t.Logf("game created: %v", games[0])
31+
require.Equal(t, uint64(1), games[0].Claims)
32+
33+
// Attack the first claimed state.
34+
t.Logf("attacking game")
35+
require.NoError(t, d.OpChallenger("move", "--attack", "--game-address", games[0].Address.Hex()))
36+
37+
// Check that the proposer correctly responds.
38+
CLAIMS_NUMBER := uint64(3) // First claim by the proposer + attack + response
39+
for {
40+
updatedGames, err := d.ListChallengeGames()
41+
require.NoError(t, err)
42+
if updatedGames[0].Claims == CLAIMS_NUMBER {
43+
require.Equal(t, updatedGames[0].OutputRoot, games[0].OutputRoot)
44+
break
45+
}
46+
47+
t.Logf("waiting for a response")
48+
time.Sleep(time.Second)
49+
}
50+
}

espresso/devnet-tests/devnet_tools.go

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/hex"
77
"fmt"
8+
"io"
89
"math/big"
910
"os"
1011
"os/exec"
@@ -50,9 +51,15 @@ func NewDevnet(ctx context.Context, t *testing.T) *Devnet {
5051

5152
d := new(Devnet)
5253
d.ctx = ctx
53-
d.secrets = *secrets.DefaultSecrets
5454

55-
var err error
55+
mnemonics := *secrets.DefaultMnemonicConfig
56+
mnemonics.Batcher = "m/44'/60'/0'/0/0"
57+
secrets, err := mnemonics.Secrets()
58+
if err != nil {
59+
panic(fmt.Sprintf("failed to create default secrets: %e", err))
60+
}
61+
d.secrets = *secrets
62+
5663
if outageTime, ok := os.LookupEnv("ESPRESSO_DEVNET_TESTS_OUTAGE_PERIOD"); ok {
5764
d.outageTime, err = time.ParseDuration(outageTime)
5865
if err != nil {
@@ -73,7 +80,7 @@ func NewDevnet(ctx context.Context, t *testing.T) *Devnet {
7380
return d
7481
}
7582

76-
func (d *Devnet) Up(verbose bool) (err error) {
83+
func (d *Devnet) Up() (err error) {
7784
cmd := exec.CommandContext(
7885
d.ctx,
7986
"docker", "compose", "up", "-d",
@@ -107,7 +114,7 @@ func (d *Devnet) Up(verbose bool) (err error) {
107114
}
108115
}()
109116

110-
if verbose {
117+
if testing.Verbose() {
111118
// Stream logs to stdout while the test runs. This goroutine will automatically exit when
112119
// the context is cancelled.
113120
go func() {
@@ -376,6 +383,161 @@ func (d *Devnet) Down() error {
376383
return cmd.Run()
377384
}
378385

386+
type TaggedWriter struct {
387+
inner io.Writer
388+
tag string
389+
newline bool
390+
}
391+
392+
func NewTaggedWriter(tag string, inner io.Writer) *TaggedWriter {
393+
return &TaggedWriter{
394+
inner: inner,
395+
tag: tag,
396+
newline: true,
397+
}
398+
}
399+
400+
// Implementation of io.Write interface for TaggedWriter.
401+
// Allows to prepend a tag to each line of output.
402+
// The `p` parameter is the tag to add at the beginning of each line.
403+
func (w *TaggedWriter) Write(p []byte) (int, error) {
404+
if w.newline {
405+
if _, err := fmt.Fprintf(w.inner, "%s | ", w.tag); err != nil {
406+
return 0, err
407+
}
408+
w.newline = false
409+
}
410+
411+
written := 0
412+
for i := range len(p) {
413+
// Buffer bytes until we hit a newline.
414+
if p[i] == '\n' {
415+
// Print everything we've buffered up to and including the newline.
416+
line := p[written : i+1]
417+
n, err := w.inner.Write(line)
418+
written += n
419+
if err != nil || n < len(line) {
420+
return written, err
421+
}
422+
423+
// If that's the end of the output, return, but make a note that the buffer ended with a
424+
// newline and we need to print the tag before the next message.
425+
if written == len(p) {
426+
w.newline = true
427+
return written, nil
428+
}
429+
430+
// Otherwise print the tag now before proceeding with the next line in `p`.
431+
if _, err := fmt.Fprintf(w.inner, "%s | ", w.tag); err != nil {
432+
return written, err
433+
}
434+
}
435+
}
436+
437+
// Print anything that was buffered after the final newline.
438+
if written < len(p) {
439+
line := p[written:]
440+
n, err := w.inner.Write(line)
441+
written += n
442+
if err != nil || n < len(line) {
443+
return written, err
444+
}
445+
}
446+
447+
return written, nil
448+
}
449+
450+
func (d *Devnet) OpChallenger(opts ...string) error {
451+
return d.opChallengerCmd(opts...).Run()
452+
}
453+
454+
type ChallengeGame struct {
455+
Index uint64
456+
Address common.Address
457+
OutputRoot []byte
458+
Claims uint64
459+
}
460+
461+
func ParseChallengeGame(s string) (ChallengeGame, error) {
462+
fields := strings.Fields(s)
463+
if len(fields) < 8 {
464+
return ChallengeGame{}, fmt.Errorf("challenge game is missing fields; expected at least 8 but got only %v", len(fields))
465+
}
466+
467+
index, err := strconv.ParseUint(fields[0], 10, 64)
468+
if err != nil {
469+
return ChallengeGame{}, fmt.Errorf("index invalid: %w", err)
470+
}
471+
472+
address := common.HexToAddress(fields[1])
473+
474+
outputRoot := common.Hex2Bytes(fields[6])
475+
476+
claims, err := strconv.ParseUint(fields[7], 10, 64)
477+
if err != nil {
478+
return ChallengeGame{}, fmt.Errorf("claims count invalid: %w", err)
479+
}
480+
481+
return ChallengeGame{
482+
Index: index,
483+
Address: address,
484+
OutputRoot: outputRoot,
485+
Claims: claims,
486+
}, nil
487+
}
488+
489+
func (d *Devnet) ListChallengeGames() ([]ChallengeGame, error) {
490+
output, err := d.OpChallengerOutput("list-games")
491+
if err != nil {
492+
return nil, err
493+
}
494+
495+
var games []ChallengeGame
496+
for i, line := range strings.Split(output, "\n") {
497+
if i == 0 {
498+
// Ignore header.
499+
continue
500+
}
501+
line = strings.TrimSpace(line)
502+
if len(line) == 0 {
503+
// Ignore empty lines (e.g. trailing newline)
504+
continue
505+
}
506+
507+
game, err := ParseChallengeGame(line)
508+
if err != nil {
509+
return nil, fmt.Errorf("game %v is invalid: %w", i, err)
510+
}
511+
games = append(games, game)
512+
}
513+
return games, nil
514+
}
515+
516+
func (d *Devnet) OpChallengerOutput(opts ...string) (string, error) {
517+
cmd := d.opChallengerCmd(opts...)
518+
buf := new(bytes.Buffer)
519+
cmd.Stdout = buf
520+
if err := cmd.Run(); err != nil {
521+
return "", err
522+
}
523+
return buf.String(), nil
524+
}
525+
526+
func (d *Devnet) opChallengerCmd(opts ...string) *exec.Cmd {
527+
opts = append([]string{"compose", "exec", "op-challenger", "entrypoint.sh", "op-challenger"}, opts...)
528+
cmd := exec.CommandContext(
529+
d.ctx,
530+
"docker",
531+
opts...,
532+
)
533+
if testing.Verbose() {
534+
cmd.Stdout = NewTaggedWriter("op-challenger-cmd", os.Stdout)
535+
cmd.Stderr = NewTaggedWriter("op-challenger-cmd", os.Stderr)
536+
}
537+
log.Info("invoking op-challenger", "cmd", cmd)
538+
return cmd
539+
}
540+
379541
// Get the host port mapped to `privatePort` for the given Docker service.
380542
func (d *Devnet) hostPort(service string, privatePort uint16) (uint16, error) {
381543
buf := new(bytes.Buffer)

espresso/devnet-tests/key_rotation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestRotateBatcherKey(t *testing.T) {
1919
// We're going to change batcher key to Bob's, verify that it won't be a no-op
2020
require.NotEqual(t, d.secrets.Batcher, d.secrets.Bob)
2121

22-
require.NoError(t, d.Up(testing.Verbose()))
22+
require.NoError(t, d.Up())
2323
defer func() {
2424
require.NoError(t, d.Down())
2525
}()
@@ -57,7 +57,7 @@ func TestChangeBatchInboxOwner(t *testing.T) {
5757

5858
d := NewDevnet(ctx, t)
5959

60-
require.NoError(t, d.Up(testing.Verbose()))
60+
require.NoError(t, d.Up())
6161
defer func() {
6262
require.NoError(t, d.Down())
6363
}()

0 commit comments

Comments
 (0)