Skip to content

Commit 0debf9c

Browse files
committed
fix(manager): reject contradictory reconstruction
1 parent f7a47c2 commit 0debf9c

11 files changed

Lines changed: 2063 additions & 672 deletions

File tree

internal/advancer/advancer_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,12 +2035,6 @@ func (m *MockMachineInstance) OutputsProof(ctx context.Context) (*OutputsProof,
20352035
}, nil
20362036
}
20372037

2038-
// Synchronize implements the MachineInstance interface for testing
2039-
func (m *MockMachineInstance) Synchronize(ctx context.Context, repo manager.MachineRepository, batchSize uint64) error {
2040-
// Not used in advancer tests, but needed to satisfy the interface
2041-
return nil
2042-
}
2043-
20442038
// CreateSnapshot implements the MachineInstance interface for testing
20452039
func (m *MockMachineInstance) CreateSnapshot(ctx context.Context, processInputs uint64, path string) error {
20462040
return m.createSnapshotError

internal/advancer/determinism_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ func newDeterminismHarness(
502502
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
503503
factory := newDeterminismRuntimeFactory(start, behaviors...)
504504
instance, err := manager.NewMachineInstanceWithFactory(
505-
context.Background(), app, processedInputs, logger, false, factory,
505+
context.Background(), app, processedInputs, logger, factory,
506506
)
507507
require.NoError(t, err)
508508
t.Cleanup(func() { require.NoError(t, instance.Close()) })
@@ -623,7 +623,6 @@ func (f *determinismRuntimeFactory) CreateMachineRuntime(
623623
ctx context.Context,
624624
_ *model.Application,
625625
_ *slog.Logger,
626-
_ bool,
627626
) (machine.Machine, error) {
628627
if err := ctx.Err(); err != nil {
629628
return nil, err
@@ -743,11 +742,16 @@ func (m *determinismRuntime) Advance(
743742
return nil, errors.New("determinism test input must not be empty")
744743
}
745744

745+
// The revert root rides with the advance request and must be the machine's
746+
// pre-input root — the instance always passes fork.Hash(). A mismatch is a
747+
// harness (or caller) bug, not a determinism scenario.
746748
if checkpointHash != m.state.machineHash {
747749
m.mu.Unlock()
748750
return nil, errors.New("determinism test requires the checkpoint hash to equal the pre-input machine root")
749751
}
750752
previous := m.state.clone()
753+
// Recording the request's own revert root is the first mutation of the
754+
// candidate, exactly like the CMIO response.
751755
m.state.checkpointHash = checkpointHash
752756
status := machine.CompletionStatusAccepted
753757
switch {

internal/appstatus/appstatus.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Repository interface {
3030
// Recovery assumptions — FAILED is safe to re-enable only when:
3131
// - The failure was a machine runtime error (not a DB desync).
3232
// - The last snapshot is consistent with the database state.
33-
// - Synchronize() will correctly replay inputs from the snapshot point.
33+
// - replay.Run will correctly verify inputs from the snapshot point.
3434
//
3535
// The reason parameter must be a pre-formatted string describing the failure.
3636
// Returns the database error if the status update fails; returns nil on success.
@@ -134,7 +134,7 @@ func setTerminalStatus(
134134
status ApplicationStatus,
135135
reason string,
136136
) error {
137-
reason = truncateReason(reason)
137+
reason = NormalizeReason(reason)
138138
dbErr := setApplicationStatus(ctx, logger, repo, app, status, reason)
139139
reasonErr := errors.New(reason)
140140
if dbErr != nil {
@@ -143,9 +143,10 @@ func setTerminalStatus(
143143
return reasonErr
144144
}
145145

146-
// truncateReason truncates a reason string to maxReasonLength to avoid
147-
// exceeding the database VARCHAR(4096) constraint.
148-
func truncateReason(reason string) string {
146+
// NormalizeReason returns the exact reason representation persisted by status
147+
// helpers. Callers that compare a later readback must normalize before keeping
148+
// their expected value.
149+
func NormalizeReason(reason string) string {
149150
if len(reason) > maxReasonLength {
150151
return reason[:maxReasonLength] + "... (truncated)"
151152
}
@@ -160,7 +161,7 @@ func setApplicationStatus(
160161
status ApplicationStatus,
161162
reason string,
162163
) error {
163-
reason = truncateReason(reason)
164+
reason = NormalizeReason(reason)
164165

165166
switch status {
166167
case ApplicationStatus_Failed:

internal/inspect/hardening_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,6 @@ func (m *erroringMachine) ProcessedInputs() uint64 { return m.inner.ProcessedI
106106
func (m *erroringMachine) OutputsProof(ctx context.Context) (*OutputsProof, error) {
107107
return m.inner.OutputsProof(ctx)
108108
}
109-
func (m *erroringMachine) Synchronize(ctx context.Context, repo manager.MachineRepository, batchSize uint64) error {
110-
return m.inner.Synchronize(ctx, repo, batchSize)
111-
}
112109
func (m *erroringMachine) CreateSnapshot(ctx context.Context, processedInputs uint64, path string) error {
113110
return m.inner.CreateSnapshot(ctx, processedInputs, path)
114111
}

internal/inspect/inspect_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,11 +461,6 @@ func (m *MockMachine) OutputsProof(ctx context.Context) (*OutputsProof, error) {
461461
return nil, nil
462462
}
463463

464-
// Not used in inspect tests, but needed to satisfy the interface
465-
func (mock *MockMachine) Synchronize(ctx context.Context, repo manager.MachineRepository, batchSize uint64) error {
466-
return nil
467-
}
468-
469464
// Not used in inspect tests, but needed to satisfy the interface
470465
func (mock *MockMachine) CreateSnapshot(ctx context.Context, processedInputs uint64, path string) error {
471466
return nil

internal/manager/errors.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
package manager
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
)
10+
11+
var ErrApplicationFailureNotDurable = errors.New("application failure status is not durably confirmed")
12+
13+
// ApplicationFailurePersistenceError means local machine work is fenced but
14+
// the repository has not confirmed the corresponding FAILED (or a stronger
15+
// terminal/deleted) application state.
16+
type ApplicationFailurePersistenceError struct {
17+
ApplicationID int64
18+
WriteErr error
19+
ReadErr error
20+
}
21+
22+
func (e *ApplicationFailurePersistenceError) Error() string {
23+
if e.ReadErr != nil {
24+
return fmt.Sprintf(
25+
"%v: application=%d write_error=%v read_error=%v",
26+
ErrApplicationFailureNotDurable, e.ApplicationID, e.WriteErr, e.ReadErr,
27+
)
28+
}
29+
return fmt.Sprintf(
30+
"%v: application=%d write_error=%v durable status remains unconfirmed",
31+
ErrApplicationFailureNotDurable, e.ApplicationID, e.WriteErr,
32+
)
33+
}
34+
35+
func (e *ApplicationFailurePersistenceError) Unwrap() []error {
36+
errList := []error{ErrApplicationFailureNotDurable}
37+
if e.WriteErr != nil {
38+
errList = append(errList, e.WriteErr)
39+
}
40+
if e.ReadErr != nil {
41+
errList = append(errList, e.ReadErr)
42+
}
43+
return errList
44+
}
45+
46+
// IsOnlyApplicationFailurePersistenceErrors reports whether err is one or
47+
// more application-local durability failures and contains no global failure.
48+
// It deliberately examines joined top-level errors without descending into a
49+
// persistence error's write/read causes.
50+
func IsOnlyApplicationFailurePersistenceErrors(err error) bool {
51+
if err == nil {
52+
return false
53+
}
54+
if _, ok := err.(*ApplicationFailurePersistenceError); ok {
55+
return true
56+
}
57+
58+
switch wrapped := err.(type) {
59+
case interface{ Unwrap() []error }:
60+
children := wrapped.Unwrap()
61+
if len(children) == 0 {
62+
return false
63+
}
64+
for _, child := range children {
65+
if !IsOnlyApplicationFailurePersistenceErrors(child) {
66+
return false
67+
}
68+
}
69+
return true
70+
case interface{ Unwrap() error }:
71+
return IsOnlyApplicationFailurePersistenceErrors(wrapped.Unwrap())
72+
default:
73+
return false
74+
}
75+
}

0 commit comments

Comments
 (0)