Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "pkg/test_data/jam-conformance"]
path = pkg/test_data/jam-conformance
url = https://github.com/davxy/jam-conformance.git
[submodule "pkg/test_data/new-gas-cost-model"]
path = pkg/test_data/new-gas-cost-model
url = https://github.com/koute/new-gas-cost-model.git
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,8 @@ run-recompiler-test:
-v "$(shell pwd)":/app \
go-jit-test \
go test -v ./PVM/recompiler/...

# Interpreter vs recompiler consistency on extracted program blobs (linux/amd64 + cgo).
.PHONY: test-backend-consistency
test-backend-consistency:
CGO_ENABLED=1 go test -count=1 -timeout 30m -v ./PVM/ -run TestInterpreterVsRecompilerProgramBlobs
2 changes: 1 addition & 1 deletion PVM/PVMtrace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package PVMtrace

const (
FormatVersion = 1
GraypaperVersion = "0.7.2"
GraypaperVersion = "0.8.0" // GP 0.8.0: conformance re-gate pending official test vectors

BackendInterpreter = "interpreter"
BackendRecompiler = "recompiler"
Expand Down
198 changes: 198 additions & 0 deletions PVM/backend_consistency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//go:build linux && amd64 && cgo

package PVM_test

import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"

PVM "github.com/New-JAMneration/JAM-Protocol/PVM"
_ "github.com/New-JAMneration/JAM-Protocol/PVM/interpreter"
_ "github.com/New-JAMneration/JAM-Protocol/PVM/recompiler"
"github.com/New-JAMneration/JAM-Protocol/internal/service_account"
"github.com/New-JAMneration/JAM-Protocol/internal/types"
)

const (
backendConsistencyBlobDir = "testdata/psi_a_consistency/blobs"
backendConsistencyMinBlobs = 30
backendConsistencyGas = types.Gas(50_000_000)
backendConsistencyEntry = PVM.ProgramCounter(5) // Ψ_A entry
)

// TestInterpreterVsRecompilerProgramBlobs runs each extracted MetaCode program
// through Psi_M_OnBackend on both backends and requires matching Gas +
// ReasonOrBytes (no process-global ExecutionBackend swap).
//
// Blob corpus is not committed: 0.7.2 traces are the wrong generation for
// 0.8.0 semantics. Regenerate locally when suitable traces exist:
//
// python3 scripts/scan_psi_a_program_blobs.py … # see script help / JSON outs
//
// then place MetaCode .bin files under testdata/psi_a_consistency/blobs/.
func TestInterpreterVsRecompilerProgramBlobs(t *testing.T) {
types.SetTinyMode()
t.Cleanup(types.SetTinyMode)

if PVM.Psi_M_interpreterHook == nil {
t.Fatal("interpreter backend not linked")
}
if PVM.Psi_M_recompilerHook == nil {
t.Fatal("recompiler backend not linked")
}

codes, err := loadProgramCodes(backendConsistencyBlobDir)
if err != nil {
t.Skipf("no local blob corpus (%v); regenerate with scripts/scan_psi_a_program_blobs.py when 0.8.0 traces are available", err)
}
if len(codes) < backendConsistencyMinBlobs {
t.Skipf("need >= %d decodable program blobs, found %d in %s (local corpus only)",
backendConsistencyMinBlobs, len(codes), backendConsistencyBlobDir)
}

arg := accumulateEmptyArgument(t)
for _, tc := range codes {
t.Run(tc.name, func(t *testing.T) {
gotI, panicI := runPsiM(t, PVM.BackendInterpreter, tc.code, arg)
gotR, panicR := runPsiM(t, PVM.BackendRecompiler, tc.code, arg)

if panicI != panicR {
t.Fatalf("panic mismatch\n interpreter: %v\n recompiler: %v", panicI, panicR)
}
if panicI != "" {
t.Logf("both panicked: %v", panicI)
return
}
if gotI.Gas != gotR.Gas {
t.Fatalf("Gas: interpreter=%d recompiler=%d", gotI.Gas, gotR.Gas)
}
if !reasonEqual(gotI.ReasonOrBytes, gotR.ReasonOrBytes) {
t.Fatalf("ReasonOrBytes mismatch\n interpreter: %#v\n recompiler: %#v",
gotI.ReasonOrBytes, gotR.ReasonOrBytes)
}
})
}
}

type programCase struct {
name string
code []byte
}

func loadProgramCodes(dir string) ([]programCase, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("read %s: %w (extract blobs first)", dir, err)
}
var out []programCase
for _, e := range entries {
if e.IsDir() || filepath.Ext(e.Name()) != ".bin" {
continue
}
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
return nil, err
}
_, code, err := service_account.DecodeMetaCode(raw)
if err != nil || len(code) == 0 {
continue
}
out = append(out, programCase{name: e.Name(), code: []byte(code)})
}
return out, nil
}

func accumulateEmptyArgument(t *testing.T) PVM.Argument {
t.Helper()
enc := types.NewEncoder()
var serialized []byte
for _, v := range []uint64{0, 1, 0} { // timeslot, serviceId, |operands|
b, err := enc.EncodeUint(v)
if err != nil {
t.Fatalf("EncodeUint: %v", err)
}
serialized = append(serialized, b...)
}
return PVM.Argument(serialized)
}

func runPsiM(t *testing.T, backend string, code []byte, arg PVM.Argument) (got PVM.Psi_M_ReturnType, panicMsg string) {
t.Helper()
addition := minimalAccumulateHostArgs()
defer func() {
if r := recover(); r != nil {
panicMsg = fmt.Sprint(r)
}
}()
var err error
got, err = PVM.Psi_M_OnBackend(
backend,
PVM.StandardCodeFormat(code),
backendConsistencyEntry,
backendConsistencyGas,
arg,
PVM.AccumulateOmegas,
addition,
)
if err != nil {
t.Fatalf("Psi_M_OnBackend(%s): %v", backend, err)
}
return got, panicMsg
}

func minimalAccumulateHostArgs() PVM.HostCallArgs {
sid := types.ServiceID(1)
acct := types.ServiceAccount{
PreimageLookup: types.PreimagesMapEntry{},
LookupDict: types.LookupMetaMapEntry{},
StorageDict: types.Storage{},
}
state := types.ServiceAccountState{sid: acct}
storage := types.StateKeyVals{}
partial := types.PartialStateSet{
ServiceAccounts: state,
AlwaysAccum: types.AlwaysAccumulateMap{},
}
return PVM.HostCallArgs{
GeneralArgs: PVM.GeneralArgs{
ServiceAccount: &acct,
ServiceID: &sid,
ServiceAccountState: &state,
StorageKeyVal: &storage,
},
AccumulateArgs: PVM.AccumulateArgs{
ResultContextX: PVM.ResultContext{
ServiceID: sid,
PartialState: partial,
DeferredTransfers: []types.DeferredTransfer{},
ServiceBlobs: map[types.OpaqueHash]types.ServiceBlob{},
StorageKeyVal: &storage,
},
ResultContextY: PVM.ResultContext{
ServiceID: sid,
PartialState: partial.DeepCopy(),
DeferredTransfers: []types.DeferredTransfer{},
ServiceBlobs: map[types.OpaqueHash]types.ServiceBlob{},
StorageKeyVal: &storage,
},
OperandOrDeferredTransfers: nil,
Timeslot: 0,
},
}
}

func reasonEqual(a, b any) bool {
if reflect.DeepEqual(a, b) {
return true
}
// Normalize []byte vs nil empty.
ab, aOK := a.([]byte)
bb, bOK := b.([]byte)
if aOK && bOK {
return string(ab) == string(bb)
}
return false
}
Loading