-
Notifications
You must be signed in to change notification settings - Fork 487
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
Changes from 13 commits
9c8b146
09c8b03
f9470c0
18a2510
71c0a3d
65e1b57
4503855
f9484da
0920d98
2832272
0432a78
1f2a7eb
feb1d90
5ed59b6
4249cc3
f7ed4f0
85d0e8d
bf60a37
27e4a82
1eff6fb
e853eff
89a800d
b90fe67
01647d1
2a09c3e
24ddd89
ea39210
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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()) | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]) | ||
} | ||
} | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
numRequiredHases
essentiallynumIterations
?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.
There was a problem hiding this comment.
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