Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Proof mode for the VM #34

Merged
merged 15 commits into from
Sep 10, 2023
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
bin/
.idea/
.python-version

.DS_Store
vendor/
.idea/
39 changes: 35 additions & 4 deletions pkg/vm/memory/memory_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,45 @@ type MemoryManager struct {
Memory *Memory
}

func CreateMemoryManager() (*MemoryManager, error) {
// Creates a new memory manager
func CreateMemoryManager() *MemoryManager {
memory := InitializeEmptyMemory()

return &MemoryManager{
Memory: memory,
}, nil
}
}

func (mm *MemoryManager) GetByteCodeAt(segmentIndex uint64, offset uint64) *f.Element {
return nil
// It returns all segments in memory but relocated as a single segment
// Each element is a pointer to a field element, if the cell was not accessed,
// nil is stored instead
func (mm *MemoryManager) RelocateMemory() []*f.Element {
maxMemoryUsed := 0
// segmentsOffsets[0] = 0
// segmentsOffsets[1] = len(segment[0])
// segmentsOffsets[N] = len(segment[n - 1]) + sum of segmentsOffsets[n - i] for i in [0, n-1]
segmentsOffsets := make([]uint64, uint64(len(mm.Memory.Segments))+1)
for i, segment := range mm.Memory.Segments {
maxMemoryUsed += len(segment.Data)
segmentsOffsets[i+1] = segmentsOffsets[i] + uint64(len(segment.Data))
}

relocatedMemory := make([]*f.Element, maxMemoryUsed)
for i, segment := range mm.Memory.Segments {
for j, cell := range segment.Data {
var felt *f.Element
if !cell.Accessed {
continue
}
if cell.Value.IsAddress() {
felt = cell.Value.address.Relocate(segmentsOffsets)
} else {
felt = cell.Value.felt
}

relocatedMemory[segmentsOffsets[i]+uint64(j)] = felt
}
}

return relocatedMemory
}
149 changes: 149 additions & 0 deletions pkg/vm/memory/memory_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package memory

import (
"fmt"
"testing"

f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
"github.com/stretchr/testify/require"
)

func TestMemoryRelocationWithFelt(t *testing.T) {
// segment 0: [2, -, -, 3]
// segment 3: [5, -, 7, -, 11, 13]
// relocated: [2, -, -, 3, 5, -, 7, -, 11, 13]

manager := CreateMemoryManager()
updateMemoryWithValues(
manager.Memory,
[]memoryWrite{
// segment zero
{0, 0, uint64(2)},
{0, 3, uint64(3)},
// segment three
{3, 0, uint64(5)},
{3, 2, uint64(7)},
{3, 4, uint64(11)},
{3, 5, uint64(13)},
},
)

res := manager.RelocateMemory()

expected := []*f.Element{
// segment zero
new(f.Element).SetUint64(2),
nil,
nil,
new(f.Element).SetUint64(3),
// segment three
new(f.Element).SetUint64(5),
nil,
new(f.Element).SetUint64(7),
nil,
new(f.Element).SetUint64(11),
new(f.Element).SetUint64(13),
}

require.Equal(t, len(expected), len(res))
require.Equal(t, expected, res)
}

func TestMemoryRelocationWithAddress(t *testing.T) {
// segment 0: [-, 1, -, 1:5] (4)
// segment 1: [1, 4:3, 7, -, -, 13] (10)
// segment 2: [0:1] (11)
// segment 3: [2:0] (12)
// segment 4: [0:0, 1:1, 1:5, 15] (16)
// relocated: [
// zero: -, 1, -, 9,
// one: 1, 15, 7, -, -, 13,
// two: 1,
// three: 10,
// four: 0, 5, 9, 15,
// ]

manager := CreateMemoryManager()
updateMemoryWithValues(
manager.Memory,
[]memoryWrite{
// segment zero
{0, 1, uint64(1)},
{0, 3, NewMemoryAddress(1, 5)},
// segment one
{1, 0, uint64(1)},
{1, 1, NewMemoryAddress(4, 3)},
{1, 2, uint64(7)},
{1, 5, uint64(13)},
// segment two
{2, 0, NewMemoryAddress(0, 1)},
// segment three
{3, 0, NewMemoryAddress(2, 0)},
// segment four
{4, 0, NewMemoryAddress(0, 0)},
{4, 1, NewMemoryAddress(1, 1)},
{4, 2, NewMemoryAddress(1, 5)},
{4, 3, uint64(15)},
},
)

res := manager.RelocateMemory()

expected := []*f.Element{
// segment zero
nil,
new(f.Element).SetUint64(1),
nil,
new(f.Element).SetUint64(9),
// segment one
new(f.Element).SetUint64(1),
new(f.Element).SetUint64(15),
new(f.Element).SetUint64(7),
nil,
nil,
new(f.Element).SetUint64(13),
// segment two
new(f.Element).SetUint64(1),
// segment three
new(f.Element).SetUint64(10),
// segment 4
new(f.Element).SetUint64(0),
new(f.Element).SetUint64(5),
new(f.Element).SetUint64(9),
new(f.Element).SetUint64(15),
}

require.Equal(t, len(expected), len(res))
require.Equal(t, expected, res)
}

type memoryWrite struct {
SegmentIndex uint64
Offset uint64
Value any
}

func updateMemoryWithValues(memory *Memory, valuesToWrite []memoryWrite) {
var max_segment uint64 = 0
for _, toWrite := range valuesToWrite {
// wrap any inside a memory value
val, err := MemoryValueFromAny(toWrite.Value)
if err != nil {
panic(err)
}

// if the destination segment does not exist, create it
for toWrite.SegmentIndex >= max_segment {
max_segment += 1
memory.AllocateEmptySegment()
}

fmt.Println("c")
// write the memory val
err = memory.Write(toWrite.SegmentIndex, toWrite.Offset, val)
if err != nil {
panic(err)
}

}
}
22 changes: 9 additions & 13 deletions pkg/vm/memory/memory_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ func (address *MemoryAddress) Sub(lhs *MemoryAddress, rhs any) (*MemoryAddress,
}
}

func (address *MemoryAddress) Relocate(segmentsOffset []uint64) *f.Element {
// no risk overflow because this sizes exists in actual Memory
// so if by chance the uint64 addition overflowed, then we have
// a machine with more than 2**64 bytes of memory (quite a lot!)
return new(f.Element).SetUint64(segmentsOffset[address.SegmentIndex] + address.Offset)
}

func (address MemoryAddress) String() string {
return fmt.Sprintf(
"Memory Address: segment: %d, offset: %d", address.SegmentIndex, address.Offset,
Expand Down Expand Up @@ -137,6 +144,8 @@ func MemoryValueFromSegmentAndOffset[T constraints.Integer](segmentIndex, offset

func MemoryValueFromAny(anyType any) (*MemoryValue, error) {
switch t := anyType.(type) {
case uint64:
return MemoryValueFromInt(anyType.(uint64)), nil
case *f.Element:
return MemoryValueFromFieldElement(anyType.(*f.Element)), nil
case *MemoryAddress:
Expand Down Expand Up @@ -279,16 +288,3 @@ func (mv *MemoryValue) Uint64() (uint64, error) {

return mv.felt.Uint64(), nil
}

// Note: Commenting this function since relocation is possibly going to look
// different.
// Given a map of segment relocation, update a memory address location
//func (r *MemoryAddress) Relocate(r1 *MemoryAddress, segmentsOffsets *map[uint64]*MemoryAddress) (*MemoryAddress, error) {
// if (*segmentsOffsets)[r1.SegmentIndex] == nil {
// return nil, fmt.Errorf("missing segment %d relocation rule", r.SegmentIndex)
// }
//
// r, err := r.Add((*segmentsOffsets)[r1.SegmentIndex], &MemoryAddress{0, r1.Offset})
//
// return r, err
//}
64 changes: 47 additions & 17 deletions pkg/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,58 @@ type Context struct {
Pc uint64
}

// relocates pc, ap and fp to be their real address value
// that is, pc + 0, ap + programSegmentOffset, fp + programSegmentOffset
func (ctx *Context) Relocate(executionSegmentOffset uint64) {
ctx.Ap += executionSegmentOffset
ctx.Fp += executionSegmentOffset
}

// This type represents the current execution context of the vm
type VirtualMachineConfig struct {
Trace bool
// Todo(rodro): Update this property to include all builtins
Builtins bool
// If true, the vm outputs the trace and the relocated memory at the end of execution
ProofMode bool
}

type VirtualMachine struct {
Context Context
MemoryManager *mem.MemoryManager
Step uint64
Config VirtualMachineConfig
Trace []Context
config VirtualMachineConfig
}

// NewVirtualMachine creates a VM from the program bytecode using a specified config.
func NewVirtualMachine(programBytecode []*f.Element, config VirtualMachineConfig) (*VirtualMachine, error) {
manager, err := mem.CreateMemoryManager()
if err != nil {
return nil, fmt.Errorf("error creating new virtual machine: %w", err)
}

// Initialize memory with to initial segments:
// the first one for the program segment and
// the second one to keep track of the execution
manager := mem.CreateMemoryManager()
// 0 (programSegment) <- segment where the bytecode is stored
_, err = manager.Memory.AllocateSegment(programBytecode)
_, err := manager.Memory.AllocateSegment(programBytecode)
if err != nil {
return nil, fmt.Errorf("error loading bytecode: %w", err)
}

// 1 (executionSegment) <- segment where ap and fp move around
manager.Memory.Segments = append(manager.Memory.Segments, mem.EmptySegmentWithCapacity(10))
manager.Memory.AllocateEmptySegment()

// Initialize the trace if necesary
var trace []Context
if config.ProofMode {
trace = make([]Context, 0)
}

return &VirtualMachine{
Context: Context{Fp: 0, Ap: 0, Pc: 0},
Step: 0,
MemoryManager: manager,
Config: config,
Trace: trace,
config: config,
}, nil
}

// todo(rodro): add a cache mechanism for not decoding the same instruction twice

// todo(rodro): how to know when te execute a hint or normal instruction

func (vm *VirtualMachine) RunStep(hintRunner HintRunner) error {
// Run hint
err := hintRunner.RunHint(vm)
Expand All @@ -93,6 +103,11 @@ func (vm *VirtualMachine) RunStep(hintRunner HintRunner) error {
return fmt.Errorf("cannot decode step at %d: %w", vm.Context.Pc, err)
}

// store the trace before state change
if vm.config.ProofMode {
vm.Trace = append(vm.Trace, vm.Context)
ElijahVlasov marked this conversation as resolved.
Show resolved Hide resolved
}

err = vm.RunInstruction(instruction)
if err != nil {
return fmt.Errorf("cannot run step at %d: %w", vm.Context.Pc, err)
Expand All @@ -107,8 +122,6 @@ func (vm *VirtualMachine) RunStepAt(hinter HintRunner, pc uint64) error {
}

func (vm *VirtualMachine) RunInstruction(instruction *Instruction) error {
// todo(rodro): any OffOpX can be negative, a better math system is required due to
// substraction. Also it will need to handle overflows and underflows
dstCell, err := vm.getCellDst(instruction)
if err != nil {
return err
Expand Down Expand Up @@ -162,6 +175,23 @@ func (vm *VirtualMachine) RunInstruction(instruction *Instruction) error {
return nil
}

// It returns the current trace entry, the public memory, and the occurrence of an error
func (vm *VirtualMachine) Proof() ([]Context, []*f.Element, error) {
if !vm.config.ProofMode {
return nil, nil, fmt.Errorf("cannot get proof if proof mode is off")
}

totalBytecode := vm.MemoryManager.Memory.Segments[ProgramSegment].Len()
for i := range vm.Trace {
vm.Trace[i].Relocate(totalBytecode)
}

// after that, get the relocated memory
relocatedMemory := vm.MemoryManager.RelocateMemory()

return vm.Trace, relocatedMemory, nil
}

func (vm *VirtualMachine) getCellDst(instruction *Instruction) (*mem.Cell, error) {
var dstRegister uint64
if instruction.DstRegister == Ap {
Expand Down
Loading