Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions internal/advancer/advancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func (s *AdvancerSuite) TestProcess() {
terminal := randomAdvanceResult(1)
terminal.Status = status
terminal.Outputs = nil
terminal.Reports = nil
terminal.Reports = [][]byte{[]byte("terminal report")}
if status == InputCompletionStatus_Exception {
terminal.ExceptionData = []byte("guest exception")
}
Expand All @@ -537,6 +537,7 @@ func (s *AdvancerSuite) TestProcess() {
require.True(stopped)
require.Len(env.repo.StoredResults, 2)
require.Equal(status, env.repo.StoredResults[1].Status)
require.Equal(terminal.Reports, env.repo.StoredResults[1].Reports)
require.Equal([]byte("must not execute"), inputs[2].RawData)
})
}
Expand All @@ -547,7 +548,7 @@ func (s *AdvancerSuite) TestProcess() {
rejected := randomAdvanceResult(0)
rejected.Status = InputCompletionStatus_Rejected
rejected.Outputs = nil
rejected.Reports = nil
rejected.Reports = [][]byte{[]byte("rejection report")}
accepted := randomAdvanceResult(1)

processed, stopped, err := env.service.processInputs(
Expand All @@ -560,6 +561,7 @@ func (s *AdvancerSuite) TestProcess() {
require.Equal(uint64(2), processed)
require.False(stopped)
require.Len(env.repo.StoredResults, 2)
require.Equal(rejected.Reports, env.repo.StoredResults[0].Reports)
})

s.Run("Noop", func() {
Expand Down Expand Up @@ -1581,7 +1583,7 @@ func (s *AdvancerSuite) TestTerminalInputStopsEpochAndFutureTicks() {
terminal := randomAdvanceResult(1)
terminal.Status = InputCompletionStatus_MachineHalted
terminal.Outputs = nil
terminal.Reports = nil
terminal.Reports = [][]byte{[]byte("terminal report")}
address := env.app.Application.IApplicationAddress
env.repo.GetEpochsReturn = map[common.Address][]*Epoch{
address: {{
Expand All @@ -1603,6 +1605,7 @@ func (s *AdvancerSuite) TestTerminalInputStopsEpochAndFutureTicks() {
require.NoError(err)
require.False(hadWork)
require.Len(env.repo.StoredResults, 2)
require.Equal(terminal.Reports, env.repo.StoredResults[1].Reports)
require.Zero(env.repo.EpochInputsProcessedCount,
"a terminal epoch must remain closed because it has no accepted terminal state")

Expand Down
7 changes: 3 additions & 4 deletions internal/advancer/determinism_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,17 +355,16 @@ func requireDeterminismTarget(
require.Len(t, target.PeriodicStateHashes, 2, "a PRT result must retain its periodic state hashes")
require.Equal(t, machine.InputEntryCapacity-uint64(len(target.PeriodicStateHashes)), target.PaddingRepetitions)

require.Equal(t, [][]byte{append([]byte("report:"), targetPayload...)}, target.Reports)
if wantStatus == model.InputCompletionStatus_Accepted {
require.True(t, target.IsComplete())
require.Equal(t, [][]byte{append([]byte("output:"), targetPayload...)}, target.Outputs)
require.Equal(t, [][]byte{append([]byte("report:"), targetPayload...)}, target.Reports)
require.NotEqual(t, prefix.MachineHash, target.MachineHash)
require.NotEqual(t, prefix.TxBufferDataBlock, target.TxBufferDataBlock)
return
}

require.Empty(t, target.Outputs, "effects are canonical only for accepted inputs")
require.Empty(t, target.Reports, "effects are canonical only for accepted inputs")
require.Empty(t, target.Outputs, "outputs are canonical only for accepted inputs")
if wantStatus.IsTerminal() {
require.True(t, target.IsComplete(),
"a terminal result must preserve its actual post-run proof")
Expand Down Expand Up @@ -823,12 +822,12 @@ func (m *determinismRuntime) Advance(
hashes := []machine.Hash{firstHash, finalHash}
response := &machine.AdvanceResponse{
Status: status,
Reports: []machine.Report{report},
PeriodicStateHashes: hashes,
PaddingRepetitions: machine.InputEntryCapacity - uint64(len(hashes)),
}
if status == machine.CompletionStatusAccepted {
response.Outputs = []machine.Output{output}
response.Reports = []machine.Report{report}
} else if status == machine.CompletionStatusException {
response.ExceptionData = append([]byte{}, input...)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/advancer/invalid_outputs_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (s *AdvancerSuite) TestInvalidOutputsRootStopsClaimPreparation() {
terminal := randomAdvanceResult(0)
terminal.Status = model.InputCompletionStatus_InvalidOutputsRoot
terminal.Outputs = nil
terminal.Reports = nil
terminal.Reports = [][]byte{[]byte("invalid outputs root report")}
address := env.app.Application.IApplicationAddress
env.repo.GetEpochsReturn = map[common.Address][]*model.Epoch{
address: {{
Expand All @@ -36,6 +36,7 @@ func (s *AdvancerSuite) TestInvalidOutputsRootStopsClaimPreparation() {
s.False(hadWork)
s.Require().Len(env.repo.StoredResults, 1)
s.Equal(model.InputCompletionStatus_InvalidOutputsRoot, env.repo.StoredResults[0].Status)
s.Equal(terminal.Reports, env.repo.StoredResults[0].Reports)
s.Zero(env.repo.EpochInputsProcessedCount, "the epoch must not become claim work")
s.Zero(env.repo.ApplicationStatusUpdates, "the status belongs to the atomic input-result write")

Expand All @@ -51,7 +52,7 @@ func (s *AdvancerSuite) TestInvalidOutputsRootStoreFailureStopsService() {
terminal := randomAdvanceResult(0)
terminal.Status = model.InputCompletionStatus_InvalidOutputsRoot
terminal.Outputs = nil
terminal.Reports = nil
terminal.Reports = [][]byte{[]byte("invalid outputs root report")}
pending := newInput(env.app.Application.ID, 0, 0, marshal(terminal))
env.repo.StoreAdvanceError = errors.New("terminal result write failed")

Expand Down
5 changes: 3 additions & 2 deletions internal/manager/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ func (m *MachineInstanceImpl) Advance(
EpochIndex: epochIndex,
InputIndex: index,
Status: status,
Reports: advanceResp.Reports,
ExceptionData: advanceResp.ExceptionData,
PeriodicStateHashes: advanceResp.PeriodicStateHashes,
PaddingRepetitions: advanceResp.PaddingRepetitions,
Expand All @@ -306,10 +307,10 @@ func (m *MachineInstanceImpl) Advance(
}
result.StateProof = *postProof
result.Outputs = advanceResp.Outputs
result.Reports = advanceResp.Reports
adoptFork = true
case model.InputCompletionStatus_Rejected:
// Rejected execution has no canonical state transition or effects.
// Rejection keeps the predecessor state and discards outputs. Reports
// remain available as diagnostics from the completed execution.
result.StateProof = *prevProof
case model.InputCompletionStatus_Exception,
model.InputCompletionStatus_MachineHalted,
Expand Down
12 changes: 10 additions & 2 deletions internal/manager/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,14 @@ func (s *MachineInstanceSuite) TestAdvance() {
require.NotNil(res)

require.Same(inner, instance.runtime)
preProof, proofErr := stateProofFromMachine(fork.StateProofReturn)
require.NoError(proofErr)
require.Equal(*preProof, res.StateProof)
require.Equal(int64(1), fork.CloseCalls.Load())
require.Zero(inner.CloseCalls.Load())
require.Equal(model.InputCompletionStatus_Rejected, res.Status)
require.Empty(res.Outputs)
require.Empty(res.Reports)
require.Equal(expectedReports1, res.Reports)
require.Equal(newHash(1), res.TxBufferDataBlock)
require.Equal(newHash(2), res.MachineHash)
require.True(res.IsComplete())
Expand Down Expand Up @@ -444,8 +449,11 @@ func (s *MachineInstanceSuite) TestAdvance() {
require.Nil(instance.runtime)
require.Equal(inputStatus, res.Status)
require.Equal(exceptionData, res.ExceptionData)
wantProof, proofErr := stateProofFromMachine(postProof)
require.NoError(proofErr)
require.Equal(*wantProof, res.StateProof)
require.Empty(res.Outputs)
require.Empty(res.Reports)
require.Equal(expectedReports1, res.Reports)
require.Equal(newHash(1), res.TxBufferDataBlock)
require.Equal(newHash(2), res.MachineHash)
require.True(res.IsComplete())
Expand Down
2 changes: 1 addition & 1 deletion internal/manager/invalid_outputs_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (s *MachineInstanceSuite) TestInvalidOutputsRootPreservesFinalEvidence() {
s.Equal(fork.AdvanceRemainingReturn, result.PaddingRepetitions)
s.Equal(collect, result.IsDaveConsensus)
s.Empty(result.Outputs)
s.Empty(result.Reports)
s.Equal(expectedReports1, result.Reports)
s.Nil(result.ExceptionData)
s.Equal(2, proofCalls)
s.Equal(uint64(6), instance.ProcessedInputs())
Expand Down
11 changes: 4 additions & 7 deletions internal/replay/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,14 @@ func compareRecord(
if err := compareBytes("outputs", record.Outputs, actual.Outputs, contradiction); err != nil {
return err
}
if err := compareBytes("reports", record.Reports, actual.Reports, contradiction); err != nil {
return err
}
} else {
// Effects of nonaccepted executions are diagnostics, not canonical.
// Outputs from nonaccepted executions must not be persisted.
if len(record.Outputs) != 0 {
return contradiction("outputs.count", 0, len(record.Outputs))
}
if len(record.Reports) != 0 {
return contradiction("reports.count", 0, len(record.Reports))
}
}
if err := compareBytes("reports", record.Reports, actual.Reports, contradiction); err != nil {
return err
}

if !isPRT {
Expand Down
37 changes: 18 additions & 19 deletions internal/replay/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ func TestCompareReplayRecordCompletionMatrix(t *testing.T) {
model.InputCompletionStatus_Rejected,
model.InputCompletionStatus_Exception,
model.InputCompletionStatus_MachineHalted,
model.InputCompletionStatus_Overflow,
model.InputCompletionStatus_UnexpectedYield,
model.InputCompletionStatus_InvalidOutputsRoot,
}
consensuses := []model.Consensus{
Expand All @@ -154,7 +156,8 @@ func TestCompareReplayRecordCompletionMatrix(t *testing.T) {
app, record, actual := replayFixture(status, consensus)
if status != model.InputCompletionStatus_Accepted {
actual.Outputs = [][]byte{[]byte("noncanonical diagnostic output")}
actual.Reports = [][]byte{[]byte("noncanonical diagnostic report")}
record.Reports = [][]byte{[]byte("diagnostic report")}
actual.Reports = [][]byte{[]byte("diagnostic report")}
}
if consensus == model.Consensus_PRT {
checkpoint := common.HexToHash("0x33")
Expand Down Expand Up @@ -271,26 +274,22 @@ func TestCompareReplayRecordPersistedRecordValidation(t *testing.T) {
ErrContradiction,
)
})
for _, effect := range []string{"output", "report"} {
t.Run("nonaccepted persisted "+effect, func(t *testing.T) {
app, record, actual := replayFixture(model.InputCompletionStatus_Rejected, model.Consensus_Authority)
if effect == "output" {
record.Outputs = [][]byte{[]byte("illegal")}
} else {
record.Reports = [][]byte{[]byte("illegal")}
}
// Replay diagnostics are ignored, but persisted effects are corruption.
actual.Outputs = [][]byte{[]byte("diagnostic")}
actual.Reports = [][]byte{[]byte("diagnostic")}
require.ErrorIs(t,
compareRecord(app.Name, app.ID, app.IsDaveConsensus(), repository.ReplayVerificationFull, record, actual),
ErrContradiction,
)
})
}
t.Run("nonaccepted replay diagnostics ignored", func(t *testing.T) {
t.Run("nonaccepted persisted output", func(t *testing.T) {
app, record, actual := replayFixture(model.InputCompletionStatus_Rejected, model.Consensus_Authority)
record.Outputs = [][]byte{[]byte("illegal")}
actual.Outputs = [][]byte{[]byte("diagnostic")}
record.Reports = [][]byte{[]byte("diagnostic")}
actual.Reports = [][]byte{[]byte("diagnostic")}
err := compareRecord(app.Name, app.ID, app.IsDaveConsensus(), repository.ReplayVerificationFull, record, actual)
require.ErrorIs(t, err, ErrContradiction)
var detail *ContradictionError
require.ErrorAs(t, err, &detail)
require.Equal(t, "outputs.count", detail.Field)
})
t.Run("nonaccepted replay outputs ignored", func(t *testing.T) {
app, record, actual := replayFixture(model.InputCompletionStatus_Exception, model.Consensus_Authority)
actual.Outputs = [][]byte{[]byte("diagnostic")}
record.Reports = [][]byte{[]byte("diagnostic")}
actual.Reports = [][]byte{[]byte("diagnostic")}
require.NoError(t, compareRecord(app.Name, app.ID, app.IsDaveConsensus(), repository.ReplayVerificationFull, record, actual))
})
Expand Down
96 changes: 96 additions & 0 deletions internal/replay/report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package replay

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/cartesi/rollups-node/internal/model"
"github.com/cartesi/rollups-node/internal/repository"
)

func TestCompareRecordNonacceptedReports(t *testing.T) {
t.Parallel()
for _, status := range model.InputCompletionStatusAllValues {
if status == model.InputCompletionStatus_Accepted || !status.IsCompleted() {
continue
}
t.Run(status.String(), func(t *testing.T) {
for _, test := range []struct {
name string
reports [][]byte
field string
}{
{"matching", [][]byte{[]byte("first"), []byte("second")}, ""},
{"payload", [][]byte{[]byte("changed"), []byte("second")}, "reports[0]"},
{"count", [][]byte{[]byte("first")}, "reports.count"},
{"order", [][]byte{[]byte("second"), []byte("first")}, "reports[0]"},
} {
t.Run(test.name, func(t *testing.T) {
app, record, actual := replayFixture(status, model.Consensus_Authority)
record.Reports = [][]byte{[]byte("first"), []byte("second")}
actual.Reports = test.reports
err := compareRecord(app.Name, app.ID, false, repository.ReplayVerificationFull, record, actual)
if test.field == "" {
require.NoError(t, err)
return
}
require.ErrorIs(t, err, ErrContradiction)
var detail *ContradictionError
require.ErrorAs(t, err, &detail)
require.Equal(t, test.field, detail.Field)
})
}
})
}
}

func TestCompareRecordLegacyReports(t *testing.T) {
t.Parallel()
app, record, actual := replayFixture(model.InputCompletionStatus_Rejected, model.Consensus_Authority)
actual.Reports = [][]byte{[]byte("historically discarded")}
require.NoError(t, compareRecord(app.Name, app.ID, false, repository.ReplayVerificationCanonical, record, actual))
var detail *ContradictionError
err := compareRecord(app.Name, app.ID, false, repository.ReplayVerificationFull, record, actual)
require.ErrorAs(t, err, &detail)
require.Equal(t, "reports.count", detail.Field)
require.Equal(t, "0", detail.Expected)
require.Equal(t, "1", detail.Actual)
}

func TestRunLegacyReports(t *testing.T) {
t.Parallel()
for _, level := range []repository.ReplayVerificationLevel{
repository.ReplayVerificationCanonical, repository.ReplayVerificationFull,
} {
t.Run(level.String(), func(t *testing.T) {
record := replayRecords(1)[0]
record.Input.Status = model.InputCompletionStatus_Rejected
source := &fakeSource{
summary: model.ReplaySummary{ApplicationID: 7, ProcessedInputs: 1, Consensus: model.Consensus_Authority},
records: []*model.ReplayRecord{record},
}
executor := &fakeExecutor{
statuses: map[uint64]model.InputCompletionStatus{0: model.InputCompletionStatus_Rejected},
reports: map[uint64][][]byte{0: {[]byte("historically discarded")}},
}
opts := replayOptions(model.Consensus_Authority, 0, 1)
opts.Verification = level
result, err := Run(context.Background(), source, executor, opts)
if level == repository.ReplayVerificationCanonical {
require.NoError(t, err)
require.Equal(t, uint64(1), result.ReplayedInputs)
} else {
require.ErrorIs(t, err, ErrContradiction)
var detail *ContradictionError
require.ErrorAs(t, err, &detail)
require.Equal(t, "reports.count", detail.Field)
}
require.Nil(t, record.Reports, "replay must leave the source record unchanged")
})
}
}
2 changes: 2 additions & 0 deletions internal/replay/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type fakeExecutor struct {
wrongResultPos bool
fullPRTResult bool
statuses map[uint64]model.InputCompletionStatus
reports map[uint64][][]byte
}

func (executor *fakeExecutor) ProcessedInputs() uint64 { return executor.processed }
Expand Down Expand Up @@ -97,6 +98,7 @@ func (executor *fakeExecutor) Advance(
EpochIndex: epochIndex,
InputIndex: resultIndex,
Status: model.InputCompletionStatus_Accepted,
Reports: executor.reports[inputIndex],
StateProof: model.StateProof{
MachineHash: common.BigToHash(newBig(inputIndex + 1)),
TxBufferDataBlock: common.BigToHash(newBig(inputIndex + 100)),
Expand Down
Loading
Loading