Skip to content

Commit 4b9c83a

Browse files
committed
fixup! feat: use collect mcycle root hashes API
1 parent 7c0cf23 commit 4b9c83a

5 files changed

Lines changed: 439 additions & 42 deletions

File tree

pkg/emulator/machine.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package emulator
2323
import "C"
2424

2525
import (
26+
"encoding/json"
2627
"runtime"
2728
"sync"
2829
"unsafe"
@@ -33,6 +34,12 @@ const HashSize = C.sizeof_cm_hash
3334
// Common type aliases
3435
type Hash = [HashSize]byte
3536

37+
// RevertUarchTail is the emulator-owned JSON array of base64-encoded state
38+
// hashes for the reset-delimited uarch period corresponding to a revert root.
39+
// It remains opaque so collector output can be passed back without decoding and
40+
// re-encoding every hash.
41+
type RevertUarchTail json.RawMessage
42+
3643
// -----------------------------------------------------------------------------
3744
// Machine Methods
3845
// -----------------------------------------------------------------------------
@@ -365,24 +372,30 @@ func (m *Machine) Run(mcycleEnd uint64) (BreakReason, error) {
365372
}
366373

367374
// collect_mcycle_root_hashes
368-
func (m *Machine) CollectMCycleRootHashes(mcycleEnd, log2McyclePeriod, mcyclePhase uint64, log2BundleMcycleCount int32, previousBackTree string) ([]byte, error) {
375+
func (m *Machine) CollectMCycleRootHashes(
376+
mcycleEnd,
377+
log2McyclePeriod,
378+
mcyclePhase uint64,
379+
log2BundleMcycleCount int32,
380+
previousPartialBundle json.RawMessage,
381+
) ([]byte, error) {
369382
var err error
370383
var result []byte
371384

372385
m.callCAPI(func() {
373386
var cResult *C.char
374-
var previousBackTreeC *C.char
375-
if previousBackTree != "" {
376-
previousBackTreeC = C.CString(previousBackTree)
377-
defer C.free(unsafe.Pointer(previousBackTreeC))
387+
var previousPartialBundleC *C.char
388+
if len(previousPartialBundle) > 0 {
389+
previousPartialBundleC = C.CString(string(previousPartialBundle))
390+
defer C.free(unsafe.Pointer(previousPartialBundleC))
378391
}
379392
err = newError(C.cm_collect_mcycle_root_hashes(
380393
m.ptr,
381394
C.uint64_t(mcycleEnd),
382395
C.uint64_t(log2McyclePeriod),
383396
C.uint64_t(mcyclePhase),
384397
C.int32_t(log2BundleMcycleCount),
385-
previousBackTreeC,
398+
previousPartialBundleC,
386399
&cResult))
387400
result = []byte(C.GoString(cResult))
388401
})
@@ -394,15 +407,19 @@ func (m *Machine) CollectMCycleRootHashes(mcycleEnd, log2McyclePeriod, mcyclePha
394407
}
395408

396409
// collect_uarch_cycle_root_hashes
397-
func (m *Machine) CollectUarchCycleRootHashes(mcycleEnd uint64, log2BundleUarchCycleCount int32, revertUarchTail string) ([]byte, error) {
410+
func (m *Machine) CollectUarchCycleRootHashes(
411+
mcycleEnd uint64,
412+
log2BundleUarchCycleCount int32,
413+
revertUarchTail RevertUarchTail,
414+
) ([]byte, error) {
398415
var err error
399416
var result []byte
400417

401418
m.callCAPI(func() {
402419
var cResult *C.char
403420
var revertUarchTailC *C.char
404-
if revertUarchTail != "" {
405-
revertUarchTailC = C.CString(revertUarchTail)
421+
if len(revertUarchTail) > 0 {
422+
revertUarchTailC = C.CString(string(revertUarchTail))
406423
defer C.free(unsafe.Pointer(revertUarchTailC))
407424
}
408425
err = newError(C.cm_collect_uarch_cycle_root_hashes(

pkg/emulator/machine_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
package emulator
5+
6+
import (
7+
"encoding/base64"
8+
"encoding/binary"
9+
"encoding/json"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
type mcycleRootHashesResult struct {
19+
Hashes []string `json:"hashes"`
20+
MCyclePhase uint64 `json:"mcycle_phase"`
21+
BreakReason string `json:"break_reason"`
22+
PartialBundle json.RawMessage `json:"partial_bundle,omitempty"`
23+
}
24+
25+
func createLoopMachine(t *testing.T) *Machine {
26+
t.Helper()
27+
28+
const ramLength = 4096
29+
image := make([]byte, ramLength)
30+
// jal x0, 0 loops forever while allowing mcycle to advance predictably.
31+
binary.LittleEndian.PutUint32(image, 0x0000006f)
32+
imagePath := filepath.Join(t.TempDir(), "loop.bin")
33+
require.NoError(t, os.WriteFile(imagePath, image, 0o600))
34+
35+
config, err := json.Marshal(map[string]any{
36+
"ram": map[string]any{
37+
"length": ramLength,
38+
"backing_store": map[string]any{
39+
"data_filename": imagePath,
40+
},
41+
},
42+
})
43+
require.NoError(t, err)
44+
45+
machine, err := CreateMachine(string(config), "", "")
46+
require.NoError(t, err)
47+
t.Cleanup(func() {
48+
assert.NoError(t, machine.Destroy())
49+
machine.Delete()
50+
})
51+
return machine
52+
}
53+
54+
func collectMCycleRootHashes(
55+
t *testing.T,
56+
machine *Machine,
57+
target,
58+
log2Period,
59+
phase uint64,
60+
log2Bundle int32,
61+
partialBundle json.RawMessage,
62+
) mcycleRootHashesResult {
63+
t.Helper()
64+
65+
rawResult, err := machine.CollectMCycleRootHashes(target, log2Period, phase, log2Bundle, partialBundle)
66+
require.NoError(t, err)
67+
var result mcycleRootHashesResult
68+
require.NoError(t, json.Unmarshal(rawResult, &result))
69+
return result
70+
}
71+
72+
func TestCollectMCycleRootHashes_PartitionedBundlingMatchesOneShot(t *testing.T) {
73+
const (
74+
mcycleStart = uint64(1)
75+
mcycleEnd = uint64(65)
76+
log2Period = uint64(2)
77+
log2Bundle = int32(2)
78+
)
79+
targets := []uint64{2, 7, 19, 34, mcycleEnd}
80+
81+
oneShotMachine := createLoopMachine(t)
82+
partitionedMachine := createLoopMachine(t)
83+
for _, machine := range []*Machine{oneShotMachine, partitionedMachine} {
84+
breakReason, err := machine.Run(mcycleStart)
85+
require.NoError(t, err)
86+
require.Equal(t, BreakReasonReachedTargetMcycle, breakReason)
87+
}
88+
89+
oneShot := collectMCycleRootHashes(
90+
t,
91+
oneShotMachine,
92+
mcycleEnd,
93+
log2Period,
94+
mcycleStart%(uint64(1)<<log2Period),
95+
log2Bundle,
96+
nil,
97+
)
98+
99+
partitioned := mcycleRootHashesResult{
100+
MCyclePhase: mcycleStart % (uint64(1) << log2Period),
101+
}
102+
sawPartialBundle := false
103+
for _, target := range targets {
104+
result := collectMCycleRootHashes(
105+
t,
106+
partitionedMachine,
107+
target,
108+
log2Period,
109+
partitioned.MCyclePhase,
110+
log2Bundle,
111+
partitioned.PartialBundle,
112+
)
113+
partitioned.Hashes = append(partitioned.Hashes, result.Hashes...)
114+
partitioned.MCyclePhase = result.MCyclePhase
115+
partitioned.BreakReason = result.BreakReason
116+
partitioned.PartialBundle = result.PartialBundle
117+
sawPartialBundle = sawPartialBundle || len(result.PartialBundle) > 0
118+
}
119+
120+
require.True(t, sawPartialBundle, "test partitions must exercise partial-bundle continuation")
121+
require.Equal(t, oneShot, partitioned)
122+
123+
oneShotRoot, err := oneShotMachine.GetRootHash()
124+
require.NoError(t, err)
125+
partitionedRoot, err := partitionedMachine.GetRootHash()
126+
require.NoError(t, err)
127+
require.Equal(t, oneShotRoot, partitionedRoot)
128+
}
129+
130+
func TestCollectUarchCycleRootHashes_AcceptsOpaqueRevertTail(t *testing.T) {
131+
machine := createLoopMachine(t)
132+
zeroHash := Hash{}
133+
encodedZeroHash := base64.StdEncoding.EncodeToString(zeroHash[:])
134+
tail, err := json.Marshal([]string{encodedZeroHash, encodedZeroHash})
135+
require.NoError(t, err)
136+
137+
rawResult, err := machine.CollectUarchCycleRootHashes(1, 0, RevertUarchTail(tail))
138+
require.NoError(t, err)
139+
140+
var result struct {
141+
Hashes []string `json:"hashes"`
142+
BreakReason string `json:"break_reason"`
143+
}
144+
require.NoError(t, json.Unmarshal(rawResult, &result))
145+
require.NotEmpty(t, result.Hashes)
146+
require.Equal(t, "reached_target_mcycle", result.BreakReason)
147+
}

pkg/machine/backend.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ const (
2121
)
2222

2323
type HashCollectorState struct {
24-
Period uint64
25-
Phase uint64
26-
MaxHashes uint64
27-
BundleLog2 int32
28-
Hashes []Hash
29-
BackTree json.RawMessage
24+
Period uint64
25+
Phase uint64
26+
MaxHashes uint64
27+
BundleLog2 int32
28+
Hashes []Hash
29+
PartialBundle json.RawMessage
3030
}
3131

3232
// This Backend interface covers the methods used from the emulator / remote machine server.
@@ -36,6 +36,9 @@ type Backend interface {
3636
Store(directory string, timeout time.Duration) error
3737

3838
Run(mcycleEnd uint64, timeout time.Duration) (BreakReason, error)
39+
// RunAndCollectRootHashes may advance the backend before returning an error.
40+
// Callers must discard the backend after an error instead of retrying it with
41+
// the previous HashCollectorState.
3942
RunAndCollectRootHashes(mcycleEnd uint64, state *HashCollectorState, timeout time.Duration,
4043
) (reason BreakReason, err error)
4144

pkg/machine/libcartesi.go

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ type RemoteMachineInterface interface {
2929
Delete()
3030
ForkServer() (*emulator.RemoteMachine, string, uint32, error)
3131
ShutdownServer() error
32-
CollectMCycleRootHashes(mcycleEnd, log2McyclePeriod, mcyclePhase uint64, log2BundleMcycleCount int32, previousBackTree string) ([]byte, error)
32+
CollectMCycleRootHashes(
33+
mcycleEnd,
34+
log2McyclePeriod,
35+
mcyclePhase uint64,
36+
log2BundleMcycleCount int32,
37+
previousPartialBundle json.RawMessage,
38+
) ([]byte, error)
3339
}
3440

3541
type proofJson struct {
@@ -232,24 +238,24 @@ func (e *LibCartesiBackend) CmioRxBufferSize() uint64 {
232238
return 1 << emulator.CmioRxBufferLog2Size
233239
}
234240

235-
func decodeBreakReason(s string) BreakReason {
241+
func decodeBreakReason(s string) (BreakReason, error) {
236242
switch s {
237243
case "yielded_automatically":
238-
return YieldedAutomatically
244+
return YieldedAutomatically, nil
239245
case "yielded_manually":
240-
return YieldedManually
246+
return YieldedManually, nil
241247
case "yielded_softly":
242-
return YieldedSoftly
248+
return YieldedSoftly, nil
243249
case "reached_target_mcycle":
244-
return ReachedTargetMcycle
250+
return ReachedTargetMcycle, nil
245251
case "mcycle_overflow":
246-
return McycleOverflow
252+
return McycleOverflow, nil
247253
case "halted":
248-
return Halted
254+
return Halted, nil
249255
case "failed":
250-
return Failed
256+
return Failed, nil
251257
default:
252-
return Failed
258+
return Failed, fmt.Errorf("unknown break reason %q", s)
253259
}
254260
}
255261

@@ -263,40 +269,61 @@ func (e *LibCartesiBackend) RunAndCollectRootHashes(
263269
return Failed, errors.New("nil state")
264270
}
265271
if state.Period == 0 {
266-
return Failed, errors.New("State.Period must be > 0")
272+
return Failed, errors.New("state period must be greater than zero")
267273
}
268274
log2Period := uint64(bits.Len64(state.Period) - 1)
269275
if uint64(1)<<log2Period != state.Period {
270-
return Failed, fmt.Errorf("period must be a power of 2, got %v.", state.Period)
276+
return Failed, fmt.Errorf("period must be a power of 2, got %v", state.Period)
277+
}
278+
if state.Phase >= state.Period {
279+
return Failed, fmt.Errorf("phase must be less than period, got phase %v and period %v", state.Phase, state.Period)
271280
}
272281
if err := e.inner.SetTimeout(timeout.Milliseconds()); err != nil {
273282
return Failed, fmt.Errorf("failed to set operation timeout: %w", err)
274283
}
275284

276-
rawResult, err := e.inner.CollectMCycleRootHashes(mcycleEnd, log2Period, state.Phase, state.BundleLog2, "")
285+
rawResult, err := e.inner.CollectMCycleRootHashes(
286+
mcycleEnd,
287+
log2Period,
288+
state.Phase,
289+
state.BundleLog2,
290+
state.PartialBundle,
291+
)
277292
if err != nil {
278293
return Failed, err
279294
}
280295
result := struct {
281-
RootHashes []string `json:"hashes"`
282-
MCyclePhase uint64 `json:"mcycle_phase"`
283-
BreakReason string `json:"break_reason"`
284-
BackTree json.RawMessage `json:"back_tree,omitempty"`
296+
RootHashes []string `json:"hashes"`
297+
MCyclePhase uint64 `json:"mcycle_phase"`
298+
BreakReason string `json:"break_reason"`
299+
PartialBundle json.RawMessage `json:"partial_bundle,omitempty"`
285300
}{}
286301
err = json.Unmarshal(rawResult, &result)
287302
if err != nil {
288303
return Failed, fmt.Errorf("failed to unmarshal CollectMCycleRootHashes result: %w", err)
289304
}
305+
reason, err = decodeBreakReason(result.BreakReason)
306+
if err != nil {
307+
return Failed, fmt.Errorf("invalid CollectMCycleRootHashes result: %w", err)
308+
}
309+
if result.MCyclePhase >= state.Period {
310+
return Failed, fmt.Errorf(
311+
"invalid CollectMCycleRootHashes result: phase %v must be less than period %v",
312+
result.MCyclePhase,
313+
state.Period,
314+
)
315+
}
290316

291-
// convert from base64 and append to collector state
317+
decodedHashes := make([]Hash, len(result.RootHashes))
292318
for i, base64Hash := range result.RootHashes {
293-
hash := Hash{}
294-
if err := decodeB64To32(&hash, base64Hash); err != nil {
295-
return Failed, fmt.Errorf("received an invalid hash during RunAndCollectRootHashes at index %v, with value: %v.", i, base64Hash)
319+
if err := decodeB64To32(&decodedHashes[i], base64Hash); err != nil {
320+
return Failed, fmt.Errorf("invalid collected hash at index %v: %w", i, err)
296321
}
297-
state.Hashes = append(state.Hashes, hash)
298322
}
323+
324+
state.Hashes = append(state.Hashes, decodedHashes...)
299325
state.Phase = result.MCyclePhase
300-
state.BackTree = result.BackTree
301-
return decodeBreakReason(result.BreakReason), nil
326+
state.PartialBundle = result.PartialBundle
327+
328+
return reason, nil
302329
}

0 commit comments

Comments
 (0)