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 13 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
4 changes: 4 additions & 0 deletions system_tests/validation_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ func (r *mockExecRun) GetStepAt(position uint64) containers.PromiseInterface[*va
}, nil)
}

func (r *mockExecRun) GetMachineHashesWithStepSize(machineStartIndex, stepSize, numRequiredHashes uint64) containers.PromiseInterface[[]common.Hash] {
Copy link
Member

Choose a reason for hiding this comment

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

Is numRequiredHases essentially numIterations?

That is, you're telling the machine to start at position x, and then perform y iterations of s opcodes per iteration.
The fact that this also produces y hashcodes (one for each iteration) is more related to the return value than the arguments. Right?

The reason I'm making a big deal of the naming, is that I actually was confused when I first read the signature. numRequiredHashes sounds like it might be independent from how far through the machine execution progresses. But, I don't think it actually is. Without a doc comment, or something explicit, it isn't clear that these "required hashes" are the machine hash after each iteration of size stepSize.

Also, "required" sort of makes me think that it's possible that the function will error out because it wasn't able to provide enough hashes, or that it should fill in the returned slice with some "filler" hash if the machine finishes before enough hashes are generated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great feedback! Renamed to num iterations

return containers.NewReadyPromise[[]common.Hash](nil, nil)
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
}

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, numRequiredHashes 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, numRequiredHashes)
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, numRequiredHashes uint64) containers.PromiseInterface[[]common.Hash]
GetLastStep() containers.PromiseInterface[*MachineStepResult]
GetProofAt(uint64) containers.PromiseInterface[[]byte]
PrepareRange(uint64, uint64) containers.PromiseInterface[struct{}]
Expand Down
100 changes: 99 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,96 @@ func (e *executionRun) GetStepAt(position uint64) containers.PromiseInterface[*v
})
}

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

func (e *executionRun) machineHashesWithStepSize(
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
machineStartIndex,
stepSize,
requiredNumHashes uint64,
) ([]common.Hash, error) {
if stepSize == 0 {
return nil, fmt.Errorf("step size cannot be 0")
}
if requiredNumHashes == 0 {
return nil, fmt.Errorf("required number of hashes 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))

// If the machine is starting at index 0, we always want to start at the "Machine finished" global state status
// to align with the machine hashes that the inbox machine will produce.
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 requiredNumHashes == 1 {
return machineHashes, nil
}

logInterval := requiredNumHashes / 20 // Log every 5% progress
Copy link
Member

Choose a reason for hiding this comment

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

Optional: Maybe this should be configurable? It seems like something we might want to be more noisy in the beginning, but then turn way down low later?

if logInterval == 0 {
logInterval = 1
}

start := time.Now()
for numIterations := uint64(0); numIterations < requiredNumHashes; numIterations++ {
// The absolute program counter the machine should be in after stepping.
absoluteMachineIndex := machineStartIndex + stepSize*(numIterations+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 numIterations%logInterval == 0 || numIterations == requiredNumHashes-1 {
progressPercent := (float64(numIterations+1) / float64(requiredNumHashes)) * 100
log.Info(
fmt.Sprintf(
"Computing BOLD subchallenge progress: %.2f%% - %d of %d hashes needed",
progressPercent,
numIterations+1,
requiredNumHashes,
),
"machinePosition", numIterations*stepSize+machineStartIndex,
"timeSinceStart", time.Since(start),
"stepSize", stepSize,
"startHash", startHash,
"machineStartIndex", machineStartIndex,
"numDesiredLeaves", requiredNumHashes,
)
}
machineHashes = append(machineHashes, machine.Hash())
if uint64(len(machineHashes)) == requiredNumHashes {
break
}
}
log.Info(
"Successfully finished computing the data needed for opening a subchallenge",
"stepSize", stepSize,
"startHash", startHash,
"machineStartIndex", machineStartIndex,
"numDesiredLeaves", requiredNumHashes,
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
"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 +186,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())
}
208 changes: 208 additions & 0 deletions validator/server_arb/execution_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package server_arb

import (
"context"
"strings"
"testing"
"time"

"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) {
mm := &mockMachine{}
e := &executionRun{}
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
ctx := context.Background()
rauljordan marked this conversation as resolved.
Show resolved Hide resolved

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.

machStartIndex := uint64(0)
stepSize := uint64(0)
numRequiredHashes := uint64(0)
_, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, numRequiredHashes)
if !strings.Contains(err.Error(), "step size cannot be 0") {
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
t.Fatal("Wrong error")
}
stepSize = uint64(1)
_, err = e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, numRequiredHashes)
if !strings.Contains(err.Error(), "required number of hashes cannot be 0") {
t.Fatal("Wrong error")
}
})
t.Run("machine at start index 0 hash is the finished state hash", func(t *testing.T) {
mm.gs = validator.GoGlobalState{
Batch: 1,
}
machStartIndex := uint64(0)
stepSize := uint64(1)
numRequiredHashes := uint64(1)
e.cache = &MachineCache{
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
buildingLock: make(chan struct{}, 1),
machines: []MachineInterface{mm},
finalMachine: mm,
}
go func() {
<-time.After(time.Millisecond * 50)
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
e.cache.buildingLock <- struct{}{}
}()
hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, numRequiredHashes)
if err != nil {
t.Fatal(err)
}
expected := machineFinishedHash(mm.gs)
if len(hashes) != 1 {
t.Fatal("Wanted one hash")
}
if expected != hashes[0] {
t.Fatalf("Wanted %#x, got %#x", expected, hashes[0])
rauljordan marked this conversation as resolved.
Show resolved Hide resolved
}
})
t.Run("can step in step size increments and collect hashes", func(t *testing.T) {
initialGs := validator.GoGlobalState{
Batch: 1,
PosInBatch: 0,
}
mm.gs = initialGs
mm.totalSteps = 20
machStartIndex := uint64(0)
stepSize := uint64(5)
numRequiredHashes := uint64(4)
e.cache = &MachineCache{
buildingLock: make(chan struct{}, 1),
machines: []MachineInterface{mm},
finalMachine: mm,
}
go func() {
<-time.After(time.Millisecond * 50)
e.cache.buildingLock <- struct{}{}
}()
hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, numRequiredHashes)
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.Fatalf("Wanted at index %d, %#x, got %#x", i, expectedHashes[i], hashes[i])
}
}
})
t.Run("if finishes execution early, simply pads the remaining desired hashes with the machine finished hash", 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.

Well, this answers my earlier question. Why is this the protocol? Why not just return fewer hashes if the machine finishes? Seems like it could reduce the size of the data passed back to the callers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed...will let the caller pad if they need to

initialGs := validator.GoGlobalState{
Batch: 1,
PosInBatch: 0,
}
mm.gs = initialGs
mm.totalSteps = 20
machStartIndex := uint64(0)
stepSize := uint64(5)
numRequiredHashes := uint64(10)
e.cache = &MachineCache{
buildingLock: make(chan struct{}, 1),
machines: []MachineInterface{mm},
finalMachine: mm,
}
go func() {
<-time.After(time.Millisecond * 50)
e.cache.buildingLock <- struct{}{}
}()
hashes, err := e.machineHashesWithStepSize(ctx, machStartIndex, stepSize, numRequiredHashes)
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())
}
// The rest of the expected hashes should be the machine finished hash repeated.
for len(expectedHashes) < 10 {
expectedHashes = append(expectedHashes, machineFinishedHash(validator.GoGlobalState{
Batch: 1,
PosInBatch: mm.totalSteps - 1,
}))
}
if len(hashes) != len(expectedHashes) {
t.Fatal("Wanted one hash")
}
for i := range hashes {
if expectedHashes[i] != hashes[i] {
t.Fatalf("Wanted at index %d, %#x, got %#x", i, expectedHashes[i], hashes[i])
}
}
})
}
13 changes: 13 additions & 0 deletions validator/valnode/validation_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ func (a *ExecServerAPI) GetStepAt(ctx context.Context, execid uint64, position u
return server_api.MachineStepResultToJson(res), nil
}

func (a *ExecServerAPI) GetMachineHashesWithStepSize(ctx context.Context, execid, fromStep, stepSize, numRequiredHashes uint64) ([]common.Hash, error) {
run, err := a.getRun(execid)
if err != nil {
return nil, err
}
leavesInRange := run.GetMachineHashesWithStepSize(fromStep, stepSize, numRequiredHashes)
res, err := leavesInRange.Await(ctx)
if err != nil {
return nil, err
}
return res, nil
}

func (a *ExecServerAPI) GetProofAt(ctx context.Context, execid uint64, position uint64) (string, error) {
run, err := a.getRun(execid)
if err != nil {
Expand Down
Loading