Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define Execution Run Method to Compute Machine Hashes With Step Size for BOLD #2392

Merged
merged 27 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c8b146
get machine hashes with step size
rauljordan Jun 13, 2024
09c8b03
tests
rauljordan Jun 13, 2024
f9470c0
rem old
rauljordan Jun 13, 2024
18a2510
done
rauljordan Jun 13, 2024
71c0a3d
tsahi feedback
rauljordan Jun 17, 2024
65e1b57
tests passing
rauljordan Jun 17, 2024
4503855
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 17, 2024
f9484da
edit
rauljordan Jun 17, 2024
0920d98
Merge branch 'get-machine-hashes-with-step' of github.com:OffchainLab…
rauljordan Jun 17, 2024
2832272
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 17, 2024
0432a78
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 18, 2024
1f2a7eb
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 18, 2024
feb1d90
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 20, 2024
5ed59b6
feedback
rauljordan Jun 24, 2024
4249cc3
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 24, 2024
f7ed4f0
feedback
rauljordan Jun 24, 2024
85d0e8d
commentary
rauljordan Jun 24, 2024
bf60a37
rename
rauljordan Jun 24, 2024
27e4a82
commentary
rauljordan Jun 24, 2024
1eff6fb
replace
rauljordan Jun 24, 2024
e853eff
rename
rauljordan Jun 24, 2024
89a800d
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jun 25, 2024
b90fe67
Merge branch 'master' into get-machine-hashes-with-step
amsanghi Jul 1, 2024
01647d1
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jul 8, 2024
2a09c3e
feedback
rauljordan Jul 8, 2024
24ddd89
Merge branch 'master' into get-machine-hashes-with-step
rauljordan Jul 10, 2024
ea39210
Merge branch 'master' into get-machine-hashes-with-step
tsahee Jul 10, 2024
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
14 changes: 14 additions & 0 deletions system_tests/validation_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ func (r *mockExecRun) GetStepAt(position uint64) containers.PromiseInterface[*va
}, nil)
}

func (r *mockExecRun) GetMachineHashesWithStepSize(machineStartIndex, stepSize, maxIterations uint64) containers.PromiseInterface[[]common.Hash] {
ctx := context.Background()
hashes := make([]common.Hash, 0)
for i := uint64(0); i < maxIterations; i++ {
absoluteMachineIndex := machineStartIndex + stepSize*(i+1)
stepResult, err := r.GetStepAt(absoluteMachineIndex).Await(ctx)
if err != nil {
return containers.NewReadyPromise[[]common.Hash](nil, err)
}
hashes = append(hashes, stepResult.Hash)
}
return containers.NewReadyPromise[[]common.Hash](hashes, nil)
}

func (r *mockExecRun) GetLastStep() containers.PromiseInterface[*validator.MachineStepResult] {
return r.GetStepAt(mockExecLastPos)
}
Expand Down
11 changes: 11 additions & 0 deletions validator/client/validation_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ func (r *ExecutionClientRun) GetStepAt(pos uint64) containers.PromiseInterface[*
})
}

func (r *ExecutionClientRun) GetMachineHashesWithStepSize(machineStartIndex, stepSize, maxIterations uint64) containers.PromiseInterface[[]common.Hash] {
return stopwaiter.LaunchPromiseThread[[]common.Hash](r, func(ctx context.Context) ([]common.Hash, error) {
var resJson []common.Hash
err := r.client.client.CallContext(ctx, &resJson, server_api.Namespace+"_getMachineHashesWithStepSize", r.id, machineStartIndex, stepSize, maxIterations)
if err != nil {
return nil, err
}
return resJson, err
})
}

func (r *ExecutionClientRun) GetProofAt(pos uint64) containers.PromiseInterface[[]byte] {
return stopwaiter.LaunchPromiseThread[[]byte](r, func(ctx context.Context) ([]byte, error) {
var resString string
Expand Down
1 change: 1 addition & 0 deletions validator/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ExecutionSpawner interface {

type ExecutionRun interface {
GetStepAt(uint64) containers.PromiseInterface[*MachineStepResult]
GetMachineHashesWithStepSize(machineStartIndex, stepSize, maxIterations uint64) containers.PromiseInterface[[]common.Hash]
GetLastStep() containers.PromiseInterface[*MachineStepResult]
GetProofAt(uint64) containers.PromiseInterface[[]byte]
PrepareRange(uint64, uint64) containers.PromiseInterface[struct{}]
Expand Down
108 changes: 107 additions & 1 deletion validator/server_arb/execution_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import (
"context"
"fmt"
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"

"github.com/ethereum/go-ethereum/log"
"github.com/offchainlabs/nitro/util/containers"
"github.com/offchainlabs/nitro/util/stopwaiter"
"github.com/offchainlabs/nitro/validator"
Expand Down Expand Up @@ -55,7 +60,6 @@ func (e *executionRun) GetStepAt(position uint64) containers.PromiseInterface[*v
if position == ^uint64(0) {
machine, err = e.cache.GetFinalMachine(ctx)
} else {
// todo cache last machine
machine, err = e.cache.GetMachineAt(ctx, position)
}
if err != nil {
Expand All @@ -79,6 +83,104 @@ func (e *executionRun) GetStepAt(position uint64) containers.PromiseInterface[*v
})
}

func (e *executionRun) GetMachineHashesWithStepSize(machineStartIndex, stepSize, maxIterations uint64) containers.PromiseInterface[[]common.Hash] {
return stopwaiter.LaunchPromiseThread(e, func(ctx context.Context) ([]common.Hash, error) {
return e.machineHashesWithStepSize(ctx, machineStartIndex, stepSize, maxIterations)
})
}

func (e *executionRun) machineHashesWithStepSize(
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
machineStartIndex,
stepSize,
maxIterations uint64,
) ([]common.Hash, error) {
if stepSize == 0 {
return nil, fmt.Errorf("step size cannot be 0")
}
if maxIterations == 0 {
return nil, fmt.Errorf("max number of iterations cannot be 0")
}
machine, err := e.cache.GetMachineAt(ctx, machineStartIndex)
eljobe marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
log.Debug(fmt.Sprintf("Advanced machine to index %d, beginning hash computation", machineStartIndex))

// In BOLD, the hash of a machine at index 0 is a special hash that is computed as the
// `machineFinishedHash(gs)` where `gs` is the global state of the machine at index 0.
// This is so that the hash aligns with the start state of the claimed challenge edge
// at the level above, as required by the BOLD protocol.
var machineHashes []common.Hash
if machineStartIndex == 0 {
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
gs := machine.GetGlobalState()
log.Debug(fmt.Sprintf("Start global state for machine index 0: %+v", gs))
machineHashes = append(machineHashes, machineFinishedHash(gs))
} else {
// Otherwise, we simply append the machine hash at the specified start index.
machineHashes = append(machineHashes, machine.Hash())
}
startHash := machineHashes[0]

// If we only want 1 hash, we can return early.
if maxIterations == 1 {
return machineHashes, nil
}

logInterval := maxIterations / 20 // Log every 5% progress
if logInterval == 0 {
logInterval = 1
}

start := time.Now()
for i := uint64(0); i < maxIterations; i++ {
// The absolute program counter the machine should be in after stepping.
absoluteMachineIndex := machineStartIndex + stepSize*(i+1)

// Advance the machine in step size increments.
if err := machine.Step(ctx, stepSize); err != nil {
return nil, fmt.Errorf("failed to step machine to position %d: %w", absoluteMachineIndex, err)
}
if i%logInterval == 0 || i == maxIterations-1 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a must..
I found that logging every x seconds (remembering when you last logged and logging again if enough time passed) is better than logging every X iterations

progressPercent := (float64(i+1) / float64(maxIterations)) * 100
log.Info(
fmt.Sprintf(
"Computing BOLD subchallenge progress: %.2f%% - %d of %d hashes",
progressPercent,
i+1,
maxIterations,
),
"machinePosition", i*stepSize+machineStartIndex,
"timeSinceStart", time.Since(start),
"stepSize", stepSize,
"startHash", startHash,
"machineStartIndex", machineStartIndex,
"maxIterations", maxIterations,
)
}
machineHashes = append(machineHashes, machine.Hash())
if uint64(len(machineHashes)) == maxIterations {
log.Info("Reached the max number of iterations for the hashes needed to open a subchallenge")
break
}
if !machine.IsRunning() {
log.Info("Machine no longer running, exiting early from hash computation loop")
break
}
}
log.Info(
"Successfully finished computing the data needed for opening a subchallenge",
"stepSize", stepSize,
"startHash", startHash,
"machineStartIndex", machineStartIndex,
"numberOfHashesComputed", len(machineHashes),
"maxIterations", maxIterations,
"finishedHash", machineHashes[len(machineHashes)-1],
"finishedGlobalState", fmt.Sprintf("%+v", machine.GetGlobalState()),
)
return machineHashes, nil
}

func (e *executionRun) GetProofAt(position uint64) containers.PromiseInterface[[]byte] {
return stopwaiter.LaunchPromiseThread[[]byte](e, func(ctx context.Context) ([]byte, error) {
machine, err := e.cache.GetMachineAt(ctx, position)
Expand All @@ -92,3 +194,7 @@ func (e *executionRun) GetProofAt(position uint64) containers.PromiseInterface[[
func (e *executionRun) GetLastStep() containers.PromiseInterface[*validator.MachineStepResult] {
return e.GetStepAt(^uint64(0))
}

func machineFinishedHash(gs validator.GoGlobalState) common.Hash {
return crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes())
}
206 changes: 206 additions & 0 deletions validator/server_arb/execution_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package server_arb

import (
"context"
"strings"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/offchainlabs/nitro/validator"
)

type mockMachine struct {
gs validator.GoGlobalState
totalSteps uint64
}

func (m *mockMachine) Hash() common.Hash {
if m.gs.PosInBatch == m.totalSteps-1 {
return machineFinishedHash(m.gs)
}
return m.gs.Hash()
}

func (m *mockMachine) GetGlobalState() validator.GoGlobalState {
return m.gs
}

func (m *mockMachine) Step(ctx context.Context, stepSize uint64) error {
for i := uint64(0); i < stepSize; i++ {
if m.gs.PosInBatch == m.totalSteps-1 {
return nil
}
m.gs.PosInBatch += 1
}
return nil
}

func (m *mockMachine) CloneMachineInterface() MachineInterface {
return &mockMachine{
gs: validator.GoGlobalState{Batch: m.gs.Batch, PosInBatch: m.gs.PosInBatch},
totalSteps: m.totalSteps,
}
}
func (m *mockMachine) GetStepCount() uint64 {
return 0
}
func (m *mockMachine) IsRunning() bool {
return m.gs.PosInBatch < m.totalSteps-1
}
func (m *mockMachine) ValidForStep(uint64) bool {
return true
}
func (m *mockMachine) Status() uint8 {
if m.gs.PosInBatch == m.totalSteps-1 {
return uint8(validator.MachineStatusFinished)
}
return uint8(validator.MachineStatusRunning)
}
func (m *mockMachine) ProveNextStep() []byte {
return nil
}
func (m *mockMachine) Freeze() {}
func (m *mockMachine) Destroy() {}

func Test_machineHashesWithStep(t *testing.T) {
t.Run("basic argument checks", func(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are all these different test cases being run using t.Run instead of just being separate top-level test functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference here. I find it more expressive to write subcases in plain english instead of having to wrangle the Go function naming convention. It also groups functionality into one place, but I'm happy to change it if the alternative is highly preferred

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked in the CI and saw that it does display these runs separately and nicely.
The advantage of separate functions is that you can run one by itself, and out CI will re-execute every failed test once to see if it first failed due to system flakiness.
I think for short low-footprint tests like these using t.Run is fine, and for anything in system_test I would still prefer separate functions.

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
e := &executionRun{}
machStartIndex := uint64(0)
stepSize := uint64(0)
maxIterations := uint64(0)
_, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, maxIterations)
if err == nil || !strings.Contains(err.Error(), "step size cannot be 0") {
t.Error("Wrong error")
}
stepSize = uint64(1)
_, err = e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, maxIterations)
if err == nil || !strings.Contains(err.Error(), "number of iterations cannot be 0") {
t.Error("Wrong error")
}
})
t.Run("machine at start index 0 hash is the finished state hash", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mm := &mockMachine{
gs: validator.GoGlobalState{
Batch: 1,
},
totalSteps: 20,
}
machStartIndex := uint64(0)
stepSize := uint64(1)
maxIterations := uint64(1)
e := &executionRun{
cache: NewMachineCache(ctx, func(_ context.Context) (MachineInterface, error) {
return mm, nil
}, &DefaultMachineCacheConfig),
}

hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, maxIterations)
if err != nil {
t.Fatal(err)
}
expected := machineFinishedHash(mm.gs)
if len(hashes) != 1 {
t.Error("Wanted one hash")
}
if expected != hashes[0] {
t.Errorf("Wanted %#x, got %#x", expected, hashes[0])
}
})
t.Run("can step in step size increments and collect hashes", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialGs := validator.GoGlobalState{
Batch: 1,
PosInBatch: 0,
}
mm := &mockMachine{
gs: initialGs,
totalSteps: 20,
}
machStartIndex := uint64(0)
stepSize := uint64(5)
maxIterations := uint64(4)
e := &executionRun{
cache: NewMachineCache(ctx, func(_ context.Context) (MachineInterface, error) {
return mm, nil
}, &DefaultMachineCacheConfig),
}
hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, maxIterations)
if err != nil {
t.Fatal(err)
}
expectedHashes := make([]common.Hash, 0)
for i := uint64(0); i < 4; i++ {
if i == 0 {
expectedHashes = append(expectedHashes, machineFinishedHash(initialGs))
continue
}
gs := validator.GoGlobalState{
Batch: 1,
PosInBatch: uint64(i * stepSize),
}
expectedHashes = append(expectedHashes, gs.Hash())
}
if len(hashes) != len(expectedHashes) {
t.Fatal("Wanted one hash")
}
for i := range hashes {
if expectedHashes[i] != hashes[i] {
t.Errorf("Wanted at index %d, %#x, got %#x", i, expectedHashes[i], hashes[i])
}
}
})
t.Run("if finishes execution early, can return a smaller number of hashes than the expected max iterations", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialGs := validator.GoGlobalState{
Batch: 1,
PosInBatch: 0,
}
mm := &mockMachine{
gs: initialGs,
totalSteps: 20,
}
machStartIndex := uint64(0)
stepSize := uint64(5)
maxIterations := uint64(10)
e := &executionRun{
cache: NewMachineCache(ctx, func(_ context.Context) (MachineInterface, error) {
return mm, nil
}, &DefaultMachineCacheConfig),
}

hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, maxIterations)
if err != nil {
t.Fatal(err)
}
expectedHashes := make([]common.Hash, 0)
for i := uint64(0); i < 4; i++ {
if i == 0 {
expectedHashes = append(expectedHashes, machineFinishedHash(initialGs))
continue
}
gs := validator.GoGlobalState{
Batch: 1,
PosInBatch: uint64(i * stepSize),
}
expectedHashes = append(expectedHashes, gs.Hash())
}
expectedHashes = append(expectedHashes, machineFinishedHash(validator.GoGlobalState{
Batch: 1,
PosInBatch: mm.totalSteps - 1,
}))
if len(hashes) >= int(maxIterations) {
t.Fatal("Wanted fewer hashes than the max iterations")
}
for i := range hashes {
if expectedHashes[i] != hashes[i] {
t.Errorf("Wanted at index %d, %#x, got %#x", i, expectedHashes[i], hashes[i])
}
}
})
}
Loading
Loading