diff --git a/.gitmodules b/.gitmodules index 39baff85..6b3c848f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/Makefile b/Makefile index 981d57b2..f75faad4 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/PVM/PVMtrace/trace.go b/PVM/PVMtrace/trace.go index 5555816a..68ae1d4d 100644 --- a/PVM/PVMtrace/trace.go +++ b/PVM/PVMtrace/trace.go @@ -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" diff --git a/PVM/backend_consistency_test.go b/PVM/backend_consistency_test.go new file mode 100644 index 00000000..01c5edcb --- /dev/null +++ b/PVM/backend_consistency_test.go @@ -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 +} diff --git a/PVM/block_info.go b/PVM/block_info.go index 56f1613d..9d45c3d2 100644 --- a/PVM/block_info.go +++ b/PVM/block_info.go @@ -5,13 +5,14 @@ import "encoding/binary" // InstrMeta holds pre-decoded metadata for a single PVM instruction. // Populated once at deblob time; never mutated afterwards. type InstrMeta struct { - PC ProgramCounter // 4B - Opcode byte // 1B - SkipLen uint8 // 1B, max 24 - Dst uint8 // 1B, destination reg index (0xFF = none) - Src [2]uint8 // 2B, source reg indices (0xFF = unused) - Exec instrMetaFn // pre-resolved handler; set at deblob time - Imm [2]uint64 // 16B, immediates / branch target PC + PC ProgramCounter // 4B + Opcode byte // 1B + SkipLen uint8 // 1B, max 24 + Dst uint8 // 1B, destination reg index (0xFF = none) + Src [2]uint8 // 2B, source reg indices (0xFF = unused) + BlockStart ProgramCounter // 4B, 𝔏(PC) (A.6): start of the enclosing basic block + Exec instrMetaFn // pre-resolved handler; set at deblob time + Imm [2]uint64 // 16B, immediates / branch target PC } // BlockMeta holds pre-decoded metadata for a single PVM basic block. @@ -21,7 +22,7 @@ type BlockMeta struct { EndPC ProgramCounter // PC of the terminating instruction (inclusive) InstrStart int // index into Program.Instrs[] InstrEnd int // exclusive upper bound into Program.Instrs[] - GasCost Gas // v0.7.2: = InstrCount; TODO(gas-model): = simulatePipeline() + GasCost Gas // A.9 gascostforblock = max(cycles − 3, 1) } // InstrCount returns the number of instructions in this block. @@ -145,14 +146,19 @@ func decodeOperands(instr *InstrMeta, idata ProgramCode, bitmask Bitmask) { } } -// preDecodeBlocks performs a single-pass scan of the entire program blob, -// populating Program.Instrs, Program.BlockAt, and Program.InstrIdxAt. -// Called once at the end of DeBlobProgramCode. +// preDecodeBlocks builds InstrMeta / BlockMeta and caches A.9 gas per block. +// Mid-stream 𝔳_inst failures are fatal. A final open block (no terminator) is +// still emitted so A.9 can cost incomplete fixtures; 𝔳_blob rejects that case +// in deblobValidatedProgram via finalInstructionIsTerminator. func (p *Program) preDecodeBlocks() ExitReason { idata := p.InstructionData bitmask := p.Bitmasks n := len(idata) + if len(bitmask) != n || n == 0 { + return ExitPanic + } + p.Instrs = make([]InstrMeta, 0, n/4) p.BlockAt = make([]*BlockMeta, n) p.InstrIdxAt = make([]int32, n) @@ -160,54 +166,72 @@ func (p *Program) preDecodeBlocks() ExitReason { p.InstrIdxAt[i] = -1 } - pc := ProgramCounter(0) - for pc < ProgramCounter(n) { - if !bitmask.IsStartOfBasicBlock(pc) { - pc++ - continue - } + blockStartPC := 0 + blockInstrStart := 0 + // emitBlock writes BlockMeta + gas for [blockStartPC, endPC]. + emitBlock := func(endPC int) { block := &BlockMeta{ - StartPC: pc, - InstrStart: len(p.Instrs), + StartPC: ProgramCounter(blockStartPC), + EndPC: ProgramCounter(endPC), + InstrStart: blockInstrStart, + InstrEnd: len(p.Instrs), } + block.GasCost = GasCostForBlock(p, block.StartPC) + for i := block.InstrStart; i < block.InstrEnd; i++ { + p.Instrs[i].BlockStart = block.StartPC + } + p.BlockAt[blockStartPC] = block + } - for { - if pc >= ProgramCounter(n) { - return ExitPanic - } - op := idata[pc] - if !IsValidOpcode(op) { - return ExitPanic + for pc := 0; ; { + if pc >= n { + // Code ended mid-block (no terminator): emit prefix for gas analysis. + // deblobValidatedProgram rejects this via 𝔳_blob (A.2). + if blockInstrStart < len(p.Instrs) { + emitBlock(int(p.Instrs[len(p.Instrs)-1].PC)) } + return ExitContinue + } - skipLen := skip(int(pc), bitmask) - - idx := len(p.Instrs) - p.Instrs = append(p.Instrs, InstrMeta{ - PC: pc, - Opcode: op, - SkipLen: uint8(skipLen), - Exec: instrMetaExecForOpcode(op), - }) - p.InstrIdxAt[pc] = int32(idx) - - decodeOperands(&p.Instrs[idx], idata, bitmask) - - if IsBlockTerminator(op) { - block.EndPC = pc - block.InstrEnd = len(p.Instrs) - block.GasCost = Gas(block.InstrEnd - block.InstrStart) - p.BlockAt[block.StartPC] = block - pc += ProgramCounter(skipLen) + 1 - break - } + skipLen := skip(pc, bitmask) + next := pc + 1 + int(skipLen) + + // 𝔳_inst: every step of the walk lands on a defined instruction. + if !validInst(idata, bitmask, uint64(pc)) { + return ExitPanic + } - pc += ProgramCounter(skipLen) + 1 + op := idata[pc] + idx := len(p.Instrs) + p.Instrs = append(p.Instrs, InstrMeta{ + PC: ProgramCounter(pc), + Opcode: op, + SkipLen: uint8(skipLen), + Exec: instrMetaExecForOpcode(op), + }) + p.InstrIdxAt[pc] = int32(idx) + + decodeOperands(&p.Instrs[idx], idata, bitmask) + + if IsBlockTerminator(op) { + emitBlock(pc) + // ϖ | A.3: the index following a terminator starts the next block. + blockStartPC = next + blockInstrStart = len(p.Instrs) } + + pc = next } +} - return ExitContinue +// finalInstructionIsTerminator reports whether 𝔳_blob's final-instruction +// rule holds: the last decoded opcode is in T (A.2). +func (p *Program) finalInstructionIsTerminator() bool { + if len(p.Instrs) == 0 { + return false + } + return IsBlockTerminator(p.Instrs[len(p.Instrs)-1].Opcode) } // LookupBlock returns the pre-decoded BlockMeta for a basic block starting at pc. @@ -219,22 +243,29 @@ func (p *Program) LookupBlock(pc ProgramCounter) *BlockMeta { return p.BlockAt[pc] } +// StartOfBasicBlock is 𝔏(ι) | A.3: the start of the basic block containing pc. +// Reports false when pc is not an instruction start, for which 𝔏 is undefined. +func (p *Program) StartOfBasicBlock(pc ProgramCounter) (ProgramCounter, bool) { + if int(pc) >= len(p.InstrIdxAt) { + return 0, false + } + idx := p.InstrIdxAt[pc] + if idx < 0 { + return 0, false + } + return p.Instrs[idx].BlockStart, true +} + // BlockContaining returns the BlockMeta whose instruction range includes pc. // LookupBlock only works at block entry PCs; this resolves mid-block resume PCs -// (e.g. after sbrk returns to Go and continues at the fallthrough instruction). +// (e.g. after a host-call returns to Go and continues at the next instruction). func (p *Program) BlockContaining(pc ProgramCounter) *BlockMeta { if b := p.LookupBlock(pc); b != nil { return b } - idx := p.InstrIdxAt[pc] - if idx < 0 { + start, ok := p.StartOfBasicBlock(pc) + if !ok { return nil } - i := int(idx) - for _, b := range p.BlockAt { - if b != nil && i >= b.InstrStart && i < b.InstrEnd { - return b - } - } - return nil + return p.LookupBlock(start) } diff --git a/PVM/branch.go b/PVM/branch.go index 6cb03ee5..5141b7d0 100644 --- a/PVM/branch.go +++ b/PVM/branch.go @@ -1,14 +1,24 @@ package PVM -func branch(pc ProgramCounter, b ProgramCounter, C bool, bitmask Bitmask, instruction ProgramCode) (ExitReason, ProgramCounter) { - switch { - case !C: - return ExitContinue, pc - case !bitmask.IsStartOfBasicBlock(b) && instruction.isOpcodeValid(b): +// sjump (A.20): unconditional static jump. +// Panics if target b is not a basic block start (b ∉ ϖ). +func sjump(pc ProgramCounter, b ProgramCounter, bitmask Bitmask) (ExitReason, ProgramCounter) { + if !bitmask.IsStartOfBasicBlock(b) { + return ExitPanic, pc + } + return ExitContinue, b +} + +// branch (A.21): conditional jump with dual-target validation. +// Both b and ft must be basic block starts; panics otherwise. +func branch(pc ProgramCounter, b ProgramCounter, C bool, ft ProgramCounter, bitmask Bitmask) (ExitReason, ProgramCounter) { + if !bitmask.IsStartOfBasicBlock(b) || !bitmask.IsStartOfBasicBlock(ft) { return ExitPanic, pc - default: - return ExitContinue, b } + if !C { + return ExitContinue, ft + } + return ExitContinue, b } // ResolveDynamicJump resolves a jump-table address to a program PC. @@ -33,12 +43,8 @@ func djump(pc ProgramCounter, a uint32, jumpTable JumpTable, bitmask Bitmask) (E return DjumpResolve(pc, a, jumpTable, bitmask) } -// DjumpResolve performs the full graypaper §4.4.4 dynamic-jump resolution and validation: -// HALT for the sentinel address, panic on misaligned / out-of-range / non-basic-block targets, -// otherwise returns the resolved program counter. -// The pc parameter is the PC reported on panic (typically the jump_ind instruction PC). -// Exported for the JIT recompiler to call from its dispatcher; the interpreter -// continues to use the lowercase djump alias above. +// DjumpResolve (A.22): dynamic jump resolution and validation. +// Exported for the JIT recompiler; interpreter uses the lowercase djump alias. func DjumpResolve(pc ProgramCounter, a uint32, jumpTable JumpTable, bitmask Bitmask) (ExitReason, ProgramCounter) { if a == 0xffff0000 { return ExitHalt, pc diff --git a/PVM/docs/0_Recompiler_Setup.md b/PVM/docs/0_Recompiler_Setup.md index 3c6a5972..5cee7783 100644 --- a/PVM/docs/0_Recompiler_Setup.md +++ b/PVM/docs/0_Recompiler_Setup.md @@ -35,7 +35,7 @@ Host 進程 - **初始**:整塊 `PROT_NONE` + `MAP_NORESERVE`(只佔虛擬位址空間,物理 RAM 按需分配) - **Control region**:立刻 `mprotect` 為 `R/W` -- **Guest memory 4GB**:`InitFromProgram` / sbrk 時才按 segment / page 設權限 +- **Guest memory 4GB**:`InitFromProgram` / `grow_heap` 時才按 segment / page 設權限 - **Guard page**:越界存取落在合法 mmap 範圍內,由 signal handler 處理(不觸發 kernel crash) 存取方式: @@ -81,10 +81,10 @@ R15 指向 guest memory 起點;VM 執行狀態存在 R15 **之前** 的 4KB |-------------------|------|------| | 8 | ReturnStack | 存 host RSP,exit trampoline / signal handler 還原 | | 16 | ReturnAddress | 存 host 返回位址 | -| 24 | HeapPointer | sbrk 維護的 heap 頂端 | +| 24 | HeapPointer | `grow_heap` 維護的 heap 頂端 | | 32 | ExitPC | JIT 退出時的 PVM PC | | 40 | ExitReason | HALT / PANIC / OOG / HOST_CALL / PAGE_FAULT | -| 48 | Gas | 每條指令 inline `sub [R15-48], 1`(disp8 範圍內) | +| 48 | Gas | block 入口 inline 扣費(disp8 範圍內) | | 49–152 | Registers[13] | 13 × 8 bytes = 104B,trampoline 邊界 save/restore | | 160 | MemAccessAddr | debug trace 用 | | 168 | MemAccessVal | debug trace 用 | diff --git a/PVM/docs/1_Recompiler_Workflow.md b/PVM/docs/1_Recompiler_Workflow.md index 9007220a..f5030212 100644 --- a/PVM/docs/1_Recompiler_Workflow.md +++ b/PVM/docs/1_Recompiler_Workflow.md @@ -60,17 +60,19 @@ host.HostCall ─────────────────── 外層 ``` CompileBasicBlock(startPC) 1. 從 Program.BlockAt[startPC] 取 BlockMeta + 指令切片 - 2. 對每條指令: - emitGasCheck (GP v0.7.2: load / test / sub / OOG label) + 2. blockGas = blockGasCostAt(startPC)(A.9,compile 時 bake 進 native code) + 3. emitBlockGasCheck(blockGas) + block OOG landing pad + 4. 對每條指令: opcodeHandlers[opcode](c, asm, instr) → emit x86 指令 - 3. Block epilogue: + terminator 最後一條 → emitGasCharged(false)(離開 block 重置 flag) + 5. Block epilogue: fallthrough 目標已編譯 → JMP NativeAddr(compile-time link) 否則 → emitChainOrExit:runtime 查 PC→native dispatch table, hit 直接 jmp 進目標;miss 才寫 CONTINUE + 下一 PC → exit_trampoline - 4. 每指令 OOG landing pad + EmitExitTrampoline - 5. Assembler.Finalize() → []byte(機器碼) - 6. em.Write(code) → 寫入 ExecutableMemory - 7. CodeCache.Put + registerDispatch(djump dispatch table) + 6. EmitExitTrampoline + 7. Assembler.Finalize() → []byte(機器碼) + 8. em.Write(code) → 寫入 ExecutableMemory + 9. CodeCache.Put + registerDispatch(djump dispatch table) ``` ### 寫入 ExecutableMemory(Dual Mapping,零 mprotect) @@ -144,7 +146,6 @@ BlockBasedInvoke(pc) [LockOSThread 一次, L1] 依 exitReason 分支: CONTINUE → pc = exitPC, continue HOST_CALL → break → 外層 host.HostCall 跑 omega → 再 MachineInvoke - sbrk (0xFF) → HandleSbrk (Go mprotect) → continue djump miss → compile target → continue HALT/PANIC/OOG/PAGE_FAULT → 結束 ``` @@ -158,7 +159,7 @@ block 再進 native——round-trip 次數 ≈ 走過的 block 數(conformance Chaining 後,block epilogue 在 native 內直接 `jmp` 進下一個已編譯 block (compile-time link 或 dispatch table hit):PVM registers 全程留在 x86 register、 不經 trampoline、不回本 loop。只有 **host call、chain/djump miss(冷啟一次性)、 -sbrk 跨頁、終止類出口** 才回 Go。同一 dataset 實測 round-trip 降至 +終止類出口** 才回 Go。同一 dataset 實測 round-trip 降至 **~40 次/invoke**(roundTrips 3,310,512 → 28,302,117×↓)——一次 invoke 的內層 loop 幾乎只在必要出口才轉一圈。 @@ -207,8 +208,7 @@ Native code 存取 `PROT_NONE` 頁面 → CPU page fault → SIGSEGV → signal |------|----------|----------------------| | chain miss(static 目標未編譯) | `BlockBasedInvoke` 編譯 + 填 dispatch table | 否(一次性;之後同出口 native chain) | | block link / chain hit JMP | native 內 | 否(完全不回 Go) | -| `ecalli` | `host.HostCall` → omega | 是(外層 loop) | -| sbrk 跨頁 | `HandleSbrk`(Go mprotect) | 否(resolve 後 continue) | +| `ecalli` | `host.HostCall` → omega(含 `grow_heap`) | 是(外層 loop) | | djump hit | native `JmpReg`(dispatch table) | 否 | | djump miss | Go compile + dispatch 更新 | 否 | | OOG / HALT / PANIC | 結束 invoke | 是 | @@ -253,7 +253,7 @@ Native code 存取 `PROT_NONE` 頁面 → CPU page fault → SIGSEGV → signal | 分類 | 項目 | |------|------| -| Graypaper 語意(另章) | Gas model、Host Call (omega)、sbrk、djump、PVMtrace | +| Graypaper 語意(另章) | Gas model、Host Call (omega)、grow_heap、djump、PVMtrace | | Recompiler 特有 | mmap layout、dual mapping、trampoline、signal handler、block linking、register map | | 兩邊共用 | `DeBlobProgramCode`、`preDecodeBlocks`、`GuestMemory` interface | diff --git a/PVM/docs/2_x86_Assembler.md b/PVM/docs/2_x86_Assembler.md index c0e1e4e5..fbccc80c 100644 --- a/PVM/docs/2_x86_Assembler.md +++ b/PVM/docs/2_x86_Assembler.md @@ -152,12 +152,12 @@ type CodeBuffer struct { Compiler 是 **single-pass**,遇到跳轉目標可能還沒 emit(forward reference): ``` -oog := a.NewLabel() -emitGasCheck → Jcc(oog) ← 目標尚未存在(unbound handle) -...(更多指令)... -BindLabel(oog) ← 現在存在了 +blockOOG := a.NewLabel() +emitBlockGasCheck → Jcc(blockOOG) ← 目標尚未存在(unbound handle) +...(block 內指令 emit)... +BindLabel(blockOOG) ← OOG landing pad ... -Finalize() → ResolveFixups() ← 回填所有 placeholder +Finalize() → ResolveFixups() ← 回填所有 placeholder ``` `Jcc` 會 emit `0F 8x [placeholder_4bytes]`,`ResolveFixups` 最後算出相對距離填回去。 @@ -188,7 +188,7 @@ Jcc(cc, l Label) // fixups append {l, 洞位置} |------|------|------| | ① 區域 label(最大宗):taken、halt、djump_miss/panic、chain_miss、div 系列 | 名字編入 PC 保唯一 | 產生與使用在同一 emit 函式內 → `l := a.NewLabel()` 區域變數,`NewLabel` 天生唯一,**不需任何註冊** | | ② 跨 emit 函式、per-block 共用:exit trampoline | 各 emit 函式用字串約定 `"exit_trampoline"` | Assembler 欄位,`Reset()` 時預配,call site 用 `a.Jmp(a.ExitTrampoline())`(return_label 只在 `EmitEntryTrampoline` 內自產自用,屬 ①) | -| ③ 成對但分離的 per-PC:OOG landing pad | hot 端先引用、cold 端後 bind,靠「同 PC 算同名」對上 | **不用 map[PC]Label**——編譯迴圈按同序走兩遍,用 index 對齊的 slice:迴圈前 `oogLabels := make([]asm.Label, len(instrs))` 一次配好,`emitGasCheck` 與 landing-pad 迴圈都用 `oogLabels[i]` | +| ③ block 入口 OOG landing pad | hot 端先引用、cold 端後 bind | block 開頭 `emitBlockGasCheck` 引用 `blockOOG`;指令 loop 後 `emitBlockOutOfGasExit` bind 同一 label | **成本對照**(每個 label 引用點): @@ -220,16 +220,23 @@ Jcc(cc, l Label) // fixups append {l, 洞位置} ## 4. Emit 設計模式(Compiler 層) -### 4.1 每條 PVM 指令的 emit 流程 +### 4.1 Block emit 流程 ```go -// compiler.go 主迴圈(oogLabels 在迴圈前一次配好,見 §3.3.1 ③) +// compiler.go(GP 0.8.0 block gas) +blockOOG := a.NewLabel() +blockGas := c.blockGasCostAt(startPC) // A.9 GasCostFromPC / GasCostForBlock +c.emitBlockGasCheck(a, blockOOG, blockGas) + for i := range instrs { instr := &instrs[i] - c.emitGasCheck(a, oogLabels[i]) // 2 條 x86(sub + js,見 §4.5) + if i == len(instrs)-1 && IsBlockTerminator(instr.Opcode) { + emitGasCharged(a, false) // 離開 block 重置 gaschargedflag + } handler := opcodeHandlers[instr.Opcode] - handler(c, a, instr) // PVM opcode → x86 序列 + handler(c, a, instr) // PVM opcode → x86 序列 } +emitBlockOutOfGasExit(a, blockOOG, blockMeta.StartPC, blockGas) ``` ### 4.2 opcodeHandlers dispatch table @@ -264,9 +271,9 @@ opcodeHandlers[131] = (*Compiler).emitAddImm32 // add_imm | `emit_memory.go` | load/store(1/2/4/8 byte,直接 / indirect) | | `emit_arith_imm.go` | 一個 reg + 一個 imm 的算術(add_imm、sub_imm、mul_imm…) | | `emit_arith_three.go` | 兩個 reg 的算術(add、sub、mul、div、shift、bitwise) | -| `emit_two_reg.go` | 兩 reg 特殊操作(move_reg、sbrk、clz、ctz、popcnt、bswap) | +| `emit_two_reg.go` | 兩 reg 特殊操作(move_reg、clz、ctz、popcnt、bswap) | | `emit_branch.go` | branch(條件跳轉)、djump(indirect jump) | -| `gas.go` | per-instruction / block-based gas check emit | +| `gas.go` | block-level gas check emit(`emitBlockGasCheck` / `emitBlockOutOfGasExit`) | | `emit_record_mem.go` | debug trace 的 memory access 記錄 | ### 4.4 Memory 存取的 emit 模式 @@ -285,37 +292,36 @@ MOV [R15 + RCX], src32 如果地址越界(碰到 PROT_NONE page)→ 硬體 SIGSEGV → signal handler 捕獲 → ExitPageFault。 -### 4.5 Gas Check emit(GP v0.7.2 per-instruction,charge+check 融合) +### 4.5 Gas Check emit(GP 0.8.0 block-level,A.4 / A.9) -**為什麼融合**:早期版本每條 PVM 指令插 4 條 x86(`MOV` 讀 gas → `TEST` → `Jcc` → -`SUB` 扣費),2 次 memory op。而 payload 本身(如 `ADD r64,r64`)常常只有 1 條 — -gas 協議是 emitted code 熱路徑的最大宗(常為 payload 的 2–6 倍)。把扣費與檢查融合 -成 `SUB` + `JS` 後開銷減半(4→2 條、2→1 次 memory op),且不必等 GP 0.8.0 的 -block-based gas:語意仍是 per-instruction,只是換一個等價的檢查形式。實測 -conformance run 桶 883ms → 838ms(-5%)。 +GP 0.8.0 改為 **basic block 入口一次性 pre-charge**(`gascostforblock`),recompiler 在 compile 時用 `blockGasCostAt(startPC)` 算出成本並 bake 進 native code。mid-block resume(host call 返回等)編譯 suffix block,gas 同樣在 suffix 入口一次扣除。 -每條 PVM 指令前插入 2 條 x86(扣費與檢查融合): +Block 開頭插入 gas check(扣費與檢查融合): ```asm -SUB qword [R15 - 48], 1 // 扣 1 gas -JS oog_i // 結果 < 0 ⟺ 扣費前 gas < 1 → 跳 OOG exit +; gaschargedflag == 0 時才扣費 +TEST byte [R15 - gasChargedOff], 0 +JNE charged +SUB qword [R15 - 48], blockGas // 扣整段 block gas +JS block_oog // 結果 < 0 → OOG +MOV byte [R15 - gasChargedOff], 1 +charged: +; ... block 指令 payload ... ``` -等價性:進 block 時 gas 永遠 ≥ 0,所以 `post < 0 ⟺ pre ≤ 0 ⟺ pre < 1`——與 -interpreter 的 `Gas < 1` 判斷相同,兩個 backend 在同一條指令停下。 - -Block epilogue 再 emit 每指令的 OOG landing pad(`oog_i` 是 §3.3.1 ③ 的 -index 對齊 label): +OOG landing pad(interpreter OOG 不扣費,需補回): ```asm -oog_i: - SUB qword [R15-48], -1 // 把融合扣掉的 1 補回(interpreter OOG 不扣費) - MOV dword [R15-32], pc // 設 ExitPC - MOV RCX, ExitOOG // 設 ExitReason +block_oog: + SUB qword [R15-48], -blockGas // 把剛扣的 block gas 補回 + MOV dword [R15-32], blockStartPC // 設 ExitPC = block 起始 PC(A.4) + MOV RCX, ExitOOG MOV [R15-40], RCX JMP exit_trampoline ``` +> **歷史**:v0.7.2 曾用 per-instruction `SUB [gas], 1` + 每指令 OOG pad;GP 0.8.0 改 block gas 後已移除。 + --- ## 5. 與 recompiler 的關係總覽 diff --git a/PVM/docs/4_HostCall_Integration.md b/PVM/docs/4_HostCall_Integration.md index 9dd8c7e2..7b7d2333 100644 --- a/PVM/docs/4_HostCall_Integration.md +++ b/PVM/docs/4_HostCall_Integration.md @@ -140,57 +140,66 @@ ctx.WriteGas(gas) --- -## 5. MachineInvoke 與 BlockBasedInvoke 的關係 +## 5. MachineInvoke 與 block 執行引擎 + +`MachineInvoke` 是 **Ψ_H 外層 loop 的統一入口**:從 `pc` 跑到 HALT / host call / OOG / panic 等非 CONTINUE 為止。 +本身幾乎不做事,只做 **build-tag 分流**(trace vs production)並轉呼叫底下的 block 引擎。 + +### 路由(依 backend) + +兩個 **outer Ψ_H** backend 都跑 **pre-decoded** basic blocks(`deblob` → `preDecodeBlocks` 產生的 `Instrs` / `BlockMeta` / `GasCost`)。 +函式名稱不同,語意對稱: + +| Backend | 檔案 | Production | 執行方式 | Trace | +|---------|------|------------|----------|-------| +| Interpreter | `interpreter/invoke_mode.go` | `BlockBasedInvokeDecodedBlocks` | Go 直譯 pre-decoded blocks | `DebugSingleStepInvoke` | +| Recompiler | `recompiler/invoke_mode.go` | `BlockBasedInvoke` | native JIT 編譯 + 執行 **同一套** pre-decoded blocks | `DebugSingleStepInvoke` | + +Recompiler 的 `lookupOrCompileBlock` → `CompileBasicBlock` 讀 `program.BlockContaining(pc)` 與 `Instrs[InstrStart:InstrEnd]`,**不是** interpreter 的 runtime `DecodeInstructionBlock`。 + +Refine inner VM(Ω_K `invoke`)同樣走 **`BlockBasedInvokeDecodedBlocks`**(interpreter only;無 recompiler / 無 `MachineInvoke` 包裝)。`machine` 註冊時預 decode 存 `IntegratedPVMType.Program`;`invoke` 僅重驗 `𝔳_inst`。 + +### 呼叫點 + +| 呼叫者 | 檔案 | 被叫 | +|--------|------|------| +| Ψ_H 外層 loop | `interpreter/host.go` | `h.MachineInvoke(pc)` | +| Ψ_H 外層 loop | `recompiler/host.go` | `h.recomp.MachineInvoke(pc)` | +| refine inner VM(Ω_K invoke) | `host_call_refine.go` | `tempInterp.BlockBasedInvokeDecodedBlocks` | + +Inner 不經 `MachineInvoke` / recompiler:每次 `invoke` 建立 ephemeral `tempInterp`,gas 由 outer `M_K + g_R` 帳務退還。 + +### Recompiler:`BlockBasedInvoke` 內部 ``` -host.HostCall (外層 loop) - │ +host.HostCall └── MachineInvoke(pc) - │ └── BlockBasedInvoke(pc) - │ for { block = lookupOrCompileBlock(pc) executeBlockLocked(block) switch exitReason: - CONTINUE → pc = exitPC, continue - sbrk → resolveSbrk, continue - djump → resolveDjump, continue - 其他 → return (HOST_CALL / HALT / ...) + CONTINUE → 下一 block + djump miss → 內部消化,不出 MachineInvoke + 其他 → 回傳 host(HOST_CALL / HALT / …) } ``` -**BlockBasedInvoke 內部消化的 exit**: -- `CONTINUE`:block fallthrough,繼續下一個 block -- `sbrk`(`0xFF`):HandleSbrk(Go mprotect),不出 MachineInvoke -- `djump miss`(`0xFE`):compile target,不出 MachineInvoke - -**上報給 host 的 exit**: -- `HOST_CALL`(真正的 ecalli):需要 omega dispatch -- `HALT` / `PANIC` / `OOG` / `PAGE_FAULT`:程式結束 - ---- +**內部消化(不上報 host)**:`CONTINUE` fallthrough、`djump miss`(0xFE)。 -## 6. sbrk 的特殊處理 +**上報 host**:`HOST_CALL`(含 `grow_heap`,omega ID=1)、`HALT`、`PANIC`、`OOG`、`PAGE_FAULT`。 -sbrk 在語意上也是「回到 Go 做事」,但它**不是真的 host call**——它不走 omega dispatch,而是在 `BlockBasedInvoke` 內部用特殊的 `SbrkCallID = 0xFF` 標記。 +### 為何保留 `MachineInvoke` 這層? -為什麼不走 omega: -- sbrk 需要 `mprotect`(kernel syscall),只有 Go 能安全呼叫 -- 但它不需要讀寫 service state,不需要 `Addition` / `HostCalls` -- 處理完就能繼續跑,不需要離開 `MachineInvoke` 的 LockOSThread 區間 +- Trace / production 分流在 `invoke_mode*.go`,不污染 `host.go` +- 兩 backend 共用同一呼叫慣例 +- JIT profile 以 `MachineInvoke` 計 `lockCalls` -```go -// recompiler.go — BlockBasedInvoke -if IsSbrkExit(exitReason) { - exitReason, pc = r.resolveSbrk(instr) - continue // 不出 MachineInvoke -} -``` +刪掉改直接呼叫 block 引擎幾乎無效能收益,還會打散 build-tag 結構。 --- -## 7. 完整時序圖 +## 6. 完整時序圖 ``` host.HostCall MachineInvoke/BlockBased Native Code @@ -225,7 +234,7 @@ MachineInvoke(pc=next) ──► --- -## 8. ExitReason 編碼 +## 7. ExitReason 編碼 ``` ExitReason = uint64 @@ -247,7 +256,6 @@ HOST_CALL: type=0x05, payload=callID (omega operation ID) | sentinel | callID | 用途 | |----------|--------|------| -| sbrk | 0xFF | HandleSbrk(mprotect) | | djump | 0xFE | indirect jump resolve | --- @@ -256,10 +264,12 @@ HOST_CALL: type=0x05, payload=callID (omega operation ID) | 檔案 | 職責 | |------|------| -| `PVM/recompiler/host.go` | host-call dispatch 層(外層 loop + omega 呼叫) | -| `PVM/recompiler/recompiler.go` | `BlockBasedInvoke`(inner loop、sbrk/djump resolve) | -| `PVM/recompiler/invoke_mode.go` | `MachineInvoke` → `BlockBasedInvoke` routing | -| `PVM/recompiler/emit_basic.go` | `emitEcalli`(native code emit) | -| `PVM/recompiler/execute.go` | `HandleSbrk`、`SbrkCallID`、`DjumpCallID` | -| `PVM/recompiler/guest_memory.go` | `GuestMemory` interface 實作(Layer 1 check) | -| `PVM/recompiler/trampoline.go` | exit trampoline(回存 regs → return Go) | +| `PVM/interpreter/host.go` | interpreter Ψ_H 外層 loop → `MachineInvoke` | +| `PVM/interpreter/invoke_mode.go` | `MachineInvoke` → `BlockBasedInvokeDecodedBlocks` | +| `PVM/interpreter/invoke_mode_trace.go` | trace 分流 → `DebugSingleStepInvoke` | +| `PVM/recompiler/host.go` | recompiler Ψ_H 外層 loop → `MachineInvoke` | +| `PVM/recompiler/recompiler.go` | `BlockBasedInvoke`(pre-decoded blocks → native JIT) | +| `PVM/recompiler/invoke_mode.go` | `MachineInvoke` → `BlockBasedInvoke` | +| `PVM/recompiler/invoke_mode_trace.go` | trace 分流 | +| `PVM/invocation.go` | `BlockBasedInvoke*` / `DebugSingleStepInvoke` 實作 | +| `PVM/host_call_refine.go` | Ω_K inner VM → `BlockBasedInvoke`(非 MachineInvoke;#15) | diff --git a/PVM/docs/6_PVMtrace.md b/PVM/docs/6_PVMtrace.md index 821741d7..b5efedbc 100644 --- a/PVM/docs/6_PVMtrace.md +++ b/PVM/docs/6_PVMtrace.md @@ -158,11 +158,11 @@ MachineInvoke(pc): ``` for each instruction at pc: - 1. CompileSingleInstruction(instr) → 只編譯一條指令的 native code + 1. CompileBlockInstruction(instr) → one instr, A.7 block gas, trampoline to Go 2. 記錄 src1_val, src2_val(執行前) 3. ClearMemAccess() 4. executeBlockLocked(block) → 執行這一條指令 - 5. 處理 sbrk / djump sentinel exits + 5. 處理 djump sentinel exits 6. 記錄 dst_val(執行後) 7. 讀取 MemAccess(load/store addr+val) 8. trace.RecordStep(...) diff --git a/PVM/docs/TODO.md b/PVM/docs/TODO.md index 6e1e3627..eb85fb98 100644 --- a/PVM/docs/TODO.md +++ b/PVM/docs/TODO.md @@ -67,13 +67,10 @@ compile 已小,純 codegen 速度優先序低。經解剖,子項的判定: ### 3.(低)eviction 後續 proactive 淘汰(從 state 訊號主動刪「已知不會再用」的 CodeHash);bytes-based cap;arena-full 優雅處理(目前滿了 → compile error,16MB/service 通常夠);`*Program` cache 也加界限。 -### 4.(暫緩)block-based gas(GP 0.8.0) -0.7.2 仍 per-instruction,改了**沒測資料可驗**。`gas.go` 已備好 `emitBlockGasCheck` / `emitBlockOutOfGasExit`,等 0.8 向量再開。 - -### 5.(低)已知語意缺口與待補測試 +### 4.(低)已知語意缺口與待補測試 - **跨頁 memory access 語意**:recompiler 靠硬體 fault——PAGE_FAULT payload 用 `si_addr`(實際 fault 位址,常為第二頁),且 store 在 fault 前可能已部分寫入第一頁;interpreter 則先檢查兩頁權限、fault 回報起始位址、all-or-nothing。僅在存取剛好跨 mapped/PROT_NONE 邊界時分歧;conformance 未覆蓋此細節,PVMtrace 對齊可能受影響。修法:emit page-aware check 對齊 interpreter 兩頁邏輯(`emit_memory.go` vs `decode.go`)。 - **待補單元測試**:`jump_ind` + HALT sentinel → `Psi_H.Counter == instr.PC`(程式碼已對齊 interpreter,conformance 測不到此項)。 -- (長期)inline sbrk 擴大(同頁內不出 native)、hot omega 的 native stub——皆需 profile 證明才動。 +- (長期)hot omega 的 native stub——需 profile 證明才動。 --- diff --git a/PVM/execution_backend.go b/PVM/execution_backend.go new file mode 100644 index 00000000..86c00a60 --- /dev/null +++ b/PVM/execution_backend.go @@ -0,0 +1,56 @@ +package PVM + +import ( + "fmt" + + "github.com/New-JAMneration/JAM-Protocol/internal/types" +) + +// SetExecutionBackend validates and assigns the process-global ExecutionBackend +// used by Psi_M. Prefer assigning once at process start; do not toggle after +// concurrent Psi_M callers exist. Dual-backend tests should use Psi_M_OnBackend +// instead of swapping this global. +func SetExecutionBackend(backend string) error { + if _, err := psiMHook(backend); err != nil { + return err + } + ExecutionBackend = backend + return nil +} + +// Psi_M_OnBackend runs Ψ_M on an explicit backend without mutating +// ExecutionBackend. Prefer this in dual-backend tests over temporarily +// swapping the process-global selector. +func Psi_M_OnBackend( + backend string, + code StandardCodeFormat, + counter ProgramCounter, + gas types.Gas, + argument Argument, + omegas Omegas, + addition HostCallArgs, +) (Psi_M_ReturnType, error) { + hook, err := psiMHook(backend) + if err != nil { + return Psi_M_ReturnType{}, err + } + return hook(code, counter, gas, argument, omegas, addition), nil +} + +func psiMHook(backend string) (PsiMBackend, error) { + switch backend { + case BackendInterpreter: + if Psi_M_interpreterHook == nil { + return nil, fmt.Errorf("pvm backend %q is not linked", backend) + } + return Psi_M_interpreterHook, nil + case BackendRecompiler: + if Psi_M_recompilerHook == nil { + return nil, fmt.Errorf("pvm backend %q is not available (requires linux/amd64 with cgo and recompiler linked)", backend) + } + return Psi_M_recompilerHook, nil + default: + return nil, fmt.Errorf("pvm backend must be %q or %q, got %q", + BackendInterpreter, BackendRecompiler, backend) + } +} diff --git a/PVM/execution_backend_test.go b/PVM/execution_backend_test.go new file mode 100644 index 00000000..86765657 --- /dev/null +++ b/PVM/execution_backend_test.go @@ -0,0 +1,29 @@ +package PVM + +import "testing" + +func TestSetExecutionBackendInterpreter(t *testing.T) { + // Interpreter hook is registered by packages that blank-import it; this + // package alone may not. Accept either success or a clear "not linked" error. + prev := ExecutionBackend + defer func() { ExecutionBackend = prev }() + + err := SetExecutionBackend(BackendInterpreter) + if err != nil && Psi_M_interpreterHook != nil { + t.Fatalf("interpreter linked but SetExecutionBackend failed: %v", err) + } + if err == nil && ExecutionBackend != BackendInterpreter { + t.Fatalf("ExecutionBackend=%q", ExecutionBackend) + } + + if err := SetExecutionBackend("nope"); err == nil { + t.Fatal("expected error for unknown backend") + } +} + +func TestPsiMOnBackendUnknown(t *testing.T) { + _, err := Psi_M_OnBackend("nope", nil, 0, 0, Argument{}, AccumulateOmegas, HostCallArgs{}) + if err == nil { + t.Fatal("expected error for unknown backend") + } +} diff --git a/PVM/gas_const.go b/PVM/gas_const.go new file mode 100644 index 00000000..98e020e0 --- /dev/null +++ b/PVM/gas_const.go @@ -0,0 +1,125 @@ +package PVM + +// Host-function gas costs from Gray Paper definitions.tex (Host-function gas costs, +// lines 302–367). Comments use PDF notation: M_□ for rates, 𝒢(L, ℓ) = ⌈L·ℓ/1024⌉ +// (eq:fnmemgas; implemented as MemGas). Order matches: +// https://github.com/gavofyork/graypaper/blob/72bee14497387d43d8ba465436efe66b281982c1/text/definitions.tex +const ( + HostGasUnknown Gas = 1000 // M_∅ — Gas cost charged for an unknown host-call. + + HostGasAssign Gas = 1818 // M_A — Ω_A (assign) base gas cost. + + HostGasBlessConst Gas = 422 // M_{B,c} — Ω_B (bless) base gas cost. + HostGasBlessItem Gas = 20 // M_{B,ℓ} — Ω_B (bless) gas per item. + + HostGasCheckpoint Gas = 103 // M_C — Ω_C (checkpoint) base gas cost. + + HostGasDesignateConst Gas = 1100 // M_{D,c} — Ω_D (designate) base gas cost. + HostGasDesignateValidator Gas = 302 // M_{D,ℓ} — Ω_D (designate) gas per validator. + + HostGasExport Gas = 3521 // M_E — Ω_E (export) base gas cost. + + HostGasForget Gas = 3250 // M_F — Ω_F (forget) base gas cost. + + HostGasGas Gas = 48 // M_G — Ω_G (gas) base gas cost. + + HostGasHistoricalLookupConst Gas = 1125 // M_{H,c} — Ω_H (historical_lookup) base gas cost. + HostGasHistoricalLookupOctets Gas = 264 // M_{H,ℓ} — Ω_H (historical_lookup) gas per 1024 octets (𝒢(M_{H,ℓ}, ℓ)). + + HostGasInfo Gas = 703 // M_I — Ω_I (info) base gas cost. + + HostGasEject Gas = 458 // M_J — Ω_J (eject) base gas cost. + + HostGasInvoke Gas = 968 // M_K — Ω_K (invoke) base gas cost. + + HostGasLookupConst Gas = 600 // M_{L,c} — Ω_L (lookup) base gas cost. + HostGasLookupOctets Gas = 248 // M_{L,ℓ} — Ω_L (lookup) gas per 1024 octets (𝒢(M_{L,ℓ}, ℓ)). + + HostGasMachineConst Gas = 1862 // M_{M,c} — Ω_M (machine) base gas cost. + HostGasMachineOctets Gas = 112 // M_{M,ℓ} — Ω_M (machine) gas per 1024 octets, program size (𝒢(M_{M,ℓ}, ℓ)). + + HostGasNew Gas = 3855 // M_N — Ω_N (new) base gas cost. + + HostGasPokeConst Gas = 297 // M_{O,c} — Ω_O (poke) base gas cost. + HostGasPokeOctets Gas = 224 // M_{O,ℓ} — Ω_O (poke) gas per 1024 octets (𝒢(M_{O,ℓ}, ℓ)). + + HostGasPeekConst Gas = 377 // M_{P,c} — Ω_P (peek) base gas cost. + HostGasPeekOctets Gas = 336 // M_{P,ℓ} — Ω_P (peek) gas per 1024 octets (𝒢(M_{P,ℓ}, ℓ)). + + HostGasQuery Gas = 643 // M_Q — Ω_Q (query) base gas cost. + + HostGasReadConst Gas = 2407 // M_{R,c} — Ω_R (read) base gas cost. + HostGasReadKeyOctets Gas = 1736 // M_{R,k,ℓ} — Ω_R (read) key gas per 1024 octets (𝒢(M_{R,k,ℓ}, ℓ)). + HostGasReadValueOctets Gas = 248 // M_{R,v,ℓ} — Ω_R (read) value gas per 1024 octets (𝒢(M_{R,v,ℓ}, ℓ)). + + HostGasSolicit Gas = 2193 // M_S — Ω_S (solicit) base gas cost. + + HostGasTransfer Gas = 575 // M_T — Ω_T (transfer) base gas cost. + + HostGasUpgrade Gas = 1028 // M_U — Ω_U (upgrade) base gas cost. + + HostGasWriteConst Gas = 2442 // M_{W,c} — Ω_W (write) base gas cost. + HostGasWriteValOctets Gas = 216 // M_{W,v,ℓ} — Ω_W (write) gas per 1024 octets (𝒢(M_{W,v,ℓ}, ℓ)). + HostGasWriteKeyOctets Gas = 3358 // M_{W,k,ℓ} — Ω_W (write) key gas per 1024 octets (𝒢(M_{W,k,ℓ}, ℓ)). + + HostGasExpunge Gas = 335 // M_X — Ω_X (expunge) base gas cost. +) + +// fetchGasCosts holds M_{Y,i,c} / M_{Y,i,ℓ} per fetch discriminator i (definitions.tex lines 337–352). +var fetchGasCosts = [16]struct { + constant Gas + octets Gas +}{ + 0: {390, 0}, // M_{Y,0,c}, M_{Y,0,ℓ} — Ω_Y (fetch) case 0: protocol parameters. + 1: {103, 0}, // M_{Y,1,c}, M_{Y,1,ℓ} — Ω_Y (fetch) case 1: entropy. + 2: {80, 96}, // M_{Y,2,c}, M_{Y,2,ℓ} — Ω_Y (fetch) case 2: auth trace. + 3: {85, 96}, // M_{Y,3,c}, M_{Y,3,ℓ} — Ω_Y (fetch) case 3: any extrinsic, by index. + 4: {85, 96}, // M_{Y,4,c}, M_{Y,4,ℓ} — Ω_Y (fetch) case 4: our extrinsic, by index. + 5: {171, 0}, // M_{Y,5,c}, M_{Y,5,ℓ} — Ω_Y (fetch) case 5: any import, by index. + 6: {171, 0}, // M_{Y,6,c}, M_{Y,6,ℓ} — Ω_Y (fetch) case 6: our import, by index. + 7: {85, 96}, // M_{Y,7,c}, M_{Y,7,ℓ} — Ω_Y (fetch) case 7: encoded work-package. + 8: {84, 0}, // M_{Y,8,c}, M_{Y,8,ℓ} — Ω_Y (fetch) case 8: auth config. + 9: {88, 0}, // M_{Y,9,c}, M_{Y,9,ℓ} — Ω_Y (fetch) case 9: auth token. + 10: {111, 0}, // M_{Y,10,c}, M_{Y,10,ℓ} — Ω_Y (fetch) case 10: refine context. + 11: {317, 0}, // M_{Y,11,c}, M_{Y,11,ℓ} — Ω_Y (fetch) case 11: items summary. + 12: {250, 0}, // M_{Y,12,c}, M_{Y,12,ℓ} — Ω_Y (fetch) case 12: any item summary. + 13: {95, 96}, // M_{Y,13,c}, M_{Y,13,ℓ} — Ω_Y (fetch) case 13: any payload. + 14: {287, 400}, // M_{Y,14,c}, M_{Y,14,ℓ} — Ω_Y (fetch) case 14: accumulate items. + 15: {355, 344}, // M_{Y,15,c}, M_{Y,15,ℓ} — Ω_Y (fetch) case 15: any accumulate item. +} + +const ( + HostGasFetchOtherConst Gas = 80 // M_{Y,∅,c} — Ω_Y (fetch) otherwise: nothing. + HostGasFetchOtherOctets Gas = 0 // M_{Y,∅,ℓ} + + HostGasPagesAllocConst Gas = 275 // M_{Z,a,c} — Ω_Z (pages) alloc base gas cost. + HostGasPagesAllocPage Gas = 121 // M_{Z,a,p} — Ω_Z (pages) alloc gas per page. + HostGasPagesFreeConst Gas = 212 // M_{Z,f,c} — Ω_Z (pages) free base gas cost. + HostGasPagesFreePage Gas = 118 // M_{Z,f,p} — Ω_Z (pages) free gas per page. + HostGasPagesSetModeConst Gas = 130 // M_{Z,s,c} — Ω_Z (pages) setmode base gas cost. + HostGasPagesSetModePage Gas = 29 // M_{Z,s,p} — Ω_Z (pages) setmode gas per page. + HostGasPagesInvalid Gas = 80 // M_{Z,i} — Ω_Z (pages) invalid-r base gas cost. + + HostGasYield Gas = 98 // M_♉ — Ω_♉ (yield) base gas cost. + + HostGasProvideConst Gas = 3980 // M_{♈,c} — Ω_♈ (provide) base gas cost. + HostGasProvideOctets Gas = 2264 // M_{♈,ℓ} — Ω_♈ (provide) gas per 1024 octets (𝒢(M_{♈,ℓ}, ℓ)). + + HostGasGrowHeapConst Gas = 275 // M_{♊,c} — Ω_♊ (grow_heap) base gas cost. + HostGasGrowHeapPage Gas = 121 // M_{♊,p} — Ω_♊ (grow_heap) gas per additional page. +) + +// Non–Gray Paper host-call gas (JIP-1 extensions). +const ( + HostGasLog Gas = 10 // Ω_log (opcode 100) — fixed cost; not defined in GP. +) + +// FetchGasCost returns M_{Y,i,c} and M_{Y,i,ℓ} (the L in 𝒢(L, ℓ)) for fetch Ω_Y +// discriminator i, falling back to M_{Y,∅,*} for undefined cases. +func FetchGasCost(discriminator uint64) (constant, octets Gas) { + if discriminator >= uint64(len(fetchGasCosts)) { + return HostGasFetchOtherConst, HostGasFetchOtherOctets + } + c := fetchGasCosts[discriminator] + return c.constant, c.octets +} diff --git a/PVM/gas_mem_test.go b/PVM/gas_mem_test.go new file mode 100644 index 00000000..222aedaa --- /dev/null +++ b/PVM/gas_mem_test.go @@ -0,0 +1,29 @@ +package PVM + +import ( + "math" + "testing" +) + +func TestFetchCost(t *testing.T) { + tests := []struct { + name string + discriminator uint64 + length uint64 + want Gas + }{ + {name: "constant only", discriminator: 0, length: 1024, want: 390}, + {name: "rounded linear term", discriminator: 2, length: 1, want: 81}, + {name: "full linear unit", discriminator: 2, length: 1024, want: 176}, + {name: "unknown discriminator", discriminator: 16, length: 1024, want: 80}, + {name: "saturates", discriminator: 2, length: math.MaxUint64, want: math.MaxInt64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fetchCost(tt.discriminator, tt.length); got != tt.want { + t.Fatalf("fetchCost(%d, %d) = %d, want %d", tt.discriminator, tt.length, got, tt.want) + } + }) + } +} diff --git a/PVM/gas_model.go b/PVM/gas_model.go new file mode 100644 index 00000000..f76b4218 --- /dev/null +++ b/PVM/gas_model.go @@ -0,0 +1,39 @@ +package PVM + +// A.9 ROB gas model (GP v0.8.0 sec:gascostmodel). + +const ( + MaxROB = 32 + + DecodeWidth = 4 // d.^(0) + ExecutionWidth = 5 // e.^(0) +) + +type ExecUnits struct { + A, L, S, M, D int +} + +var InitialUnits = ExecUnits{A: 4, L: 4, S: 4, M: 1, D: 1} // x.^(0) + +type Reg uint8 + +type BlockState struct { + Iota ProgramCounter + Cyc Gas + DecodeSlots int + ExecutionSlots int + UnitsAvail ExecUnits + ROB []robEntry +} + +// InitBlockState returns Ξ₀(ι) — initial gas simulation state (A.9). +func InitBlockState(startIota ProgramCounter) *BlockState { + return &BlockState{ + Iota: startIota, + Cyc: 0, + DecodeSlots: DecodeWidth, + ExecutionSlots: ExecutionWidth, + UnitsAvail: InitialUnits, + ROB: make([]robEntry, 0, MaxROB), + } +} diff --git a/PVM/gas_model_test.go b/PVM/gas_model_test.go new file mode 100644 index 00000000..06b24698 --- /dev/null +++ b/PVM/gas_model_test.go @@ -0,0 +1,118 @@ +package PVM + +import "testing" + +func TestBlockGasFromCycles(t *testing.T) { + tests := []struct { + cycles Gas + want Gas + }{ + {0, 1}, + {1, 1}, + {3, 1}, + {4, 1}, + {5, 2}, + {10, 7}, + } + for _, tt := range tests { + if got := blockGasFromCycles(tt.cycles); got != tt.want { + t.Fatalf("blockGasFromCycles(%d) = %d, want %d", tt.cycles, got, tt.want) + } + } +} + +func TestGasCostSingleTrapBlock(t *testing.T) { + prog := decodedGasTestProgram(t, + ProgramCode{0}, + Bitmask{0x01}, + ) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + if block.GasCost != 2 { + t.Fatalf("trap block gas = %d, want 2 (max(cycles-3,1))", block.GasCost) + } +} + +func TestGasCostMoveRegBlock(t *testing.T) { + // move_reg r0<-r1; trap + prog := decodedGasTestProgram(t, + ProgramCode{100, 0x10, 0}, + Bitmask{0x03, 0x00, 0x03}, + ) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + if block.GasCost < 1 { + t.Fatalf("move_reg+trap block gas = %d, want >= 1", block.GasCost) + } +} + +func TestGasCostBranchToTrap(t *testing.T) { + // branch_eq r0,r1 -> trap (PC 7); fallthrough: unlikely (PC 6) + prog := decodedGasTestProgram(t, + ProgramCode{ + 170, 0x01, 7, 0, 0, 0, // branch_eq at 0, target PC 7 + 2, // unlikely (fallthrough at PC 6) + 0, // trap (branch target at PC 7) + }, + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03}, + ) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + cost := InstructionCost(&prog, &prog.Instrs[0]) + if cost.Cycles != 1 { + t.Fatalf("branch to trap/unlikely cycles = %d, want 1", cost.Cycles) + } + if block.GasCost < 1 { + t.Fatalf("branch block gas = %d, want >= 1", block.GasCost) + } +} + +func TestBranchCyclesOutOfRangeTargetIsTrap(t *testing.T) { + prog := decodedGasTestProgram(t, + ProgramCode{ + 170, 0x01, 100, 0, 0, 0, // branch_eq target PC 100 (past end) + 0, // trap fallthrough at PC 6 + }, + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}, + ) + cost := InstructionCost(&prog, &prog.Instrs[0]) + if cost.Cycles != 1 { + t.Fatalf("out-of-range branch target cycles = %d, want 1", cost.Cycles) + } +} + +func TestInstructionCostEcalli(t *testing.T) { + instr := InstrMeta{Opcode: 10} + cost := InstructionCost(&Program{}, &instr) + if cost.Cycles != 100 || cost.Decode != 4 || cost.Units.A != 1 { + t.Fatalf("ecalli cost = %+v, want cycles=100 decode=4 A=1", cost) + } +} + +func TestInstRegsEcalli(t *testing.T) { + instr := InstrMeta{Opcode: 10, Dst: 0, Src: [2]uint8{0, 1}} + if got := instSrcRegs(&instr); len(got) != 0 { + t.Fatalf("ecalli src regs = %v, want none", got) + } + if got := instDstRegs(&instr); len(got) != 0 { + t.Fatalf("ecalli dst regs = %v, want none", got) + } +} + +func decodedGasTestProgram(t *testing.T, code ProgramCode, bitmask Bitmask) Program { + t.Helper() + prog := Program{ + InstructionData: code, + Bitmasks: bitmask, + } + if reason := prog.preDecodeBlocks(); reason != ExitContinue { + t.Fatalf("preDecodeBlocks() = %v, want %v", reason, ExitContinue) + } + return prog +} diff --git a/PVM/gas_opcode.go b/PVM/gas_opcode.go new file mode 100644 index 00000000..490b39c8 --- /dev/null +++ b/PVM/gas_opcode.go @@ -0,0 +1,243 @@ +package PVM + +// memoryAccessCycles (𝔐) is the per-access cycle cost for load/store in A.10. +const memoryAccessCycles = 25 + +// InstrCost holds A.10 cost_cycles, cost_decodeslots, and cost_execunits. +// Table column order is (cycles, decode_slots, ALU, LOAD, STORE, MUL, DIV). +// 𝔓(a,b) / 𝔓_S(a,b) select decode_slots for ALU ops (cycles are fixed). +type InstrCost struct { + Cycles int + Decode int + Units ExecUnits +} + +// selectOverlapCost implements 𝔓(a,b) for the decode-slots column: +// a when dst∩src ≠ ∅, else b. +func selectOverlapCost(a, b int, instr *InstrMeta) int { + if regsOverlap(instSrcRegs(instr), instDstRegs(instr)) { + return a + } + return b +} + +// selectOverlapShiftCost implements 𝔓_S(a,b) for shift/rotate decode slots. +func selectOverlapShiftCost(a, b int, instr *InstrMeta) int { + if instr.Src[0] != 0xFF && instr.Dst != 0xFF && instr.Src[0] == instr.Dst { + return a + } + return b +} + +func isTrapOrUnlikely(p *Program, pc int) bool { + // eq:instructions — code is zero-padded; beyond |c| the opcode is trap. + if pc < 0 { + return false + } + if pc >= len(p.InstructionData) { + return true + } + op := p.InstructionData[pc] + return op == 0 || op == 2 +} + +// branchCycles is A.10 cost_cycles for branch / branch_imm (1 or 20). +func branchCycles(p *Program, instr *InstrMeta) int { + pc := int(instr.PC) + fallthroughPC := pc + 1 + int(instr.SkipLen) + targetPC := int(instr.Imm[0]) + if opcodeInfoTable[instr.Opcode].Category == InstrCatOneRegImmOff { + targetPC = int(instr.Imm[1]) + } + if isTrapOrUnlikely(p, fallthroughPC) || isTrapOrUnlikely(p, targetPC) { + return 1 + } + return 20 +} + +func unitsALU() ExecUnits { return ExecUnits{A: 1} } + +// Load/store/mul/div also take an ALU unit (GP A.10 cost_execunits table). +func unitsLoad() ExecUnits { return ExecUnits{A: 1, L: 1} } +func unitsStore() ExecUnits { return ExecUnits{A: 1, S: 1} } +func unitsMul() ExecUnits { return ExecUnits{A: 1, M: 1} } +func unitsDiv() ExecUnits { return ExecUnits{A: 1, D: 1} } + +// A.10 \simplealuthreeop: cycles=1, decode=𝔓(1,2) +func simpleAluThreeOp(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 1, Decode: selectOverlapCost(1, 2, instr), Units: unitsALU()} +} + +// A.10 \simplealuthreeopthirtytwo: cycles=2, decode=𝔓(2,3) +func simpleAluThreeOp32(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 2, Decode: selectOverlapCost(2, 3, instr), Units: unitsALU()} +} + +// A.10 \simplealutwoop: cycles=1, decode=𝔓(1,2) +func simpleAluTwoOp(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 1, Decode: selectOverlapCost(1, 2, instr), Units: unitsALU()} +} + +// A.10 \simplealutwoopthirtytwo: cycles=2, decode=𝔓(2,3) +func simpleAluTwoOp32(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 2, Decode: selectOverlapCost(2, 3, instr), Units: unitsALU()} +} + +// A.10 \trivialtwooponecycle +func trivialTwoOpOneCycle() InstrCost { + return InstrCost{Cycles: 1, Decode: 1, Units: unitsALU()} +} + +// A.10 \trivialtwooptwocycles +func trivialTwoOpTwoCycles() InstrCost { + return InstrCost{Cycles: 2, Decode: 1, Units: ExecUnits{A: 2}} +} + +// A.10 \shiftsandrotates: cycles=1, decode=𝔓_S(2,3) +func shiftsAndRotates(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 1, Decode: selectOverlapShiftCost(2, 3, instr), Units: unitsALU()} +} + +// A.10 \shiftsandrotatesthirtytwo: cycles=2, decode=𝔓_S(3,4) +func shiftsAndRotates32(instr *InstrMeta) InstrCost { + return InstrCost{Cycles: 2, Decode: selectOverlapShiftCost(3, 4, instr), Units: unitsALU()} +} + +// A.10 \finish{n} +func finishCost(cycles int) InstrCost { + return InstrCost{Cycles: cycles, Decode: 1, Units: ExecUnits{}} +} + +// memLoadCost is A.10 \directload / \indirectload (identical cost rows). +func memLoadCost() InstrCost { + return InstrCost{Cycles: memoryAccessCycles, Decode: 1, Units: unitsLoad()} +} + +// memStoreCost is A.10 \directstore / \indirectstore / \storeimm / \indirectstoreimm. +func memStoreCost() InstrCost { + return InstrCost{Cycles: memoryAccessCycles, Decode: 1, Units: unitsStore()} +} + +// InstructionCost returns A.10 cost for a pre-decoded instruction. +// Opcode numbers follow PVM/instructions.go name table. +func InstructionCost(p *Program, instr *InstrMeta) InstrCost { + switch instr.Opcode { + case 0, 1: // trap, fallthrough + return finishCost(2) + case 2: // unlikely + return InstrCost{Cycles: 40, Decode: 1, Units: ExecUnits{}} + case 10: // ecalli + return InstrCost{Cycles: 100, Decode: 4, Units: unitsALU()} + case 20: // load_imm_64 + return InstrCost{Cycles: 1, Decode: 2, Units: ExecUnits{}} + + case 30, 31, 32, 33: // store_imm_u* + return memStoreCost() + case 40: // jump + return finishCost(15) + case 50: // jump_ind + return InstrCost{Cycles: 22, Decode: 1, Units: ExecUnits{}} + case 51: // load_imm + return InstrCost{Cycles: 1, Decode: 1, Units: ExecUnits{}} + case 52, 53, 54, 55, 56, 57, 58: // load_u*/i* + return memLoadCost() + case 59, 60, 61, 62: // store_u* + return memStoreCost() + case 70, 71, 72, 73: // store_imm_ind_u* + return memStoreCost() + case 80: // load_imm_jump + return finishCost(15) + case 81, 82, 83, 84, 85, 86, 87, 88, 89, 90: // branch_*_imm + return InstrCost{Cycles: branchCycles(p, instr), Decode: 1, Units: unitsALU()} + case 100: // move_reg + return InstrCost{Cycles: 0, Decode: 1, Units: ExecUnits{}} + + case 101, 102, 103, 104, 107, 108, 109: // count/leading/sign/zero extend + return trivialTwoOpOneCycle() + case 105, 106: // trailing_zero_bits_* + return trivialTwoOpTwoCycles() + case 110: // reverse_bytes + return simpleAluTwoOp(instr) + + case 120, 121, 122, 123: // store_ind_u* + return memStoreCost() + case 124, 125, 126, 127, 128, 129, 130: // load_ind_* + return memLoadCost() + + case 131: // add_imm_32 + return simpleAluTwoOp32(instr) + case 132, 133, 134: // and/xor/or_imm + return simpleAluTwoOp(instr) + case 135: // mul_imm_32 + return InstrCost{Cycles: 4, Decode: selectOverlapCost(2, 3, instr), Units: unitsMul()} + case 136, 137, 142, 143: // set_lt/gt_*_imm + return InstrCost{Cycles: 3, Decode: 3, Units: unitsALU()} + case 138, 139, 140: // sh*_imm_32 + return simpleAluTwoOp32(instr) + case 141: // neg_add_imm_32 + return InstrCost{Cycles: 3, Decode: 4, Units: unitsALU()} + case 144, 145, 146: // sh*_imm_alt_32 + return InstrCost{Cycles: 2, Decode: 4, Units: unitsALU()} + case 147, 148: // cmov_*_imm + return InstrCost{Cycles: 2, Decode: 3, Units: unitsALU()} + case 149: // add_imm_64 + return simpleAluTwoOp(instr) + case 150: // mul_imm_64 + return InstrCost{Cycles: 3, Decode: selectOverlapCost(1, 2, instr), Units: unitsMul()} + case 151, 152, 153, 158: // sh*_imm_64, rot_r_64_imm + return simpleAluTwoOp(instr) + case 154: // neg_add_imm_64 + return InstrCost{Cycles: 2, Decode: 3, Units: unitsALU()} + case 155, 156, 157, 159: // sh*_imm_alt_64, rot_r_64_imm_alt + return InstrCost{Cycles: 1, Decode: 3, Units: unitsALU()} + case 160: // rot_r_32_imm + return simpleAluTwoOp32(instr) + case 161: // rot_r_32_imm_alt + return InstrCost{Cycles: 2, Decode: 4, Units: unitsALU()} + + case 170, 171, 172, 173, 174, 175: // branch_* + return InstrCost{Cycles: branchCycles(p, instr), Decode: 1, Units: unitsALU()} + case 180: // load_imm_jump_ind + return InstrCost{Cycles: 22, Decode: 1, Units: ExecUnits{}} + + case 190, 191: // add/sub_32 + return simpleAluThreeOp32(instr) + case 192: // mul_32 + return InstrCost{Cycles: 4, Decode: selectOverlapCost(2, 3, instr), Units: unitsMul()} + case 193, 194, 195, 196: // div/rem_32 + return InstrCost{Cycles: 60, Decode: 4, Units: unitsDiv()} + case 197, 198, 199: // sh*_32 + return shiftsAndRotates32(instr) + case 200, 201: // add/sub_64 + return simpleAluThreeOp(instr) + case 202: // mul_64 + return InstrCost{Cycles: 3, Decode: selectOverlapCost(1, 2, instr), Units: unitsMul()} + case 203, 204, 205, 206: // div/rem_64 + return InstrCost{Cycles: 60, Decode: 4, Units: unitsDiv()} + case 207, 208, 209: // sh*_64 + return shiftsAndRotates(instr) + case 210, 211, 212: // and/xor/or + return simpleAluThreeOp(instr) + case 213, 214: // mul_upper_s_s / u_u + return InstrCost{Cycles: 4, Decode: 4, Units: unitsMul()} + case 215: // mul_upper_s_u + return InstrCost{Cycles: 6, Decode: 4, Units: unitsMul()} + case 216, 217: // set_lt_* + return InstrCost{Cycles: 3, Decode: 3, Units: unitsALU()} + case 218, 219: // cmov_* + return InstrCost{Cycles: 2, Decode: 2, Units: unitsALU()} + case 220, 222: // rot_*_64 + return shiftsAndRotates(instr) + case 221, 223: // rot_*_32 + return shiftsAndRotates32(instr) + case 224, 225: // and_inv / or_inv + return InstrCost{Cycles: 2, Decode: 3, Units: unitsALU()} + case 226: // xnor + return InstrCost{Cycles: 2, Decode: selectOverlapCost(2, 3, instr), Units: unitsALU()} + case 227, 228, 229, 230: // min/max + return InstrCost{Cycles: 3, Decode: selectOverlapCost(2, 3, instr), Units: unitsALU()} + + default: + return InstrCost{Cycles: 1, Decode: 1, Units: unitsALU()} + } +} diff --git a/PVM/gas_regs.go b/PVM/gas_regs.go new file mode 100644 index 00000000..bd3b5bb3 --- /dev/null +++ b/PVM/gas_regs.go @@ -0,0 +1,126 @@ +package PVM + +// instSrcRegs returns source registers for gas modelling (A.9 / A.10). +// Values follow the instruction tables' read set (inst_srcregs), which may differ +// from decode-time InstrMeta.Dst/Src packing used by the interpreter. +func instSrcRegs(instr *InstrMeta) []Reg { + switch instr.Opcode { + case 10: // ecalli + return nil + case 20, 51: // load_imm_64, load_imm — dst only + return nil + case 30, 31, 32, 33: // store_imm_* — immediates only + return nil + case 40, 80: // jump, load_imm_jump — no register reads in the gas model + return nil + case 50: // jump_ind — base (decoder packs rA into Dst) + return regOrNil(instr.Dst) + case 52, 53, 54, 55, 56, 57, 58: // absolute loads — address is immediate + return nil + case 59, 60, 61, 62: // store_u* — value (decoder packs rA into Dst) + return regOrNil(instr.Dst) + case 70, 71, 72, 73: // store_imm_ind_* — base (decoder packs rA into Dst) + return regOrNil(instr.Dst) + case 81, 82, 83, 84, 85, 86, 87, 88, 89, 90: // branch_*_imm — compare reg (decoder packs rA into Dst) + return regOrNil(instr.Dst) + case 120, 121, 122, 123: // store_ind_* — value (Dst field) + base (Src[0]) + var regs []Reg + if instr.Dst != 0xFF { + regs = append(regs, Reg(instr.Dst)) + } + if instr.Src[0] != 0xFF { + regs = append(regs, Reg(instr.Src[0])) + } + return regs + case 180: // load_imm_jump_ind — base only in the gas model + return regOrNil(instr.Src[0]) + default: + var regs []Reg + for _, s := range instr.Src { + if s != 0xFF { + regs = append(regs, Reg(s)) + } + } + return regs + } +} + +// instDstRegs returns destination registers for gas modelling (A.9 / A.10). +// Stores and branches have an empty write set (memory / control-flow only). +func instDstRegs(instr *InstrMeta) []Reg { + switch instr.Opcode { + case 10: // ecalli + return nil + case 30, 31, 32, 33: // store_imm_* + return nil + case 40, 50, 80, 180: // jump / jump_ind / load_imm_jump* + return nil + case 59, 60, 61, 62: // store_u* + return nil + case 70, 71, 72, 73: // store_imm_ind_* + return nil + case 81, 82, 83, 84, 85, 86, 87, 88, 89, 90: // branch_*_imm + return nil + case 120, 121, 122, 123: // store_ind_* + return nil + case 170, 171, 172, 173, 174, 175: // branch_* + return nil + default: + return regOrNil(instr.Dst) + } +} + +func regOrNil(r uint8) []Reg { + if r == 0xFF { + return nil + } + return []Reg{Reg(r)} +} + +type regSet map[Reg]struct{} + +func regSetFrom(regs []Reg) regSet { + if len(regs) == 0 { + return nil + } + s := make(regSet, len(regs)) + for _, r := range regs { + s[r] = struct{}{} + } + return s +} + +func (s regSet) intersects(other regSet) bool { + if len(s) == 0 || len(other) == 0 { + return false + } + for r := range s { + if _, ok := other[r]; ok { + return true + } + } + return false +} + +func (s regSet) union(other regSet) regSet { + if len(other) == 0 { + return s + } + if s == nil { + s = make(regSet, len(other)) + } + for r := range other { + s[r] = struct{}{} + } + return s +} + +func (s regSet) subtract(other regSet) { + for r := range other { + delete(s, r) + } +} + +func regsOverlap(dst, src []Reg) bool { + return regSetFrom(dst).intersects(regSetFrom(src)) +} diff --git a/PVM/gas_sim.go b/PVM/gas_sim.go new file mode 100644 index 00000000..03a17e71 --- /dev/null +++ b/PVM/gas_sim.go @@ -0,0 +1,344 @@ +package PVM + +const ( + iotaNone = ^ProgramCounter(0) + + robNone = 0 + robDEC = 1 + robWAIT = 2 + robEXE = 3 + robFIN = 4 +) + +type robEntry struct { + state uint8 + cyclesLeft int + deps []int + regs regSet + units ExecUnits +} + +func (u ExecUnits) fits(avail ExecUnits) bool { + return u.A <= avail.A && u.L <= avail.L && u.S <= avail.S && + u.M <= avail.M && u.D <= avail.D +} + +func (u ExecUnits) subFrom(avail *ExecUnits) { + avail.A -= u.A + avail.L -= u.L + avail.S -= u.S + avail.M -= u.M + avail.D -= u.D +} + +func (u ExecUnits) addTo(avail *ExecUnits) { + avail.A += u.A + avail.L += u.L + avail.S += u.S + avail.M += u.M + avail.D += u.D +} + +func (b *BlockState) robActiveCount() int { + n := 0 + for _, e := range b.ROB { + if e.state != robNone { + n++ + } + } + return n +} + +func (b *BlockState) instrAt(p *Program) *InstrMeta { + if b.Iota == iotaNone { + return nil + } + // GP eq:instructions: code is zero-padded; a fetch past the end is trap. + if int(b.Iota) >= len(p.InstructionData) { + return &InstrMeta{ + PC: b.Iota, + Opcode: 0, // trap + SkipLen: 0, + Dst: 0xFF, + Src: [2]uint8{0xFF, 0xFF}, + } + } + if int(b.Iota) >= len(p.InstrIdxAt) { + return nil + } + idx := p.InstrIdxAt[b.Iota] + if idx < 0 { + return nil + } + return &p.Instrs[idx] +} + +func (b *BlockState) canDecode(p *Program) bool { + if b.Iota == iotaNone { + return false + } + if b.robActiveCount() >= MaxROB { + return false + } + instr := b.instrAt(p) + if instr == nil { + return false + } + cost := InstructionCost(p, instr) + return cost.Decode <= b.DecodeSlots +} + +func (b *BlockState) decodeMoveReg(p *Program, instr *InstrMeta) { + srcRegs := instSrcRegs(instr) + dstRegs := instDstRegs(instr) + dstSet := regSetFrom(dstRegs) + + for j := range b.ROB { + if b.ROB[j].state == robNone { + continue + } + if b.ROB[j].regs.intersects(regSetFrom(srcRegs)) { + b.ROB[j].regs = b.ROB[j].regs.union(dstSet) + } else { + b.ROB[j].regs.subtract(dstSet) + } + } + + b.Iota = b.nextIota(instr) + b.DecodeSlots-- +} + +func (b *BlockState) nextIota(instr *InstrMeta) ProgramCounter { + if IsBlockTerminator(instr.Opcode) { + return iotaNone + } + return instr.PC + ProgramCounter(1+instr.SkipLen) +} + +func appendROBDep(deps []int, j int) []int { + for _, d := range deps { + if d == j { + return deps + } + } + return append(deps, j) +} + +func (b *BlockState) decodeToROB(p *Program, instr *InstrMeta) { + dstRegs := instDstRegs(instr) + srcRegs := instSrcRegs(instr) + dstSet := regSetFrom(dstRegs) + srcSet := regSetFrom(srcRegs) + cost := InstructionCost(p, instr) + + // RAW deps against current ROB register tracking (before dst rename). + // When dst∩src ≠ ∅ this also captures the prior writer of dst. + var deps []int + for j, e := range b.ROB { + if e.state != robNone && e.regs.intersects(srcSet) { + deps = appendROBDep(deps, j) + } + } + // Rename: drop dst from prior ROB entries. + for j := range b.ROB { + if b.ROB[j].state != robNone { + b.ROB[j].regs.subtract(dstSet) + } + } + + b.ROB = append(b.ROB, robEntry{ + state: robDEC, + cyclesLeft: cost.Cycles, + deps: deps, + regs: dstSet, + units: cost.Units, + }) + + b.Iota = b.nextIota(instr) + b.DecodeSlots -= cost.Decode +} + +func (b *BlockState) decode(p *Program) { + instr := b.instrAt(p) + if instr == nil { + b.Iota = iotaNone + return + } + if instr.Opcode == 100 { + b.decodeMoveReg(p, instr) + } else { + b.decodeToROB(p, instr) + } +} + +func (b *BlockState) findReady() int { + for j, e := range b.ROB { + if e.state != robWAIT { + continue + } + if !e.units.fits(b.UnitsAvail) { + continue + } + ready := true + for _, dep := range e.deps { + if dep >= len(b.ROB) || b.ROB[dep].state == robNone { + continue + } + // Dependency satisfied when producer finished executing (cyclesLeft==0), + // not when it has been purged from the ROB. + if b.ROB[dep].cyclesLeft != 0 { + ready = false + break + } + } + if ready { + return j + } + } + return -1 +} + +func (b *BlockState) startExec(j int) { + b.ROB[j].state = robEXE + b.ROB[j].units.subFrom(&b.UnitsAvail) + b.ExecutionSlots-- +} + +func (b *BlockState) advanceCycle() { + // In-order retire: purge leading FIN entries, then progress EXE/DEC. + for j := range b.ROB { + if b.ROB[j].state == robNone { + continue + } + if b.ROB[j].state != robFIN { + break + } + b.ROB[j].state = robNone + } + b.trimLeadingNone() + + // GP: return units for EXE entries with cyclesLeft==1 (about to hit 0). + var returned ExecUnits + for j := range b.ROB { + e := &b.ROB[j] + if e.state == robEXE && e.cyclesLeft == 1 { + returned.A += e.units.A + returned.L += e.units.L + returned.S += e.units.S + returned.M += e.units.M + returned.D += e.units.D + } + } + + for j := range b.ROB { + e := &b.ROB[j] + switch e.state { + case robDEC: + e.state = robWAIT + case robEXE: + if e.cyclesLeft > 0 { + // After cyclesLeft reaches 0, remain EXE for one more cycle + // before transitioning to FIN. + e.cyclesLeft-- + } else { + e.state = robFIN + } + } + } + + returned.addTo(&b.UnitsAvail) + b.Cyc++ + b.DecodeSlots = DecodeWidth + b.ExecutionSlots = ExecutionWidth +} + +// trimLeadingNone drops retired leading ROB slots and remaps dependency indices +// so physical ROB length stays proportional to MaxROB (active bound). +func (b *BlockState) trimLeadingNone() { + n := 0 + for n < len(b.ROB) && b.ROB[n].state == robNone { + n++ + } + if n == 0 { + return + } + b.ROB = b.ROB[n:] + for i := range b.ROB { + if len(b.ROB[i].deps) == 0 { + continue + } + deps := make([]int, 0, len(b.ROB[i].deps)) + for _, d := range b.ROB[i].deps { + if d < n { + continue + } + deps = append(deps, d-n) + } + b.ROB[i].deps = deps + } +} + +// simulateBlock runs A.9 gas_sim until the basic block converges. +// A non-converging simulation is a logic bug — never return a partial gas value. +func (b *BlockState) simulateBlock(p *Program) Gas { + const maxSteps = 100000 + for step := 0; step < maxSteps; step++ { + if b.canDecode(p) { + b.decode(p) + continue + } + // Open block (no terminator): iota advanced past the last instruction. + if b.Iota != iotaNone && b.instrAt(p) == nil { + b.Iota = iotaNone + continue + } + if j := b.findReady(); j >= 0 && b.ExecutionSlots > 0 { + b.startExec(j) + continue + } + if b.Iota == iotaNone && b.robActiveCount() == 0 { + return blockGasFromCycles(b.Cyc) + } + b.advanceCycle() + } + panic("gas_sim: basic block did not converge within step limit") +} + +// blockGasFromCycles implements eq:gascostforblock = max(cycles - 3, 1). +func blockGasFromCycles(cycles Gas) Gas { + if cycles <= 3 { + return 1 + } + return cycles - 3 +} + +// invalidBlockGas is the A.9 cost of a trap-equivalent invalid instruction +// (trap timeline DeeER → max(5−3, 1) = 2), used for trailing block entries +// that appear after a final terminator but are outside code. +const invalidBlockGas Gas = 2 + +// GasCostForBlock returns A.9 gascostforblock for the basic block at startPC. +func GasCostForBlock(p *Program, startPC ProgramCounter) Gas { + if int(startPC) >= len(p.InstrIdxAt) { + // Past end of code: treat as an invalid/trap block. + return invalidBlockGas + } + if p.InstrIdxAt[startPC] < 0 { + return 1 + } + state := InitBlockState(startPC) + return state.simulateBlock(p) +} + +// GasCostFromPC returns A.9 gascostforblock for the suffix from pc through the +// end of its containing basic block (used when resuming mid-block). +func GasCostFromPC(p *Program, pc ProgramCounter) Gas { + if int(pc) >= len(p.InstrIdxAt) { + return invalidBlockGas + } + if p.InstrIdxAt[pc] < 0 { + return 1 + } + state := InitBlockState(pc) + return state.simulateBlock(p) +} diff --git a/PVM/gas_vectors_test.go b/PVM/gas_vectors_test.go new file mode 100644 index 00000000..a0a5bc3f --- /dev/null +++ b/PVM/gas_vectors_test.go @@ -0,0 +1,451 @@ +package PVM + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" +) + +// GP A.9 gas-model vectors under new-gas-cost-model. program is a full PVM blob; +// block-gas-costs holds expected gascostforblock per basic-block entry PC. +const ( + gasModelProgramsDir = "../pkg/test_data/new-gas-cost-model/tests/programs" + gasModelIntegrationDir = "../pkg/test_data/new-gas-cost-model/integration-tests" +) + +type blockGasEntry struct { + PC ProgramCounter `json:"pc"` + Cost Gas `json:"cost"` +} + +// blockGasCosts accepts vectors in either array form +// [{"pc":0,"cost":1},...] or map form {"0":1,"6":52,...} (integration tests). +type blockGasCosts []blockGasEntry + +func (b *blockGasCosts) UnmarshalJSON(data []byte) error { + var arr []blockGasEntry + if err := json.Unmarshal(data, &arr); err == nil { + *b = arr + return nil + } + var m map[string]Gas + if err := json.Unmarshal(data, &m); err != nil { + return err + } + entries := make([]blockGasEntry, 0, len(m)) + for pcStr, cost := range m { + pc, err := strconv.ParseUint(pcStr, 10, 64) + if err != nil { + return err + } + entries = append(entries, blockGasEntry{PC: ProgramCounter(pc), Cost: cost}) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].PC < entries[j].PC }) + *b = entries + return nil +} + +type gasModelMemSlice struct { + Address uint64 `json:"address"` + Contents []byte `json:"contents"` +} + +type gasModelAssert struct { + Status string `json:"status"` + Hostcall *uint64 `json:"hostcall"` + PageFaultAddress *uint64 `json:"page-fault-address"` + Gas uint64 `json:"gas"` + PC ProgramCounter `json:"pc"` + Regs []uint64 `json:"regs"` + Memory []gasModelMemSlice `json:"memory"` +} + +type gasModelStep struct { + Run *struct{} `json:"run"` + Assert *gasModelAssert `json:"assert"` + SetReg *struct { + Reg uint8 `json:"reg"` + Value uint64 `json:"value"` + } `json:"set-reg"` + Map *struct { + Address uint64 `json:"address"` + Length uint64 `json:"length"` + IsWritable bool `json:"is-writable"` + } `json:"map"` + Write *gasModelMemSlice `json:"write"` +} + +type gasModelVector struct { + Name string `json:"name"` + InitialPC ProgramCounter `json:"initial-pc"` + InitialGas uint64 `json:"initial-gas"` + Program []byte `json:"program"` + BlockGasCost blockGasCosts `json:"block-gas-costs"` + Steps []gasModelStep `json:"steps"` +} + +func loadGasModelVector(t *testing.T, path string) gasModelVector { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + var vec gasModelVector + if err := json.Unmarshal(raw, &vec); err != nil { + t.Fatalf("json %s: %v", path, err) + } + if vec.Name == "" { + vec.Name = filepath.Base(path) + } + return vec +} + +func programFromGasVectorBlob(t *testing.T, blob []byte) *Program { + t.Helper() + // Gas fixtures may omit a final terminator; use the gas-model decode path. + prog, reason := deblobProgramForGasModel(blob) + if reason != ExitContinue { + t.Fatalf("deblobProgramForGasModel: %v", reason) + } + return &prog +} + +func assertBlockGasCosts(t *testing.T, prog *Program, expected blockGasCosts) { + t.Helper() + for _, e := range expected { + got := GasCostForBlock(prog, e.PC) + if got != e.Cost { + t.Errorf("block @pc=%d: got %d, want %d", e.PC, got, e.Cost) + } + } +} + +func finalAssertGas(vec gasModelVector) (uint64, string, bool) { + for _, s := range vec.Steps { + if s.Assert != nil { + return s.Assert.Gas, s.Assert.Status, true + } + } + return 0, "", false +} + +// TestGasVectorHarnessSanity cross-checks vector JSON: deblob, block PCs, and that +// block-gas-costs for single-block programs match integration gas consumption. +func TestGasVectorHarnessSanity(t *testing.T) { + for _, path := range gasModelJSONFiles(t, gasModelProgramsDir, "gas_") { + path := path + t.Run(filepath.Base(path), func(t *testing.T) { + vec := loadGasModelVector(t, path) + if len(vec.BlockGasCost) == 0 { + t.Fatal("missing block-gas-costs") + } + + prog, reason := deblobProgramForGasModel(vec.Program) + if reason != ExitContinue { + t.Fatalf("deblobProgramForGasModel: %v", reason) + } + + for _, e := range vec.BlockGasCost { + if int(e.PC) >= len(prog.InstrIdxAt) || prog.InstrIdxAt[e.PC] < 0 { + t.Fatalf("block-gas-costs pc=%d is not a valid instruction entry", e.PC) + } + block := prog.LookupBlock(e.PC) + if block == nil || block.StartPC != e.PC { + t.Fatalf("pc=%d is not a basic block entry", e.PC) + } + fromAPI := GasCostForBlock(&prog, e.PC) + if fromAPI != block.GasCost { + t.Errorf("pc=%d: GasCostForBlock=%d != preDecode=%d", e.PC, fromAPI, block.GasCost) + } + } + + // Single-block programs that halt with panic charge exactly one + // block; out-of-gas / multi-iteration runs consume many charges. + if len(vec.BlockGasCost) == 1 { + finalGas, status, ok := finalAssertGas(vec) + if !ok { + t.Fatal("no assert.gas in steps") + } + if status == "" || status == "panic" { + consumed := vec.InitialGas - finalGas + if Gas(consumed) != vec.BlockGasCost[0].Cost { + t.Errorf("integration consumed %d gas, block-gas-costs want %d", consumed, vec.BlockGasCost[0].Cost) + } + } + } + }) + } +} + +func gasModelJSONFiles(t *testing.T, dir, prefix string) []string { + t.Helper() + if _, err := os.Stat(dir); err != nil { + t.Fatalf("gas model vectors not found at %s (init submodule?): %v", dir, err) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read dir %s: %v", dir, err) + } + var files []string + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { + continue + } + if prefix != "" && !strings.HasPrefix(e.Name(), prefix) { + continue + } + files = append(files, filepath.Join(dir, e.Name())) + } + return files +} + +func runGasModelProgramPrefix(t *testing.T, prefix string) { + t.Helper() + for _, path := range gasModelJSONFiles(t, gasModelProgramsDir, prefix) { + path := path + t.Run(filepath.Base(path), func(t *testing.T) { + vec := loadGasModelVector(t, path) + if len(vec.BlockGasCost) == 0 { + t.Fatal("missing block-gas-costs") + } + prog, reason := deblobProgramForGasModel(vec.Program) + if reason != ExitContinue { + t.Fatalf("deblobProgramForGasModel: %v", reason) + } + assertBlockGasCosts(t, &prog, vec.BlockGasCost) + }) + } +} + +// TestGasModelProgramVectors checks gas_*.json against GasCostForBlock +// (A.9 eq:gascostforblock). +func TestGasModelProgramVectors(t *testing.T) { + runGasModelProgramPrefix(t, "gas_") +} + +// TestGasModelInstVectors checks per-opcode inst_*.json block-gas-costs. +func TestGasModelInstVectors(t *testing.T) { + runGasModelProgramPrefix(t, "inst_") +} + +// TestGasModelRiscvVectors checks compiled riscv_*.json block-gas-costs. +func TestGasModelRiscvVectors(t *testing.T) { + runGasModelProgramPrefix(t, "riscv_") +} + +// TestGasModelMultistepVectors executes multistep_*.json step sequences +// (run / assert / set-reg / map / write) and still checks block-gas-costs. +func TestGasModelMultistepVectors(t *testing.T) { + for _, path := range gasModelJSONFiles(t, gasModelProgramsDir, "multistep_") { + path := path + t.Run(filepath.Base(path), func(t *testing.T) { + vec := loadGasModelVector(t, path) + if len(vec.BlockGasCost) == 0 { + t.Fatal("missing block-gas-costs") + } + prog := programFromGasVectorBlob(t, vec.Program) + assertBlockGasCosts(t, prog, vec.BlockGasCost) + runGasModelMultistep(t, prog, vec) + }) + } +} + +// TestGasModelIntegrationVectors checks large program-only vectors (program + block-gas-costs). +func TestGasModelIntegrationVectors(t *testing.T) { + for _, path := range gasModelJSONFiles(t, gasModelIntegrationDir, "") { + path := path + t.Run(filepath.Base(path), func(t *testing.T) { + vec := loadGasModelVector(t, path) + if len(vec.BlockGasCost) == 0 { + t.Fatal("missing block-gas-costs") + } + prog := programFromGasVectorBlob(t, vec.Program) + assertBlockGasCosts(t, prog, vec.BlockGasCost) + }) + } +} + +type gasModelHarness struct { + interp *Interpreter + pc ProgramCounter + lastExit ExitReason + assertPC ProgramCounter +} + +func runGasModelMultistep(t *testing.T, prog *Program, vec gasModelVector) { + t.Helper() + mem := &Memory{Pages: map[uint32]*Page{}} + gas := Gas(vec.InitialGas) + h := &gasModelHarness{ + interp: &Interpreter{ + Program: prog, + Registers: Registers{}, + Memory: mem, + Gas: gas, + GasCharged: false, + }, + pc: vec.InitialPC, + } + + for i, step := range vec.Steps { + switch { + case step.Run != nil: + h.lastExit, h.assertPC, h.pc = gasModelInvokeUntilExit(h.interp, h.pc) + + case step.Assert != nil: + assertGasModelState(t, i, h.interp, h.lastExit, h.assertPC, step.Assert) + + case step.SetReg != nil: + if int(step.SetReg.Reg) >= len(h.interp.Registers) { + t.Fatalf("step %d: set-reg r%d out of range", i, step.SetReg.Reg) + } + h.interp.Registers[step.SetReg.Reg] = step.SetReg.Value + + case step.Map != nil: + gasModelMapPages(mem, step.Map.Address, step.Map.Length, step.Map.IsWritable) + + case step.Write != nil: + if !isWriteable(step.Write.Address, uint64(len(step.Write.Contents)), *mem) { + t.Fatalf("step %d: write at %#x not writable", i, step.Write.Address) + } + mem.Write(step.Write.Address, step.Write.Contents) + + default: + t.Fatalf("step %d: unrecognized action", i) + } + } +} + +// gasModelInvokeUntilExit runs like BlockBasedInvokeDecodedBlocks but reports +// assertPC as the interrupting instruction (ecalli / fault / halt), while +// resumePC is where the next run should continue. +func gasModelInvokeUntilExit(interp *Interpreter, pc ProgramCounter) (exit ExitReason, assertPC, resumePC ProgramCounter) { + prog := interp.Program + for { + if int(pc) >= len(prog.InstrIdxAt) { + return ExitPanic, pc, pc + } + instrIdx := prog.InstrIdxAt[pc] + block := prog.BlockContaining(pc) + if instrIdx < 0 || block == nil { + return ExitPanic, pc, pc + } + startIdx := int(instrIdx) + if startIdx < block.InstrStart || startIdx >= block.InstrEnd { + return ExitPanic, pc, pc + } + + if !interp.GasCharged { + blockGas := blockGasAtPC(prog, pc, block) + if interp.Gas < blockGas { + return ExitOOG, pc, pc + } + interp.Gas -= blockGas + interp.GasCharged = true + } + + instrs := prog.Instrs[startIdx:block.InstrEnd] + branchTaken := false + for i := range instrs { + instr := &instrs[i] + exitReason, newPC := instr.Exec(interp, instr) + reason := exitReason.GetReasonType() + if IsBlockTerminator(instr.Opcode) && (reason == CONTINUE || reason == HOST_CALL) { + interp.GasCharged = false + } + switch reason { + case PANIC, HALT: + return exitReason, instr.PC, instr.PC + case PAGE_FAULT, OUT_OF_GAS: + return exitReason, instr.PC, instr.PC + case HOST_CALL: + next := instr.PC + ProgramCounter(instr.SkipLen) + 1 + return exitReason, instr.PC, next + } + if IsBlockTerminator(instr.Opcode) { + pc = newPC + branchTaken = true + break + } + } + if !branchTaken { + last := &instrs[len(instrs)-1] + pc = last.PC + ProgramCounter(last.SkipLen) + 1 + } + } +} + +func gasModelMapPages(mem *Memory, addr, length uint64, writable bool) { + access := MemoryReadOnly + if writable { + access = MemoryReadWrite + } + end := addr + length + allocateMemorySegment(mem, uint32(addr), uint32(end), nil, access) +} + +func assertGasModelState(t *testing.T, step int, interp *Interpreter, exit ExitReason, assertPC ProgramCounter, want *gasModelAssert) { + t.Helper() + gotStatus := gasModelStatus(exit) + if gotStatus != want.Status { + t.Fatalf("step %d: status = %q, want %q (exit=%v)", step, gotStatus, want.Status, exit) + } + if uint64(interp.Gas) != want.Gas { + t.Fatalf("step %d: gas = %d, want %d", step, interp.Gas, want.Gas) + } + if assertPC != want.PC { + t.Fatalf("step %d: pc = %d, want %d", step, assertPC, want.PC) + } + if want.Hostcall != nil { + if exit.GetReasonType() != HOST_CALL || uint64(exit.GetHostCallID()) != *want.Hostcall { + t.Fatalf("step %d: hostcall = %d, want %d", step, exit.GetHostCallID(), *want.Hostcall) + } + } + if want.PageFaultAddress != nil { + // Draft vectors report the faulting page base; interpreter payload is the + // access address (may be mid-page). + gotFault := uint64(exit.GetPageFaultAddress()) &^ (uint64(ZP) - 1) + if exit.GetReasonType() != PAGE_FAULT || gotFault != *want.PageFaultAddress { + t.Fatalf("step %d: page-fault page = %#x (raw %#x), want %#x", + step, gotFault, exit.GetPageFaultAddress(), *want.PageFaultAddress) + } + } + if len(want.Regs) > 0 { + if len(want.Regs) != len(interp.Registers) { + t.Fatalf("step %d: regs len = %d, want %d", step, len(interp.Registers), len(want.Regs)) + } + for i, w := range want.Regs { + if interp.Registers[i] != w { + t.Fatalf("step %d: r%d = %#x, want %#x", step, i, interp.Registers[i], w) + } + } + } + for _, m := range want.Memory { + got := interp.Memory.Read(m.Address, uint64(len(m.Contents))) + if !bytes.Equal(got, m.Contents) { + t.Fatalf("step %d: memory[%#x] = %x, want %x", step, m.Address, got, m.Contents) + } + } +} + +func gasModelStatus(exit ExitReason) string { + switch exit.GetReasonType() { + case HALT: + return "halt" + case PANIC: + return "panic" + case OUT_OF_GAS: + return "out-of-gas" + case PAGE_FAULT: + return "page-fault" + case HOST_CALL: + return "ecalli" + default: + return exit.String() + } +} diff --git a/PVM/guest_memory.go b/PVM/guest_memory.go index 1449271f..a6647834 100644 --- a/PVM/guest_memory.go +++ b/PVM/guest_memory.go @@ -32,6 +32,11 @@ type GuestMemory interface { IsWriteable(addr, length uint64) bool Read(addr, length uint64) []byte // caller must have checked IsReadable Write(addr uint64, data []byte) // caller must have checked IsWriteable + + // B.5 Ω_♊: heap state in page indices. + HeapPages() uint64 // h: current heap-top page index + HeapMaxPages() uint64 // b: max possible heap-top page index + GrowHeapTo(targetPage uint64) error // expand heap to targetPage (caller ensures h < target ≤ b) } // pagedGuestMemory adapts the interpreter's paged Memory to GuestMemory. @@ -75,3 +80,25 @@ func (p pagedGuestMemory) Read(addr, length uint64) []byte { func (p pagedGuestMemory) Write(addr uint64, data []byte) { p.mem.Write(addr, data) } + +func (p pagedGuestMemory) HeapPages() uint64 { + return p.mem.heapPointer / uint64(ZP) +} + +func (p pagedGuestMemory) HeapMaxPages() uint64 { + return (p.mem.heapLimit - uint64(ZZ)) / uint64(ZP) +} + +// GrowHeapTo expands the heap so pages [h..targetPage) become writable. +// Caller has already verified h < targetPage ≤ b. +func (p pagedGuestMemory) GrowHeapTo(targetPage uint64) error { + mem := p.mem + newHP := targetPage * uint64(ZP) + oldBound := P(int(mem.heapPointer)) + newBound := P(int(newHP)) + if newHP > uint64(oldBound) { + allocateMemorySegment(mem, uint32(mem.heapPointer), uint32(newBound), nil, MemoryReadWrite) + } + mem.heapPointer = newHP + return nil +} diff --git a/PVM/guest_memory_test.go b/PVM/guest_memory_test.go new file mode 100644 index 00000000..3d875cf6 --- /dev/null +++ b/PVM/guest_memory_test.go @@ -0,0 +1,13 @@ +package PVM + +import "testing" + +func TestHeapMaxPagesReservesMajorZone(t *testing.T) { + stackStart := uint64(1<<32 - 2*ZZ - ZI) + mem := &Memory{heapLimit: stackStart} + got := NewPagedGuestMemory(mem).HeapMaxPages() + want := (stackStart - uint64(ZZ)) / uint64(ZP) + if got != want { + t.Fatalf("HeapMaxPages = %d, want %d", got, want) + } +} diff --git a/PVM/host_call_accumulate.go b/PVM/host_call_accumulate.go index 01ae0db8..fb85193d 100644 --- a/PVM/host_call_accumulate.go +++ b/PVM/host_call_accumulate.go @@ -9,15 +9,16 @@ import ( "github.com/New-JAMneration/JAM-Protocol/internal/utilities/hash" ) -// bless = 14 +// bless = 15 func bless(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { - return *result - } - m, a, v := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9] r, o, n := input.VM.Registers[10], input.VM.Registers[11], input.VM.Registers[12] + // g = M_{B,c} + n · M_{B,ℓ} + if result := chargeGasAndCheck(&input, unitGasCost(HostGasBlessConst, HostGasBlessItem, n)); result != nil { + return *result + } + // if N_{a...+4C} not readable offset := uint64(4 * types.CoresCount) if !input.VM.Mem.IsReadable(a, offset) { @@ -89,6 +90,15 @@ func bless(input OmegaInput) (output OmegaOutput) { } } + // GP v0.8.0 / PR #519: only the manager service may bless. + if input.Addition.ResultContextX.ServiceID != input.Addition.ResultContextX.PartialState.Bless { + input.VM.Registers[7] = HUH + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + // (m, v, r) \not in N_s limit := uint64(1 << 32) @@ -114,9 +124,9 @@ func bless(input OmegaInput) (output OmegaOutput) { } } -// assign = 15 +// assign = 16 func assign(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasAssign); result != nil { // M_A return *result } @@ -181,16 +191,30 @@ func assign(input OmegaInput) (output OmegaOutput) { } } -// designate = 16 +// isValidValidatorCount: designate (B.7) z ∈ 𝕍 with V = 3·C , z is a multiple +// of 3 and 6 ≤ z ≤ 3·C. Invalid z → HUH before reading 336·z bytes; bad keys +// still panic at Decode below. +func isValidValidatorCount(z uint64) bool { + if z%3 != 0 { + return false + } + c := z / 3 + return c >= 2 && c <= uint64(types.CoresCount) +} + +const validatorKeyBytes = 336 + +// designate = 17 func designate(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + o, z := input.VM.Registers[7], input.VM.Registers[8] + + // g = M_{D,c} + z · M_{D,ℓ} + if result := chargeGasAndCheck(&input, unitGasCost(HostGasDesignateConst, HostGasDesignateValidator, z)); result != nil { return *result } - o := input.VM.Registers[7] - - offset := uint64(336 * types.ValidatorsCount) - if !input.VM.Mem.IsReadable(o, offset) { // not readable, panic + offset, overflow := checkOverflow(validatorKeyBytes, z) + if overflow || !input.VM.Mem.IsReadable(o, offset) { // not readable, panic input.VM.Registers[7] = OOB return OmegaOutput{ ExitReason: ExitPanic, @@ -198,8 +222,9 @@ func designate(input OmegaInput) (output OmegaOutput) { } } - // otherwise if x_s ≠ (x_u)_v - if input.Addition.ResultContextX.ServiceID != input.Addition.ResultContextX.PartialState.Designate { + // otherwise if z ∉ 𝕍 or x_s ≠ (x_u)_v (delegator) + if !isValidValidatorCount(z) || + input.Addition.ResultContextX.ServiceID != input.Addition.ResultContextX.PartialState.Designate { input.VM.Registers[7] = HUH return OmegaOutput{ ExitReason: ExitContinue, @@ -207,17 +232,18 @@ func designate(input OmegaInput) (output OmegaOutput) { } } - // 336 * types.ValidatorsCount might cross many pages - rawData := input.VM.Mem.Read(o, offset) // bold{v} - - validatorsData := types.ValidatorsData{} + // Memory layout is a raw concatenation of z validator keys (no length prefix). + rawData := input.VM.Mem.Read(o, offset) + validatorsData := make(types.ValidatorsData, z) decoder := types.NewDecoder() - err := decoder.Decode(rawData, &validatorsData) - if err != nil { - pvmLogger.Errorf("host-call function \"designate\" decode validatorsData error : %v", err) - return OmegaOutput{ - ExitReason: ExitPanic, - Addition: input.Addition, + for i := uint64(0); i < z; i++ { + start := i * validatorKeyBytes + if err := decoder.Decode(rawData[start:start+validatorKeyBytes], &validatorsData[i]); err != nil { + pvmLogger.Errorf("host-call function \"designate\" decode validator %d error : %v", i, err) + return OmegaOutput{ + ExitReason: ExitPanic, + Addition: input.Addition, + } } } @@ -230,9 +256,9 @@ func designate(input OmegaInput) (output OmegaOutput) { } } -// checkpoint = 17 +// checkpoint = 18 func checkpoint(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasCheckpoint); result != nil { // M_C return *result } @@ -246,9 +272,9 @@ func checkpoint(input OmegaInput) (output OmegaOutput) { } } -// new = 18 +// new = 19 func new(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasNew); result != nil { // M_N return *result } @@ -369,9 +395,9 @@ func new(input OmegaInput) (output OmegaOutput) { } } -// upgrade = 19 +// upgrade = 20 func upgrade(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasUpgrade); result != nil { // M_U return *result } @@ -410,9 +436,9 @@ func upgrade(input OmegaInput) (output OmegaOutput) { } } -// transfer = 20 +// transfer = 21 func transfer(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasTransfer); result != nil { // M_T return *result } @@ -486,9 +512,9 @@ func transfer(input OmegaInput) (output OmegaOutput) { } } -// eject = 21 +// eject = 22 func eject(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasEject); result != nil { // M_J return *result } @@ -574,14 +600,28 @@ func eject(input OmegaInput) (output OmegaOutput) { } } -// query = 22 +// isBlobLength reports z ∈ bloblength = ℕ_{2^{32}} (GP PR #520). +func isBlobLength(z uint64) bool { + return z < (1 << 32) +} + +// query = 23 func query(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasQuery); result != nil { // M_Q return *result } o, z := input.VM.Registers[7], input.VM.Registers[8] + // z ∉ bloblength → HUH (checked before memory panic) + if !isBlobLength(z) { + input.VM.Registers[7] = HUH + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + offset := uint64(32) if !input.VM.Mem.IsReadable(o, offset) { // not readable, return input.VM.Registers[7] = OOB @@ -700,13 +740,22 @@ func processSolicitLookupData(account *types.ServiceAccount, lookupKey types.Loo return &OmegaOutput{ExitReason: ExitContinue} } -// solicit = 23 +// solicit = 24 func solicit(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasSolicit); result != nil { // M_S return *result } o, z := input.VM.Registers[7], input.VM.Registers[8] + + if !isBlobLength(z) { + input.VM.Registers[7] = HUH + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + offset := uint64(32) if !input.VM.Mem.IsReadable(o, offset) { input.VM.Registers[7] = OOB @@ -752,14 +801,22 @@ func solicit(input OmegaInput) (output OmegaOutput) { } } -// forget = 24 +// forget = 25 func forget(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasForget); result != nil { // M_F return *result } o, z := input.VM.Registers[7], input.VM.Registers[8] + if !isBlobLength(z) { + input.VM.Registers[7] = HUH + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + offset := uint64(32) if !input.VM.Mem.IsReadable(o, offset) { // not readable, return input.VM.Registers[7] = OOB @@ -853,9 +910,9 @@ func forget(input OmegaInput) (output OmegaOutput) { } } -// yield = 25 +// yield = 26 func yield(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasYield); result != nil { // M_♉ return *result } @@ -881,13 +938,23 @@ func yield(input OmegaInput) (output OmegaOutput) { } } -// provide = 26 +// provide = 27 func provide(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + o, z := input.VM.Registers[8], input.VM.Registers[9] + // g = M_{♈,c} + 𝒢(M_{♈,ℓ}, z) + if result := chargeGasAndCheck(&input, addGas(HostGasProvideConst, MemGas(HostGasProvideOctets, z))); result != nil { return *result } - o, z := input.VM.Registers[8], input.VM.Registers[9] + // z ∉ bloblength → HUH (before memory panic) + if !isBlobLength(z) { + input.VM.Registers[7] = HUH + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + // i = panic offset := uint64(z) if !input.VM.Mem.IsReadable(o, offset) { diff --git a/PVM/host_call_gas_test.go b/PVM/host_call_gas_test.go new file mode 100644 index 00000000..ac4c505c --- /dev/null +++ b/PVM/host_call_gas_test.go @@ -0,0 +1,268 @@ +package PVM + +import ( + "math" + "testing" + + "github.com/New-JAMneration/JAM-Protocol/internal/types" +) + +func TestAddGasAndUnitGasCost(t *testing.T) { + if got := addGas(HostGasLookupConst, MemGas(HostGasLookupOctets, 0)); got != HostGasLookupConst { + t.Fatalf("lookup length 0: got %d want %d", got, HostGasLookupConst) + } + if got := addGas(HostGasLookupConst, MemGas(HostGasLookupOctets, 1024)); got != HostGasLookupConst+HostGasLookupOctets { + t.Fatalf("lookup length 1024: got %d want %d", got, HostGasLookupConst+HostGasLookupOctets) + } + if got := addGas(1, math.MaxInt64); got != math.MaxInt64 { + t.Fatalf("addGas saturate: got %d", got) + } + if got := unitGasCost(HostGasBlessConst, HostGasBlessItem, 0); got != HostGasBlessConst { + t.Fatalf("bless n=0: got %d", got) + } + if got := unitGasCost(HostGasBlessConst, HostGasBlessItem, 3); got != HostGasBlessConst+3*HostGasBlessItem { + t.Fatalf("bless n=3: got %d", got) + } + if got := unitGasCost(1, math.MaxInt64, math.MaxUint64); got != math.MaxInt64 { + t.Fatalf("unitGasCost saturate: got %d", got) + } +} + +func TestPagesGasCost(t *testing.T) { + tests := []struct { + name string + r, c uint64 + want Gas + }{ + {"free", 0, 2, HostGasPagesFreeConst + 2*HostGasPagesFreePage}, + {"alloc r=1", 1, 2, HostGasPagesAllocConst + 2*HostGasPagesAllocPage}, + {"alloc r=2", 2, 0, HostGasPagesAllocConst}, + {"setmode r=3", 3, 4, HostGasPagesSetModeConst + 4*HostGasPagesSetModePage}, + {"setmode r=4", 4, 1, HostGasPagesSetModeConst + HostGasPagesSetModePage}, + {"invalid", 5, 10, HostGasPagesInvalid}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pagesGasCost(tt.r, tt.c); got != tt.want { + t.Fatalf("pagesGasCost(%d,%d)=%d want %d", tt.r, tt.c, got, tt.want) + } + }) + } +} + +func TestIsBlobLength(t *testing.T) { + if !isBlobLength(0) || !isBlobLength((1<<32)-1) { + t.Fatal("expected in-range lengths to be valid") + } + if isBlobLength(1<<32) || isBlobLength(math.MaxUint64) { + t.Fatal("expected z >= 2^32 to be invalid") + } +} + +func TestIsValidValidatorCount(t *testing.T) { + types.SetTinyMode() + t.Cleanup(types.SetTinyMode) + + if !isValidValidatorCount(6) { + t.Fatal("tiny mode should accept z=6") + } + for _, z := range []uint64{0, 3, 9, 7, 5} { + if isValidValidatorCount(z) { + t.Fatalf("tiny mode should reject z=%d", z) + } + } + + types.SetFullMode() + if !isValidValidatorCount(6) || !isValidValidatorCount(1023) || !isValidValidatorCount(9) { + t.Fatal("full mode should accept 6, 9, 1023") + } + if isValidValidatorCount(1026) || isValidValidatorCount(1) { + t.Fatal("full mode should reject out-of-range counts") + } + types.SetTinyMode() +} + +func TestLookupLinearGasOOG(t *testing.T) { + regs := Registers{} + regs[11] = 1024 // z → MemGas(LookupOctets, 1024) > 0 + gas := Gas(HostGasLookupConst) // const only → OOG after linear term + state := types.ServiceAccountState{} + acct := types.ServiceAccount{} + sid := types.ServiceID(1) + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(&Memory{})} + out := lookup(OmegaInput{ + VM: vm, + Addition: HostCallArgs{ + GeneralArgs: GeneralArgs{ + ServiceID: &sid, + ServiceAccount: &acct, + ServiceAccountState: &state, + }, + }, + }) + if out.ExitReason != ExitOOG { + t.Fatalf("lookup OOG: got %v want %v", out.ExitReason, ExitOOG) + } +} + +func TestQueryBlobLengthHUH(t *testing.T) { + regs := Registers{} + regs[8] = 1 << 32 // z ∉ bloblength + gas := Gas(HostGasQuery + 100) + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(&Memory{Pages: map[uint32]*Page{}})} + out := query(OmegaInput{ + VM: vm, + Addition: HostCallArgs{ + AccumulateArgs: AccumulateArgs{ + ResultContextX: ResultContext{ + PartialState: types.PartialStateSet{ + ServiceAccounts: types.ServiceAccountState{}, + }, + }, + }, + }, + }) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if regs[7] != HUH { + t.Fatalf("reg7 = %d, want HUH(%d)", regs[7], HUH) + } +} + +func TestBlessManagerOnly(t *testing.T) { + types.SetTinyMode() + t.Cleanup(types.SetTinyMode) + + regs := Registers{} + // m,a,v,r,o,n — n=0 so always-accum payload is empty + regs[7] = 1 + regs[8] = 16 * ZP + regs[9] = 2 + regs[10] = 3 + regs[11] = 16*ZP + uint64(4*types.CoresCount) + regs[12] = 0 + + mem := &Memory{Pages: map[uint32]*Page{}} + page := uint32(16) + mem.Pages[page] = &Page{Value: make([]byte, ZP), Access: MemoryReadWrite} + + gas := Gas(HostGasBlessConst + 1000) + caller := types.ServiceID(99) // not manager + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := bless(OmegaInput{ + VM: vm, + Addition: HostCallArgs{ + AccumulateArgs: AccumulateArgs{ + ResultContextX: ResultContext{ + ServiceID: caller, + PartialState: types.PartialStateSet{ + Bless: types.ServiceID(1), // manager + }, + }, + }, + }, + }) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if regs[7] != HUH { + t.Fatalf("reg7 = %d, want HUH(%d)", regs[7], HUH) + } +} + +func newGrowHeapTestMem() *Memory { + return &Memory{ + Pages: map[uint32]*Page{}, + heapPointer: 3 * ZZ, // h = 3*ZZ/ZP + heapLimit: 1<<32 - 2*ZZ - ZI, + } +} + +func TestGrowHeapGasOutcomes(t *testing.T) { + t.Run("noGrowthChargesConstEvenIfNegative", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + gas := HostGasGrowHeapConst / 2 + regs := Registers{} + regs[7] = h // no growth + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if gas != HostGasGrowHeapConst/2-HostGasGrowHeapConst { + t.Fatalf("gas = %d, want %d", gas, HostGasGrowHeapConst/2-HostGasGrowHeapConst) + } + if regs[7] != h { + t.Fatalf("reg7 = %d, want h=%d", regs[7], h) + } + }) + + t.Run("growthOOGLeavesGasUnchanged", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + b := NewPagedGuestMemory(mem).HeapMaxPages() + want := h + 1 + if want > b { + t.Skip("heap already at max") + } + g := HostGasGrowHeapConst + HostGasGrowHeapPage + gas := g - 1 + regs := Registers{} + regs[7] = want + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitOOG { + t.Fatalf("exit = %v, want OOG", out.ExitReason) + } + if gas != g-1 { + t.Fatalf("gas changed to %d, want unchanged %d", gas, g-1) + } + if regs[7] != h { + t.Fatalf("reg7 = %d, want h=%d", regs[7], h) + } + }) + + t.Run("growthSucceeds", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + b := NewPagedGuestMemory(mem).HeapMaxPages() + want := h + 1 + if want > b { + t.Skip("heap already at max") + } + g := HostGasGrowHeapConst + HostGasGrowHeapPage + gas := g + 10 + regs := Registers{} + regs[7] = want + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if gas != 10 { + t.Fatalf("gas = %d, want 10", gas) + } + if regs[7] != want { + t.Fatalf("reg7 = %d, want %d", regs[7], want) + } + }) +} + +func TestFetchConstantsOmitRemovedV080Fields(t *testing.T) { + types.SetTinyMode() + t.Cleanup(types.SetTinyMode) + + data := getFetchConstantsData() + // v0.8.0 fetch(0) dropped N, V, W_E, W_P (2+2+4+4 = 12 bytes vs 0.7.2). + // Spot-check: after L (u32) comes O (u16), not N (u16 tickets). + if len(data) < 80 { + t.Fatalf("constants too short: %d", len(data)) + } + off := 8 + 8 + 8 + 2 + 4 + 4 + 8 + 8 + 8 + 8 + 2 + 2 + 2 + 2 + 4 // through L + gotO := uint16(data[off]) | uint16(data[off+1])<<8 + if gotO != uint16(types.AuthPoolMaxSize) { + t.Fatalf("after L expected O=%d, got %d (N/V still present?)", types.AuthPoolMaxSize, gotO) + } +} diff --git a/PVM/host_call_general.go b/PVM/host_call_general.go index f68209b2..8292d064 100644 --- a/PVM/host_call_general.go +++ b/PVM/host_call_general.go @@ -2,6 +2,8 @@ package PVM import ( "fmt" + "math" + "math/bits" "sync" "time" @@ -14,39 +16,40 @@ import ( type OperationType int const ( - // ----------------- General Functions ----------------- - GasOp OperationType = iota // gas = 0 - FetchOp // fetch = 1 - LookupOp // lookup = 2 - ReadOp // read = 3 - WriteOp // write = 4 - InfoOp // info = 5 - - // ----------------- Refine Functions ----------------- - HistoricalLookupOp // historical_lookup = 6 - ExportOp // export = 7 - MachineOp // machine = 8 - PeekOp // peek = 9 - PokeOp // poke = 10 - PagesOp // pages = 11 - InvokeOp // invoke = 12 - ExpungeOp // expunge = 13 - - // ----------------- Accumulate Functions ----------------- - BlessOp // bless = 14 - AssignOp // assign = 15 - DesignateOp // designate = 16 - CheckpointOp // checkpoint = 17 - NewOp // new = 18 - UpgradeOp // upgrade = 19 - TransferOp // transfer = 20 - EjectOp // eject = 21 - QueryOp // query = 22 - SolicitOp // solicit = 23 - ForgetOp // forget = 24 - YieldOp // yield = 25 - ProvideOp // provide = 26 - LogOp = OperationType(100) + // ----------------- General Functions (B.5) ----------------- + GasOp OperationType = 0 // Ω_G + GrowHeapOp OperationType = 1 // Ω_♊ + FetchOp OperationType = 2 // Ω_Y + LookupOp OperationType = 3 // Ω_L + ReadOp OperationType = 4 // Ω_R + WriteOp OperationType = 5 // Ω_W + InfoOp OperationType = 6 // Ω_I + + // ----------------- Refine Functions (B.6) ----------------- + HistoricalLookupOp OperationType = 7 // Ω_H + ExportOp OperationType = 8 // Ω_E + MachineOp OperationType = 9 // Ω_M + PeekOp OperationType = 10 // Ω_P + PokeOp OperationType = 11 // Ω_O + PagesOp OperationType = 12 // Ω_Z + InvokeOp OperationType = 13 // Ω_K + ExpungeOp OperationType = 14 // Ω_X + + // ----------------- Accumulate Functions (B.7) ----------------- + BlessOp OperationType = 15 // Ω_B + AssignOp OperationType = 16 // Ω_A + DesignateOp OperationType = 17 // Ω_D + CheckpointOp OperationType = 18 // Ω_C + NewOp OperationType = 19 // Ω_N + UpgradeOp OperationType = 20 // Ω_U + TransferOp OperationType = 21 // Ω_T + EjectOp OperationType = 22 // Ω_J + QueryOp OperationType = 23 // Ω_Q + SolicitOp OperationType = 24 // Ω_S + ForgetOp OperationType = 25 // Ω_F + YieldOp OperationType = 26 // Ω_Taurus + ProvideOp OperationType = 27 // Ω_Aries + LogOp OperationType = 100 MaxOperationType OperationType = 100 ) @@ -97,32 +100,33 @@ func getPtr[T any](v T) *T { return &v } var hostCallName = []string{ 0: "gas", - 1: "fetch", - 2: "lookup", - 3: "read", - 4: "write", - 5: "info", - 6: "historicalLookup", - 7: "export", - 8: "machine", - 9: "peek", - 10: "poke", - 11: "pages", - 12: "invoke", - 13: "expunge", - 14: "bless", - 15: "assign", - 16: "designate", - 17: "checkpoint", - 18: "new", - 19: "upgrade", - 20: "transfer", - 21: "eject", - 22: "query", - 23: "solicit", - 24: "forget", - 25: "yield", - 26: "provide", + 1: "grow_heap", + 2: "fetch", + 3: "lookup", + 4: "read", + 5: "write", + 6: "info", + 7: "historicalLookup", + 8: "export", + 9: "machine", + 10: "peek", + 11: "poke", + 12: "pages", + 13: "invoke", + 14: "expunge", + 15: "bless", + 16: "assign", + 17: "designate", + 18: "checkpoint", + 19: "new", + 20: "upgrade", + 21: "transfer", + 22: "eject", + 23: "query", + 24: "solicit", + 25: "forget", + 26: "yield", + 27: "provide", 100: "log", } @@ -137,6 +141,7 @@ func HostCallName(op int) string { var HostCallFunctions Omegas = func() Omegas { f := make([]Omega, MaxOperationType+1) f[GasOp] = gas + f[GrowHeapOp] = growHeap f[FetchOp] = fetch f[LookupOp] = lookup f[ReadOp] = read @@ -175,7 +180,7 @@ var ( ) func hostCallException(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasUnknown); result != nil { return *result } input.VM.Registers[7] = WHAT @@ -199,8 +204,9 @@ func hostCallOutOfGas(input OmegaInput) (output OmegaOutput) { } } -func chargeGasAndCheck(input *OmegaInput) *OmegaOutput { - *input.VM.Gas -= 10 +// chargeGasAndCheck deducts cost from gas and returns OOG output if negative. +func chargeGasAndCheck(input *OmegaInput, cost Gas) *OmegaOutput { + *input.VM.Gas -= cost if *input.VM.Gas < 0 { return &OmegaOutput{ ExitReason: ExitOOG, @@ -212,15 +218,11 @@ func chargeGasAndCheck(input *OmegaInput) *OmegaOutput { // Gas Function(ΩG), gas = 0 func gas(input OmegaInput) OmegaOutput { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasGas); result != nil { // M_G return *result } - input.VM.Registers[7] = uint64(*input.VM.Gas) - return OmegaOutput{ - ExitReason: ExitContinue, - Addition: input.Addition, - } + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} } type fetchHandler func(OmegaInput, *types.Encoder) ([]byte, error) @@ -477,19 +479,49 @@ func fetchOperandOrDeferredTransferAt(input OmegaInput, enc *types.Encoder) ([]b return val, nil } -// fetch = 1 +// growHeap = 1 | Ω_♊(g, ω, μ, jam_blob) +// ω₇ = desired heap-top page index; ω'₇ = resulting heap-top page index. +// g = M_{♊,c} + (ω₇ − h) · M_{♊,p} +func growHeap(input OmegaInput) OmegaOutput { + n := input.VM.Registers[7] // ω₇ + h := input.VM.Mem.HeapPages() // h = a + c + b := input.VM.Mem.HeapMaxPages() // b + + if n <= h || n > b { + *input.VM.Gas -= HostGasGrowHeapConst + input.VM.Registers[7] = h + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} + } + + g := HostGasGrowHeapConst + Gas(n-h)*HostGasGrowHeapPage + if *input.VM.Gas < g { + input.VM.Registers[7] = h + return OmegaOutput{ExitReason: ExitOOG, Addition: input.Addition} + } + *input.VM.Gas -= g + + // GrowHeapTo failure is a host/OS fault (e.g. mprotect), not a GP Ω_♊ + // outcome — do not invent ExitPanic/PAGE_FAULT for the guest. + if err := input.VM.Mem.GrowHeapTo(n); err != nil { + panic("grow_heap: " + err.Error()) + } + input.VM.Registers[7] = n + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} +} + +// fetch = 2 func fetch(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + discriminator := input.VM.Registers[10] + if result := chargeGasAndCheck(&input, fetchCost(discriminator, input.VM.Registers[9])); result != nil { return *result } encoder := types.NewEncoder() - idx := input.VM.Registers[10] var v *[]byte var val []byte var err error - if idx < uint64(len(fetchHandlers)) { - val, err = fetchHandlers[idx](input, encoder) + if discriminator < uint64(len(fetchHandlers)) { + val, err = fetchHandlers[discriminator](input, encoder) if err == nil && val != nil { v = &val } @@ -541,9 +573,43 @@ func fetch(input OmegaInput) (output OmegaOutput) { } } -// ΩL(ϱ, ω, μ, s, s, d) , lookup = 2 +func fetchCost(discriminator, length uint64) Gas { + constant, rate := FetchGasCost(discriminator) + return addGas(constant, MemGas(rate, length)) +} + +// addGas saturates at MaxInt64 so huge linear terms never wrap. +func addGas(parts ...Gas) Gas { + var sum Gas + for _, part := range parts { + if part <= 0 { + continue + } + if sum > math.MaxInt64-part { + return math.MaxInt64 + } + sum += part + } + return sum +} + +// unitGasCost is const + rate·n with saturation (pages / bless / designate). +func unitGasCost(constant, rate Gas, n uint64) Gas { + if rate <= 0 || n == 0 { + return constant + } + hi, lo := bits.Mul64(uint64(rate), n) + if hi != 0 || lo > uint64(math.MaxInt64) { + return math.MaxInt64 + } + return addGas(constant, Gas(lo)) +} + +// lookup = 3 func lookup(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + // g = M_{L,c} + 𝒢(M_{L,ℓ}, z); z = ω₁₁ + cost := addGas(HostGasLookupConst, MemGas(HostGasLookupOctets, input.VM.Registers[11])) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } @@ -610,7 +676,7 @@ func lookup(input OmegaInput) (output OmegaOutput) { } } -// ΩR(ϱ, ω, μ, s, s, d) , read = 3 +// read = 4 /* ϱ: gas ω: registers @@ -620,7 +686,15 @@ s(italic): ServiceID d: ServiceAccountState (map[ServiceID]ServiceAccount) */ func read(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + // g = M_{R,c} + 𝒢(M_{R,k,ℓ}, k_Z) + 𝒢(M_{R,v,ℓ}, v_Z) + ko, kz, o := input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] + vZ := input.VM.Registers[12] + cost := addGas( + HostGasReadConst, + MemGas(HostGasReadKeyOctets, kz), + MemGas(HostGasReadValueOctets, vZ), + ) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } @@ -635,7 +709,6 @@ func read(input OmegaInput) (output OmegaOutput) { } // assign ko, kz, o first and check v = panic ? // since v = panic is the first condition to check - ko, kz, o := input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] if !input.VM.Mem.IsReadable(ko, kz) { input.VM.Registers[7] = OOB return OmegaOutput{ @@ -719,13 +792,18 @@ func read(input OmegaInput) (output OmegaOutput) { } } -// ΩW (ϱ, ω, μ, s, s) , write = 4 +// write = 5 func write(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + // g = M_{W,c} + 𝒢(M_{W,k,ℓ}, k_Z) + 𝒢(M_{W,v,ℓ}, v_Z) + ko, kz, vo, vz := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] + cost := addGas( + HostGasWriteConst, + MemGas(HostGasWriteKeyOctets, kz), + MemGas(HostGasWriteValOctets, vz), + ) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } - - ko, kz, vo, vz := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] if !input.VM.Mem.IsReadable(ko, kz) { input.VM.Registers[7] = OOB return OmegaOutput{ @@ -811,7 +889,7 @@ func write(input OmegaInput) (output OmegaOutput) { } } -// ΩR(ϱ, ω, μ, s, d) , info = 5 +// info = 6 /* ϱ: gas ω: registers @@ -821,7 +899,7 @@ s(italic): ServiceID d: ServiceAccountState (map[ServiceID]ServiceAccount) */ func info(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasInfo); result != nil { // M_I return *result } @@ -890,10 +968,11 @@ func info(input OmegaInput) (output OmegaOutput) { } // log = 100 , [JIP-1](https://hackmd.io/@polkadot/jip1) +// g = 10 (fixed; not in Gray Paper) // Output registers: {} (none modified per spec) // Side-effects: "No side-effects if memory access is invalid." func logHostCall(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasLog); result != nil { return *result } @@ -1047,20 +1126,16 @@ func getFetchConstantsData() []byte { getPtr(types.U16(types.MaximumDependencyItems)), // J getPtr(types.U16(types.MaxTicketsPerBlock)), // K getPtr(types.U32(types.MaxLookupAge)), // L - getPtr(types.U16(types.TicketsPerValidator)), // N getPtr(types.U16(types.AuthPoolMaxSize)), // O getPtr(types.U16(types.SlotPeriod)), // P getPtr(types.U16(types.AuthQueueSize)), // Q getPtr(types.U16(types.RotationPeriod)), // R getPtr(types.U16(types.MaxExtrinsics)), // T getPtr(types.U16(types.WorkReportTimeout)), // U - getPtr(types.U16(types.ValidatorsCount)), // V getPtr(types.U32(types.MaxIsAuthorizedCodeSize)), // W_A getPtr(types.U32(types.MaxTotalSize)), // W_B getPtr(types.U32(types.MaxServiceCodeSize)), // W_C - getPtr(types.U32(types.ECBasicSize)), // W_E getPtr(types.U32(types.MaxImportCount)), // W_M - getPtr(types.U32(types.ECPiecesPerSegment)), // W_P getPtr(types.U32(types.WorkReportOutputBlobsMaximumSize)), // W_R getPtr(types.U32(types.TransferMemoSize)), // W_T getPtr(types.U32(types.MaxExportCount)), // W_X @@ -1073,3 +1148,32 @@ func getFetchConstantsData() []byte { }) return fetchConstantsData } + +// memGasUnit is the octet granularity of the linear gas term: rates in +// gas_const.go are quoted per this many octets. +const memGasUnit = 1024 + +// MemGas is 𝒢(L, ℓ) (formula B.17): the memory term of +// a host-call gas cost, ⌈L × ℓ / 1024⌉, for a rate L of gas per 1024 octets over +// a length ℓ. +// +// Callers pass ℓ straight from a guest register, so the product can exceed the +// range of Gas. Because such a cost is unpayable regardless of its exact value, +// the result saturates at the maximum instead of wrapping. +func MemGas(rate Gas, length uint64) Gas { + if rate <= 0 || length == 0 { + return 0 + } + + hi, lo := bits.Mul64(uint64(rate), length) + if hi != 0 { + return math.MaxInt64 + } + + rounded := lo + memGasUnit - 1 + if rounded < lo { + return math.MaxInt64 + } + + return Gas(rounded / memGasUnit) +} diff --git a/PVM/host_call_invocation.go b/PVM/host_call_invocation.go index 3713bada..0ee5ccaf 100644 --- a/PVM/host_call_invocation.go +++ b/PVM/host_call_invocation.go @@ -30,12 +30,14 @@ func init() { // is authorized host-call functions IsAuthorizedOmegas = make(Omegas, len(HostCallFunctions)) IsAuthorizedOmegas[GasOp] = HostCallFunctions[GasOp] + IsAuthorizedOmegas[GrowHeapOp] = HostCallFunctions[GrowHeapOp] IsAuthorizedOmegas[FetchOp] = HostCallFunctions[FetchOp] IsAuthorizedOmegas[100] = logHostCall // accumulate host-call functions AccumulateOmegas = make(Omegas, len(HostCallFunctions)) AccumulateOmegas[GasOp] = HostCallFunctions[GasOp] + AccumulateOmegas[GrowHeapOp] = HostCallFunctions[GrowHeapOp] AccumulateOmegas[FetchOp] = HostCallFunctions[FetchOp] AccumulateOmegas[ReadOp] = readWrapWithG AccumulateOmegas[WriteOp] = writeWrapWithG @@ -59,6 +61,7 @@ func init() { // refine host-call functions RefineOmegas = make(Omegas, len(HostCallFunctions)) RefineOmegas[GasOp] = HostCallFunctions[GasOp] + RefineOmegas[GrowHeapOp] = HostCallFunctions[GrowHeapOp] RefineOmegas[FetchOp] = HostCallFunctions[FetchOp] RefineOmegas[HistoricalLookupOp] = HostCallFunctions[HistoricalLookupOp] RefineOmegas[ExportOp] = HostCallFunctions[ExportOp] diff --git a/PVM/host_call_invoke_test.go b/PVM/host_call_invoke_test.go new file mode 100644 index 00000000..11fa3d34 --- /dev/null +++ b/PVM/host_call_invoke_test.go @@ -0,0 +1,220 @@ +package PVM + +import ( + "encoding/binary" + "testing" + + "github.com/New-JAMneration/JAM-Protocol/internal/types" +) + +func encodeUintVariable(val uint64) []byte { + if val < 0x80 { + return []byte{byte(val)} + } + buf := make([]byte, 9) + buf[0] = 0xFF + for i := range 8 { + buf[1+i] = byte(val >> (8 * i)) + } + return buf +} + +func buildTestBlob(t *testing.T, instBytes []byte, instrBoundaries []int) []byte { + t.Helper() + instSize := len(instBytes) + bitmaskByteCount := instSize / 8 + if instSize%8 > 0 { + bitmaskByteCount++ + } + bitmaskData := make([]byte, bitmaskByteCount) + for _, idx := range instrBoundaries { + bitmaskData[idx/8] |= 1 << (idx % 8) + } + var blob []byte + blob = append(blob, encodeUintVariable(0)...) + blob = append(blob, 0) + blob = append(blob, encodeUintVariable(uint64(instSize))...) + blob = append(blob, instBytes...) + blob = append(blob, bitmaskData...) + return blob +} + +func TestGasChargedForIntegratedResume(t *testing.T) { + prog := decodedGasTestProgram(t, + ProgramCode{10, 0, 1, 0}, + Bitmask{0x03, 0x00, 0x01, 0x03}, + ) + // A.4: fault on the first instruction keeps ⊤ at the block-entry PC. + if !gasChargedForIntegratedResume(&prog, 0, true) { + t.Fatal("block entry with stored flag should keep ⊤") + } + if !gasChargedForIntegratedResume(&prog, 2, true) { + t.Fatal("mid-block resume should keep flag") + } + if gasChargedForIntegratedResume(&prog, 2, false) { + t.Fatal("stored false should stay false") + } + if gasChargedForIntegratedResume(nil, 0, true) { + t.Fatal("nil program should clear flag") + } + if gasChargedForIntegratedResume(&prog, 1, true) { + t.Fatal("invalid instruction PC should not restore flag") + } +} + +func TestInvokeInnerTrapDeductsBlockGas(t *testing.T) { + blob := buildTestBlob(t, []byte{0}, []int{0}) + + outerMem := &Memory{Pages: map[uint32]*Page{ + 0: {Value: make([]byte, ZP), Access: MemoryReadWrite}, + }} + + innerBudget := Gas(1000) + var w Registers + scratch := make([]byte, 112) + binary.LittleEndian.PutUint64(scratch[0:8], uint64(innerBudget)) + for i := uint64(1); i < 14; i++ { + binary.LittleEndian.PutUint64(scratch[8*i:8*(i+1)], w[i-1]) + } + outerMem.Write(0, scratch) + + outerGas := addGas(HostGasInvoke, innerBudget+1000) + regs := Registers{} + regs[7] = 0 // n + regs[8] = 0 // o + + vm := &VMState{Registers: ®s, Gas: &outerGas, Mem: NewPagedGuestMemory(outerMem)} + addition := HostCallArgs{ + RefineArgs: RefineArgs{ + IntegratedPVMMap: IntegratedPVMMap{ + 0: { + ProgramCode: ProgramCode(blob), + Memory: Memory{}, + PC: 0, + GasCharged: false, + }, + }, + }, + } + + out := invoke(OmegaInput{VM: vm, Addition: addition}) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if regs[7] != INNERPANIC { + t.Fatalf("reg7 = %d, want INNERPANIC(%d)", regs[7], INNERPANIC) + } + + prog, reason := DeBlobProgramCode(blob, 0) + if reason != ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) + } + blockGas := GasCostForBlock(&prog, 0) + wantInnerRemain := innerBudget - blockGas + + var gRPrime uint64 + decoder := types.NewDecoder() + if err := decoder.Decode(outerMem.Read(0, 8), &gRPrime); err != nil { + t.Fatalf("decode g_R': %v", err) + } + if Gas(gRPrime) != wantInnerRemain { + t.Fatalf("inner g_R' = %d, want %d (budget %d - block gas %d)", gRPrime, wantInnerRemain, innerBudget, blockGas) + } +} + +func TestDeBlobProgramCodeEntryPC(t *testing.T) { + blob := buildTestBlob(t, []byte{0}, []int{0}) + if _, got := DeBlobProgramCode(blob, 0); got != ExitContinue { + t.Fatalf("valid blob: got %v", got) + } + if _, got := DeBlobProgramCode(blob, 1); got != ExitPanic { + t.Fatalf("invalid entry PC: got %v, want panic", got) + } +} + +func TestMachineStoresDecodedProgram(t *testing.T) { + blob := buildTestBlob(t, []byte{0}, []int{0}) + outerMem := &Memory{Pages: map[uint32]*Page{ + 0: {Value: make([]byte, ZP), Access: MemoryReadWrite}, + }} + outerMem.Write(0, blob) + + regs := Registers{} + regs[7] = 0 // po + regs[8] = uint64(len(blob)) + regs[9] = 0 // i + + gas := Gas(HostGasMachineConst + MemGas(HostGasMachineOctets, uint64(len(blob))) + 1000) + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(outerMem)} + addition := HostCallArgs{RefineArgs: RefineArgs{IntegratedPVMMap: IntegratedPVMMap{}}} + + out := machine(OmegaInput{VM: vm, Addition: addition}) + if out.ExitReason != ExitContinue { + t.Fatalf("machine exit = %v", out.ExitReason) + } + n := regs[7] + entry, ok := out.Addition.IntegratedPVMMap[n] + if !ok { + t.Fatal("machine did not register integrated VM") + } + if entry.Program == nil { + t.Fatal("machine should store decoded Program") + } + if len(entry.Program.Instrs) == 0 { + t.Fatal("decoded program should have InstrMeta") + } + if len(entry.Program.BlockAt) == 0 { + t.Fatal("decoded program should have pre-decoded blocks") + } +} + +func TestIntegratedProgramForInvokeUsesCachedDecode(t *testing.T) { + blob := buildTestBlob(t, []byte{0}, []int{0}) + prog, reason := DeBlobProgramCode(blob, 0) + if reason != ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) + } + + integrated := IntegratedPVMType{ + ProgramCode: ProgramCode(blob), + Program: &prog, + PC: 0, + } + got, reason := IntegratedProgramForInvoke(integrated) + if reason != ExitContinue { + t.Fatalf("invoke resolve: %v", reason) + } + if got != &prog { + t.Fatal("should reuse cached program pointer") + } + + integrated.PC = 1 + if _, reason := IntegratedProgramForInvoke(integrated); reason != ExitPanic { + t.Fatalf("invalid entry PC: got %v, want panic", reason) + } +} + +func TestMachineCapacityFullBeforeMemory(t *testing.T) { + regs := Registers{} + regs[7] = 0 + regs[8] = 1 + regs[9] = 0 + gas := Gas(HostGasMachineConst + 10_000) + m := IntegratedPVMMap{} + for i := uint64(0); i < 63; i++ { + m[i] = IntegratedPVMType{} + } + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(&Memory{Pages: map[uint32]*Page{}})} + out := machine(OmegaInput{ + VM: vm, + Addition: HostCallArgs{ + RefineArgs: RefineArgs{IntegratedPVMMap: m}, + }, + }) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if regs[7] != FULL { + t.Fatalf("reg7 = %d, want FULL(%d)", regs[7], FULL) + } +} diff --git a/PVM/host_call_mem_trace.go b/PVM/host_call_mem_trace.go index adc54e27..2c796037 100644 --- a/PVM/host_call_mem_trace.go +++ b/PVM/host_call_mem_trace.go @@ -72,6 +72,18 @@ func (t *hostCallMemTracer) Write(addr uint64, data []byte) { t.inner.Write(addr, data) } +func (t *hostCallMemTracer) HeapPages() uint64 { + return t.inner.HeapPages() +} + +func (t *hostCallMemTracer) HeapMaxPages() uint64 { + return t.inner.HeapMaxPages() +} + +func (t *hostCallMemTracer) GrowHeapTo(targetPage uint64) error { + return t.inner.GrowHeapTo(targetPage) +} + func (t *hostCallMemTracer) detailsJSON() json.RawMessage { if len(t.reads) == 0 && len(t.writes) == 0 { return nil diff --git a/PVM/host_call_refine.go b/PVM/host_call_refine.go index 68ac09dd..a9f34274 100644 --- a/PVM/host_call_refine.go +++ b/PVM/host_call_refine.go @@ -1,13 +1,26 @@ package PVM import ( + "encoding/binary" + "math" + "github.com/New-JAMneration/JAM-Protocol/internal/service_account" "github.com/New-JAMneration/JAM-Protocol/internal/types" ) -// historical_lookup = 6 +// gasFromUint64 clamps a guest-supplied gas budget into signed Gas. +func gasFromUint64(v uint64) Gas { + if v > uint64(math.MaxInt64) { + return math.MaxInt64 + } + return Gas(v) +} + +// historical_lookup = 7 func historicalLookup(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + // g = M_{H,c} + 𝒢(M_{H,ℓ}, z); z = ω₁₁ + cost := addGas(HostGasHistoricalLookupConst, MemGas(HostGasHistoricalLookupOctets, input.VM.Registers[11])) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } @@ -70,9 +83,9 @@ func historicalLookup(input OmegaInput) (output OmegaOutput) { } } -// export = 7 +// export = 8 func export(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasExport); result != nil { // M_E return *result } @@ -112,16 +125,20 @@ func export(input OmegaInput) (output OmegaOutput) { } } -// machine = 8 +// machine = 9 func machine(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + po, pz, i := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9] + // g = M_{M,c} + 𝒢(M_{M,ℓ}, p_Z) + cost := addGas(HostGasMachineConst, MemGas(HostGasMachineOctets, pz)) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } - - po, pz, i := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9] - // pz = offset - if !input.VM.Mem.IsReadable(po, pz) { // not readable, return - input.VM.Registers[7] = OOB + // B.6: capacity check precedes memory read; result is FULL (not HUH). + if uint64(len(input.Addition.IntegratedPVMMap)) >= 63 { + input.VM.Registers[7] = FULL + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} + } + if !input.VM.Mem.IsReadable(po, pz) { return OmegaOutput{ ExitReason: ExitPanic, Addition: input.Addition, @@ -130,7 +147,7 @@ func machine(input OmegaInput) (output OmegaOutput) { p := input.VM.Mem.Read(po, pz) - // find first i not in K(m) + // find first n not in K(m) n := uint64(0) for ; n <= ^uint64(0); n++ { if _, pvmTypeExists := input.Addition.IntegratedPVMMap[n]; !pvmTypeExists { @@ -138,39 +155,34 @@ func machine(input OmegaInput) (output OmegaOutput) { } } - var u Memory - _, exitReason := DeBlobProgramCode(p) - // otherwise if deblob(p) = PANIC - if exitReason == ExitPanic { + prog, exitReason := DeBlobProgramCode(p, i) + if exitReason != ExitContinue { input.VM.Registers[7] = HUH - return OmegaOutput{ - ExitReason: ExitContinue, - Addition: input.Addition, - } + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} } // otherwise input.VM.Registers[7] = n input.Addition.IntegratedPVMMap[n] = IntegratedPVMType{ ProgramCode: ProgramCode(p), - Memory: u, + Program: &prog, + Memory: Memory{}, PC: ProgramCounter(i), + GasCharged: false, // GP B.6 Ω_M: gaschargedflag = ⊥ at creation } - return OmegaOutput{ - ExitReason: ExitContinue, - Addition: input.Addition, - } + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} } -// peek = 9 +// peek = 10 func peek(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + n, o, s, z := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] + // g = M_{P,c} + 𝒢(M_{P,ℓ}, z) + cost := addGas(HostGasPeekConst, MemGas(HostGasPeekOctets, z)) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } - n, o, s, z := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] - if z == 0 { input.VM.Registers[7] = OK return OmegaOutput{ @@ -220,14 +232,15 @@ func peek(input OmegaInput) (output OmegaOutput) { } } -// poke = 10 +// poke = 11 func poke(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + n, s, o, z := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] + // g = M_{O,c} + 𝒢(M_{O,ℓ}, z) + cost := addGas(HostGasPokeConst, MemGas(HostGasPokeOctets, z)) + if result := chargeGasAndCheck(&input, cost); result != nil { return *result } - n, s, o, z := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] - if !input.VM.Mem.IsReadable(s, z) { // not readable, return input.VM.Registers[7] = OOB return OmegaOutput{ @@ -268,13 +281,27 @@ func poke(input OmegaInput) (output OmegaOutput) { } } -// pages = 11 -func pages(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { - return *result +// pagesGasCost is Ω_Z gas from B.6: free (r=0), alloc (r∈{1,2}), +// setmode (r∈{3,4}), else invalid. Linear term is per page, not MemGas. +func pagesGasCost(r, c uint64) Gas { + switch r { + case 0: + return unitGasCost(HostGasPagesFreeConst, HostGasPagesFreePage, c) + case 1, 2: + return unitGasCost(HostGasPagesAllocConst, HostGasPagesAllocPage, c) + case 3, 4: + return unitGasCost(HostGasPagesSetModeConst, HostGasPagesSetModePage, c) + default: + return HostGasPagesInvalid } +} +// pages = 12 +func pages(input OmegaInput) (output OmegaOutput) { n, p, c, r := input.VM.Registers[7], input.VM.Registers[8], input.VM.Registers[9], input.VM.Registers[10] + if result := chargeGasAndCheck(&input, pagesGasCost(r, c)); result != nil { + return *result + } // u = panic if _, nExists := input.Addition.IntegratedPVMMap[n]; !nExists { // u = panic @@ -340,17 +367,16 @@ func pages(input OmegaInput) (output OmegaOutput) { } } -// invoke = 12 +// invoke = 13 | B.6 Ω_K: g = M_K + g_R; success path refunds g_R' to outer gas. func invoke(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { - return *result - } - n, o := input.VM.Registers[7], input.VM.Registers[8] offset := uint64(112) - // g = panic + // w = error ⇒ g_R = 0, g = M_K; charge then panic. if !input.VM.Mem.IsWriteable(o, offset) { + if result := chargeGasAndCheck(&input, HostGasInvoke); result != nil { + return *result + } input.VM.Registers[7] = OOB return OmegaOutput{ ExitReason: ExitPanic, @@ -358,25 +384,15 @@ func invoke(input OmegaInput) (output OmegaOutput) { } } - // otherwise if n not in M - if _, nExists := input.Addition.IntegratedPVMMap[n]; !nExists { - input.VM.Registers[7] = WHO - return OmegaOutput{ - ExitReason: ExitContinue, - Addition: input.Addition, - } - } - - // assign g, w | g => gas , w => registers[13] , 8(gas) + 8(uint64) * 13 = 112 - var g uint64 + // assign g_R, w | g_R => gas , w => registers[13] , 8(gas) + 8(uint64) * 13 = 112 + var gR uint64 var w Registers - // first read data from memory + // read data from memory data := input.VM.Mem.Read(o, offset) - decoder := types.NewDecoder() // decode gas - err := decoder.Decode(data[:8], &g) + err := decoder.Decode(data[:8], &gR) if err != nil { pvmLogger.Errorf("host-call function \"invoke\" decode gas error : %v", err) } @@ -387,36 +403,46 @@ func invoke(input OmegaInput) (output OmegaOutput) { pvmLogger.Errorf("host-call function \"invoke\" decode register:%d error : %v", i-1, err) } } - // inner-invoke executor (A.38 invoke): runs a nested program blob. It is - // deliberately NOT one of the pluggable Psi_M backends and does not go - // through Psi_M dispatch — it is a core, instruction-only executor: - // - dynamic decode: tmpProgram carries only the raw blob (no pre-decoded - // blocks), so SingleStepInvoke decodes per instruction; nested PVM is not - // recompiled. - // - single-step, no host calls (NewInterpreter takes no omegas). - // Keeping it on the core Interpreter (not the host-call Host) also keeps this - // executor free of any interpreter-backend package, avoiding an - // omega -> PVM/interpreter import cycle. - tmpProgram := Program{ - InstructionData: input.Addition.IntegratedPVMMap[n].ProgramCode, - } - tempMemory := input.Addition.IntegratedPVMMap[n].Memory - // wrap m[n]_p (program), w (registers), m[n]_u (memory), g (gas) - tempInterp := NewInterpreter(&tmpProgram, w, &tempMemory, Gas(g)) - - var c ExitReason - var pcPrime ProgramCounter - - c, pcPrime = tempInterp.SingleStepInvoke(input.Addition.IntegratedPVMMap[n].PC) - - // mu* = mu - encoder := types.NewEncoder() + + innerBudget := gasFromUint64(gR) + // Charge M_K + g_R up front; remaining g_R' is refunded after Ψ returns. + if result := chargeGasAndCheck(&input, addGas(HostGasInvoke, innerBudget)); result != nil { + return *result + } + + // otherwise if n not in M — no refund + if _, nExists := input.Addition.IntegratedPVMMap[n]; !nExists { + input.VM.Registers[7] = WHO + return OmegaOutput{ + ExitReason: ExitContinue, + Addition: input.Addition, + } + } + + integrated := input.Addition.IntegratedPVMMap[n] + tmpProgram, decodeReason := IntegratedProgramForInvoke(integrated) + if decodeReason != ExitContinue { + // Inner never ran; treat as panic host-result and refund full g_R + // (g_R' = g_R) so outer only paid M_K. + *input.VM.Gas += innerBudget + input.VM.Registers[7] = INNERPANIC + return OmegaOutput{ExitReason: ExitContinue, Addition: input.Addition} + } + + tempMemory := integrated.Memory + tempInterp := NewInterpreter(tmpProgram, w, &tempMemory, innerBudget) + tempInterp.GasCharged = gasChargedForIntegratedResume(tmpProgram, integrated.PC, integrated.GasCharged) + + c, pcPrime := tempInterp.BlockBasedInvokeDecodedBlocks(integrated.PC) + + // gascounter' = gascounter − g + g_R' (refund remaining inner gas) + *input.VM.Gas += tempInterp.Gas + + // mu* = mu — same fixed 8-byte little-endian layout as the read path above. data = types.ByteSequence(make([]byte, offset)) - encoded, _ := encoder.Encode(&tempInterp.Gas) // encode g' - copy(data, encoded) + binary.LittleEndian.PutUint64(data[0:8], uint64(tempInterp.Gas)) for i := uint64(1); i < offset/8; i++ { - encoded, _ := encoder.Encode(&tempInterp.Registers[i-1]) - copy(data[8*i:8*(i+1)], encoded) + binary.LittleEndian.PutUint64(data[8*i:8*(i+1)], tempInterp.Registers[i-1]) } // write data into memory (mu) input.VM.Mem.Write(o, data) @@ -424,11 +450,8 @@ func invoke(input OmegaInput) (output OmegaOutput) { // m* = m tmp := input.Addition.IntegratedPVMMap[n] tmp.Memory = *tempInterp.Memory - if c.GetReasonType() == HOST_CALL { - tmp.PC = pcPrime + 1 + ProgramCounter(skip(int(pcPrime), input.Addition.Program.Bitmasks)) - } else { - tmp.PC = pcPrime - } + tmp.GasCharged = tempInterp.GasCharged + tmp.PC = pcPrime input.Addition.IntegratedPVMMap[n] = tmp switch c.GetReasonType() { @@ -457,9 +480,9 @@ func invoke(input OmegaInput) (output OmegaOutput) { } } -// expunge = 13 +// expunge = 14 func expunge(input OmegaInput) (output OmegaOutput) { - if result := chargeGasAndCheck(&input); result != nil { + if result := chargeGasAndCheck(&input, HostGasExpunge); result != nil { // M_X return *result } diff --git a/PVM/instructions.go b/PVM/instructions.go index cda900b7..0db7491b 100644 --- a/PVM/instructions.go +++ b/PVM/instructions.go @@ -18,6 +18,7 @@ var zeta = map[opcode]string{ // Ins w/o Arg 0: "trap", 1: "fallthrough", + 2: "unlikely", // Ins w/ Arg of One Imm 10: "ecalli", // Ins w/ Arg of One Reg and One Extended Width Imm @@ -62,17 +63,16 @@ var zeta = map[opcode]string{ 90: "branch_gt_s_imm", // Ins w/ Arg of Two Reg 100: "move_reg", - 101: "sbrk", - 102: "count_set_bits_64", - 103: "count_set_bits_32", - 104: "leading_zero_bits_64", - 105: "leading_zero_bits_32", - 106: "trailing_zero_bits_64", - 107: "trailing_zero_bits_32", - 108: "sign_extend_8", - 109: "sign_extend_16", - 110: "zero_extend_16", - 111: "reverse_bytes", + 101: "count_set_bits_64", + 102: "count_set_bits_32", + 103: "leading_zero_bits_64", + 104: "leading_zero_bits_32", + 105: "trailing_zero_bits_64", + 106: "trailing_zero_bits_32", + 107: "sign_extend_8", + 108: "sign_extend_16", + 109: "zero_extend_16", + 110: "reverse_bytes", // Ins w/ Arg of Two Reg & One Imm 120: "store_ind_u8", 121: "store_ind_u16", @@ -186,9 +186,10 @@ func abs(x int64) int64 { // input: interpreter, programCounter, skipLength var execInstructions = [231]func(*Interpreter, ProgramCounter, ProgramCounter) (ExitReason, ProgramCounter){ - // A.5.1 Instructiopns without Arguments + // A.5.1 Instructions without Arguments 0: instTrap, 1: instFallthrough, + 2: instUnlikely, // A.5.2 Instructions with Arguments of One Immediate 10: instEcalli, // A.5.3 Instructions with Arguments of One Register & One Extended With Immediate @@ -219,8 +220,8 @@ var execInstructions = [231]func(*Interpreter, ProgramCounter, ProgramCounter) ( 71: instStoreImmIndU16, 72: instStoreImmIndU32, 73: instStoreImmIndU64, - // A.5.8 Instructions without Arguments of One Register, One Immediate and One Offset - 80: instImmediateBranch, + // A.5.8 One register, one immediate and one offset + 80: instLoadImmJump, 81: instImmediateBranch, 82: instImmediateBranch, 83: instImmediateBranch, @@ -231,19 +232,18 @@ var execInstructions = [231]func(*Interpreter, ProgramCounter, ProgramCounter) ( 88: instImmediateBranch, 89: instImmediateBranch, 90: instImmediateBranch, - // A.5.9 Instructions with arguments of Two Registers - 100: instMoveReg, // passed testvector - 101: instSbrk, - 102: instCountSetBits64, - 103: instCountSetBits32, - 104: instLeadingZeroBits64, - 105: instLeadingZeroBits32, - 106: instTrailZeroBits64, - 107: instTrailZeroBits32, - 108: instSignExtend8, - 109: instSignExtend16, - 110: instZeroExtend16, - 111: instReverseBytes, + // A.5.9 Two Registers + 100: instMoveReg, + 101: instCountSetBits64, + 102: instCountSetBits32, + 103: instLeadingZeroBits64, + 104: instLeadingZeroBits32, + 105: instTrailZeroBits64, + 106: instTrailZeroBits32, + 107: instSignExtend8, + 108: instSignExtend16, + 109: instZeroExtend16, + 110: instReverseBytes, 120: instStoreIndU8, 121: instStoreIndU16, 122: instStoreIndU32, @@ -344,8 +344,13 @@ func instTrap(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) return ExitPanic, pc } -// opcode 1 +// opcode 1: sjump(ι + 1 + skip(ι)) func instFallthrough(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { + return sjump(pc, pc+1+skipLength, interp.Program.Bitmasks) +} + +// opcode 2: unlikely — hint only, no mutation +func instUnlikely(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { return ExitContinue, pc } @@ -434,21 +439,14 @@ func instStoreImmU64(interp *Interpreter, pc ProgramCounter, skipLength ProgramC return exitReason, pc } -// opcode 40 +// opcode 40: sjump(imm_X) func instJump(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { vX, err := decodeOneOffset(interp.Program.InstructionData, pc, skipLength) if err != nil { pvmLogger.Errorf("instJump decodeOneOffset error: %v", err) return ExitPanic, pc } - - reason, newPC := branch(pc, vX, true, interp.Program.Bitmasks, interp.Program.InstructionData) - - if reason != ExitContinue { - return reason, pc - } - - return reason, newPC + return sjump(pc, vX, interp.Program.Bitmasks) } // opcode 50 @@ -738,50 +736,51 @@ func instStoreImmIndU64(interp *Interpreter, pc ProgramCounter, skipLength Progr return exitReason, pc } -// opcode in [80, 90] -func instImmediateBranch(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { +// opcode 80: sjump(imm_Y), reg'_A = imm_X +func instLoadImmJump(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rA, vX, vY, err := decodeOneRegisterOneImmediateAndOneOffset(interp.Program.InstructionData, pc, skipLength) if err != nil { - pvmLogger.Errorf("instImmediateBranch decodeOneRegisterOneImmediateAndOneOffset error: %v", err) - return ExitHalt, pc + pvmLogger.Errorf("instLoadImmJump decode error: %v", err) + return ExitPanic, pc } - branchCondition := false + interp.Registers[rA] = vX + return sjump(pc, vY, interp.Program.Bitmasks) +} +// opcode in [81, 90]: branch(imm_Y, cond) — dual-target validation +func instImmediateBranch(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { + rA, vX, vY, err := decodeOneRegisterOneImmediateAndOneOffset(interp.Program.InstructionData, pc, skipLength) + if err != nil { + pvmLogger.Errorf("instImmediateBranch decode error: %v", err) + return ExitPanic, pc + } + var cond bool switch interp.Program.InstructionData[pc] { - case 80: - interp.Registers[rA] = vX - branchCondition = true case 81: - branchCondition = interp.Registers[rA] == vX + cond = interp.Registers[rA] == vX case 82: - branchCondition = interp.Registers[rA] != vX + cond = interp.Registers[rA] != vX case 83: - branchCondition = interp.Registers[rA] < vX + cond = interp.Registers[rA] < vX case 84: - branchCondition = interp.Registers[rA] <= vX + cond = interp.Registers[rA] <= vX case 85: - branchCondition = interp.Registers[rA] >= vX + cond = interp.Registers[rA] >= vX case 86: - branchCondition = interp.Registers[rA] > vX + cond = interp.Registers[rA] > vX case 87: - branchCondition = int64(interp.Registers[rA]) < int64(vX) + cond = int64(interp.Registers[rA]) < int64(vX) case 88: - branchCondition = int64(interp.Registers[rA]) <= int64(vX) + cond = int64(interp.Registers[rA]) <= int64(vX) case 89: - branchCondition = int64(interp.Registers[rA]) >= int64(vX) + cond = int64(interp.Registers[rA]) >= int64(vX) case 90: - branchCondition = int64(interp.Registers[rA]) > int64(vX) + cond = int64(interp.Registers[rA]) > int64(vX) default: - pvmLogger.Errorf("instImmediateBranch: unexpected opcode %d, expected [80, 90]", interp.Program.InstructionData[pc]) + pvmLogger.Errorf("instImmediateBranch: unexpected opcode %d", interp.Program.InstructionData[pc]) return ExitPanic, pc } - - reason, newPC := branch(pc, vY, branchCondition, interp.Program.Bitmasks, interp.Program.InstructionData) - if reason != ExitContinue { - return reason, pc - } - - return reason, newPC + return branch(pc, vY, cond, pc+1+skipLength, interp.Program.Bitmasks) } // opcode 100 @@ -797,39 +796,10 @@ func instMoveReg(interp *Interpreter, pc ProgramCounter, skipLength ProgramCount return ExitContinue, pc } -// opcode 101 -func instSbrk(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { - rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) - if err != nil { - pvmLogger.Errorf("instSbrk decodeTwoRegisters error: %v", err) - return ExitHalt, pc - } +// GP 0.8.0: sbrk (old opcode 101) removed; heap growth via grow_heap host call (B.5). +// Opcodes 102–111 shifted to 101–110. - // this reivision is according to jam-test-vector traces: Note on SBRK - if interp.Registers[rA] == 0 { - interp.Registers[rD] = interp.Memory.heapPointer - return ExitContinue, pc - } - - mem := interp.Memory - newHeapPointer := mem.heapPointer + interp.Registers[rA] - if newHeapPointer < mem.heapPointer || newHeapPointer > mem.heapLimit { - interp.Registers[rD] = 0 - return ExitContinue, pc - } - - nextPageBoundary := P(int(mem.heapPointer)) - if newHeapPointer > uint64(nextPageBoundary) { - finalBoundary := P(int(newHeapPointer)) - allocateMemorySegment(mem, uint32(mem.heapPointer), uint32(finalBoundary), nil, MemoryReadWrite) - } - - mem.heapPointer = newHeapPointer - interp.Registers[rD] = newHeapPointer - return ExitContinue, pc -} - -// opcode 102 +// opcode 101 func instCountSetBits64(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -841,7 +811,7 @@ func instCountSetBits64(interp *Interpreter, pc ProgramCounter, skipLength Progr return ExitContinue, pc } -// opcode 103 +// opcode 102 func instCountSetBits32(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -853,7 +823,7 @@ func instCountSetBits32(interp *Interpreter, pc ProgramCounter, skipLength Progr return ExitContinue, pc } -// opcode 104 +// opcode 103 func instLeadingZeroBits64(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -865,7 +835,7 @@ func instLeadingZeroBits64(interp *Interpreter, pc ProgramCounter, skipLength Pr return ExitContinue, pc } -// opcode 105 +// opcode 104 func instLeadingZeroBits32(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -877,7 +847,7 @@ func instLeadingZeroBits32(interp *Interpreter, pc ProgramCounter, skipLength Pr return ExitContinue, pc } -// opcode 106 +// opcode 105 func instTrailZeroBits64(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -889,7 +859,7 @@ func instTrailZeroBits64(interp *Interpreter, pc ProgramCounter, skipLength Prog return ExitContinue, pc } -// opcode 107 +// opcode 106 func instTrailZeroBits32(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -901,7 +871,7 @@ func instTrailZeroBits32(interp *Interpreter, pc ProgramCounter, skipLength Prog return ExitContinue, pc } -// opcode 108 +// opcode 107 func instSignExtend8(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -918,7 +888,7 @@ func instSignExtend8(interp *Interpreter, pc ProgramCounter, skipLength ProgramC return ExitContinue, pc } -// opcode 109 +// opcode 108 func instSignExtend16(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -935,7 +905,7 @@ func instSignExtend16(interp *Interpreter, pc ProgramCounter, skipLength Program return ExitContinue, pc } -// opcode 110 +// opcode 109 func instZeroExtend16(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -949,7 +919,7 @@ func instZeroExtend16(interp *Interpreter, pc ProgramCounter, skipLength Program return ExitContinue, pc } -// opcode 111 +// opcode 110 func instReverseBytes(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rD, rA, err := decodeTwoRegisters(interp.Program.InstructionData, pc) if err != nil { @@ -1634,38 +1604,31 @@ func instRotR32ImmAlt(interp *Interpreter, pc ProgramCounter, skipLength Program return ExitContinue, pc } -// opcode in [170, 175] +// opcode in [170, 175]: branch(imm_X, cond) — dual-target validation func instBranch(interp *Interpreter, pc ProgramCounter, skipLength ProgramCounter) (ExitReason, ProgramCounter) { rA, rB, vX, err := decodeTwoRegistersAndOneOffset(interp.Program.InstructionData, pc, skipLength) if err != nil { - return ExitHalt, pc + return ExitPanic, pc } - var branchCondition bool + var cond bool switch interp.Program.InstructionData[pc] { case 170: - branchCondition = interp.Registers[rA] == interp.Registers[rB] + cond = interp.Registers[rA] == interp.Registers[rB] case 171: - branchCondition = interp.Registers[rA] != interp.Registers[rB] + cond = interp.Registers[rA] != interp.Registers[rB] case 172: - branchCondition = interp.Registers[rA] < interp.Registers[rB] + cond = interp.Registers[rA] < interp.Registers[rB] case 173: - branchCondition = int64(interp.Registers[rA]) < int64(interp.Registers[rB]) + cond = int64(interp.Registers[rA]) < int64(interp.Registers[rB]) case 174: - branchCondition = interp.Registers[rA] >= interp.Registers[rB] + cond = interp.Registers[rA] >= interp.Registers[rB] case 175: - branchCondition = int64(interp.Registers[rA]) >= int64(interp.Registers[rB]) + cond = int64(interp.Registers[rA]) >= int64(interp.Registers[rB]) default: - pvmLogger.Errorf("instBranch: unexpected opcode %d, expected [170, 175]", interp.Program.InstructionData[pc]) + pvmLogger.Errorf("instBranch: unexpected opcode %d", interp.Program.InstructionData[pc]) return ExitPanic, pc } - - reason, newPC := branch(pc, vX, branchCondition, interp.Program.Bitmasks, interp.Program.InstructionData) - if reason != ExitContinue { - pvmLogger.Errorf("instBranch branch error at pc: %d, opcode: %s", pc, zeta[opcode(interp.Program.InstructionData[pc])]) - return ExitReason(reason), pc - } - - return reason, newPC + return branch(pc, vX, cond, pc+1+skipLength, interp.Program.Bitmasks) } // opcode 180 diff --git a/PVM/instructions_instrmeta.go b/PVM/instructions_instrmeta.go index caa88caf..163aee05 100644 --- a/PVM/instructions_instrmeta.go +++ b/PVM/instructions_instrmeta.go @@ -10,6 +10,8 @@ func instrMetaExecForOpcode(op byte) instrMetaFn { return instTrapMeta case 1: return instFallthroughMeta + case 2: + return instUnlikelyMeta case 10: return instEcalliMeta case 20: @@ -58,31 +60,31 @@ func instrMetaExecForOpcode(op byte) instrMetaFn { return instStoreImmIndU32Meta case 73: return instStoreImmIndU64Meta - case 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90: + case 80: + return instLoadImmJumpMeta + case 81, 82, 83, 84, 85, 86, 87, 88, 89, 90: return instImmediateBranchMeta case 100: return instMoveRegMeta case 101: - return instSbrkMeta - case 102: return instCountSetBits64Meta - case 103: + case 102: return instCountSetBits32Meta - case 104: + case 103: return instLeadingZeroBits64Meta - case 105: + case 104: return instLeadingZeroBits32Meta - case 106: + case 105: return instTrailZeroBits64Meta - case 107: + case 106: return instTrailZeroBits32Meta - case 108: + case 107: return instSignExtend8Meta - case 109: + case 108: return instSignExtend16Meta - case 110: + case 109: return instZeroExtend16Meta - case 111: + case 110: return instReverseBytesMeta case 120: return instStoreIndU8Meta @@ -264,8 +266,13 @@ func instTrapMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCou return ExitPanic, instr.PC } -// opcode 1 +// opcode 1: sjump(ι + 1 + skip(ι)) func instFallthroughMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { + return sjump(instr.PC, instr.PC+ProgramCounter(instr.SkipLen)+1, interp.Program.Bitmasks) +} + +// opcode 2: unlikely — hint only, no mutation +func instUnlikelyMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { return ExitContinue, instr.PC } @@ -309,14 +316,9 @@ func instStoreImmU64Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, Pro return exitReason, instr.PC } -// opcode 40 +// opcode 40: sjump(imm_X) func instJumpMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { - vX := ProgramCounter(instr.Imm[0]) - reason, newPC := branch(instr.PC, vX, true, interp.Program.Bitmasks, interp.Program.InstructionData) - if reason != ExitContinue { - return reason, instr.PC - } - return reason, newPC + return sjump(instr.PC, ProgramCounter(instr.Imm[0]), interp.Program.Bitmasks) } // opcode 50 @@ -489,48 +491,46 @@ func instStoreImmIndU64Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, return exitReason, instr.PC } -// opcode in [80, 90] +// opcode 80: sjump(imm_Y), reg'_A = imm_X +func instLoadImmJumpMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { + interp.Registers[instr.Src[0]] = instr.Imm[0] + return sjump(instr.PC, ProgramCounter(instr.Imm[1]), interp.Program.Bitmasks) +} + +// opcode in [81, 90] | GP 0.8.0 A.5: branch(imm_Y, cond) — dual-target validation func instImmediateBranchMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rA := instr.Src[0] vX := instr.Imm[0] vY := ProgramCounter(instr.Imm[1]) - branchCondition := false + ft := instr.PC + ProgramCounter(instr.SkipLen) + 1 + var cond bool switch instr.Opcode { - case 80: - interp.Registers[rA] = vX - branchCondition = true case 81: - branchCondition = interp.Registers[rA] == vX + cond = interp.Registers[rA] == vX case 82: - branchCondition = interp.Registers[rA] != vX + cond = interp.Registers[rA] != vX case 83: - branchCondition = interp.Registers[rA] < vX + cond = interp.Registers[rA] < vX case 84: - branchCondition = interp.Registers[rA] <= vX + cond = interp.Registers[rA] <= vX case 85: - branchCondition = interp.Registers[rA] >= vX + cond = interp.Registers[rA] >= vX case 86: - branchCondition = interp.Registers[rA] > vX + cond = interp.Registers[rA] > vX case 87: - branchCondition = int64(interp.Registers[rA]) < int64(vX) + cond = int64(interp.Registers[rA]) < int64(vX) case 88: - branchCondition = int64(interp.Registers[rA]) <= int64(vX) + cond = int64(interp.Registers[rA]) <= int64(vX) case 89: - branchCondition = int64(interp.Registers[rA]) >= int64(vX) + cond = int64(interp.Registers[rA]) >= int64(vX) case 90: - branchCondition = int64(interp.Registers[rA]) > int64(vX) + cond = int64(interp.Registers[rA]) > int64(vX) default: - pvmLogger.Errorf("instImmediateBranchMeta: unexpected opcode %d, expected [80, 90]", instr.Opcode) + pvmLogger.Errorf("instImmediateBranchMeta: unexpected opcode %d", instr.Opcode) return ExitPanic, instr.PC } - - reason, newPC := branch(instr.PC, vY, branchCondition, interp.Program.Bitmasks, interp.Program.InstructionData) - if reason != ExitContinue { - return reason, instr.PC - } - - return reason, newPC + return branch(instr.PC, vY, cond, ft, interp.Program.Bitmasks) } // opcode 100 @@ -542,76 +542,48 @@ func instMoveRegMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, Program } // opcode 101 -func instSbrkMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { - rD, rA := instr.Dst, instr.Src[0] - - // this reivision is according to jam-test-vector traces: Note on SBRK - if interp.Registers[rA] == 0 { - interp.Registers[rD] = interp.Memory.heapPointer - return ExitContinue, instr.PC - } - - mem := interp.Memory - newHeapPointer := mem.heapPointer + interp.Registers[rA] - if newHeapPointer < mem.heapPointer || newHeapPointer > mem.heapLimit { - interp.Registers[rD] = 0 - return ExitContinue, instr.PC - } - - nextPageBoundary := P(int(mem.heapPointer)) - if newHeapPointer > uint64(nextPageBoundary) { - finalBoundary := P(int(newHeapPointer)) - allocateMemorySegment(mem, uint32(mem.heapPointer), uint32(finalBoundary), nil, MemoryReadWrite) - } - - mem.heapPointer = newHeapPointer - interp.Registers[rD] = newHeapPointer - return ExitContinue, instr.PC -} - -// opcode 102 func instCountSetBits64Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.OnesCount64(interp.Registers[rA])) return ExitContinue, instr.PC } -// opcode 103 +// opcode 102 func instCountSetBits32Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.OnesCount32(uint32(interp.Registers[rA]))) return ExitContinue, instr.PC } -// opcode 104 +// opcode 103 func instLeadingZeroBits64Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.LeadingZeros64(interp.Registers[rA])) return ExitContinue, instr.PC } -// opcode 105 +// opcode 104 func instLeadingZeroBits32Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.LeadingZeros32(uint32(interp.Registers[rA]))) return ExitContinue, instr.PC } -// opcode 106 +// opcode 105 func instTrailZeroBits64Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.TrailingZeros64(interp.Registers[rA])) return ExitContinue, instr.PC } -// opcode 107 +// opcode 106 func instTrailZeroBits32Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = uint64(bits.TrailingZeros32(uint32(interp.Registers[rA]))) return ExitContinue, instr.PC } -// opcode 108 +// opcode 107 func instSignExtend8Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] // mutation @@ -623,7 +595,7 @@ func instSignExtend8Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, Pro return ExitContinue, instr.PC } -// opcode 109 +// opcode 108 func instSignExtend16Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] // mutation @@ -635,7 +607,7 @@ func instSignExtend16Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, Pr return ExitContinue, instr.PC } -// opcode 110 +// opcode 109 func instZeroExtend16Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] // mutation @@ -644,7 +616,7 @@ func instZeroExtend16Meta(interp *Interpreter, instr *InstrMeta) (ExitReason, Pr return ExitContinue, instr.PC } -// opcode 111 +// opcode 110 func instReverseBytesMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rD, rA := instr.Dst, instr.Src[0] interp.Registers[rD] = bits.ReverseBytes64(interp.Registers[rA]) @@ -1125,36 +1097,32 @@ func instRotR32ImmAltMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, Pr return ExitContinue, instr.PC } -// opcode in [170, 175] +// opcode in [170, 175]: branch(imm_X, cond) — dual-target validation func instBranchMeta(interp *Interpreter, instr *InstrMeta) (ExitReason, ProgramCounter) { rA := instr.Src[0] rB := instr.Src[1] vX := ProgramCounter(instr.Imm[0]) - branchCondition := false + ft := instr.PC + ProgramCounter(instr.SkipLen) + 1 + + var cond bool switch instr.Opcode { case 170: - branchCondition = interp.Registers[rA] == interp.Registers[rB] + cond = interp.Registers[rA] == interp.Registers[rB] case 171: - branchCondition = interp.Registers[rA] != interp.Registers[rB] + cond = interp.Registers[rA] != interp.Registers[rB] case 172: - branchCondition = interp.Registers[rA] < interp.Registers[rB] + cond = interp.Registers[rA] < interp.Registers[rB] case 173: - branchCondition = int64(interp.Registers[rA]) < int64(interp.Registers[rB]) + cond = int64(interp.Registers[rA]) < int64(interp.Registers[rB]) case 174: - branchCondition = interp.Registers[rA] >= interp.Registers[rB] + cond = interp.Registers[rA] >= interp.Registers[rB] case 175: - branchCondition = int64(interp.Registers[rA]) >= int64(interp.Registers[rB]) + cond = int64(interp.Registers[rA]) >= int64(interp.Registers[rB]) default: - pvmLogger.Errorf("instBranchMeta: unexpected opcode %d, expected [170, 175]", instr.Opcode) + pvmLogger.Errorf("instBranchMeta: unexpected opcode %d", instr.Opcode) return ExitPanic, instr.PC } - - reason, newPC := branch(instr.PC, vX, branchCondition, interp.Program.Bitmasks, interp.Program.InstructionData) - if reason != ExitContinue { - pvmLogger.Errorf("instBranchMeta branch error at pc: %d, opcode: %s", instr.PC, zeta[opcode(instr.Opcode)]) - return ExitReason(reason), instr.PC - } - return reason, newPC + return branch(instr.PC, vX, cond, ft, interp.Program.Bitmasks) } // opcode 180 diff --git a/PVM/interpreter/host.go b/PVM/interpreter/host.go index a506f508..74857129 100644 --- a/PVM/interpreter/host.go +++ b/PVM/interpreter/host.go @@ -42,22 +42,23 @@ func NewHost(program *PVM.Program, registers PVM.Registers, memory *PVM.Memory, } } -// (A.34) Ψ_H +// (A.34) Ψ_H — outer loop: MachineInvoke → omega on HOST_CALL (see docs/4_HostCall_Integration.md §5). func (h *Host) HostCall(pc PVM.ProgramCounter, instrCount uint64) (psi_result PVM.Psi_H_ReturnType) { for { var exitReason PVM.ExitReason var pcPrime PVM.ProgramCounter - exitReason, pcPrime = h.Interpreter.SingleStepInvokeDecodedBlocks(pc) + exitReason, pcPrime = h.MachineInvoke(pc) switch exitReason.GetReasonType() { case PVM.HALT, PVM.PANIC, PVM.OUT_OF_GAS, PVM.PAGE_FAULT: psi_result.ExitReason = exitReason psi_result.Counter = uint32(pcPrime) psi_result.VM = &PVM.VMState{ - Registers: &h.Interpreter.Registers, - Mem: PVM.NewPagedGuestMemory(h.Interpreter.Memory), - Gas: &h.Interpreter.Gas, + Registers: &h.Interpreter.Registers, + Mem: PVM.NewPagedGuestMemory(h.Interpreter.Memory), + Gas: &h.Interpreter.Gas, + GasCharged: h.Interpreter.GasCharged, } psi_result.Addition = h.Addition return @@ -70,9 +71,10 @@ func (h *Host) HostCall(pc PVM.ProgramCounter, instrCount uint64) (psi_result PV var input PVM.OmegaInput input.Operation = PVM.OperationType(exitReason.GetHostCallID()) input.VM = &PVM.VMState{ - Registers: &h.Interpreter.Registers, - Mem: tracedMem, - Gas: &h.Interpreter.Gas, + Registers: &h.Interpreter.Registers, + Mem: tracedMem, + Gas: &h.Interpreter.Gas, + GasCharged: h.Interpreter.GasCharged, } input.Addition = h.Addition input.HostCalls = h.HostCalls @@ -90,6 +92,7 @@ func (h *Host) HostCall(pc PVM.ProgramCounter, instrCount uint64) (psi_result PV ecalliPC := PVM.HostCallInstrPC(h.Interpreter.Program, pcPrime) omegaResult := omega(input) + h.Interpreter.GasCharged = input.VM.GasCharged var rout [13]uint64 var details json.RawMessage @@ -108,17 +111,16 @@ func (h *Host) HostCall(pc PVM.ProgramCounter, instrCount uint64) (psi_result PV switch omegaResult.ExitReason { case PVM.ExitContinue: h.Addition = omegaResult.Addition - // SingleStepInvokeDecodedBlocks already returns the next instruction PC - // (ecalli.PC + skipLen + 1 = fallthrough PC), so no skip needed. pc = pcPrime continue default: psi_result.ExitReason = omegaResult.ExitReason psi_result.Counter = uint32(pcPrime) psi_result.VM = &PVM.VMState{ - Registers: &h.Interpreter.Registers, - Mem: PVM.NewPagedGuestMemory(h.Interpreter.Memory), - Gas: &h.Interpreter.Gas, + Registers: &h.Interpreter.Registers, + Mem: PVM.NewPagedGuestMemory(h.Interpreter.Memory), + Gas: &h.Interpreter.Gas, + GasCharged: h.Interpreter.GasCharged, } psi_result.Addition = omegaResult.Addition return diff --git a/PVM/interpreter/invoke_mode.go b/PVM/interpreter/invoke_mode.go new file mode 100644 index 00000000..caebd849 --- /dev/null +++ b/PVM/interpreter/invoke_mode.go @@ -0,0 +1,11 @@ +//go:build !pvmtrace + +package interpreter + +import PVM "github.com/New-JAMneration/JAM-Protocol/PVM" + +// MachineInvoke: run until non-CONTINUE exit. Forwards to +// BlockBasedInvokeDecodedBlocks (pre-decoded Go path). Trace in invoke_mode_trace.go. +func (h *Host) MachineInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter) { + return h.Interpreter.BlockBasedInvokeDecodedBlocks(pc) +} diff --git a/PVM/interpreter/invoke_mode_trace.go b/PVM/interpreter/invoke_mode_trace.go new file mode 100644 index 00000000..c697cd95 --- /dev/null +++ b/PVM/interpreter/invoke_mode_trace.go @@ -0,0 +1,13 @@ +//go:build pvmtrace + +package interpreter + +import PVM "github.com/New-JAMneration/JAM-Protocol/PVM" + +// MachineInvoke: trace → DebugSingleStepInvoke; else BlockBasedInvokeDecodedBlocks. +func (h *Host) MachineInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter) { + if h.Interpreter.Trace != nil { + return h.Interpreter.DebugSingleStepInvoke(pc) + } + return h.Interpreter.BlockBasedInvokeDecodedBlocks(pc) +} diff --git a/PVM/interpreter/psi_m.go b/PVM/interpreter/psi_m.go index c84e604f..7907cdaa 100644 --- a/PVM/interpreter/psi_m.go +++ b/PVM/interpreter/psi_m.go @@ -33,7 +33,7 @@ func psiMInterpreter( // Cross-invocation cache: reuse the deblob'd *Program by CodeHash (shared with // the recompiler backend; keeps the comparison fair). Read-only, safe to share. - program, exitReason := PVM.GetOrDeblobProgram(addition.CodeHash, programCode) + program, exitReason := PVM.GetOrDeblobProgram(addition.CodeHash, programCode, uint64(counter)) if exitReason != PVM.ExitContinue { return PVM.Psi_M_ReturnType{ Gas: 0, diff --git a/PVM/interpreter_notrace.go b/PVM/interpreter_notrace.go index 416d9d90..7ec19dba 100644 --- a/PVM/interpreter_notrace.go +++ b/PVM/interpreter_notrace.go @@ -9,4 +9,5 @@ type Interpreter struct { Memory *Memory Gas Gas InstrCount uint64 + GasCharged bool // formula A.7 (0.8.0): true when current block's gas is pre-charged } diff --git a/PVM/interpreter_trace.go b/PVM/interpreter_trace.go index a7faefc6..f275701a 100644 --- a/PVM/interpreter_trace.go +++ b/PVM/interpreter_trace.go @@ -12,6 +12,7 @@ type Interpreter struct { Memory *Memory Gas Gas InstrCount uint64 + GasCharged bool // formula A.7 (0.8.0): true when current block's gas is pre-charged LastLoad struct { Addr uint32 Val uint64 diff --git a/PVM/invocation.go b/PVM/invocation.go index 7352ed31..1b9858f9 100644 --- a/PVM/invocation.go +++ b/PVM/invocation.go @@ -1,7 +1,7 @@ package PVM -// Currently keep for refine host-call -> invoke host-call transition -// per-instruction based of (A.1) ψ_1, +// SingleStepInvoke is kept for GP 0.7.2; deprecated for v0.8.0 and later. +// Used by refine host-call → invoke host-call transition (per-instruction gas). func (interp *Interpreter) SingleStepInvoke(pc ProgramCounter) (ExitReason, ProgramCounter) { for { exitReason, pcPrime := interp.SingleStepStateTransition(pc) @@ -17,7 +17,8 @@ func (interp *Interpreter) SingleStepInvoke(pc ProgramCounter) (ExitReason, Prog } } -// (v.0.7.1 A.6, A.7) SingleStepStateTransition +// SingleStepStateTransition is kept for GP 0.7.2; deprecated for v0.8.0 and later. +// Per-instruction gas charging (1 gas per instruction). func (interp *Interpreter) SingleStepStateTransition(pc ProgramCounter) (ExitReason, ProgramCounter) { // check program-counter exceed blob length if int(pc) >= len(interp.Program.InstructionData) { @@ -60,13 +61,11 @@ func (interp *Interpreter) SingleStepStateTransition(pc ProgramCounter) (ExitRea // iota' = iota + 1 +skip(iota) newPC += skipLength + 1 - // detailed instruction print - // logger.Debugf("instr:%s(%d) pc=%d operand=%v gas=%d registers=%x", zeta[opcode(opcodeData)], opcodeData, programCounter, instructionCode[programCounter:programCounter+skipLength+1], interp.Gas, interp.Registers) - // logger.Debugf(" gas : %d -> %d", interp.Gas+gasDelta, interp.Gas) - return exitReason, newPC } +// SingleStepInvokeDecodedBlocks is kept for GP 0.7.2; deprecated for v0.8.0 and later. +// Pre-decoded block execution with per-instruction gas charging. func (interp *Interpreter) SingleStepInvokeDecodedBlocks(pc ProgramCounter) (ExitReason, ProgramCounter) { prog := interp.Program instrSlice := prog.Instrs @@ -78,7 +77,6 @@ func (interp *Interpreter) SingleStepInvokeDecodedBlocks(pc ProgramCounter) (Exi } var startIdx, endIdx int - if block := prog.BlockAt[pc]; block != nil { startIdx = block.InstrStart endIdx = block.InstrEnd @@ -143,27 +141,232 @@ func (interp *Interpreter) SingleStepInvokeDecodedBlocks(pc ProgramCounter) (Exi } } -// block based version of (A.1) ψ_1 +// BlockBasedInvoke: runtime DecodeInstructionBlock + A.9 block gas via pre-decoded +// BlockMeta (tests / legacy). Requires preDecodeBlocks on prog. +// Production: outer Ψ_M → BlockBasedInvokeDecodedBlocks; refine Ω_K invoke → same. func (interp *Interpreter) BlockBasedInvoke(pc ProgramCounter) (ExitReason, ProgramCounter) { - // decode instructions in a block - pcPrime, _, exitReason := DecodeInstructionBlock(interp.Program.InstructionData, pc, interp.Program.Bitmasks) - if exitReason.GetReasonType() != CONTINUE { - pvmLogger.Errorf("DecodeInstructionBlock error : %v", exitReason) - return exitReason, 0 + prog := interp.Program + if prog == nil { + return ExitPanic, 0 } - // execute instructions in the block - pc, exitReason = interp.ExecuteInstructions(pc, pcPrime) - reason := exitReason.GetReasonType() - switch reason { - case PANIC, HALT: - return exitReason, 0 - case HOST_CALL, OUT_OF_GAS: - return exitReason, pc + for { + blockStart, ok := containingBlockStart(pc, prog.Bitmasks) + if !ok { + return ExitPanic, 0 + } + + pcPrime, _, exitReason := DecodeInstructionBlock(prog.InstructionData, blockStart, prog.Bitmasks) + if exitReason.GetReasonType() != CONTINUE { + pvmLogger.Errorf("DecodeInstructionBlock error : %v", exitReason) + return exitReason, 0 + } + + block := prog.BlockContaining(pc) + if block == nil { + return ExitPanic, 0 + } + + if !interp.GasCharged { + cost := blockGasAtPC(prog, pc, block) + if interp.Gas < cost { + return ExitOOG, pc + } + interp.Gas -= cost + interp.GasCharged = true + } + + pc, exitReason = interp.ExecuteInstructions(pc, pcPrime) + switch exitReason.GetReasonType() { + case PANIC, HALT: + return exitReason, 0 + case HOST_CALL, OUT_OF_GAS, PAGE_FAULT: + return exitReason, pc + case CONTINUE: + // fallthrough to next block + default: + return ExitPanic, 0 + } + } +} + +// containingBlockStart is 𝔏(pc) via the bitmask: nearest basic-block start +// at or before pc. pc itself must be an instruction start. +func containingBlockStart(pc ProgramCounter, bitmask Bitmask) (ProgramCounter, bool) { + if !bitmask.IsStartOfInstruction(int(pc)) { + return 0, false + } + for b := pc; ; b-- { + if bitmask.IsStartOfBasicBlock(b) { + return b, true + } + if b == 0 { + return 0, false + } + } +} + +// gasChargedForIntegratedResume restores integrated gaschargedflag for inner Ψ. +// A.4 keeps ⊤ across fault/halt/panic; only CONTINUE/HOST_CALL terminators clear it. +// A stored ⊤ at a block-entry PC is therefore valid (e.g. fault on the first instr). +func gasChargedForIntegratedResume(prog *Program, pc ProgramCounter, stored bool) bool { + return stored && prog != nil && prog.ValidInstructionAt(uint64(pc)) +} + +// blockGasAtPC returns A.9 block gas for entering at pc within block. +// Uses cached block.GasCost at block entry; suffix GasCostFromPC mid-block. +func blockGasAtPC(prog *Program, pc ProgramCounter, block *BlockMeta) Gas { + if pc == block.StartPC { + return block.GasCost + } + return GasCostFromPC(prog, pc) +} + +// BlockBasedInvokeDecodedBlocks: pre-decoded blocks + A.7 gas. Production path for +// outer Ψ_M (MachineInvoke) and refine Ω_K invoke (host_call_refine). +func (interp *Interpreter) BlockBasedInvokeDecodedBlocks(pc ProgramCounter) (ExitReason, ProgramCounter) { + prog := interp.Program + if prog == nil { + return ExitPanic, 0 + } + + for { + if int(pc) >= len(prog.InstrIdxAt) { + return ExitPanic, 0 + } + + instrIdx := prog.InstrIdxAt[pc] + block := prog.BlockContaining(pc) + if instrIdx < 0 || block == nil { + return ExitPanic, 0 + } + + startIdx := int(instrIdx) + if startIdx < block.InstrStart || startIdx >= block.InstrEnd { + return ExitPanic, 0 + } + + if !interp.GasCharged { + blockGas := blockGasAtPC(prog, pc, block) + if interp.Gas < blockGas { + return ExitOOG, pc + } + interp.Gas -= blockGas + interp.GasCharged = true + } + + instrs := prog.Instrs[startIdx:block.InstrEnd] + branchTaken := false + for i := range instrs { + instr := &instrs[i] + + var src1Val, src2Val uint64 + if instr.Src[0] != 0xff { + src1Val = interp.Registers[instr.Src[0]] + } + if instr.Src[1] != 0xff { + src2Val = interp.Registers[instr.Src[1]] + } + + exitReason, newPC := instr.Exec(interp, instr) + interp.recordInstrTraceStepAfterMeta(instr, src1Val, src2Val) + + reason := exitReason.GetReasonType() + if IsBlockTerminator(instr.Opcode) && (reason == CONTINUE || reason == HOST_CALL) { + interp.GasCharged = false + } + + switch reason { + case PANIC, HALT: + return exitReason, 0 + case PAGE_FAULT, OUT_OF_GAS: + return exitReason, instr.PC + case HOST_CALL: + return exitReason, instr.PC + ProgramCounter(instr.SkipLen) + 1 + } + + // Terminator defines control flow even when newPC == instr.PC (self-loop). + if IsBlockTerminator(instr.Opcode) { + pc = newPC + branchTaken = true + break + } + } + + if !branchTaken { + last := &instrs[len(instrs)-1] + pc = last.PC + ProgramCounter(last.SkipLen) + 1 + } + } +} + +// DebugSingleStepInvoke runs one pre-decoded instruction per iteration with +// block-level gas pre-charge (A.7). Used by pvmtrace to emit per-instruction +// streams aligned with recompiler DebugSingleStepInvoke. +func (interp *Interpreter) DebugSingleStepInvoke(pc ProgramCounter) (ExitReason, ProgramCounter) { + prog := interp.Program + if prog == nil { + return ExitPanic, 0 } - // reason == CONTINUE - return interp.BlockBasedInvoke(pc) + for { + if int(pc) >= len(prog.InstrIdxAt) { + return ExitPanic, 0 + } + + instrIdx := prog.InstrIdxAt[pc] + block := prog.BlockContaining(pc) + if instrIdx < 0 || block == nil { + return ExitPanic, 0 + } + + startIdx := int(instrIdx) + if startIdx < block.InstrStart || startIdx >= block.InstrEnd { + return ExitPanic, 0 + } + + if !interp.GasCharged { + blockGas := blockGasAtPC(prog, pc, block) + if interp.Gas < blockGas { + return ExitOOG, pc + } + interp.Gas -= blockGas + interp.GasCharged = true + } + + instr := &prog.Instrs[startIdx] + + var src1Val, src2Val uint64 + if instr.Src[0] != 0xff { + src1Val = interp.Registers[instr.Src[0]] + } + if instr.Src[1] != 0xff { + src2Val = interp.Registers[instr.Src[1]] + } + + exitReason, newPC := instr.Exec(interp, instr) + interp.recordInstrTraceStepAfterMeta(instr, src1Val, src2Val) + + reason := exitReason.GetReasonType() + if IsBlockTerminator(instr.Opcode) && (reason == CONTINUE || reason == HOST_CALL) { + interp.GasCharged = false + } + + switch reason { + case PANIC, HALT: + return exitReason, 0 + case PAGE_FAULT, OUT_OF_GAS: + return exitReason, instr.PC + case HOST_CALL: + return exitReason, instr.PC + ProgramCounter(instr.SkipLen) + 1 + } + + if IsBlockTerminator(instr.Opcode) { + pc = newPC + continue + } + pc = instr.PC + ProgramCounter(instr.SkipLen) + 1 + } } func DecodeInstructionBlock(instructionData ProgramCode, pc ProgramCounter, bitmask Bitmask) (ProgramCounter, int64, ExitReason) { @@ -177,9 +380,7 @@ func DecodeInstructionBlock(instructionData ProgramCode, pc ProgramCounter, bitm return pc, 0, ExitPanic } - // check opcode is valid after computing with skip - if !instructionData.isOpcodeValid(pcPrime) { - // pvmLogger.Debugf("PVM panic: decode program failed: opcode invalid") + if !IsValidOpcode(instructionData[pcPrime]) { return pc, 0, ExitPanic } @@ -193,28 +394,33 @@ func DecodeInstructionBlock(instructionData ProgramCode, pc ProgramCounter, bitm } } -// execute each instruction in block[pc:pcPrime] , pcPrime is computed by DecodeInstructionBlock -func (interp *Interpreter) ExecuteInstructions(pc ProgramCounter, pcPrime ProgramCounter) (ProgramCounter, ExitReason) { // no need to worry about gas, opcode valid here, it's checked in HostCall and DecodeInstructionBlock respectively +// ExecuteInstructions executes block[pc:pcPrime]. Gas is pre-charged at block entry. +func (interp *Interpreter) ExecuteInstructions(pc ProgramCounter, pcPrime ProgramCounter) (ProgramCounter, ExitReason) { for pc <= pcPrime { - if interp.Gas < 1 { - return pc, ExitOOG - } opcodeData := interp.Program.InstructionData[pc] skipLength := ProgramCounter(skip(int(pc), interp.Program.Bitmasks)) - exitReason, newPC := execInstructions[opcodeData](interp, pc, skipLength) - interp.Gas -= 1 - // logger.Debug("gasPrime: ", interp.Gas) + target := execInstructions[opcodeData] + if target == nil { + return pc, ExitPanic + } + exitReason, newPC := target(interp, pc, skipLength) + reason := exitReason.GetReasonType() + if IsBlockTerminator(opcodeData) && (reason == CONTINUE || reason == HOST_CALL) { + interp.GasCharged = false + } + switch reason { case PANIC, HALT: return 0, exitReason + case PAGE_FAULT, OUT_OF_GAS: + return pc, exitReason case HOST_CALL: return pc + skipLength + 1, exitReason } - if pc != newPC { - // check branch + if IsBlockTerminator(opcodeData) { return newPC, exitReason } diff --git a/PVM/invocation_test.go b/PVM/invocation_test.go new file mode 100644 index 00000000..cfabfd9b --- /dev/null +++ b/PVM/invocation_test.go @@ -0,0 +1,283 @@ +package PVM + +import "testing" + +func TestBlockBasedInvokeDecodedBlocksChargesContainingBlock(t *testing.T) { + prog := decodedInvocationTestProgram( + t, + ProgramCode{2, 1}, + Bitmask{0x03, 0x01}, + ) + var memory Memory + interp := NewInterpreter(&prog, Registers{}, &memory, 0) + + reason, pc := interp.BlockBasedInvokeDecodedBlocks(1) + if reason != ExitOOG { + t.Fatalf("exit reason = %v, want %v", reason, ExitOOG) + } + if pc != 1 { + t.Fatalf("pc = %d, want 1", pc) + } + if interp.Gas != 0 { + t.Fatalf("gas = %d, want 0", interp.Gas) + } + if interp.GasCharged { + t.Fatal("gas flag set after failed block charge") + } +} + +func TestBlockBasedInvokeDecodedBlocksResumesAfterHostCall(t *testing.T) { + prog := ecalliFallthroughTrapProgram(t) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + + var memory Memory + suffixBlock := prog.BlockContaining(2) + if suffixBlock == nil { + t.Fatal("missing suffix block at PC 2") + } + suffixGas := blockGasAtPC(&prog, 2, suffixBlock) + interp := NewInterpreter(&prog, Registers{}, &memory, block.GasCost+suffixGas) + + reason, pc := interp.BlockBasedInvokeDecodedBlocks(0) + if reason.GetReasonType() != HOST_CALL { + t.Fatalf("first exit reason = %v, want host call", reason) + } + if pc != 2 { + t.Fatalf("first pc = %d, want 2", pc) + } + if interp.Gas != suffixGas { + t.Fatalf("gas after block charge = %d, want %d", interp.Gas, suffixGas) + } + + // Fund exactly the suffix segment; flag was cleared at ecalli. + suffixCost := blockGasAtPC(&prog, 2, suffixBlock) + interp.Gas = suffixCost + interp.GasCharged = false + + reason, pc = interp.BlockBasedInvokeDecodedBlocks(pc) + if reason != ExitPanic { + t.Fatalf("second exit reason = %v, want panic (trap)", reason) + } + if pc != 0 { + t.Fatalf("second pc = %d, want 0 after trap", pc) + } + if interp.Gas != 0 { + t.Fatalf("gas after suffix block = %d, want 0", interp.Gas) + } +} + +func TestBlockBasedInvokeChargesContainingBlock(t *testing.T) { + prog := decodedInvocationTestProgram( + t, + ProgramCode{2, 1}, + Bitmask{0x03, 0x01}, + ) + var memory Memory + interp := NewInterpreter(&prog, Registers{}, &memory, 0) + + reason, pc := interp.BlockBasedInvoke(1) + if reason != ExitOOG { + t.Fatalf("exit reason = %v, want %v", reason, ExitOOG) + } + if pc != 1 { + t.Fatalf("pc = %d, want 1", pc) + } + if interp.Gas != 0 { + t.Fatalf("gas = %d, want 0", interp.Gas) + } + if interp.GasCharged { + t.Fatal("gas flag set after failed block charge") + } +} + +func TestBlockBasedInvokeResumesAfterHostCall(t *testing.T) { + prog := ecalliFallthroughTrapProgram(t) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + + var memory Memory + suffixBlock := prog.BlockContaining(2) + if suffixBlock == nil { + t.Fatal("missing suffix block at PC 2") + } + suffixGas := blockGasAtPC(&prog, 2, suffixBlock) + interp := NewInterpreter(&prog, Registers{}, &memory, block.GasCost+suffixGas) + + reason, pc := interp.BlockBasedInvoke(0) + if reason.GetReasonType() != HOST_CALL { + t.Fatalf("first exit reason = %v, want host call", reason) + } + if pc != 2 { + t.Fatalf("first pc = %d, want 2", pc) + } + if interp.Gas != suffixGas { + t.Fatalf("gas after block charge = %d, want %d", interp.Gas, suffixGas) + } + + suffixCost := blockGasAtPC(&prog, 2, suffixBlock) + interp.Gas = suffixCost + interp.GasCharged = false + + reason, pc = interp.BlockBasedInvoke(pc) + if reason != ExitPanic { + t.Fatalf("second exit reason = %v, want panic (trap)", reason) + } + if pc != 0 { + t.Fatalf("second pc = %d, want 0 after trap", pc) + } + if interp.Gas != 0 { + t.Fatalf("gas after resume = %d, want 0", interp.Gas) + } +} + +func TestBlockBasedInvokeReturnsPageFault(t *testing.T) { + // store_u8 r0, 0x10000; trap — address is above ZZ but unmapped. + prog := decodedInvocationTestProgram( + t, + ProgramCode{59, 0, 0x00, 0x00, 0x01, 0}, + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00, 0x03}, + ) + var memory Memory + interp := NewInterpreter(&prog, Registers{}, &memory, 30) + + reason, pc := interp.BlockBasedInvoke(0) + if reason.GetReasonType() != PAGE_FAULT { + t.Fatalf("exit reason = %v, want page fault", reason) + } + if pc != 0 { + t.Fatalf("pc = %d, want 0 (faulting instruction)", pc) + } + if got := reason.GetPageFaultAddress(); got != 0x10000 { + t.Fatalf("fault address = 0x%x, want 0x10000", got) + } +} + +func TestDebugSingleStepInvokeChargesContainingBlock(t *testing.T) { + prog := decodedInvocationTestProgram( + t, + ProgramCode{2, 1}, + Bitmask{0x03, 0x01}, + ) + var memory Memory + interp := NewInterpreter(&prog, Registers{}, &memory, 0) + + reason, pc := interp.DebugSingleStepInvoke(1) + if reason != ExitOOG { + t.Fatalf("exit reason = %v, want %v", reason, ExitOOG) + } + if pc != 1 { + t.Fatalf("pc = %d, want 1", pc) + } + if interp.Gas != 0 { + t.Fatalf("gas = %d, want 0", interp.Gas) + } + if interp.GasCharged { + t.Fatal("gas flag set after failed block charge") + } +} + +func TestDebugSingleStepInvokeResumesAfterHostCall(t *testing.T) { + prog := ecalliFallthroughTrapProgram(t) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + + var memory Memory + suffixBlock := prog.BlockContaining(2) + if suffixBlock == nil { + t.Fatal("missing suffix block at PC 2") + } + suffixGas := blockGasAtPC(&prog, 2, suffixBlock) + interp := NewInterpreter(&prog, Registers{}, &memory, block.GasCost+suffixGas) + + reason, pc := interp.DebugSingleStepInvoke(0) + if reason.GetReasonType() != HOST_CALL { + t.Fatalf("first exit reason = %v, want host call", reason) + } + if pc != 2 { + t.Fatalf("first pc = %d, want 2", pc) + } + if interp.Gas != suffixGas { + t.Fatalf("gas after block charge = %d, want %d", interp.Gas, suffixGas) + } + + suffixCost := blockGasAtPC(&prog, 2, suffixBlock) + interp.Gas = suffixCost + interp.GasCharged = false + + reason, pc = interp.DebugSingleStepInvoke(pc) + if reason != ExitPanic { + t.Fatalf("second exit reason = %v, want panic (trap)", reason) + } + if pc != 0 { + t.Fatalf("second pc = %d, want 0 after trap", pc) + } +} + +func TestBlockGasAtPCUsesCacheAtBlockEntry(t *testing.T) { + prog := ecalliFallthroughTrapProgram(t) + block := prog.LookupBlock(0) + if block == nil { + t.Fatal("missing block at PC 0") + } + if got := blockGasAtPC(&prog, 0, block); got != block.GasCost { + t.Fatalf("block entry: got %d, want cached %d", got, block.GasCost) + } + suffixBlock := prog.BlockContaining(2) + if suffixBlock == nil { + t.Fatal("missing suffix block") + } + suffix := blockGasAtPC(&prog, 2, suffixBlock) + if suffix >= block.GasCost { + t.Fatalf("suffix at pc=2 = %d, want < full block %d", suffix, block.GasCost) + } +} + +func TestSelfLoopJumpRetakesTerminator(t *testing.T) { + // jump to PC 0 (self); fallthrough unreachable. + prog := decodedGasTestProgram(t, + ProgramCode{40, 0, 0, 0, 0}, // jump imm=0 + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00}, + ) + gas := Gas(10_000) + interp := &Interpreter{Program: &prog, Gas: gas, GasCharged: false} + exit, pc := interp.BlockBasedInvokeDecodedBlocks(0) + // Should OOG eventually while looping, with PC at the jump. + if exit.GetReasonType() != OUT_OF_GAS { + t.Fatalf("exit = %v, want OOG (self-loop must re-enter block)", exit) + } + if pc != 0 { + t.Fatalf("OOG pc = %d, want 0 (self-loop)", pc) + } +} + +func ecalliFallthroughTrapProgram(t *testing.T) Program { + t.Helper() + // ecalli 0; load_imm_64 r0, 42; trap — matches recompiler host-call gas tests. + inst := []byte{10, 0, 51, 0, 42, 0} + blob := buildTestBlob(t, inst, []int{0, 2, 5}) + prog, reason := DeBlobProgramCode(blob, 0) + if reason != ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) + } + return prog +} + +func decodedInvocationTestProgram(t *testing.T, code ProgramCode, bitmask Bitmask) Program { + t.Helper() + + prog := Program{ + InstructionData: code, + Bitmasks: bitmask, + } + if reason := prog.preDecodeBlocks(); reason != ExitContinue { + t.Fatalf("preDecodeBlocks() = %v, want %v", reason, ExitContinue) + } + return prog +} diff --git a/PVM/opcode_info.go b/PVM/opcode_info.go index b2cb7dbc..7617af1b 100644 --- a/PVM/opcode_info.go +++ b/PVM/opcode_info.go @@ -5,7 +5,7 @@ type InstrCategory uint8 const ( InstrCatInvalid InstrCategory = iota // not a valid opcode - InstrCatNoArg // 0, 1 + InstrCatNoArg // 0-2 InstrCatOneImm // 10 InstrCatOneRegExtImm // 20 InstrCatTwoImm // 30-33 @@ -13,7 +13,7 @@ const ( InstrCatOneRegOneImm // 50-62 InstrCatOneRegTwoImm // 70-73 InstrCatOneRegImmOff // 80-90 - InstrCatTwoReg // 100-111 + InstrCatTwoReg // 100-110 InstrCatTwoRegOneImm // 120-161 InstrCatTwoRegOneOff // 170-175 InstrCatTwoRegTwoImm // 180 @@ -27,16 +27,16 @@ type OpcodeInfo struct { IsTerminator bool // ends a basic block IsLoad bool // guest memory read (μ) IsStore bool // guest memory write (μ) - // TODO(gas-model): add OpcodeResource when integrating GP v0.8.0 gas cost model. - // Resource OpcodeResource // cycles, decode slots, exec units (A.10) + // Gas costs (A.10) are resolved in gas_opcode.go / InstructionCost. } // opcodeInfoTable is indexed by the raw opcode byte (0–255). // Invalid opcodes have zero-value entries (Category == InstrCatInvalid). var opcodeInfoTable = [256]OpcodeInfo{ - // A.5.1 No-argument (terminators) + // A.5.1 No-argument 0: {Name: "trap", Category: InstrCatNoArg, IsTerminator: true}, 1: {Name: "fallthrough", Category: InstrCatNoArg, IsTerminator: true}, + 2: {Name: "unlikely", Category: InstrCatNoArg, IsTerminator: false}, // GP 0.8.0: hint only // A.5.2 One immediate 10: {Name: "ecalli", Category: InstrCatOneImm, IsTerminator: false}, @@ -87,19 +87,18 @@ var opcodeInfoTable = [256]OpcodeInfo{ 89: {Name: "branch_ge_s_imm", Category: InstrCatOneRegImmOff, IsTerminator: true}, 90: {Name: "branch_gt_s_imm", Category: InstrCatOneRegImmOff, IsTerminator: true}, - // A.5.9 Two registers + // A.5.9 Two registers | GP 0.8.0: sbrk removed, 102-111→101-110 100: {Name: "move_reg", Category: InstrCatTwoReg, IsTerminator: false}, - 101: {Name: "sbrk", Category: InstrCatTwoReg, IsTerminator: false}, - 102: {Name: "count_set_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, - 103: {Name: "count_set_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, - 104: {Name: "leading_zero_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, - 105: {Name: "leading_zero_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, - 106: {Name: "trailing_zero_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, - 107: {Name: "trailing_zero_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, - 108: {Name: "sign_extend_8", Category: InstrCatTwoReg, IsTerminator: false}, - 109: {Name: "sign_extend_16", Category: InstrCatTwoReg, IsTerminator: false}, - 110: {Name: "zero_extend_16", Category: InstrCatTwoReg, IsTerminator: false}, - 111: {Name: "reverse_bytes", Category: InstrCatTwoReg, IsTerminator: false}, + 101: {Name: "count_set_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, + 102: {Name: "count_set_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, + 103: {Name: "leading_zero_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, + 104: {Name: "leading_zero_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, + 105: {Name: "trailing_zero_bits_64", Category: InstrCatTwoReg, IsTerminator: false}, + 106: {Name: "trailing_zero_bits_32", Category: InstrCatTwoReg, IsTerminator: false}, + 107: {Name: "sign_extend_8", Category: InstrCatTwoReg, IsTerminator: false}, + 108: {Name: "sign_extend_16", Category: InstrCatTwoReg, IsTerminator: false}, + 109: {Name: "zero_extend_16", Category: InstrCatTwoReg, IsTerminator: false}, + 110: {Name: "reverse_bytes", Category: InstrCatTwoReg, IsTerminator: false}, // A.5.10 Two reg + one imm (store_ind, load_ind, arithmetic) 120: {Name: "store_ind_u8", Category: InstrCatTwoRegOneImm, IsTerminator: false, IsStore: true}, diff --git a/PVM/program_cache.go b/PVM/program_cache.go index f9326c1d..258bac7c 100644 --- a/PVM/program_cache.go +++ b/PVM/program_cache.go @@ -6,18 +6,16 @@ import ( "github.com/New-JAMneration/JAM-Protocol/internal/types" ) -// Cross-invocation program cache — shared by BOTH PVM backends. +// Cross-invocation program cache, shared by both PVM backends. // -// Deblobbing a program (DeBlobProgramCode) is identical baseline work for the -// interpreter and the recompiler and currently runs once per invocation. The -// same service code recurs across blocks, so caching the deblob'd *Program by -// CodeHash makes it once-per-distinct-code. Caching it for both backends keeps -// the interpreter-vs-recompiler comparison fair: only the recompiler's extra -// per-invocation native-code generation (cached separately) should differ. +// Deblobbing is identical baseline work for the interpreter and recompiler; +// service code might repeat across blocks, so caching the deblob'd *Program by CodeHash +// makes decode once-per-distinct-code instead of once-per-invocation. Both backends +// share this cache so interpreter-vs-recompiler comparisons stay fair: only the +// recompiler's extra native-code generation (cached separately) should differ. // -// The Program is fully precomputed by DeBlobProgramCode and only read afterwards -// (LookupBlock/BlockContaining are pure reads), so a single *Program is safe to -// share read-only across goroutines. Cached entries are not evicted. +// After decode, *Program is read-only (LookupBlock/BlockContaining are pure reads), +// so entries are safe to share across goroutines. Nothing is evicted. type programCacheEntry struct { ready chan struct{} // closed once program/reason are set (single-flight) @@ -30,32 +28,37 @@ var programCache = struct { m map[types.OpaqueHash]*programCacheEntry }{m: make(map[types.OpaqueHash]*programCacheEntry)} -// GetOrDeblobProgram returns the deblob'd program for programCode, caching it by -// hash (CodeHash) across invocations. A zero hash bypasses the cache (e.g. -// is_authorized). DeBlobProgramCode runs at most once per distinct hash even -// under concurrent callers (single-flight); a deblob failure is not cached so it -// can be retried. The returned *Program is read-only and safe to share. -func GetOrDeblobProgram(hash types.OpaqueHash, programCode []byte) (*Program, ExitReason) { +// GetOrDeblobProgram returns the deblob'd program for programCode, caching by +// CodeHash across invocations. A zero hash bypasses the cache (e.g. is_authorized). +// Each distinct hash decodes at most once, even under concurrency (single-flight); +// failures are not cached and may be retried. The returned *Program is read-only. +// +// Only the entry-point-independent half of deblob is cached (𝔳_blob). 𝔳_inst for +// pc is checked on every call—the same code may be entered at different counters. +func GetOrDeblobProgram(hash types.OpaqueHash, programCode []byte, pc uint64) (*Program, ExitReason) { var zero types.OpaqueHash if hash == zero { - p, reason := DeBlobProgramCode(programCode) + p, reason := deblobValidatedProgram(programCode) if reason != ExitContinue { return nil, reason } - return &p, ExitContinue + return validEntry(&p, pc) } programCache.mu.Lock() if e, ok := programCache.m[hash]; ok { programCache.mu.Unlock() <-e.ready - return e.program, e.reason + if e.reason != ExitContinue { + return e.program, e.reason + } + return validEntry(e.program, pc) } e := &programCacheEntry{ready: make(chan struct{})} programCache.m[hash] = e programCache.mu.Unlock() - p, reason := DeBlobProgramCode(programCode) + p, reason := deblobValidatedProgram(programCode) if reason == ExitContinue { e.program = &p } @@ -63,10 +66,21 @@ func GetOrDeblobProgram(hash types.OpaqueHash, programCode []byte) (*Program, Ex close(e.ready) if reason != ExitContinue { - // Don't keep a failed entry; allow a retry on the next call. + // Drop failed entries so the next call can retry. programCache.mu.Lock() delete(programCache.m, hash) programCache.mu.Unlock() + return e.program, reason + } + return validEntry(e.program, pc) +} + +// validEntry applies 𝔳_inst(c, k, ι) for pc on an already-decoded program. +// Invalid entry points return ExitPanic, matching DeBlobProgramCode. +func validEntry(p *Program, pc uint64) (*Program, ExitReason) { + if !p.ValidInstructionAt(pc) { + pvmLogger.Errorf("instruction counter %d is not a valid entry point", pc) + return nil, ExitPanic } - return e.program, reason + return p, ExitContinue } diff --git a/PVM/program_code.go b/PVM/program_code.go index aad8ecc6..48f7ea62 100644 --- a/PVM/program_code.go +++ b/PVM/program_code.go @@ -75,8 +75,53 @@ type Program struct { InstrIdxAt []int32 // PC-indexed: InstrIdxAt[pc] = index into Instrs[], -1 if not an instruction start } -// DeBlobProgramCode deblob code, jump table, bitmask | A.2 -func DeBlobProgramCode(data []byte) (_ Program, _ ExitReason) { +// DeBlobProgramCode is deblob(pvm_blob, ι): it decodes the blob and +// validates both the program as a whole and pc as an entry point for the +// instruction counter, returning ExitPanic where the Gray Paper yields an error. +func DeBlobProgramCode(data []byte, pc uint64) (Program, ExitReason) { + prog, exitReason := deblobValidatedProgram(data) + if exitReason != ExitContinue { + return Program{}, exitReason + } + + // 𝔳_inst(c, k, ι) (A.2) + if !prog.ValidInstructionAt(pc) { + pvmLogger.Errorf("instruction counter %d is not a valid entry point", pc) + return Program{}, ExitPanic + } + + return prog, ExitContinue +} + +// deblobValidatedProgram is the entry-point-independent half of deblob: it +// already stored a decoded program, only 𝔳_inst(c, k, ι) is rechecked; otherwise +// falls back to full deblob(p, ι) (e.g. tests that omit Program). +func IntegratedProgramForInvoke(integrated IntegratedPVMType) (*Program, ExitReason) { + if integrated.Program != nil { + return validEntry(integrated.Program, uint64(integrated.PC)) + } + p, reason := DeBlobProgramCode(integrated.ProgramCode, uint64(integrated.PC)) + if reason != ExitContinue { + return nil, reason + } + return &p, ExitContinue +} + +// deblobValidatedProgram is the entry-point-independent half of deblob: it +// decodes the blob and checks 𝔳_blob(c, k, 0). Callers that cache a decoded +// program across invocations (GetOrDeblobProgram) share this result and apply +// the per-entry 𝔳_inst check themselves. +func deblobValidatedProgram(data []byte) (Program, ExitReason) { + return decodeProgramBlob(data, true) +} + +// deblobProgramForGasModel decodes A.9 vector fixtures that may omit a final +// terminator. Production deblobValidatedProgram still enforces 𝔳_blob. +func deblobProgramForGasModel(data []byte) (Program, ExitReason) { + return decodeProgramBlob(data, false) +} + +func decodeProgramBlob(data []byte, requireFinalTerminator bool) (_ Program, _ ExitReason) { // E_(|j|) : size of jumpTable jumpTableSize, dataUsed, exitReason := ReadUintVariable(data) if exitReason != ExitContinue { @@ -104,7 +149,6 @@ func DeBlobProgramCode(data []byte) (_ Program, _ ExitReason) { if jumpTableLength*jumpTableSize >= 1<<32 { pvmLogger.Errorf("jump table size %d bits exceed litmit of 32 bits", jumpTableLength*jumpTableSize) return Program{}, ExitPanic - // panic("the jump table's size is supposed to be at most 32 bits") } // E_z(j) = jumpTableSize * jumpTableLength = E_(|j|) * E_1(z) @@ -114,6 +158,11 @@ func DeBlobProgramCode(data []byte) (_ Program, _ ExitReason) { return Program{}, ExitPanic } + if instSize > uint64(len(data)) { + pvmLogger.Errorf("instruction size %d exceeds remaining blob length %d", instSize, len(data)) + return Program{}, ExitPanic + } + instructions := data[:instSize] bitmaskData := data[instSize:] bitmask, exitReason := MakeBitMasks(instructions, bitmaskData) @@ -135,10 +184,33 @@ func DeBlobProgramCode(data []byte) (_ Program, _ ExitReason) { if exitReason := prog.preDecodeBlocks(); exitReason != ExitContinue { return Program{}, exitReason } + // A.2 𝔳_blob: final instruction must be a basic-block terminator. + if requireFinalTerminator && !prog.finalInstructionIsTerminator() { + return Program{}, ExitPanic + } return prog, ExitContinue } +// ValidInstructionAt is 𝔳_inst(c, k, ι): whether the instruction counter +// may enter this program at pc. +// +// Unlike 𝔳_blob, this depends on the entry point rather than the program, so it +// is re-checked on every entry instead of being carried by a cached *Program. +// Three lookups make that free, and the entry point is not always trustworthy: +// the machine and invoke host-calls take it from a guest register. +func (p *Program) ValidInstructionAt(pc uint64) bool { + return validInst(p.InstructionData, p.Bitmasks, pc) +} + +// validInst is 𝔳_inst(c, k, ι) (A.2) +func validInst(c ProgramCode, k Bitmask, i uint64) bool { + return len(k) == len(c) && + i < uint64(len(k)) && + k.IsStartOfInstruction(int(i)) && + IsValidOpcode(c[i]) +} + // skip computes the distance to the next opcode A.3 func skip(pc int, bitmask Bitmask) uint32 { j := 1 @@ -292,6 +364,7 @@ func (code ProgramCode) isOpcodeValid(pc ProgramCounter) bool { } // GP 0.6.7 formula A.19 +// Kept for SingleStepStateTransition (GP 0.7.2 path); deprecated for v0.8.0 and later. func (code ProgramCode) isOpcode(pc ProgramCounter) opcode { if IsValidOpcode(code[pc]) { return opcode(code[pc]) diff --git a/PVM/program_code_test.go b/PVM/program_code_test.go index 8e747eab..9b0bf23e 100644 --- a/PVM/program_code_test.go +++ b/PVM/program_code_test.go @@ -24,7 +24,7 @@ func TestLoadPVMFile(t *testing.T) { t.Errorf("Error parsing %s: %v", filename, err) } // exitReason will not be used in this test - programBlob, _ := DeBlobProgramCode(programCode) + programBlob, _ := DeBlobProgramCode(programCode, 0) expected := map[string]int{ "InstructionDataSize": 53963, @@ -81,7 +81,7 @@ func TestSkip(t *testing.T) { } // exitReason will not be used in this test - programBlob, _ := DeBlobProgramCode(programCode) + programBlob, _ := DeBlobProgramCode(programCode, 0) // the expected is stick to pvm debugger and only get the program counter < 40 instructions expected := [][]byte{ @@ -100,8 +100,11 @@ func TestSkip(t *testing.T) { for pc, j := 0, 0; pc < 40; j++ { l := skip(pc, programBlob.Bitmasks) - if !reflect.DeepEqual(expected[j], programBlob.InstructionData[pc:pc+int(l)+1]) { - t.Errorf("Expected %v, but got %v", expected[j], programBlob.InstructionData[pc:pc+int(l)+1]) + // []byte(...) matters: DeepEqual compares types first, and + // InstructionData is the named type ProgramCode. + got := []byte(programBlob.InstructionData[pc : pc+int(l)+1]) + if !reflect.DeepEqual(expected[j], got) { + t.Errorf("Expected %v, but got %v", expected[j], got) } pc = pc + 1 + int(l) if pc > 40 { @@ -127,3 +130,21 @@ func TestInBasicBlock(t *testing.T) { } } } + +func TestDeblobRejectsOpenFinalBlock(t *testing.T) { + // Minimal blob: empty jump table, 3-byte load_imm (non-terminator), bitmask. + open := []byte{0, 0, 3, 51, 0x00, 0, 1} + if _, got := DeBlobProgramCode(open, 0); got != ExitPanic { + t.Fatalf("open final block via deblob: got %v, want panic", got) + } + // Same fixture is acceptable for gas-model decode. + if _, got := deblobProgramForGasModel(open); got != ExitContinue { + t.Fatalf("gas-model decode of open block: got %v, want continue", got) + } + + // Valid: trap terminator. + ok := []byte{0, 0, 1, 0, 1} + if _, got := DeBlobProgramCode(ok, 0); got != ExitContinue { + t.Fatalf("terminator final block: got %v, want continue", got) + } +} diff --git a/PVM/pvm_types.go b/PVM/pvm_types.go index ff6fc941..e1c74643 100644 --- a/PVM/pvm_types.go +++ b/PVM/pvm_types.go @@ -84,8 +84,9 @@ func NewInterpreter(program *Program, registers Registers, memory *Memory, gas G } type VMState struct { - Registers *Registers - Gas *Gas + Registers *Registers + Gas *Gas + GasCharged bool // Mem is the GuestMemory abstraction that omega host-calls and R() use for // all guest memory access. The interpreter sets it via NewPagedGuestMemory diff --git a/PVM/recompiler/SPEC_architecture.md b/PVM/recompiler/SPEC_architecture.md index 1506804d..f456896b 100644 --- a/PVM/recompiler/SPEC_architecture.md +++ b/PVM/recompiler/SPEC_architecture.md @@ -30,17 +30,17 @@ PVM/ │ ├── executable.go # ExecutableMemory: dual-mapping (memfd + RW/RX views) │ ├── trampoline.go # EmitEntryTrampoline, EmitExitTrampoline, EmitHostCallExit │ ├── compiler.go # Compiler struct, opcodeHandlers[231], CompileBasicBlock -│ ├── gas.go # emitGasCheck (per-instr v0.7.2), emitBlockGasCheck (v0.8.0) +│ ├── gas.go # emitBlockGasCheck / emitBlockOutOfGasExit (GP 0.8.0 A.4/A.9) │ ├── emit_basic.go # emitTrap, emitFallthrough, emitEcalli, emitLoadImm, etc. │ ├── emit_branch.go # emitJump, emitJumpInd, emitBranchImm, emitBranch │ ├── emit_arith_three.go # 32/64-bit add/sub/mul/div/rem/shift/bitwise │ ├── emit_arith_imm.go # arithmetic with immediate operand -│ ├── emit_two_reg.go # sbrk, move_reg, bit manipulation, sign/zero extend +│ ├── emit_two_reg.go # move_reg, bit manipulation, sign/zero extend │ ├── emit_memory.go # load/store (1/2/4/8 bytes, signed/unsigned) │ ├── djump_native.go # djumpSupport, emitDjumpNative, registerDispatch │ ├── recompiler.go # Recompiler struct, BlockBasedInvoke, lookupOrCompileBlock -│ ├── execute.go # executeBlockLocked, callNative, HandleSbrk -│ ├── host.go # host struct, HostCall dispatch loop (omega integration) +│ ├── execute.go # executeBlockLocked, callNative +│ ├── host.go # host struct, HostCall dispatch loop (omega / grow_heap) │ ├── invoke_mode.go # MachineInvoke → BlockBasedInvoke │ ├── code_cache.go # CodeCache: PC→CompiledBlock map │ └── x86signal/ # Signal handler (CGo) @@ -89,7 +89,7 @@ same physical pages, no mprotect needed ``` R15 - 8: ReturnStack (uintptr) — Go's RSP, for signal handler restore R15 - 16: ReturnAddr (uintptr) — Go's return_label address -R15 - 24: HeapPointer (uint64) — current sbrk boundary +R15 - 24: HeapPointer (uint64) — current heap top (grow_heap) R15 - 32: ExitPC (uint32) — PVM PC on exit (+4B padding) R15 - 40: ExitReason (uint64) — why execution stopped R15 - 48: Gas (int64) — remaining gas (disp8 reachable!) @@ -149,10 +149,9 @@ RSP (implicit) native stack pointer │ │ │ │ │ │ switch: │ │ │ CONTINUE → loop │ - │ │ HOST_CALL → dispatch omega + │ │ HOST_CALL → dispatch omega (incl. grow_heap) │ │ HALT/PANIC/OOG → return │ │ DjumpCallID → resolveDjump - │ │ SbrkCallID → HandleSbrk + recompile suffix │ └────────────────────┘ ``` @@ -170,13 +169,12 @@ RSP (implicit) native stack pointer ### 4.2 Exit Path Exit reasons(control region 使用 PVM package 的 `ExitReason` 格式:`type<<56 | payload`): -- **Gas exhaustion**: `emitGasCheck` → JS oog_label → ExitReason=ExitOOG +- **Gas exhaustion**: `emitBlockGasCheck` → JS block_oog → `emitBlockOutOfGasExit` → ExitReason=ExitOOG - **Block end / branch**: `emitExitToPC` → ExitReason=ExitContinue(0), ExitPC=target - **Host call**: `emitEcalli` → ExitReason=ExitHostCall|callID - **Halt**: jump_ind to 0xFFFF0000 → ExitReason=ExitHalt - **Panic**: trap / invalid target → ExitReason=ExitPanic - **Signal**: SIGSEGV → signal_handler → ExitReason=PAGE_FAULT|faultAddr or ExitPanic -- **sbrk runtime**: → ExitReason=ExitHostCall|SbrkCallID(0xFF)(內部 sentinel,不是真的 host call) - **djump miss**: → ExitReason=ExitHostCall|DjumpCallID(0xFE)(內部 sentinel) All paths store registers → `exit_trampoline` → restore RSP → JMP return_label → back to Go. @@ -184,11 +182,11 @@ All paths store registers → `exit_trampoline` → restore RSP → JMP return_l ### 4.3 Internal Sentinel Exit IDs ```go -SbrkCallID = 0xFF // sbrk 跨頁需要 mprotect → exit to Go DjumpCallID = 0xFE // djump dispatch miss → exit to Go for compile + retry ``` -Go 側 `BlockBasedInvoke` 先檢查 sentinel,不傳給外部 host。 +Go 側 `BlockBasedInvoke` 先檢查 djump sentinel,不傳給外部 host。Heap 成長走正式 +`grow_heap` host-call(omega ID=1),不再使用 sbrk opcode / SbrkCallID。 ### 4.4 Signal Handler @@ -287,15 +285,11 @@ Layer 1 check: validates against `ctx.pages` before pointer arithmetic. --- -## 8. sbrk Handling +## 8. Heap growth (`grow_heap`) -Two paths: -- **Inline (no page crossing)**: update heapPointer + set rD in native code -- **Runtime exit (page crossing)**: exit with `SbrkCallID=0xFF`, Go calls `HandleSbrk`: - - `mprotect(newPages, PROT_READ|PROT_WRITE)` - - Update `ctx.pages` (Layer 1 sync) - - Write back heapPointer and rD to control region - - Recompile block suffix starting from next instruction +GP 0.8.0 removed the `sbrk` opcode. Heap expansion is host-call `grow_heap` (Ω_♊): +`ecalli` → omega dispatch → `GuestMemory.GrowHeapTo` → JIT `mprotect` + update +`HeapPointer` / Layer-1 `pages`. No internal SbrkCallID sentinel. --- @@ -308,7 +302,7 @@ Two paths: | RCX = scratch | Required by x86 DIV (CL) and shift instructions | | Dual mapping (no mprotect toggle) | Eliminates 49% overhead from W^X switching | | Signal handler instead of bounds checks | Zero overhead on valid accesses; hardware MMU does the work | -| Per-instruction gas (v0.7.2) | Exact PC on OOG; v0.8.0 switches to per-block | +| Block-level gas (v0.8.0 A.4/A.9) | Charge once per basic block; `gaschargedflag` | | Block linking (depth-limited) | Eliminates Go dispatcher round-trip for sequential/branch targets | | pvmRegSlot reordering | RA/SP at slots 10,11 → disp8 offsets for frequent DIV spill paths | | MAP_NORESERVE | 4GB virtual space without committing physical memory | diff --git a/PVM/recompiler/SPEC_opcode_emit.md b/PVM/recompiler/SPEC_opcode_emit.md index 219c2e65..9058b434 100644 --- a/PVM/recompiler/SPEC_opcode_emit.md +++ b/PVM/recompiler/SPEC_opcode_emit.md @@ -168,35 +168,15 @@ Signed load(`load_i8` / `load_i16` / `load_i32`)用 `MOVSX` / `MOVSXD`。 --- -## 6. sbrk — 三路 Inline + Runtime Exit +## 6. Heap growth — `grow_heap` host call (not an opcode) -``` -TEST aReg, aReg // amount == 0? -JE queryLabel // → rD = heapPointer - -// amount != 0: -oldHP = [R15 - OffsetHeapPointer] -newHP = oldHP + amount -if newHP < oldHP → overflow → rD = 0 -if newHP > heapLimit → rD = 0 - -nextPageBoundary = (oldHP + 0xFFF) & ~0xFFF -if newHP <= nextPageBoundary: - // 同一頁,不需 mprotect - [R15 - OffsetHeapPointer] = newHP - rD = newHP -else: - // 跨頁:MUST exit to Go for mprotect - emitRuntimeExit(SbrkCallID = 0xFF) - // Go 側 HandleSbrk → unix.Mprotect → 寫回 heapPointer + rD - -queryLabel: - rD = [R15 - OffsetHeapPointer] -``` +GP 0.8.0 removed `sbrk`. Heap expansion is omega host-call ID=1 (`grow_heap`): +guest issues `ecalli` → exit to Go → `GrowHeapTo` → `mprotect` + update +`HeapPointer`. There is no native sbrk emit path and no `SbrkCallID` sentinel. --- -## 7. Gas Check — Per-Instruction (GP v0.7.2) +## 7. Gas Check — Per-Instruction (GP v0.7.2, historical) 每條 PVM 指令前: @@ -384,15 +364,17 @@ Opcode Handler Category CompileBasicBlock(startPC): 1. ensureDjumpSupport() — lazy init jump table rodata 2. 找到 BlockMeta (pre-decoded block boundaries) - 3. Pre-compile link targets (strategy-a block linking): + 3. blockGas = blockGasCostAt(startPC) — A.9, bake at compile time + 4. Pre-compile link targets (strategy-a block linking): - compileForLink(fallthroughPC) → linkFallthrough - compileForLink(branchTarget) → linkTaken - 4. Loop: for each instruction in block: - a. emitGasCheck(instrPC) → gas decrement + OOG check - b. opcodeHandlers[opcode](...) → emit native code - 5. Epilogue: JMP block_epilogue - 6. Emit per-instruction OOG landing pads - 7. block_epilogue: emitFallthroughEpilogue → ExitTrampoline - 8. Finalize → resolve labels → write to ExecutableMemory - 9. cache.Put(block) + registerDispatch(block) + 5. emitBlockGasCheck(blockGas) + block OOG landing pad + 6. Loop: for each instruction in block: + opcodeHandlers[opcode](...) → emit native code + (GasCharged cleared only on CONTINUE exits: emitLinkOrExit / + emitFallthroughEpilogue — not on trap/halt/ecalli) + 7. Epilogue: JMP block_epilogue + 8. block_epilogue: emitFallthroughEpilogue → ExitTrampoline + 9. Finalize → resolve labels → write to ExecutableMemory + 10. cache.Put(block) + registerDispatch(block) ``` diff --git a/PVM/recompiler/SPEC_pvm_summary.md b/PVM/recompiler/SPEC_pvm_summary.md index 07aa1590..6fb13dd3 100644 --- a/PVM/recompiler/SPEC_pvm_summary.md +++ b/PVM/recompiler/SPEC_pvm_summary.md @@ -61,8 +61,8 @@ Address 0x00000000 ──────────────────── readWritePad = readWriteStart + P(len(data)) + z_pages * ZP — Read-Write segment(初始化 data + z-page) - heapStart = readWritePad — Heap 起始(sbrk 可擴展到 stackStart) - ...(PROT_NONE,sbrk 時 mprotect 啟用) + heapStart = readWritePad — Heap 起始(grow_heap 可擴展到 stackStart−ZZ) + ...(PROT_NONE,grow_heap 時 mprotect 啟用) stackStart = 2^32 - 2*ZZ - ZI - P(stackSize) stackEnd = 2^32 - 2*ZZ - ZI — Stack(RW) @@ -113,7 +113,7 @@ Operand encoding 依 `InstrCategory` 決定: | OneRegImm | `[opcode][reg_byte][imm: lX bytes]` | load_imm, jump_ind | | OneRegTwoImm | `[opcode][reg_byte][vX: lX bytes][vY: lY bytes]` | store_imm_ind | | BranchOneRegImm | `[opcode][reg_byte][vX: lX bytes][offset: lY bytes]` | branch_eq_imm | -| TwoReg | `[opcode][reg_byte]` | move_reg, sbrk | +| TwoReg | `[opcode][reg_byte]` | move_reg, bit ops | | TwoRegImm | `[opcode][reg_byte][imm: lX bytes]` | add_imm_32 | | TwoRegOffset | `[opcode][reg_byte][offset: lX bytes]` | branch_eq | | TwoRegTwoImm | `[opcode][reg_byte][lX_byte][vX: lX bytes][vY: lY bytes]` | load_imm_jump_ind | @@ -254,28 +254,22 @@ else → jump to newPC --- -## 7. sbrk 語意 +## 7. Heap growth(`grow_heap`,B.5) -``` -sbrk(rD, rA): - amount = Reg[rA] - oldHP = heapPointer - - if amount == 0: - Reg[rD] = oldHP - return - - newHP = oldHP + amount - if newHP < oldHP (overflow) or newHP > heapLimit: - Reg[rD] = 0 - return +GP 0.8.0 移除 `sbrk` opcode。Heap 擴張為 host-call `grow_heap`(ω₇ = 目標 +page index;回傳新的 heap-top page index): - // activate pages from oldHP to newHP - for page in [oldHP, pageCeil(newHP)): - mprotect(page, PROT_READ|PROT_WRITE) - - heapPointer = newHP - Reg[rD] = newHP +``` +h = heapPointer / ZP +b = (heapLimit - ZZ) / ZP // major guard reserved +if n ≤ h or n > b: + charge M_c only; CONTINUE; ω₇ = h +else if gas < M_c + (n−h)·M_p: + OOG; gas unchanged; ω₇ = h +else: + charge full cost + GrowHeapTo(n) // mprotect [h..n) RW, update heapPointer + CONTINUE; ω₇ = n ``` --- diff --git a/PVM/recompiler/block_link.go b/PVM/recompiler/block_link.go index c00a7fd5..6377c641 100644 --- a/PVM/recompiler/block_link.go +++ b/PVM/recompiler/block_link.go @@ -18,6 +18,7 @@ func emitJmpNativeAddr(a *asm.Assembler, addr uintptr) { // emitFallthroughEpilogue emits block epilogue: native JMP when linkTarget is // known, otherwise a runtime chain via the dispatch table. func (c *Compiler) emitFallthroughEpilogue(a *asm.Assembler, fallthroughPC PVM.ProgramCounter, linkTarget *CompiledBlock) { + emitGasCharged(a, false) // A.4: CONTINUE leaves the block if linkTarget != nil { emitJmpNativeAddr(a, linkTarget.NativeAddr) return @@ -36,6 +37,7 @@ func (c *Compiler) emitFallthroughEpilogue(a *asm.Assembler, fallthroughPC PVM.P // register reload — is exactly what the Go dispatcher would have run, minus the // trampoline round-trip. func (c *Compiler) emitLinkOrExit(a *asm.Assembler, link *CompiledBlock, targetPC PVM.ProgramCounter) { + emitGasCharged(a, false) // A.4: CONTINUE jump/branch transfer if link != nil && link.PVMStartPC == targetPC { emitJmpNativeAddr(a, link.NativeAddr) return @@ -57,6 +59,7 @@ func (c *Compiler) emitLinkOrExit(a *asm.Assembler, link *CompiledBlock, targetP // runtime cannot preempt it — so execution stays bounded by the gas check every // block runs on entry. func (c *Compiler) emitChainOrExit(a *asm.Assembler, targetPC PVM.ProgramCounter) { + // Caller (emitLinkOrExit / emitFallthroughEpilogue) already cleared GasCharged. if c.djump == nil || c.singleStep || int(targetPC) >= len(c.djump.dispatch) { emitExitToPC(a, targetPC) return diff --git a/PVM/recompiler/code_cache.go b/PVM/recompiler/code_cache.go index bf2609ac..6d80dde4 100644 --- a/PVM/recompiler/code_cache.go +++ b/PVM/recompiler/code_cache.go @@ -17,7 +17,7 @@ type CompiledBlock struct { NativeAddr uintptr // callable native code address (precomputed) NativeOffset int // byte offset into ExecutableMemory (for debug) NativeSize int // size of emitted native code in bytes - GasCost int64 // total gas cost for this block (= number of PVM instructions) + GasCost int64 // A.9 gascostforblock baked into native gas check (suffix from PVMStartPC) InstrCount int // number of PVM instructions in this block } diff --git a/PVM/recompiler/compiled_program_test.go b/PVM/recompiler/compiled_program_test.go index bd58e50e..a8688cd6 100644 --- a/PVM/recompiler/compiled_program_test.go +++ b/PVM/recompiler/compiled_program_test.go @@ -30,7 +30,7 @@ func storeTestProgram(t *testing.T) *PVM.Program { t.Helper() instBytes, boundaries := buildMinimalInstr(1, PVM.GetOpcodeInfo(1)) // fallthrough blob := buildBlobExact(instBytes, boundaries) - prog, exitReason := PVM.DeBlobProgramCode(blob) + prog, exitReason := PVM.DeBlobProgramCode(blob, 0) if exitReason != PVM.ExitContinue { t.Fatalf("DeBlobProgramCode: %v", exitReason) } diff --git a/PVM/recompiler/compiler.go b/PVM/recompiler/compiler.go index ec03c290..f260b040 100644 --- a/PVM/recompiler/compiler.go +++ b/PVM/recompiler/compiler.go @@ -21,6 +21,7 @@ func init() { // 4.3 No-argument opcodeHandlers[0] = (*Compiler).emitTrap opcodeHandlers[1] = (*Compiler).emitFallthrough + opcodeHandlers[2] = (*Compiler).emitUnlikely // GP 0.8.0: hint only // 4.4 Immediate load opcodeHandlers[10] = (*Compiler).emitEcalli @@ -69,19 +70,18 @@ func init() { opcodeHandlers[89] = makeBranchImm(asm.CondGE) opcodeHandlers[90] = makeBranchImm(asm.CondGT) - // 4.8 Two-register + // A.5.9 Two-register | GP 0.8.0: sbrk removed, 102-111→101-110 opcodeHandlers[100] = (*Compiler).emitMoveReg - opcodeHandlers[101] = (*Compiler).emitSbrk - opcodeHandlers[102] = (*Compiler).emitCountSetBits64 - opcodeHandlers[103] = (*Compiler).emitCountSetBits32 - opcodeHandlers[104] = (*Compiler).emitLeadingZeroBits64 - opcodeHandlers[105] = (*Compiler).emitLeadingZeroBits32 - opcodeHandlers[106] = (*Compiler).emitTrailingZeroBits64 - opcodeHandlers[107] = (*Compiler).emitTrailingZeroBits32 - opcodeHandlers[108] = (*Compiler).emitSignExtend8 - opcodeHandlers[109] = (*Compiler).emitSignExtend16 - opcodeHandlers[110] = (*Compiler).emitZeroExtend16 - opcodeHandlers[111] = (*Compiler).emitReverseBytes + opcodeHandlers[101] = (*Compiler).emitCountSetBits64 + opcodeHandlers[102] = (*Compiler).emitCountSetBits32 + opcodeHandlers[103] = (*Compiler).emitLeadingZeroBits64 + opcodeHandlers[104] = (*Compiler).emitLeadingZeroBits32 + opcodeHandlers[105] = (*Compiler).emitTrailingZeroBits64 + opcodeHandlers[106] = (*Compiler).emitTrailingZeroBits32 + opcodeHandlers[107] = (*Compiler).emitSignExtend8 + opcodeHandlers[108] = (*Compiler).emitSignExtend16 + opcodeHandlers[109] = (*Compiler).emitZeroExtend16 + opcodeHandlers[110] = (*Compiler).emitReverseBytes // 4.5 Two reg + one imm (store_ind, load_ind) opcodeHandlers[120] = makeStoreInd(1) @@ -255,7 +255,7 @@ type Compiler struct { linkFallthrough *CompiledBlock // not-taken / sequential successor linkTaken *CompiledBlock // static jump / branch-taken target - singleStep bool // set by CompileSingleInstruction: exits must reach Go every instruction, no chaining + singleStep bool // set by CompileBlockInstruction: trampoline to Go after each instr } func NewCompiler(program *PVM.Program, ctx *JITContext, cache *CodeCache) *Compiler { @@ -324,12 +324,11 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept EndPC: c.program.Instrs[endIdx-1].PC, InstrStart: startIdx, InstrEnd: endIdx, - GasCost: PVM.Gas(endIdx - startIdx), } } else if startPC != blockMeta.StartPC { - // Resume inside a decoded block (e.g. after sbrk). Compile the suffix + // Resume inside a decoded block (e.g. after host call exit). Compile the suffix // from startPC only — executing from the block head would re-run earlier - // instructions and can loop on sbrk until the process SIGSEGVs. + // instructions. idx := c.program.InstrIdxAt[startPC] if idx < 0 { return nil, fmt.Errorf("no instruction at PC=%d", startPC) @@ -343,10 +342,11 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept EndPC: c.program.Instrs[blockMeta.InstrEnd-1].PC, InstrStart: startIdx, InstrEnd: blockMeta.InstrEnd, - GasCost: PVM.Gas(blockMeta.InstrEnd - startIdx), } } + blockGas := c.blockGasCostAt(startPC) + instrs := c.program.Instrs[blockMeta.InstrStart:blockMeta.InstrEnd] lastInstr := &instrs[len(instrs)-1] fallthroughPC := lastInstr.PC + PVM.ProgramCounter(lastInstr.SkipLen) + 1 @@ -370,24 +370,11 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept a := c.asm a.Reset() - // Per-instruction OOG landing-pad labels: the gas check (hot, in the loop) - // references instruction i's label before the landing pad (cold, after the - // loop) binds it — the two loops walk instrs in the same order, so an - // index-aligned slice pairs them. - oogLabels := make([]asm.Label, len(instrs)) - for i := range oogLabels { - oogLabels[i] = a.NewLabel() - } - - // blockBased gas charging (0.8.0 uncommented this): - // c.emitBlockGasCheck(a, blockOOG, int64(blockMeta.GasCost)) + blockOOG := a.NewLabel() + c.emitBlockGasCheck(a, blockOOG, blockGas) for i := range instrs { instr := &instrs[i] - - // per-instruction gas charging (GP v0.7.2, remove this in 0.8.0): - c.emitGasCheck(a, oogLabels[i]) - handler := opcodeHandlers[instr.Opcode] if handler == nil { return nil, fmt.Errorf("unsupported opcode %d at PC=%d", instr.Opcode, instr.PC) @@ -397,16 +384,10 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept } } - // blockBased gas charging (0.8.0 uncommented this): - // emitBlockOutOfGasExit(a, blockOOG, blockMeta.StartPC) - blockEpilogue := a.NewLabel() a.Jmp(blockEpilogue) - // per-instruction gas charging (GP v0.7.2, remove this in 0.8.0): - for i := range instrs { - emitOutOfGasExit(a, oogLabels[i], instrs[i].PC) - } + emitBlockOutOfGasExit(a, blockOOG, blockMeta.StartPC, blockGas) _ = a.BindLabel(blockEpilogue) c.emitFallthroughEpilogue(a, fallthroughPC, linkFallthrough) @@ -433,7 +414,7 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept NativeAddr: em.GetPtr(offset), NativeOffset: offset, NativeSize: len(code), - GasCost: int64(blockMeta.GasCost), + GasCost: blockGas, InstrCount: blockMeta.InstrCount(), } c.cache.Put(block) @@ -441,3 +422,9 @@ func (c *Compiler) compileBasicBlockAtDepth(startPC PVM.ProgramCounter, linkDept c.registerDispatch(block) return block, nil } + +// blockGasCostAt returns A.9 block gas for the suffix from pc through the +// containing basic block end (same rule as compileBasicBlockAtDepth suffix). +func (c *Compiler) blockGasCostAt(pc PVM.ProgramCounter) int64 { + return int64(PVM.GasCostFromPC(c.program, pc)) +} diff --git a/PVM/recompiler/compiler_debug.go b/PVM/recompiler/compiler_debug.go index 85b0d766..7e07bd23 100644 --- a/PVM/recompiler/compiler_debug.go +++ b/PVM/recompiler/compiler_debug.go @@ -8,25 +8,23 @@ import ( PVM "github.com/New-JAMneration/JAM-Protocol/PVM" ) -// CompileSingleInstruction compiles a single PVM instruction as a debug block. -// After execution, native code trampolines back to Go with ExitPC set to the -// fallthrough PC (for non-branch instructions) or the computed target PC (for branches). -// -// This enables debug single-step mode where each instruction acts as its own block, -// allowing per-instruction trace capture from the recompiler backend. -func (c *Compiler) CompileSingleInstruction(instr *PVM.InstrMeta) (*CompiledBlock, error) { +// CompileBlockInstruction compiles one pre-decoded instruction as a native block +// with A.7 block gas (same model as CompileBasicBlock). Used by +// DebugSingleStepInvoke: one instruction per native run, then trampoline back +// to Go for trace capture — not per-instruction (0.7.2) gas. +func (c *Compiler) CompileBlockInstruction(instr *PVM.InstrMeta) (*CompiledBlock, error) { a := c.asm a.Reset() - // Single-step must trampoline to Go after every instruction; block chaining - // would jump block-to-block natively and skip per-instruction trace capture. + // Trace single-step must not chain blocks natively; each step returns to Go. c.singleStep = true pc := instr.PC fallthroughPC := fallthroughPC(instr) + gasCost := c.blockGasCostAt(pc) oog := a.NewLabel() - c.emitGasCheck(a, oog) + c.emitBlockGasCheck(a, oog, gasCost) handler := opcodeHandlers[instr.Opcode] if handler == nil { @@ -36,13 +34,18 @@ func (c *Compiler) CompileSingleInstruction(instr *PVM.InstrMeta) (*CompiledBloc return nil, fmt.Errorf("emit instruction at PC=%d: %w", pc, err) } + // Reached only when the handler falls through (CONTINUE). Trap/ecalli/halt + // jump to the trampoline inside the handler and never clear here. + if PVM.IsBlockTerminator(instr.Opcode) { + emitGasCharged(a, false) + } emitExitToPC(a, fallthroughPC) EmitExitTrampoline(a) - emitOutOfGasExit(a, oog, instr.PC) + emitBlockOutOfGasExit(a, oog, pc, gasCost) code, err := a.Finalize() if err != nil { - return nil, fmt.Errorf("finalize single instruction at PC=%d: %w", pc, err) + return nil, fmt.Errorf("finalize block instruction at PC=%d: %w", pc, err) } em := c.ctx.executableMem @@ -61,7 +64,7 @@ func (c *Compiler) CompileSingleInstruction(instr *PVM.InstrMeta) (*CompiledBloc NativeAddr: em.GetPtr(offset), NativeOffset: offset, NativeSize: len(code), - GasCost: 1, + GasCost: gasCost, InstrCount: 1, }, nil } diff --git a/PVM/recompiler/compiler_test.go b/PVM/recompiler/compiler_test.go index 94391a62..bb51de89 100644 --- a/PVM/recompiler/compiler_test.go +++ b/PVM/recompiler/compiler_test.go @@ -81,7 +81,7 @@ func TestCompileAllOpcodes(t *testing.T) { instBytes, boundaries := buildMinimalInstr(opByte, info) blob := buildBlobExact(instBytes, boundaries) - prog, exitReason := PVM.DeBlobProgramCode(blob) + prog, exitReason := PVM.DeBlobProgramCode(blob, 0) if exitReason != PVM.ExitContinue { t.Fatalf("DeBlobProgramCode failed: %v", exitReason) } @@ -162,12 +162,13 @@ func buildMinimalInstr(op byte, info *PVM.OpcodeInfo) ([]byte, []int) { // --------------------------------------------------------------------------- type execTestCase struct { - name string - inst []byte // instruction bytes (without trailing trap) - initRegs PVM.Registers - wantRegs PVM.Registers - wantExit PVM.ExitReason - setupMem func(t *testing.T, ctx *JITContext) + name string + inst []byte // instruction bytes (without trailing trap unless boundaries set) + boundaries []int // optional explicit instruction boundaries for buildBlobExact + initRegs PVM.Registers + wantRegs PVM.Registers + wantExit PVM.ExitReason + setupMem func(t *testing.T, ctx *JITContext) } func TestExecuteInstructions(t *testing.T) { @@ -178,15 +179,26 @@ func TestExecuteInstructions(t *testing.T) { t.Run("LoadInd", testLoadInd) } -// TestSbrkExpandAmountInT0 verifies expand limit check when amount is in T0 (rA=2). -// A RegScratch clobber bug computed 2*heapPointer instead of heapPointer+amount. -func TestSbrkExpandAmountInT0(t *testing.T) { - const heapLimit = uint64(0x50000) +func runExecTest(t *testing.T, tc execTestCase) { + t.Helper() - instBytes := []byte{101, packRegs(0, 2), 0} - blob := buildBlobExact(instBytes, []int{0, 2}) + instBytes := make([]byte, len(tc.inst)) + copy(instBytes, tc.inst) + + var boundaries []int + if tc.boundaries != nil { + boundaries = tc.boundaries + } else { + lastOp := instBytes[0] + boundaries = []int{0} + if !PVM.IsBlockTerminator(lastOp) { + boundaries = append(boundaries, len(instBytes)) + instBytes = append(instBytes, 0) // trap + } + } - prog, exitReason := PVM.DeBlobProgramCode(blob) + blob := buildBlobExact(instBytes, boundaries) + prog, exitReason := PVM.DeBlobProgramCode(blob, 0) if exitReason != PVM.ExitContinue { t.Fatalf("DeBlobProgramCode: %v", exitReason) } @@ -203,31 +215,41 @@ func TestSbrkExpandAmountInT0(t *testing.T) { } defer em.Close() ctx.SetExecutableMemory(em) - ctx.WriteHeapPointer(0x30000) - ctx.heapLimit = heapLimit + + if tc.setupMem != nil { + tc.setupMem(t, ctx) + ctx.heapLimit = GuestMemorySize + } cache := NewCodeCache() compiler := NewCompiler(&prog, ctx, cache) + block, err := compiler.CompileBasicBlock(0) if err != nil { t.Fatalf("CompileBasicBlock: %v", err) } - ctx.WriteRegisters(regsWithValues(2, 100)) + ctx.WriteRegisters(tc.initRegs) + ctx.WriteExitReason(0) ctx.WriteGas(1000) gotExit := ExecuteBlock(ctx, block) - if gotExit != PVM.ExitHostCall|PVM.ExitReason(SbrkCallID) { - t.Fatalf("exit reason: got %v, want sbrk expand exit (bug returns inline fail / panic)", gotExit) + gotRegs := ctx.ReadRegisters() + + if gotRegs != tc.wantRegs { + t.Errorf("registers mismatch:\n got = %v\n want = %v", gotRegs, tc.wantRegs) + } + if gotExit != tc.wantExit { + t.Errorf("exit reason: got %v, want %v", gotExit, tc.wantExit) } } -func TestSbrkExpandExitPC(t *testing.T) { - instBytes := []byte{101, packRegs(0, 7), 0} - blob := buildBlobExact(instBytes, []int{0, 2}) - prog, exitReason := PVM.DeBlobProgramCode(blob) - if exitReason != PVM.ExitContinue { - t.Fatalf("DeBlobProgramCode: %v", exitReason) +func TestBlockEntryOOGLeavesGasUnchanged(t *testing.T) { + // unlikely; fallthrough — block cost 2; gas 1 → OOG with gas left at 1 + inst := []byte{2, 1} + prog, reason := PVM.DeBlobProgramCode(buildBlobExact(inst, []int{0, 1}), 0) + if reason != PVM.ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) } ctx, err := NewJITContext() @@ -242,50 +264,58 @@ func TestSbrkExpandExitPC(t *testing.T) { } defer em.Close() ctx.SetExecutableMemory(em) - ctx.WriteHeapPointer(0x30000) - ctx.heapLimit = GuestMemorySize - cache := NewCodeCache() - compiler := NewCompiler(&prog, ctx, cache) + compiler := NewCompiler(&prog, ctx, NewCodeCache()) block, err := compiler.CompileBasicBlock(0) if err != nil { - t.Fatalf("CompileBasicBlock: %v", err) + t.Fatalf("compile block: %v", err) } - ctx.WriteRegisters(regsWithValues(7, 4096)) - ctx.WriteGas(1000) - - gotExit := ExecuteBlock(ctx, block) - if gotExit != PVM.ExitHostCall|PVM.ExitReason(SbrkCallID) { - t.Fatalf("exit reason: got %v", gotExit) + ctx.WriteGas(1) + ctx.WriteGasCharged(false) + if got := ExecuteBlock(ctx, block); got != PVM.ExitOOG { + t.Fatalf("exit = %v, want OOG", got) } - - wantPC := PVM.ProgramCounter(2) // PC 0 + skip 1 + 1 - if gotPC := ctx.ReadExitPC(); gotPC != wantPC { - t.Fatalf("ExitPC: got %d, want fallthrough %d", gotPC, wantPC) + if gas := ctx.ReadGas(); gas != 1 { + t.Fatalf("gas after OOG = %d, want 1 (unchanged)", gas) + } + if ctx.ReadGasCharged() { + t.Fatal("gas flag set after failed block charge") } } -// TestSbrkExpandResumeSuffix verifies control-flow B: after sbrk expand exits to Go, -// resume compiles only the suffix from fallthroughPC and does not re-run earlier instructions. -func TestSbrkExpandResumeSuffix(t *testing.T) { - const addImm64 = 149 +func TestCompileUnlikelyOpcode(t *testing.T) { + inst := []byte{2, 1} // unlikely; fallthrough + prog, reason := PVM.DeBlobProgramCode(buildBlobExact(inst, []int{0, 1}), 0) + if reason != PVM.ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) + } - instBytes := []byte{ - addImm64, packRegs(1, 1), 1, // PC 0: r1 += 1 - 101, packRegs(0, 7), // PC 3: sbrk expand, fallthrough PC 5 - addImm64, packRegs(1, 1), 1, // PC 5: r1 += 1 - 0, // PC 8: trap + ctx, err := NewJITContext() + if err != nil { + t.Fatalf("NewJITContext: %v", err) } - boundaries := []int{0, 3, 5, 8} - blob := buildBlobExact(instBytes, boundaries) + defer ctx.Close() - prog, exitReason := PVM.DeBlobProgramCode(blob) - if exitReason != PVM.ExitContinue { - t.Fatalf("DeBlobProgramCode: %v", exitReason) + em, err := NewExecutableMemory(0) + if err != nil { + t.Fatalf("NewExecutableMemory: %v", err) } - if prog.Instrs[1].PC != 3 || fallthroughPC(&prog.Instrs[1]) != 5 { - t.Fatalf("unexpected sbrk layout: pc=%d fallthrough=%d", prog.Instrs[1].PC, fallthroughPC(&prog.Instrs[1])) + defer em.Close() + ctx.SetExecutableMemory(em) + + compiler := NewCompiler(&prog, ctx, NewCodeCache()) + if _, err := compiler.CompileBasicBlock(0); err != nil { + t.Fatalf("compile unlikely block: %v", err) + } +} + +func TestBlockGasChargedAcrossHostCall(t *testing.T) { + // ecalli 0; load_imm r0, 42; trap + inst := []byte{10, 0, 51, 0, 42, 0} + prog, reason := PVM.DeBlobProgramCode(buildBlobExact(inst, []int{0, 2, 5}), 0) + if reason != PVM.ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) } ctx, err := NewJITContext() @@ -300,51 +330,56 @@ func TestSbrkExpandResumeSuffix(t *testing.T) { } defer em.Close() ctx.SetExecutableMemory(em) - ctx.WriteHeapPointer(0x30000) - ctx.heapLimit = GuestMemorySize - ctx.WriteRegisters(regsWithValues(1, 0, 7, 4096)) - ctx.WriteGas(1000) - recomp := NewRecompiler(&prog, ctx) - gotExit, _ := recomp.BlockBasedInvoke(0) - if gotExit != PVM.ExitPanic { - t.Fatalf("BlockBasedInvoke exit: got %v, want trap/panic", gotExit) + compiler := NewCompiler(&prog, ctx, NewCodeCache()) + first, err := compiler.CompileBasicBlock(0) + if err != nil { + t.Fatalf("compile block: %v", err) + } + blockGas := first.GasCost + if blockGas < 1 { + t.Fatalf("block gas = %d, want >= 1", blockGas) } - regs := ctx.ReadRegisters() - if regs[1] != 2 { - t.Fatalf("r1=%d want 2 (re-running block head would leave r1=1)", regs[1]) + initialGas := PVM.Gas(blockGas + 5) + ctx.WriteGas(initialGas) + ctx.WriteGasCharged(false) + if got := ExecuteBlock(ctx, first); got.GetReasonType() != PVM.HOST_CALL { + t.Fatalf("first exit = %v, want host call", got) } - if regs[0] != 0x31000 { - t.Fatalf("r0=%#x want 0x31000 (sbrk result)", regs[0]) + wantGasAfterCharge := initialGas - PVM.Gas(blockGas) + if gas := ctx.ReadGas(); gas != wantGasAfterCharge { + t.Fatalf("gas after block charge = %d, want %d", gas, wantGasAfterCharge) + } + if !ctx.ReadGasCharged() { + t.Fatal("gas flag cleared at non-terminating host call") } - suffixBlock, err := recomp.compiler.CompileBasicBlock(5) + suffix, err := compiler.CompileBasicBlock(ctx.ReadExitPC()) if err != nil { - t.Fatalf("CompileBasicBlock suffix: %v", err) + t.Fatalf("compile suffix: %v", err) } - if suffixBlock.PVMStartPC != 5 { - t.Fatalf("suffix cache key PVMStartPC=%d want 5", suffixBlock.PVMStartPC) + if suffix.GasCost < 1 { + t.Fatalf("suffix block gas = %d, want >= 1", suffix.GasCost) } -} - -func runExecTest(t *testing.T, tc execTestCase) { - t.Helper() - - instBytes := make([]byte, len(tc.inst)) - copy(instBytes, tc.inst) - - lastOp := instBytes[0] - boundaries := []int{0} - if !PVM.IsBlockTerminator(lastOp) { - boundaries = append(boundaries, len(instBytes)) - instBytes = append(instBytes, 0) // trap + if got := ExecuteBlock(ctx, suffix); got != PVM.ExitPanic { + t.Fatalf("suffix exit = %v, want panic", got) + } + if gas := ctx.ReadGas(); gas != wantGasAfterCharge { + t.Fatalf("suffix charged block twice: gas = %d, want %d", gas, wantGasAfterCharge) } + // A.4: PANIC preserves gaschargedflag (only CONTINUE terminators clear it). + if !ctx.ReadGasCharged() { + t.Fatal("gas flag cleared on trap/panic; want preserved") + } +} - blob := buildBlobExact(instBytes, boundaries) - prog, exitReason := PVM.DeBlobProgramCode(blob) - if exitReason != PVM.ExitContinue { - t.Fatalf("DeBlobProgramCode: %v", exitReason) +func TestSuffixBlockGasMatchesGasCostFromPC(t *testing.T) { + // ecalli 0; load_imm r0, 42; trap — suffix from PC 2 is load_imm + trap + inst := []byte{10, 0, 51, 0, 42, 0} + prog, reason := PVM.DeBlobProgramCode(buildBlobExact(inst, []int{0, 2, 5}), 0) + if reason != PVM.ExitContinue { + t.Fatalf("DeBlobProgramCode: %v", reason) } ctx, err := NewJITContext() @@ -360,31 +395,15 @@ func runExecTest(t *testing.T, tc execTestCase) { defer em.Close() ctx.SetExecutableMemory(em) - if tc.setupMem != nil { - tc.setupMem(t, ctx) - ctx.heapLimit = GuestMemorySize - } - - cache := NewCodeCache() - compiler := NewCompiler(&prog, ctx, cache) - - block, err := compiler.CompileBasicBlock(0) + compiler := NewCompiler(&prog, ctx, NewCodeCache()) + const suffixPC PVM.ProgramCounter = 2 + want := int64(PVM.GasCostFromPC(&prog, suffixPC)) + suffix, err := compiler.CompileBasicBlock(suffixPC) if err != nil { - t.Fatalf("CompileBasicBlock: %v", err) + t.Fatalf("compile suffix: %v", err) } - - ctx.WriteRegisters(tc.initRegs) - ctx.WriteExitReason(0) - ctx.WriteGas(1000) - - gotExit := ExecuteBlock(ctx, block) - gotRegs := ctx.ReadRegisters() - - if gotRegs != tc.wantRegs { - t.Errorf("registers mismatch:\n got = %v\n want = %v", gotRegs, tc.wantRegs) - } - if gotExit != tc.wantExit { - t.Errorf("exit reason: got %v, want %v", gotExit, tc.wantExit) + if suffix.GasCost != want { + t.Fatalf("suffix baked gas = %d, want GasCostFromPC = %d", suffix.GasCost, want) } } @@ -698,86 +717,56 @@ func testTwoReg(t *testing.T) { wantRegs: regsWithValues(0, 42, 1, 42), wantExit: PVM.ExitPanic, }, + // GP 0.8.0: opcodes 102-111 → 101-110 { name: "sign_extend_8: r0 = sext8(r1)", - inst: []byte{108, packRegs(0, 1)}, - initRegs: regsWithValues(1, 0x80), // -128 in i8 + inst: []byte{107, packRegs(0, 1)}, + initRegs: regsWithValues(1, 0x80), wantRegs: regsWithValues(0, 0xFFFFFFFFFFFFFF80, 1, 0x80), wantExit: PVM.ExitPanic, }, { name: "sign_extend_16: r0 = sext16(r1)", - inst: []byte{109, packRegs(0, 1)}, - initRegs: regsWithValues(1, 0x8000), // -32768 in i16 + inst: []byte{108, packRegs(0, 1)}, + initRegs: regsWithValues(1, 0x8000), wantRegs: regsWithValues(0, 0xFFFFFFFFFFFF8000, 1, 0x8000), wantExit: PVM.ExitPanic, }, { name: "zero_extend_16: r0 = zext16(r1)", - inst: []byte{110, packRegs(0, 1)}, + inst: []byte{109, packRegs(0, 1)}, initRegs: regsWithValues(1, 0xDEADBEEF12340000|0xABCD), wantRegs: regsWithValues(0, 0xABCD, 1, 0xDEADBEEF12340000|0xABCD), wantExit: PVM.ExitPanic, }, { name: "reverse_bytes: r0 = bswap64(r1)", - inst: []byte{111, packRegs(0, 1)}, + inst: []byte{110, packRegs(0, 1)}, initRegs: regsWithValues(1, 0x0102030405060708), wantRegs: regsWithValues(0, 0x0807060504030201, 1, 0x0102030405060708), wantExit: PVM.ExitPanic, }, { name: "count_set_bits_64: r0 = popcnt64(r1)", - inst: []byte{102, packRegs(0, 1)}, + inst: []byte{101, packRegs(0, 1)}, initRegs: regsWithValues(1, 0xFF), wantRegs: regsWithValues(0, 8, 1, 0xFF), wantExit: PVM.ExitPanic, }, { name: "leading_zero_bits_64: r0 = lzcnt64(r1)", - inst: []byte{104, packRegs(0, 1)}, - initRegs: regsWithValues(1, 1), // 63 leading zeros + inst: []byte{103, packRegs(0, 1)}, + initRegs: regsWithValues(1, 1), wantRegs: regsWithValues(0, 63, 1, 1), wantExit: PVM.ExitPanic, }, { name: "trailing_zero_bits_64: r0 = tzcnt64(r1)", - inst: []byte{106, packRegs(0, 1)}, - initRegs: regsWithValues(1, 0x100), // 8 trailing zeros + inst: []byte{105, packRegs(0, 1)}, + initRegs: regsWithValues(1, 0x100), wantRegs: regsWithValues(0, 8, 1, 0x100), wantExit: PVM.ExitPanic, }, - { - name: "sbrk query: r0 = heap pointer when r7==0", - inst: []byte{101, packRegs(0, 7)}, - initRegs: regsWithValues(7, 0), - wantRegs: regsWithValues(0, 0x33000, 7, 0), - wantExit: PVM.ExitPanic, - setupMem: func(_ *testing.T, ctx *JITContext) { - ctx.WriteHeapPointer(0x33000) - }, - }, - { - name: "sbrk expand: exits to Go when r7!=0", - inst: []byte{101, packRegs(0, 7)}, - initRegs: regsWithValues(7, 4096), - wantRegs: regsWithValues(0, 0, 7, 4096), - wantExit: PVM.ExitHostCall | PVM.ExitReason(SbrkCallID), - setupMem: func(_ *testing.T, ctx *JITContext) { - ctx.WriteHeapPointer(0x30000) - ctx.heapLimit = GuestMemorySize - }, - }, - { - name: "sbrk overflow: r0=0 inline when newHP wraps", - inst: []byte{101, packRegs(0, 7)}, - initRegs: regsWithValues(7, 1), - wantRegs: regsWithValues(0, 0, 7, 1), - wantExit: PVM.ExitPanic, - setupMem: func(_ *testing.T, ctx *JITContext) { - ctx.WriteHeapPointer(^uint64(0)) - }, - }, } for _, tc := range cases { @@ -829,11 +818,12 @@ func testBasicOps(t *testing.T) { wantExit: PVM.ExitPanic, }, { - name: "fallthrough: NOP then exit", - inst: []byte{1}, // fallthrough emits NOP; exit trampoline returns ExitContinue - initRegs: PVM.Registers{}, - wantRegs: PVM.Registers{}, - wantExit: PVM.ExitContinue, + name: "fallthrough: NOP then exit", + inst: []byte{1, 0}, // fallthrough; trap (fallthrough target @ PC 1) + boundaries: []int{0, 1}, + initRegs: PVM.Registers{}, + wantRegs: PVM.Registers{}, + wantExit: PVM.ExitContinue, }, { name: "load_imm: r0 = 42", diff --git a/PVM/recompiler/context.go b/PVM/recompiler/context.go index 91ef1ec9..8e9ff389 100644 --- a/PVM/recompiler/context.go +++ b/PVM/recompiler/context.go @@ -44,7 +44,7 @@ const ( // touch. // // GuestMemory permission checks (Layer 1) mirror interpreter isReadable/isWriteable: -// walk the page table (ctx.pages) populated by mapSegment and sbrk SetPageAccess. +// walk the page table (ctx.pages) populated by mapSegment and grow_heap SetPageAccess. // stackStart == JITContext.heapLimit. type guestSegments struct { roStart, roEnd uint64 // read-only program code/data (padded) @@ -62,7 +62,7 @@ type JITContext struct { guestMem []byte // rawMem[ControlRegionSize : ControlRegionSize+GuestMemorySize] executableMem *ExecutableMemory trampolineAddr uintptr // cached entry trampoline for this executable memory - heapLimit uint64 // stackStart; sbrk must not grow past this (A.36 / instSbrkMeta) + heapLimit uint64 // stackStart; grow_heap must not grow past this seg guestSegments // segment boundaries (init layout metadata) pages map[uint32]pageAccess // Layer-1 page permissions for host-call checks } @@ -221,8 +221,27 @@ const ( OffsetDjumpTable = 176 // R15 - 176: uintptr — jump table rodata in ExecutableMemory OffsetDjumpBitmask = 184 // R15 - 184: uintptr — bitmask rodata in ExecutableMemory OffsetDjumpDispatch = 192 // R15 - 192: uintptr — PC→native dispatch table ([]uintptr) + + // GP 0.8.0 formula A.7: block gas pre-charge flag (gaschargedflag) + OffsetGasCharged = 200 // R15 - 200: uint8 (1=charged, 0=not) ) +// ReadGasCharged returns the block gas pre-charge flag. +func (ctx *JITContext) ReadGasCharged() bool { + off := ControlRegionSize - OffsetGasCharged + return ctx.rawMem[off] != 0 +} + +// WriteGasCharged sets the block gas pre-charge flag. +func (ctx *JITContext) WriteGasCharged(v bool) { + off := ControlRegionSize - OffsetGasCharged + if v { + ctx.rawMem[off] = 1 + } else { + ctx.rawMem[off] = 0 + } +} + // HasMemAccess returns true if the last instruction recorded a memory access. func (ctx *JITContext) HasMemAccess() bool { off := ControlRegionSize - OffsetMemAccessAddr diff --git a/PVM/recompiler/debug_single_step.go b/PVM/recompiler/debug_single_step.go index 540a69ca..c47b0340 100644 --- a/PVM/recompiler/debug_single_step.go +++ b/PVM/recompiler/debug_single_step.go @@ -30,7 +30,7 @@ func (r *Recompiler) DebugSingleStepInvoke(pc PVM.ProgramCounter) (PVM.ExitReaso } instr := &r.program.Instrs[int(idx)] - block, err := r.compiler.CompileSingleInstruction(instr) + block, err := r.compiler.CompileBlockInstruction(instr) if err != nil { return PVM.ExitPanic, 0 } @@ -50,38 +50,7 @@ func (r *Recompiler) DebugSingleStepInvoke(pc PVM.ProgramCounter) (PVM.ExitReaso exitReason := executeBlockLocked(r.ctx, block) exitPC := r.ctx.ReadExitPC() - if IsSbrkExit(exitReason) { - exitReason = HandleSbrk(r.ctx, instr.Dst, instr.Src[0]) - if exitReason != PVM.ExitContinue { - if trace != nil { - var dstVal, src1Val, src2Val uint64 - if instr.Dst != 0xff { - dstVal = r.ctx.ReadRegister(instr.Dst) - } - if instr.Src[0] != 0xff { - src1Val = r.ctx.ReadRegister(instr.Src[0]) - } - if instr.Src[1] != 0xff { - src2Val = r.ctx.ReadRegister(instr.Src[1]) - } - trace.RecordStep( - uint32(instr.PC), instr.Opcode, - instr.Dst, instr.Src[0], instr.Src[1], - dstVal, src1Val, src2Val, - int64(r.ctx.ReadGas()), - 0, 0, 0, 0, - ) - } - switch exitReason.GetReasonType() { - case PVM.HALT, PVM.PANIC: - return exitReason, r.ctx.ReadExitPC() - default: - return exitReason, exitPC - } - } - exitPC = fallthroughPC(instr) - exitReason = PVM.ExitContinue - } + // GP 0.8.0: sbrk exit path removed; heap growth via grow_heap host call if IsDjumpExit(exitReason) { exitReason, exitPC = r.resolveDjump(instr.PC, uint32(exitPC)) diff --git a/PVM/recompiler/djump_native.go b/PVM/recompiler/djump_native.go index cff546d9..a3aaf363 100644 --- a/PVM/recompiler/djump_native.go +++ b/PVM/recompiler/djump_native.go @@ -239,11 +239,13 @@ func (c *Compiler) emitDjumpNative(a *asm.Assembler, targetReg asm.Register, ins a.TestRegReg(RegScratch, RegScratch) a.Jcc(asm.CondEQ, missLabel) a.AddRegImm32(asm.RSP, 16) // drop saved jump addr + dest PC + emitGasCharged(a, false) a.JmpReg(RegScratch) _ = a.BindLabel(missLabel) a.Pop(RegScratch) // dest PC a.AddRegImm32(asm.RSP, 8) // drop saved jump addr + emitGasCharged(a, false) c.emitDjumpMiss(a, RegScratch) _ = a.BindLabel(panicLabel) diff --git a/PVM/recompiler/emit_basic.go b/PVM/recompiler/emit_basic.go index f7a7d7cc..78c871a0 100644 --- a/PVM/recompiler/emit_basic.go +++ b/PVM/recompiler/emit_basic.go @@ -19,8 +19,23 @@ func (c *Compiler) emitTrap(a *asm.Assembler, instr *PVM.InstrMeta) error { return nil } -// opcode 1: fallthrough — no-op +// opcode 1: fallthrough — sjump(ι + 1 + skip(ι)); epilogue links to the target. func (c *Compiler) emitFallthrough(a *asm.Assembler, instr *PVM.InstrMeta) error { + pc := instr.PC + targetPC := fallthroughPC(instr) + if !c.program.Bitmasks.IsStartOfBasicBlock(targetPC) { + a.MovImm64ToReg(RegScratch, uint64(PVM.ExitPanic)) + a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) + a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(pc)) + a.Jmp(a.ExitTrampoline()) + return nil + } + a.Nop() + return nil +} + +// opcode 2: unlikely — hint only, no mutation (GP 0.8.0) +func (c *Compiler) emitUnlikely(a *asm.Assembler, instr *PVM.InstrMeta) error { _ = instr a.Nop() return nil @@ -28,7 +43,10 @@ func (c *Compiler) emitFallthrough(a *asm.Assembler, instr *PVM.InstrMeta) error // ---- 4.4 Immediate instructions ---- -// opcode 10: ecalli — host call exit +// opcode 10: ecalli — host call exit. ecalli is not a basic-block terminator +// (opcode_info), so gaschargedflag stays set across the host-call interrupt; +// resume continues the same block without a second charge (matches interpreter +// and A.9 multistep vectors). func (c *Compiler) emitEcalli(a *asm.Assembler, instr *PVM.InstrMeta) error { callID := int(instr.Imm[0]) nextPC := fallthroughPC(instr) diff --git a/PVM/recompiler/emit_branch.go b/PVM/recompiler/emit_branch.go index 0f8dc556..79026b3a 100644 --- a/PVM/recompiler/emit_branch.go +++ b/PVM/recompiler/emit_branch.go @@ -107,66 +107,64 @@ func (c *Compiler) emitLoadImmJump(a *asm.Assembler, instr *PVM.InstrMeta) error return nil } -// opcode 81-90: branch_xx_imm — conditional branch with immediate comparison +// opcode 81-90: branch_xx_imm: dual-target validation (taken + fallthrough) func (c *Compiler) emitBranchImm(a *asm.Assembler, instr *PVM.InstrMeta, cc asm.ConditionCode) error { pc := instr.PC xReg, vX, targetPC := branchOneRegImmFromMeta(instr) - takenLabel := a.NewLabel() + nextPC := fallthroughPC(instr) + + // GP 0.8.0: both targets must be valid basic block starts + if !c.program.Bitmasks.IsStartOfBasicBlock(targetPC) || !c.program.Bitmasks.IsStartOfBasicBlock(nextPC) { + a.MovImm64ToReg(RegScratch, uint64(PVM.ExitPanic)) + a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) + a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(pc)) + a.Jmp(a.ExitTrampoline()) + return nil + } - // Compare Reg[rA] with vX + takenLabel := a.NewLabel() if fitsInt32(vX) { a.CmpRegImm32(xReg, int32(int64(vX))) } else { a.MovImm64ToReg(RegScratch, vX) a.CmpRegReg(xReg, RegScratch) } - a.Jcc(cc, takenLabel) - // Not taken: fall through to next instruction (block exit handled by caller) - nextPC := fallthroughPC(instr) c.emitLinkOrExit(a, c.linkFallthrough, nextPC) - // Taken: exit to target PC _ = a.BindLabel(takenLabel) - if !c.program.Bitmasks.IsStartOfBasicBlock(targetPC) { - a.MovImm64ToReg(RegScratch, uint64(PVM.ExitPanic)) - a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) - a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(pc)) - a.Jmp(a.ExitTrampoline()) - } else { - c.emitLinkOrExit(a, c.linkTaken, targetPC) - } + c.emitLinkOrExit(a, c.linkTaken, targetPC) return nil } // ---- 4.9.4 Conditional branch (two registers + offset) ---- -// opcode 170-175: branch_xx — two-register comparison +// opcode 170-175: branch_xx: dual-target validation (taken + fallthrough) func (c *Compiler) emitBranch(a *asm.Assembler, instr *PVM.InstrMeta, cc asm.ConditionCode) error { pc := instr.PC aReg, bReg, targetPC := branchTwoRegFromMeta(instr) - takenLabel := a.NewLabel() - - a.CmpRegReg(aReg, bReg) - a.Jcc(cc, takenLabel) - - // Not taken nextPC := fallthroughPC(instr) - c.emitLinkOrExit(a, c.linkFallthrough, nextPC) - // Taken - _ = a.BindLabel(takenLabel) - if !c.program.Bitmasks.IsStartOfBasicBlock(targetPC) { + // GP 0.8.0: both targets must be valid basic block starts + if !c.program.Bitmasks.IsStartOfBasicBlock(targetPC) || !c.program.Bitmasks.IsStartOfBasicBlock(nextPC) { a.MovImm64ToReg(RegScratch, uint64(PVM.ExitPanic)) a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(pc)) a.Jmp(a.ExitTrampoline()) - } else { - c.emitLinkOrExit(a, c.linkTaken, targetPC) + return nil } + takenLabel := a.NewLabel() + a.CmpRegReg(aReg, bReg) + a.Jcc(cc, takenLabel) + + c.emitLinkOrExit(a, c.linkFallthrough, nextPC) + + _ = a.BindLabel(takenLabel) + c.emitLinkOrExit(a, c.linkTaken, targetPC) + return nil } diff --git a/PVM/recompiler/emit_two_reg.go b/PVM/recompiler/emit_two_reg.go index 2a4250f1..34e3fe4e 100644 --- a/PVM/recompiler/emit_two_reg.go +++ b/PVM/recompiler/emit_two_reg.go @@ -16,159 +16,72 @@ func (c *Compiler) emitMoveReg(a *asm.Assembler, instr *PVM.InstrMeta) error { return nil } -// opcode 101: sbrk — heap expansion. -// -// The inline paths are an optimization that short-circuits the runtime exit only -// when no mprotect is needed: -// - amount == 0: rD = heapPointer (query only, no growth) -// - amount != 0, no page crossing: pages already mapped — update heapPointer + rD inline -// -// The page-crossing case MUST take the runtime exit to Go (HandleSbrk → mprotect); -// skipping it would leave the new pages PROT_NONE and the next guest write would -// raise a hardware SIGSEGV. -func (c *Compiler) emitSbrk(a *asm.Assembler, instr *PVM.InstrMeta) error { - dReg, aReg := twoRegFromMeta(instr) - queryLabel := a.NewLabel() - doneLabel := a.NewLabel() - - a.TestRegReg(aReg, aReg) - a.Jcc(asm.CondEQ, queryLabel) - - c.emitSbrkExpand(a, dReg, aReg, instr, doneLabel) - - _ = a.BindLabel(queryLabel) - a.MovMemToReg(dReg, RegGuestBase, -int32(OffsetHeapPointer)) - _ = a.BindLabel(doneLabel) - return nil -} - -// emitSbrkExpand handles amount != 0. Three outcomes: -// - overflow or limit exceeded → rD = 0, continue in JIT -// - no page crossing → update heapPointer and rD inline, continue in JIT -// - page crossing → exit to Go for mprotect (HandleSbrk) -func (c *Compiler) emitSbrkExpand(a *asm.Assembler, dReg, aReg asm.Register, instr *PVM.InstrMeta, doneLabel asm.Label) { - heapLimit := c.ctx.heapLimit - t0 := PVMToX86[2] - fail := a.NewLabel() - mprotect := a.NewLabel() - - // Save t0 (PVM T0 / RBX) — we borrow it as scratch. - a.MovRegToMem(RegGuestBase, regOffset(2), t0) - - // t0 = oldHP - a.MovMemToReg(t0, RegGuestBase, -int32(OffsetHeapPointer)) - - // RegScratch = amount (handle aReg == t0 clobber) - if aReg == t0 { - a.MovMemToReg(RegScratch, RegGuestBase, regOffset(2)) - } else { - a.MovRegToReg(RegScratch, aReg) - } - - // RegScratch = newHP = oldHP + amount - a.AddRegReg(RegScratch, t0) - - // Overflow: newHP < oldHP → fail - a.CmpRegReg(RegScratch, t0) - a.Jcc(asm.CondB, fail) - - // Page boundary: pageCeil(oldHP) = (oldHP + 0xFFF) & ~0xFFF - a.AddRegImm32(t0, 0xFFF) - a.AndRegImm32(t0, ^int32(0xFFF)) - // t0 = pageCeil(oldHP), RegScratch = newHP - - // If newHP > pageCeil(oldHP) → need mprotect via Go - a.CmpRegReg(RegScratch, t0) - a.Jcc(asm.CondA, mprotect) - - // Limit check (only for inline path; mprotect path lets HandleSbrk check) - a.MovImm64ToReg(t0, heapLimit) - a.CmpRegReg(RegScratch, t0) - a.Jcc(asm.CondA, fail) - - // Inline success: no page crossing, within limits. - // Update heapPointer = newHP, rD = newHP. - a.MovRegToMem(RegGuestBase, -int32(OffsetHeapPointer), RegScratch) - a.MovMemToReg(t0, RegGuestBase, regOffset(2)) // restore t0 - a.MovRegToReg(dReg, RegScratch) - a.Jmp(doneLabel) - - // Page crossing → exit to Go for mprotect. - _ = a.BindLabel(mprotect) - a.MovMemToReg(t0, RegGuestBase, regOffset(2)) // restore t0 - emitRuntimeExit(a, uint64(PVM.ExitHostCall)|uint64(SbrkCallID), fallthroughPC(instr)) - - // Fail: overflow or heapLimit exceeded → rD = 0. - _ = a.BindLabel(fail) - a.MovMemToReg(t0, RegGuestBase, regOffset(2)) // restore t0 - a.MovImm64ToReg(dReg, 0) - a.Jmp(doneLabel) -} +// GP 0.8.0: sbrk removed; heap growth is now via grow_heap host call (B.5) -// opcode 102: count_set_bits_64 — Reg[rA] = popcnt(Reg[rB]) +// opcode 101: count_set_bits_64 — Reg[rA] = popcnt(Reg[rB]) func (c *Compiler) emitCountSetBits64(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Popcnt(dReg, aReg) return nil } -// opcode 103: count_set_bits_32 — Reg[rA] = popcnt(uint32(Reg[rB])) +// opcode 102: count_set_bits_32 — Reg[rA] = popcnt(uint32(Reg[rB])) func (c *Compiler) emitCountSetBits32(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Popcnt32(dReg, aReg) return nil } -// opcode 104: leading_zero_bits_64 — Reg[rA] = lzcnt(Reg[rB]) +// opcode 103: leading_zero_bits_64 — Reg[rA] = lzcnt(Reg[rB]) func (c *Compiler) emitLeadingZeroBits64(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Lzcnt(dReg, aReg) return nil } -// opcode 105: leading_zero_bits_32 — Reg[rA] = lzcnt(uint32(Reg[rB])) +// opcode 104: leading_zero_bits_32 — Reg[rA] = lzcnt(uint32(Reg[rB])) func (c *Compiler) emitLeadingZeroBits32(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Lzcnt32(dReg, aReg) return nil } -// opcode 106: trailing_zero_bits_64 — Reg[rA] = tzcnt(Reg[rB]) +// opcode 105: trailing_zero_bits_64 — Reg[rA] = tzcnt(Reg[rB]) func (c *Compiler) emitTrailingZeroBits64(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Tzcnt(dReg, aReg) return nil } -// opcode 107: trailing_zero_bits_32 — Reg[rA] = tzcnt(uint32(Reg[rB])) +// opcode 106: trailing_zero_bits_32 — Reg[rA] = tzcnt(uint32(Reg[rB])) func (c *Compiler) emitTrailingZeroBits32(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.Tzcnt32(dReg, aReg) return nil } -// opcode 108: sign_extend_8 — Reg[rA] = sign_extend_8_to_64(Reg[rB]) +// opcode 107: sign_extend_8 — Reg[rA] = sign_extend_8_to_64(Reg[rB]) func (c *Compiler) emitSignExtend8(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.MovFromByteToRegSx(dReg, aReg) return nil } -// opcode 109: sign_extend_16 — Reg[rA] = sign_extend_16_to_64(Reg[rB]) +// opcode 108: sign_extend_16 — Reg[rA] = sign_extend_16_to_64(Reg[rB]) func (c *Compiler) emitSignExtend16(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.MovFromWordToRegSx(dReg, aReg) return nil } -// opcode 110: zero_extend_16 — Reg[rA] = zero_extend_16_to_64(Reg[rB]) +// opcode 109: zero_extend_16 — Reg[rA] = zero_extend_16_to_64(Reg[rB]) func (c *Compiler) emitZeroExtend16(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) a.MovFromWordToRegZx(dReg, aReg) return nil } -// opcode 111: reverse_bytes — Reg[rA] = bswap64(Reg[rB]) +// opcode 110: reverse_bytes — Reg[rA] = bswap64(Reg[rB]) func (c *Compiler) emitReverseBytes(a *asm.Assembler, instr *PVM.InstrMeta) error { dReg, aReg := twoRegFromMeta(instr) if dReg != aReg { diff --git a/PVM/recompiler/execute.go b/PVM/recompiler/execute.go index bf6f83da..e840cd51 100644 --- a/PVM/recompiler/execute.go +++ b/PVM/recompiler/execute.go @@ -8,7 +8,6 @@ import ( PVM "github.com/New-JAMneration/JAM-Protocol/PVM" "github.com/New-JAMneration/JAM-Protocol/PVM/recompiler/asm" x86_signal_linux "github.com/New-JAMneration/JAM-Protocol/PVM/recompiler/x86signal" - "golang.org/x/sys/unix" ) // ExecuteBlock runs a single compiled basic block through the JIT entry/exit @@ -90,62 +89,11 @@ func (e execError) Error() string { return string(e) } const errNoExecMem = execError("executable memory not initialized") -// SbrkCallID is the sentinel host-call ID used by emitSbrk to exit to Go. -const SbrkCallID = 0xFF // DjumpCallID is the sentinel host-call ID for indirect jumps (jump_ind, load_imm_jump_ind). const DjumpCallID = 0xFE -// IsSbrkExit returns true if the exit reason is an sbrk request. -func IsSbrkExit(reason PVM.ExitReason) bool { - return reason.GetReasonType() == PVM.HOST_CALL && reason.GetHostCallID() == SbrkCallID -} - // IsDjumpExit returns true if the exit reason is an indirect jump request. func IsDjumpExit(reason PVM.ExitReason) bool { return reason.GetReasonType() == PVM.HOST_CALL && reason.GetHostCallID() == DjumpCallID } - -// HandleSbrk performs the sbrk heap expansion in Go, updating the control -// region heap pointer and mprotecting newly required pages. -// rD and rA are the PVM register indices from the sbrk instruction encoding. -// Returns the ExitReason to propagate (ExitContinue on success). -func HandleSbrk(ctx *JITContext, rD, rA uint8) PVM.ExitReason { - regs := ctx.ReadRegisters() - amount := regs[rA] - - oldHP := ctx.ReadHeapPointer() - - if amount == 0 { - regs[rD] = oldHP - ctx.WriteRegisters(regs) - return PVM.ExitContinue - } - - newHP := oldHP + amount - if newHP < oldHP || newHP > ctx.heapLimit { - regs[rD] = 0 - ctx.WriteRegisters(regs) - return PVM.ExitContinue - } - - nextPageBoundary := pageCeil(uint32(oldHP)) - if newHP > uint64(nextPageBoundary) { - finalBoundary := pageCeil(uint32(newHP)) - // Match interpreter allocateMemorySegment: activate from oldHP, not P(oldHP). - for addr := uint32(oldHP); addr < finalBoundary; addr += PVM.ZP { - if err := ctx.SetPageAccess(addr/PVM.ZP, unix.PROT_READ|unix.PROT_WRITE); err != nil { - return PVM.ExitPanic - } - } - } - - ctx.WriteHeapPointer(newHP) - regs[rD] = newHP - ctx.WriteRegisters(regs) - return PVM.ExitContinue -} - -func pageCeil(addr uint32) uint32 { - return PVM.P(int(addr)) -} diff --git a/PVM/recompiler/gas.go b/PVM/recompiler/gas.go index b5b149c5..886204d1 100644 --- a/PVM/recompiler/gas.go +++ b/PVM/recompiler/gas.go @@ -7,54 +7,36 @@ import ( "github.com/New-JAMneration/JAM-Protocol/PVM/recompiler/asm" ) -// Two inline instructions per PVM instruction (GP v0.7.2), fused charge+check: -// - SubMemImm32: charge 1 gas -// - Jcc(S): branch to the OOG landing pad when the result went negative -// -// (GP A.6) OOG when pre-charge Gas < 1. Gas is never negative at entry, so -// post-charge < 0 (SF=1) ⟺ pre-charge <= 0 ⟺ pre-charge < 1 — the same -// condition the interpreter checks, so a gas-exhausted program stops at the -// same instruction on both backends. The landing pad un-charges the 1 so the -// reported remaining gas also matches (the interpreter never charges on OOG). -// -// oog is the instruction's landing-pad label, allocated by the compile loop and -// bound later by emitOutOfGasExit (see compileBasicBlockAtDepth's oogLabels). -// -// If/when we switch to blockBased gas charging in GP v0.8.0, this helper -// should stop being called from the per-instruction compile loop. -func (c *Compiler) emitGasCheck(a *asm.Assembler, oog asm.Label) { - a.SubMemImm32(RegGuestBase, -int32(OffsetGas), 1) - a.Jcc(asm.CondS, oog) -} +// emitBlockGasCheck charges once for the current basic block (A.9 gascostforblock +// baked in at compile time). Subtracts first; on OOG, emitBlockOutOfGasExit +// restores the cost so gas is left unchanged (matches interpreter A.4). +func (c *Compiler) emitBlockGasCheck(a *asm.Assembler, blockOOG asm.Label, blockGas int64) { + charged := a.NewLabel() + a.LoadByte(RegScratch, RegGuestBase, -int32(OffsetGasCharged)) + a.TestRegReg(RegScratch, RegScratch) + a.Jcc(asm.CondNE, charged) -// emitOutOfGasExit emits the temporary GP v0.7.2 per-instruction OOG landing pad. -// Each instruction gets its own exit label so ExitPC matches interpreter semantics. -// -// TODO: when switch to blockBased gas charging in GP v0.8.0 -// the per-instruction callers should be commented out and replaced by a single block-entry OOG exit. -func emitOutOfGasExit(a *asm.Assembler, oog asm.Label, instrPC PVM.ProgramCounter) { - _ = a.BindLabel(oog) - // Undo the fused charge: on OOG the interpreter leaves gas unchanged. - a.SubMemImm32(RegGuestBase, -int32(OffsetGas), -1) - // per-instruction (GP v0.7.2): report the exact instruction PC that failed. - a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(instrPC)) - a.MovImm64ToReg(RegScratch, uint64(PVM.ExitOOG)) - a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) - a.Jmp(a.ExitTrampoline()) + a.SubMemImm32(RegGuestBase, -int32(OffsetGas), int32(blockGas)) + a.Jcc(asm.CondS, blockOOG) + emitGasCharged(a, true) + + _ = a.BindLabel(charged) } -// emitBlockGasCheck is the prepared block-based gas charging path for GP v0.8.0. -// It is intentionally not called yet; `compiler.go` keeps the call sites commented -// out until the JIT path switches from current per-instruction semantics. -func (c *Compiler) emitBlockGasCheck(a *asm.Assembler, blockOOG asm.Label, instrCount int64) { - a.SubMemImm32(RegGuestBase, -int32(OffsetGas), int32(instrCount)) - a.Jcc(asm.CondS, blockOOG) +func emitGasCharged(a *asm.Assembler, charged bool) { + value := int32(0) + if charged { + value = 1 + } + a.MovMemImm32_32(RegGuestBase, -int32(OffsetGasCharged), value) } -// emitBlockOutOfGasExit is the prepared block-entry OOG landing pad for GP v0.8.0. -// It reports the block start PC, matching block-based charging semantics. -func emitBlockOutOfGasExit(a *asm.Assembler, blockOOG asm.Label, blockStartPC PVM.ProgramCounter) { +// emitBlockOutOfGasExit emits the block-entry OOG landing pad. +// Restores the just-subtracted block cost, reports block start PC, then exits. +func emitBlockOutOfGasExit(a *asm.Assembler, blockOOG asm.Label, blockStartPC PVM.ProgramCounter, blockGas int64) { _ = a.BindLabel(blockOOG) + // SUB of a negative imm adds the cost back (same pattern as legacy per-instr OOG). + a.SubMemImm32(RegGuestBase, -int32(OffsetGas), -int32(blockGas)) a.MovMemImm32_32(RegGuestBase, -int32(OffsetExitPC), int32(blockStartPC)) a.MovImm64ToReg(RegScratch, uint64(PVM.ExitOOG)) a.MovRegToMem(RegGuestBase, -int32(OffsetExitReason), RegScratch) diff --git a/PVM/recompiler/guest_memory.go b/PVM/recompiler/guest_memory.go index f6ea5797..128d60ec 100644 --- a/PVM/recompiler/guest_memory.go +++ b/PVM/recompiler/guest_memory.go @@ -230,3 +230,32 @@ func (g jitGuestMemory) Read(addr, length uint64) []byte { func (g jitGuestMemory) Write(addr uint64, data []byte) { copy(g.ctx.guestMem[addr:addr+uint64(len(data))], data) } + +func (g jitGuestMemory) HeapPages() uint64 { + return g.ctx.ReadHeapPointer() / PVM.ZP +} + +func (g jitGuestMemory) HeapMaxPages() uint64 { + // B.5: b = (2^32 - 3·ZZ - ZI - P(s)) / ZP; heapLimit is stackStart. + return (g.ctx.heapLimit - PVM.ZZ) / PVM.ZP +} + +// GrowHeapTo expands the heap to targetPage, mprotecting new pages as RW. +// Caller has already verified h < targetPage ≤ b. The heap pointer is updated +// only after every requested page protection succeeds. +func (g jitGuestMemory) GrowHeapTo(targetPage uint64) error { + ctx := g.ctx + oldHP := ctx.ReadHeapPointer() + newHP := targetPage * PVM.ZP + oldBound := PVM.P(int(oldHP)) + newBound := PVM.P(int(newHP)) + if newHP > uint64(oldBound) { + for addr := uint32(oldHP); addr < uint32(newBound); addr += PVM.ZP { + if err := ctx.SetPageAccess(addr/PVM.ZP, unix.PROT_READ|unix.PROT_WRITE); err != nil { + return err + } + } + } + ctx.WriteHeapPointer(newHP) + return nil +} diff --git a/PVM/recompiler/host.go b/PVM/recompiler/host.go index 4b48dc7c..45462cef 100644 --- a/PVM/recompiler/host.go +++ b/PVM/recompiler/host.go @@ -31,6 +31,7 @@ func newHost(r *Recompiler, addition PVM.HostCallArgs, hostCalls PVM.Omegas) *ho } } +// Ψ_H outer loop → MachineInvoke → BlockBasedInvoke (see docs/4_HostCall_Integration.md §5). func (h *host) HostCall(pc PVM.ProgramCounter) PVM.Psi_H_ReturnType { ctx := h.recomp.Ctx() var vm PVM.VMState @@ -39,6 +40,7 @@ func (h *host) HostCall(pc PVM.ProgramCounter) PVM.Psi_H_ReturnType { snapshot := func() { ctx.ReadRegistersInto(regsBuf) ctx.ReadGasInto(gasBuf) + vm.GasCharged = ctx.ReadGasCharged() vm.Mem = ctx.GuestMemory() vm.BindInlineSnapshot() } @@ -58,8 +60,6 @@ func (h *host) HostCall(pc PVM.ProgramCounter) PVM.Psi_H_ReturnType { snapshot() - // unreachable: sbrk is resolved inside BlockBasedInvoke / DebugSingleStepInvoke - input := PVM.OmegaInput{ Operation: PVM.OperationType(exitReason.GetHostCallID()), VM: &vm, @@ -103,6 +103,7 @@ func (h *host) HostCall(pc PVM.ProgramCounter) PVM.Psi_H_ReturnType { regs, gas := vm.InlineSnapshotValues() ctx.WriteRegisters(regs) ctx.WriteGas(gas) + ctx.WriteGasCharged(vm.GasCharged) if trace != nil { copy(rout[:], regs[:]) diff --git a/PVM/recompiler/invoke_mode.go b/PVM/recompiler/invoke_mode.go index a3719481..6f37a5c6 100644 --- a/PVM/recompiler/invoke_mode.go +++ b/PVM/recompiler/invoke_mode.go @@ -4,7 +4,9 @@ package recompiler import PVM "github.com/New-JAMneration/JAM-Protocol/PVM" -// MachineInvoke runs native PVM execution until a non-CONTINUE exit. +// MachineInvoke: run until non-CONTINUE exit. Forwards to BlockBasedInvoke +// (pre-decoded blocks → native JIT; symmetrical to interpreter +// BlockBasedInvokeDecodedBlocks). Trace routing in invoke_mode_trace.go. func (r *Recompiler) MachineInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter) { return r.BlockBasedInvoke(pc) } diff --git a/PVM/recompiler/invoke_mode_trace.go b/PVM/recompiler/invoke_mode_trace.go index 6f8f2032..8b0005dc 100644 --- a/PVM/recompiler/invoke_mode_trace.go +++ b/PVM/recompiler/invoke_mode_trace.go @@ -4,8 +4,7 @@ package recompiler import PVM "github.com/New-JAMneration/JAM-Protocol/PVM" -// MachineInvoke runs native PVM execution. When trace is active, uses debug single-step -// mode to produce per-instruction streams aligned with the interpreter trace. +// MachineInvoke: trace/debug → DebugSingleStepInvoke; else BlockBasedInvoke. func (r *Recompiler) MachineInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter) { if r.Trace != nil { return r.DebugSingleStepInvoke(pc) diff --git a/PVM/recompiler/jit_hotpath.go b/PVM/recompiler/jit_hotpath.go index b7af9285..75fa92b9 100644 --- a/PVM/recompiler/jit_hotpath.go +++ b/PVM/recompiler/jit_hotpath.go @@ -17,7 +17,7 @@ import ( // are kept for cheap online monitoring and per-call averages. The old // subtracted "exec(est) = run - compile - host" bucket has been retired: it // conflated native execution with dispatch glue (LockOSThread, SetFaultWindow, -// snapshot, cache lookup, djump/sbrk resolution) and so could not tell a slow +// snapshot, cache lookup, djump resolution) and so could not tell a slow // JIT from slow glue. For native-vs-glue attribution use pprof; for per-PVM-block // hotness use perf + a JIT symbol map. See PVM/docs/JIT_PROFILE_ANALYSIS.md. // diff --git a/PVM/recompiler/psi_m_recompiler.go b/PVM/recompiler/psi_m_recompiler.go index a313099c..5a8eca64 100644 --- a/PVM/recompiler/psi_m_recompiler.go +++ b/PVM/recompiler/psi_m_recompiler.go @@ -53,7 +53,7 @@ func Psi_M_recompiler( if jitProfile { tDeblob = time.Now() } - program, exitReason := PVM.GetOrDeblobProgram(addition.CodeHash, programCode) + program, exitReason := PVM.GetOrDeblobProgram(addition.CodeHash, programCode, uint64(counter)) if jitProfile { jm.deblobNanos.Add(int64(time.Since(tDeblob))) } @@ -79,6 +79,7 @@ func Psi_M_recompiler( ctx.WriteRegisters(registers) ctx.WriteGas(PVM.Gas(gas)) + ctx.WriteGasCharged(false) ctx.WriteExitReason(PVM.ExitContinue) ctx.WriteExitPC(counter) diff --git a/PVM/recompiler/recompiler.go b/PVM/recompiler/recompiler.go index 5199f36b..f05cf033 100644 --- a/PVM/recompiler/recompiler.go +++ b/PVM/recompiler/recompiler.go @@ -14,9 +14,8 @@ import ( // Recompiler is the machine layer of the JIT backend, symmetrical to // PVM.Interpreter on the interpreter backend. It owns compilation and // native execution over a JITContext, but not host-call dispatch state. -// Host-call orchestration (OOG, HALT, PANIC handling, sbrk, Omega -// dispatch) lives in host, which drives Recompiler.BlockBasedInvoke in -// a loop. +// Host-call orchestration (OOG, HALT, PANIC handling, Omega dispatch) +// lives in host, which drives Recompiler.BlockBasedInvoke in a loop. type Recompiler struct { compiler *Compiler program *PVM.Program @@ -45,8 +44,8 @@ func newRecompiler(cp *CompiledProgram, ctx *JITContext) *Recompiler { } } -// BlockBasedInvoke runs one or more compiled basic blocks until the -// native side signals a non-CONTINUE exit. +// BlockBasedInvoke runs pre-decoded basic blocks as native code — symmetrical to +// interpreter BlockBasedInvokeDecodedBlocks (same Program.Instrs/BlockMeta/GasCost). func (r *Recompiler) BlockBasedInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter) { x86_signal_linux.SetupSignalHandler() runtime.LockOSThread() @@ -84,21 +83,7 @@ func (r *Recompiler) BlockBasedInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PV } } - if IsSbrkExit(exitReason) { - instr, ok := r.sbrkInstrForRuntimeExit(exitPC) - if !ok { - return PVM.ExitPanic, 0 - } - exitReason, pc = r.resolveSbrk(instr) - switch exitReason.GetReasonType() { - case PVM.CONTINUE: - continue - case PVM.HALT, PVM.PANIC: - return exitReason, r.ctx.ReadExitPC() - default: - return exitReason, exitPC - } - } + // GP 0.8.0: sbrk exit path removed; heap growth via grow_heap host call switch exitReason.GetReasonType() { case PVM.CONTINUE: diff --git a/PVM/recompiler/runtime_exit.go b/PVM/recompiler/runtime_exit.go index 0f476e71..44e0feb8 100644 --- a/PVM/recompiler/runtime_exit.go +++ b/PVM/recompiler/runtime_exit.go @@ -7,10 +7,6 @@ import ( "github.com/New-JAMneration/JAM-Protocol/PVM/recompiler/asm" ) -// sbrkOpcode is opcode 101 (heap expansion). Expand paths exit to Go mid-block; -// resume must compile a suffix from fallthroughPC, not re-run the block head. -const sbrkOpcode = 101 - // emitRuntimeExit stores ExitReason and ExitPC (the PVM PC to resume at) and // jumps to the shared exit trampoline. Matches ecalli: exitPC is fallthrough, // not the exiting instruction's PC. diff --git a/PVM/recompiler/sbrk.go b/PVM/recompiler/sbrk.go deleted file mode 100644 index 68b83857..00000000 --- a/PVM/recompiler/sbrk.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux && amd64 && cgo - -package recompiler - -import PVM "github.com/New-JAMneration/JAM-Protocol/PVM" - -// resolveSbrk runs HandleSbrk for the instruction at instr and returns the -// fallthrough PC on success. -func (r *Recompiler) resolveSbrk(instr *PVM.InstrMeta) (PVM.ExitReason, PVM.ProgramCounter) { - exitReason := HandleSbrk(r.ctx, instr.Dst, instr.Src[0]) - if exitReason != PVM.ExitContinue { - return exitReason, 0 - } - return PVM.ExitContinue, fallthroughPC(instr) -} - -// sbrkInstrForRuntimeExit resolves the sbrk InstrMeta from an expand exit's ExitPC. -// ExitPC is fallthrough (like ecalli). If exitPC still points at the sbrk opcode -// (legacy emit), that path is accepted too. -func (r *Recompiler) sbrkInstrForRuntimeExit(exitPC PVM.ProgramCounter) (*PVM.InstrMeta, bool) { - idx := r.program.InstrIdxAt[exitPC] - if idx < 0 { - return nil, false - } - instr := &r.program.Instrs[int(idx)] - if instr.Opcode == sbrkOpcode && instr.PC == exitPC { - return instr, true - } - if int(idx) > 0 { - prev := &r.program.Instrs[int(idx)-1] - if prev.Opcode == sbrkOpcode && fallthroughPC(prev) == exitPC { - return prev, true - } - } - return nil, false -} diff --git a/PVM/recompiler/signal_handler_test.go b/PVM/recompiler/signal_handler_test.go index e37640c6..1e7e98b9 100644 --- a/PVM/recompiler/signal_handler_test.go +++ b/PVM/recompiler/signal_handler_test.go @@ -73,7 +73,7 @@ func compileTestBlock(t *testing.T, ctx *JITContext, instBytes []byte, boundarie t.Helper() blob := buildBlobExact(instBytes, boundaries) - prog, exitReason := PVM.DeBlobProgramCode(blob) + prog, exitReason := PVM.DeBlobProgramCode(blob, 0) if exitReason != PVM.ExitContinue { t.Fatalf("DeBlobProgramCode: %v", exitReason) } @@ -347,66 +347,8 @@ func TestSignalHandler_RegisterPreservation(t *testing.T) { } } -func TestSbrk_ExpandHeap(t *testing.T) { - ctx, cleanup := setupTestContext(t) - defer cleanup() - - rwStart := uint32(2 * PVM.ZZ) - rwEnd := rwStart + uint32(PVM.ZP) - if err := ctx.mapSegment(rwStart, rwEnd, nil, unix.PROT_READ|unix.PROT_WRITE); err != nil { - t.Fatalf("mapSegment: %v", err) - } - ctx.WriteHeapPointer(uint64(rwEnd)) - - amount := uint64(2 * PVM.ZP) - regs := PVM.Registers{} - regs[7] = amount - ctx.WriteRegisters(regs) - - result := HandleSbrk(ctx, 0, 7) - if result != PVM.ExitContinue { - t.Fatalf("HandleSbrk: got %v, want ExitContinue", result) - } - - gotHP := ctx.ReadHeapPointer() - wantHP := uint64(rwEnd) + amount - if gotHP != wantHP { - t.Errorf("heap pointer: got 0x%x, want 0x%x", gotHP, wantHP) - } - - gotRegs := ctx.ReadRegisters() - if gotRegs[0] != wantHP { - t.Errorf("Reg[rD]: got 0x%x, want 0x%x", gotRegs[0], wantHP) - } - - newAddr := uint32(rwEnd) + uint32(PVM.ZP) - ctx.guestMem[newAddr] = 0x42 - if ctx.guestMem[newAddr] != 0x42 { - t.Error("new heap page not writable") - } -} - -func TestSbrk_ZeroAmount(t *testing.T) { - ctx, cleanup := setupTestContext(t) - defer cleanup() - - hp := uint64(0x30000) - ctx.WriteHeapPointer(hp) - - regs := PVM.Registers{} - regs[7] = 0 - ctx.WriteRegisters(regs) - - result := HandleSbrk(ctx, 0, 7) - if result != PVM.ExitContinue { - t.Fatalf("HandleSbrk: got %v, want ExitContinue", result) - } - - gotRegs := ctx.ReadRegisters() - if gotRegs[0] != hp { - t.Errorf("Reg[rD]: got 0x%x, want 0x%x", gotRegs[0], hp) - } -} +// GP 0.8.0: TestSbrk_ExpandHeap, TestSbrk_ZeroAmount removed; +// sbrk replaced by grow_heap host call (B.5). func TestGoRuntimeCoexistence(t *testing.T) { ctx, cleanup := setupTestContext(t) diff --git a/PVM/refine_invocation.go b/PVM/refine_invocation.go index 295f13ac..64e356cb 100644 --- a/PVM/refine_invocation.go +++ b/PVM/refine_invocation.go @@ -24,11 +24,13 @@ type RefineOutput struct { Gas types.Gas } -// B.4 M +// B.4 M | GP 0.8.0: added GasCharged (gaschargedflag) type IntegratedPVMType struct { - ProgramCode ProgramCode // p + ProgramCode ProgramCode // p — raw blob from Ω_M + Program *Program // decoded at Ω_M; invoke re-validates entry PC only Memory Memory // u PC ProgramCounter // i + GasCharged bool // GP 0.8.0 A.4: block gas pre-charge flag } type ( diff --git a/PVM/review_fixes_test.go b/PVM/review_fixes_test.go new file mode 100644 index 00000000..f3cbc4f5 --- /dev/null +++ b/PVM/review_fixes_test.go @@ -0,0 +1,191 @@ +package PVM + +import ( + "testing" + + "github.com/New-JAMneration/JAM-Protocol/internal/types" +) + +func TestDeblobRejectsOpenFinalBlock(t *testing.T) { + // Minimal blob: empty jump table, 3-byte load_imm (non-terminator), bitmask. + open := []byte{0, 0, 3, 51, 0x00, 0, 1} + if _, got := DeBlobProgramCode(open, 0); got != ExitPanic { + t.Fatalf("open final block via deblob: got %v, want panic", got) + } + // Same fixture is acceptable for gas-model decode. + if _, got := deblobProgramForGasModel(open); got != ExitContinue { + t.Fatalf("gas-model decode of open block: got %v, want continue", got) + } + + // Valid: trap terminator. + ok := []byte{0, 0, 1, 0, 1} + if _, got := DeBlobProgramCode(ok, 0); got != ExitContinue { + t.Fatalf("terminator final block: got %v, want continue", got) + } +} + +func TestBranchCyclesOutOfRangeTargetIsTrap(t *testing.T) { + prog := decodedGasTestProgram(t, + ProgramCode{ + 170, 0x01, 100, 0, 0, 0, // branch_eq target PC 100 (past end) + 0, // trap fallthrough at PC 6 + }, + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}, + ) + cost := InstructionCost(&prog, &prog.Instrs[0]) + if cost.Cycles != 1 { + t.Fatalf("out-of-range branch target cycles = %d, want 1", cost.Cycles) + } +} + +func TestSelfLoopJumpRetakesTerminator(t *testing.T) { + // jump to PC 0 (self); fallthrough unreachable. + prog := decodedGasTestProgram(t, + ProgramCode{40, 0, 0, 0, 0}, // jump imm=0 + Bitmask{0x03, 0x00, 0x00, 0x00, 0x00}, + ) + gas := Gas(10_000) + interp := &Interpreter{Program: &prog, Gas: gas, GasCharged: false} + exit, pc := interp.BlockBasedInvokeDecodedBlocks(0) + // Should OOG eventually while looping, with PC at the jump. + if exit.GetReasonType() != OUT_OF_GAS { + t.Fatalf("exit = %v, want OOG (self-loop must re-enter block)", exit) + } + if pc != 0 { + t.Fatalf("OOG pc = %d, want 0 (self-loop)", pc) + } +} + +func newGrowHeapTestMem() *Memory { + return &Memory{ + Pages: map[uint32]*Page{}, + heapPointer: 3 * ZZ, // h = 3*ZZ/ZP + heapLimit: 1<<32 - 2*ZZ - ZI, + } +} + +func TestGrowHeapGasOutcomes(t *testing.T) { + t.Run("noGrowthChargesConstEvenIfNegative", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + gas := HostGasGrowHeapConst / 2 + regs := Registers{} + regs[7] = h // no growth + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if gas != HostGasGrowHeapConst/2-HostGasGrowHeapConst { + t.Fatalf("gas = %d, want %d", gas, HostGasGrowHeapConst/2-HostGasGrowHeapConst) + } + if regs[7] != h { + t.Fatalf("reg7 = %d, want h=%d", regs[7], h) + } + }) + + t.Run("growthOOGLeavesGasUnchanged", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + b := NewPagedGuestMemory(mem).HeapMaxPages() + want := h + 1 + if want > b { + t.Skip("heap already at max") + } + g := HostGasGrowHeapConst + HostGasGrowHeapPage + gas := g - 1 + regs := Registers{} + regs[7] = want + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitOOG { + t.Fatalf("exit = %v, want OOG", out.ExitReason) + } + if gas != g-1 { + t.Fatalf("gas changed to %d, want unchanged %d", gas, g-1) + } + if regs[7] != h { + t.Fatalf("reg7 = %d, want h=%d", regs[7], h) + } + }) + + t.Run("growthSucceeds", func(t *testing.T) { + mem := newGrowHeapTestMem() + h := NewPagedGuestMemory(mem).HeapPages() + b := NewPagedGuestMemory(mem).HeapMaxPages() + want := h + 1 + if want > b { + t.Skip("heap already at max") + } + g := HostGasGrowHeapConst + HostGasGrowHeapPage + gas := g + 10 + regs := Registers{} + regs[7] = want + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(mem)} + out := growHeap(OmegaInput{VM: vm}) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if gas != 10 { + t.Fatalf("gas = %d, want 10", gas) + } + if regs[7] != want { + t.Fatalf("reg7 = %d, want %d", regs[7], want) + } + }) +} + +func TestHeapMaxPagesReservesMajorZone(t *testing.T) { + stackStart := uint64(1<<32 - 2*ZZ - ZI) + mem := &Memory{heapLimit: stackStart} + got := NewPagedGuestMemory(mem).HeapMaxPages() + want := (stackStart - uint64(ZZ)) / uint64(ZP) + if got != want { + t.Fatalf("HeapMaxPages = %d, want %d", got, want) + } +} + +func TestMachineCapacityFullBeforeMemory(t *testing.T) { + regs := Registers{} + regs[7] = 0 + regs[8] = 1 + regs[9] = 0 + gas := Gas(HostGasMachineConst + 10_000) + m := IntegratedPVMMap{} + for i := uint64(0); i < 63; i++ { + m[i] = IntegratedPVMType{} + } + vm := &VMState{Registers: ®s, Gas: &gas, Mem: NewPagedGuestMemory(&Memory{Pages: map[uint32]*Page{}})} + out := machine(OmegaInput{ + VM: vm, + Addition: HostCallArgs{ + RefineArgs: RefineArgs{IntegratedPVMMap: m}, + }, + }) + if out.ExitReason != ExitContinue { + t.Fatalf("exit = %v, want continue", out.ExitReason) + } + if regs[7] != FULL { + t.Fatalf("reg7 = %d, want FULL(%d)", regs[7], FULL) + } +} + +func TestFetchConstantsOmitRemovedV080Fields(t *testing.T) { + types.SetTinyMode() + t.Cleanup(types.SetTinyMode) + + data := getFetchConstantsData() + // v0.8.0 fetch(0) dropped N, V, W_E, W_P (2+2+4+4 = 12 bytes vs 0.7.2). + // EncodeMany of remaining fields: count fixed-width scalars. + // Spot-check: after L (u32) comes O (u16), not N (u16 tickets). + // Layout through L: 8*3 + 2 + 4*2 + 8*4 + 2*4 + 4 = 24+2+8+32+8+4 = 78, then O at offset 78. + if len(data) < 80 { + t.Fatalf("constants too short: %d", len(data)) + } + // O = AuthPoolMaxSize as u16 LE at offset after L. + off := 8 + 8 + 8 + 2 + 4 + 4 + 8 + 8 + 8 + 8 + 2 + 2 + 2 + 2 + 4 // through L + gotO := uint16(data[off]) | uint16(data[off+1])<<8 + if gotO != uint16(types.AuthPoolMaxSize) { + t.Fatalf("after L expected O=%d, got %d (N/V still present?)", types.AuthPoolMaxSize, gotO) + } +} diff --git a/PVM/signed_unsigned_transitions_test.go b/PVM/signed_unsigned_transitions_test.go index dc307f10..17e28595 100644 --- a/PVM/signed_unsigned_transitions_test.go +++ b/PVM/signed_unsigned_transitions_test.go @@ -153,11 +153,11 @@ func TestSignExtend(t *testing.T) { {"ValidInput4", 3, 123456, 0x1e240, nil}, {"ValidInput5", 4, 123456789, 0x75bcd15, nil}, {"ValidInput6", 0, 0, 0, nil}, - {"InvalidInput1", 9, 0, 0, fmt.Errorf("invalid byte count")}, - {"InvalidInput2", -1, 0, 0, fmt.Errorf("invalid byte count")}, - {"InvalidInput3", 1, 1000, 0, fmt.Errorf("x (1000) exceeds the maximum value for 8 bytes")}, - {"InvalidInput4", 7, 11, 0, fmt.Errorf("invalid byte count")}, - {"InvalidInput5", 0, 1, 0, fmt.Errorf("x (1) exceeds the maximum value for 0 bytes")}, + {"InvalidInput1", 9, 0, 0, fmt.Errorf("invalid byte count: got 9")}, + {"InvalidInput2", -1, 0, 0, fmt.Errorf("invalid byte count: got 255")}, + {"InvalidInput3", 1, 1000, 0xffffffffffffffe8, nil}, + {"InvalidInput4", 7, 11, 0, fmt.Errorf("invalid byte count: got 7")}, + {"InvalidInput5", 0, 1, 1, nil}, } for _, tc := range testCases { diff --git a/PVM/single_initializer_test.go b/PVM/single_initializer_test.go index 4b76af7b..d472b894 100644 --- a/PVM/single_initializer_test.go +++ b/PVM/single_initializer_test.go @@ -69,8 +69,8 @@ func TestSingleInitializer(t *testing.T) { } // validate a in argument memory - stackEnd := uint32(1<<32 - 2*ZZ - ZI) - argumentPageNum := uint32(stackEnd / ZP) + argumentStart := uint32(1<<32 - ZZ - ZI) + argumentPageNum := argumentStart / ZP if page, exists := mem.Pages[argumentPageNum]; !exists { t.Errorf("Expected argument memory at page %d, but not found", argumentPageNum) } else if page.Access != MemoryReadOnly { @@ -80,6 +80,7 @@ func TestSingleInitializer(t *testing.T) { } // validate stack memory + stackEnd := uint32(1<<32 - 2*ZZ - ZI) stackStart := stackEnd - P(int(32)) for addr := stackStart; addr < stackEnd; addr += ZP { pageNum := addr / ZP diff --git a/cmd/pvmtrace/deblob_program.go b/cmd/pvmtrace/deblob_program.go index a773a3ee..5fd750ce 100644 --- a/cmd/pvmtrace/deblob_program.go +++ b/cmd/pvmtrace/deblob_program.go @@ -96,7 +96,7 @@ func tryWriteDeBlobOutput(targetFile string, programBlob []byte, codeHashHex, ou if exitReason != PVM.ExitContinue { return fmt.Errorf("SingleInitializer failed: exit=%v", exitReason) } - program, exitReason := PVM.DeBlobProgramCode(programCode) + program, exitReason := PVM.DeBlobProgramCode(programCode, 0) if exitReason != PVM.ExitContinue { return fmt.Errorf("DeBlobProgramCode failed: exit=%v", exitReason) } diff --git a/pkg/test_data/new-gas-cost-model b/pkg/test_data/new-gas-cost-model new file mode 160000 index 00000000..0b9ef892 --- /dev/null +++ b/pkg/test_data/new-gas-cost-model @@ -0,0 +1 @@ +Subproject commit 0b9ef892914a23a804a2a33a1a902fd76b8ac25b diff --git a/scripts/scan_psi_a_program_blobs.py b/scripts/scan_psi_a_program_blobs.py new file mode 100755 index 00000000..e62d8f14 --- /dev/null +++ b/scripts/scan_psi_a_program_blobs.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Scan jam-conformance fuzz JSON for large keyvals (likely PVM program blobs). + +Heuristic: hex-decoded value length > 20000 octets ≈ MetaCode preimage / program blob. +Prints candidate trace folders with unique blob digests (sha256 of value bytes). +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +from collections import defaultdict +from pathlib import Path + + +def hex_to_len(value: str) -> int: + s = value[2:] if value.startswith(("0x", "0X")) else value + return len(s) // 2 + + +def hex_to_bytes(value: str) -> bytes: + s = value[2:] if value.startswith(("0x", "0X")) else value + return bytes.fromhex(s) + + +def scan_json(path: Path, min_octets: int) -> list[bytes]: + try: + data = json.loads(path.read_text()) + except Exception as e: + print(f"skip {path}: {e}", file=sys.stderr) + return [] + blobs: list[bytes] = [] + for side in ("pre_state", "post_state"): + state = data.get(side) or {} + for kv in state.get("keyvals") or []: + val = kv.get("value") + if not isinstance(val, str): + continue + if hex_to_len(val) <= min_octets: + continue + blobs.append(hex_to_bytes(val)) + return blobs + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument( + "traces_root", + nargs="?", + default="pkg/test_data/jam-conformance/fuzz-reports/0.7.2/traces", + ) + ap.add_argument("--min-octets", type=int, default=20000) + ap.add_argument("--limit-folders", type=int, default=0, help="0 = all folders with blobs") + ap.add_argument("--json-out", default="", help="optional JSON summary path") + args = ap.parse_args() + + root = Path(args.traces_root) + if not root.is_dir(): + print(f"ERROR: traces root not found: {root}", file=sys.stderr) + return 1 + + folder_blobs: dict[str, set[str]] = defaultdict(set) + global_blobs: set[str] = set() + + for folder in sorted(p for p in root.iterdir() if p.is_dir()): + for jf in sorted(folder.glob("*.json")): + for blob in scan_json(jf, args.min_octets): + digest = hashlib.sha256(blob).hexdigest() + folder_blobs[folder.name].add(digest) + global_blobs.add(digest) + + ranked = sorted(folder_blobs.items(), key=lambda kv: (-len(kv[1]), kv[0])) + if args.limit_folders > 0: + ranked = ranked[: args.limit_folders] + + print(f"unique_program_blobs={len(global_blobs)} min_octets={args.min_octets}") + print(f"folders_with_blobs={len(folder_blobs)}") + for name, digests in ranked: + print(f"{name}\tunique_blobs={len(digests)}") + + if args.json_out: + payload = { + "unique_program_blobs": len(global_blobs), + "folders": [ + {"folder": name, "unique_blobs": sorted(digests)} + for name, digests in ranked + ], + } + Path(args.json_out).write_text(json.dumps(payload, indent=2) + "\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())