Skip to content
Snippets Groups Projects
Unverified Commit 05c73b77 authored by Silas Davis's avatar Silas Davis
Browse files

Use big.Int on Memory interface

This also checks for UInt64 containment and prevents panic from negative
offsets via a check on forwarding to the underlying implementation
(which now uses uint64s)

Signed-off-by: default avatarSilas Davis <silas@monax.io>
parent 1af7abd6
No related branches found
No related tags found
No related merge requests found
......@@ -3,6 +3,7 @@ package evm
import (
"fmt"
"math"
"math/big"
)
const (
......@@ -16,7 +17,7 @@ const (
// unlikely to make a lot of difference.
var zeroBlock []byte = make([]byte, 32)
// Interface for a bounded linear memory indexed by a single int64 parameter
// Interface for a bounded linear memory indexed by a single *big.Int parameter
// for each byte in the memory.
type Memory interface {
// Read a value from the memory store starting at offset
......@@ -26,20 +27,23 @@ type Memory interface {
//
// The value returned should be copy of any underlying memory, not a reference
// to the underlying store.
Read(offset, length int64) ([]byte, error)
Read(offset, length *big.Int) ([]byte, error)
// Write a value to the memory starting at offset (the index of the first byte
// written will equal offset). The value is provided as bytes to be written
// consecutively to the memory store. Return an error if the memory cannot be
// written or allocated.
Write(offset int64, value []byte) error
Write(offset *big.Int, value []byte) error
// Returns the current capacity of the memory. For dynamically allocating
// memory this capacity can be used as a write offset that is guaranteed to be
// unused. Solidity in particular makes this assumption when using MSIZE to
// get the current allocated memory.
Capacity() int64
Capacity() *big.Int
}
func NewDynamicMemory(initialCapacity, maximumCapacity int64) Memory {
// Get a new DynamicMemory (note that although we take a maximumCapacity of uint64 we currently
// limit the maximum to int32 at runtime because we are using a single slice which we cannot guarantee
// to be indexable above int32 or all validators
func NewDynamicMemory(initialCapacity, maximumCapacity uint64) Memory {
return &dynamicMemory{
slice: make([]byte, initialCapacity),
maximumCapacity: maximumCapacity,
......@@ -54,10 +58,22 @@ func DefaultDynamicMemoryProvider() Memory {
// array allocation via a backing slice
type dynamicMemory struct {
slice []byte
maximumCapacity int64
maximumCapacity uint64
}
func (mem *dynamicMemory) Read(offset, length int64) ([]byte, error) {
func (mem *dynamicMemory) Read(offset, length *big.Int) ([]byte, error) {
// Ensures positive and not too wide
if !offset.IsUint64() {
return nil, fmt.Errorf("offset %v does not fit inside an unsigned 64-bit integer", offset)
}
// Ensures positive and not too wide
if !length.IsUint64() {
return nil, fmt.Errorf("length %v does not fit inside an unsigned 64-bit integer", offset)
}
return mem.read(offset.Uint64(), length.Uint64())
}
func (mem *dynamicMemory) read(offset, length uint64) ([]byte, error) {
capacity := offset + length
err := mem.ensureCapacity(capacity)
if err != nil {
......@@ -68,8 +84,16 @@ func (mem *dynamicMemory) Read(offset, length int64) ([]byte, error) {
return value, nil
}
func (mem *dynamicMemory) Write(offset int64, value []byte) error {
capacity := offset + int64(len(value))
func (mem *dynamicMemory) Write(offset *big.Int, value []byte) error {
// Ensures positive and not too wide
if !offset.IsUint64() {
return fmt.Errorf("offset %v does not fit inside an unsigned 64-bit integer", offset)
}
return mem.write(offset.Uint64(), value)
}
func (mem *dynamicMemory) write(offset uint64, value []byte) error {
capacity := offset + uint64(len(value))
err := mem.ensureCapacity(capacity)
if err != nil {
return err
......@@ -78,18 +102,21 @@ func (mem *dynamicMemory) Write(offset int64, value []byte) error {
return nil
}
func (mem *dynamicMemory) Capacity() int64 {
return int64(len(mem.slice))
func (mem *dynamicMemory) Capacity() *big.Int {
return big.NewInt(int64(len(mem.slice)))
}
// Ensures the current memory store can hold newCapacity. Will only grow the
// memory (will not shrink).
func (mem *dynamicMemory) ensureCapacity(newCapacity int64) error {
func (mem *dynamicMemory) ensureCapacity(newCapacity uint64) error {
// Maximum length of a slice that allocates memory is the same as the native int max size
// We could rethink this limit, but we don't want different validators to disagree on
// transaction validity so we pick the lowest common denominator
if newCapacity > math.MaxInt32 {
// If we ever did want more than an int32 of space then we would need to
// maintain multiple pages of memory
return fmt.Errorf("cannot address memory beyond a maximum index "+
"of Int32 type (%v bytes)", math.MaxInt32)
"with int32 width (%v bytes)", math.MaxInt32)
}
newCapacityInt := int(newCapacity)
// We're already big enough so return
......
......@@ -3,14 +3,16 @@ package evm
import (
"testing"
"math/big"
"github.com/stretchr/testify/assert"
)
// Test static memory allocation with maximum == initial capacity - memory should not grow
func TestDynamicMemory_StaticAllocation(t *testing.T) {
mem := NewDynamicMemory(4, 4).(*dynamicMemory)
mem.Write(0, []byte{1})
mem.Write(1, []byte{0, 0, 1})
mem.Write(big.NewInt(0), []byte{1})
mem.Write(big.NewInt(1), []byte{0, 0, 1})
assert.Equal(t, []byte{1, 0, 0, 1}, mem.slice)
assert.Equal(t, 4, cap(mem.slice), "Slice capacity should not grow")
}
......@@ -18,31 +20,31 @@ func TestDynamicMemory_StaticAllocation(t *testing.T) {
// Test reading beyond the current capacity - memory should grow
func TestDynamicMemory_ReadAhead(t *testing.T) {
mem := NewDynamicMemory(4, 8).(*dynamicMemory)
value, err := mem.Read(2, 4)
value, err := mem.Read(big.NewInt(2), big.NewInt(4))
assert.NoError(t, err)
// Value should be size requested
assert.Equal(t, []byte{0, 0, 0, 0}, value)
// Slice should have grown to that plus offset
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0}, mem.slice)
value, err = mem.Read(2, 6)
value, err = mem.Read(big.NewInt(2), big.NewInt(6))
assert.NoError(t, err)
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0}, value)
assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0}, mem.slice)
// Check cannot read out of bounds
_, err = mem.Read(2, 7)
_, err = mem.Read(big.NewInt(2), big.NewInt(7))
assert.Error(t, err)
}
// Test writing beyond the current capacity - memory should grow
func TestDynamicMemory_WriteAhead(t *testing.T) {
mem := NewDynamicMemory(4, 8).(*dynamicMemory)
err := mem.Write(4, []byte{1, 2, 3, 4})
err := mem.Write(big.NewInt(4), []byte{1, 2, 3, 4})
assert.NoError(t, err)
assert.Equal(t, []byte{0, 0, 0, 0, 1, 2, 3, 4}, mem.slice)
err = mem.Write(4, []byte{1, 2, 3, 4, 5})
err = mem.Write(big.NewInt(4), []byte{1, 2, 3, 4, 5})
assert.Error(t, err)
}
......@@ -56,21 +58,21 @@ to describe: his sensation of a few hours before on Grattan Bridge, for
example. If he could get back again into that mood....`)
// Write the bytes
offset := 0x1000000
err := mem.Write(int64(offset), bytesToWrite)
offset := big.NewInt(0x1000000)
err := mem.Write(offset, bytesToWrite)
assert.NoError(t, err)
assert.Equal(t, append(make([]byte, offset), bytesToWrite...), mem.slice)
assert.Equal(t, offset+len(bytesToWrite), len(mem.slice))
assert.Equal(t, append(make([]byte, offset.Uint64()), bytesToWrite...), mem.slice)
assert.Equal(t, offset.Uint64()+uint64(len(bytesToWrite)), uint64(len(mem.slice)))
// Read them back
value, err := mem.Read(int64(offset), int64(len(bytesToWrite)))
value, err := mem.Read(offset, big.NewInt(int64(len(bytesToWrite))))
assert.NoError(t, err)
assert.Equal(t, bytesToWrite, value)
}
func TestDynamicMemory_ZeroInitialMemory(t *testing.T) {
mem := NewDynamicMemory(0, 16).(*dynamicMemory)
err := mem.Write(4, []byte{1, 2, 3, 4})
err := mem.Write(big.NewInt(4), []byte{1, 2, 3, 4})
assert.NoError(t, err)
assert.Equal(t, []byte{0, 0, 0, 0, 1, 2, 3, 4}, mem.slice)
}
......@@ -78,15 +80,15 @@ func TestDynamicMemory_ZeroInitialMemory(t *testing.T) {
func TestDynamicMemory_Capacity(t *testing.T) {
mem := NewDynamicMemory(1, 0x10000000).(*dynamicMemory)
assert.Equal(t, int64(1), mem.Capacity())
assert.Equal(t, big.NewInt(1), mem.Capacity())
capacity := int64(1234)
err := mem.ensureCapacity(capacity)
capacity := big.NewInt(1234)
err := mem.ensureCapacity(capacity.Uint64())
assert.NoError(t, err)
assert.Equal(t, capacity, mem.Capacity())
capacity = int64(123456789)
err = mem.ensureCapacity(capacity)
capacity = big.NewInt(123456789)
err = mem.ensureCapacity(capacity.Uint64())
assert.NoError(t, err)
assert.Equal(t, capacity, mem.Capacity())
......
......@@ -425,7 +425,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
if useGasNegative(gas, GasSha3, &err) {
return nil, err
}
offset, size := stack.Pop64(), stack.Pop64()
offset, size := stack.PopBigInt(), stack.PopBigInt()
data, memErr := memory.Read(offset, size)
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
......@@ -482,7 +482,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf(" => %d\n", len(input))
case CALLDATACOPY: // 0x37
memOff := stack.Pop64()
memOff := stack.PopBigInt()
inputOff := stack.Pop64()
length := stack.Pop64()
data, ok := subslice(input, inputOff, length)
......@@ -502,7 +502,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf(" => %d\n", l)
case CODECOPY: // 0x39
memOff := stack.Pop64()
memOff := stack.PopBigInt()
codeOff := stack.Pop64()
length := stack.Pop64()
data, ok := subslice(code, codeOff, length)
......@@ -558,7 +558,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
return nil, firstErr(err, ErrUnknownAddress)
}
code := acc.Code()
memOff := stack.Pop64()
memOff := stack.PopBigInt()
codeOff := stack.Pop64()
length := stack.Pop64()
data, ok := subslice(code, codeOff, length)
......@@ -599,8 +599,8 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf(" => 0x%X\n", popped)
case MLOAD: // 0x51
offset := stack.Pop64()
data, memErr := memory.Read(offset, 32)
offset := stack.PopBigInt()
data, memErr := memory.Read(offset, BigWord256Length)
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
return nil, firstErr(err, ErrMemoryOutOfBounds)
......@@ -609,7 +609,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf(" => 0x%X @ 0x%X\n", data, offset)
case MSTORE: // 0x52
offset, data := stack.Pop64(), stack.Pop()
offset, data := stack.PopBigInt(), stack.Pop()
memErr := memory.Write(offset, data.Bytes())
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
......@@ -618,7 +618,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf(" => 0x%X @ 0x%X\n", data, offset)
case MSTORE8: // 0x53
offset, val := stack.Pop64(), byte(stack.Pop64()&0xFF)
offset, val := stack.PopBigInt(), byte(stack.Pop64()&0xFF)
memErr := memory.Write(offset, []byte{val})
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
......@@ -670,7 +670,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
// free memory to be allocated for it if a subsequent MSTORE is made to
// this offset.
capacity := memory.Capacity()
stack.Push64(capacity)
stack.PushBigInt(capacity)
vm.Debugf(" => 0x%X\n", capacity)
case GAS: // 0x5A
......@@ -707,7 +707,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
case LOG0, LOG1, LOG2, LOG3, LOG4:
n := int(op - LOG0)
topics := make([]Word256, n)
offset, size := stack.Pop64(), stack.Pop64()
offset, size := stack.PopBigInt(), stack.PopBigInt()
for i := 0; i < n; i++ {
topics[i] = stack.Pop()
}
......@@ -733,7 +733,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
return nil, ErrPermission{"create_contract"}
}
contractValue := stack.PopU64()
offset, size := stack.Pop64(), stack.Pop64()
offset, size := stack.PopBigInt(), stack.PopBigInt()
input, memErr := memory.Read(offset, size)
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
......@@ -773,8 +773,8 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
if op != DELEGATECALL {
value = stack.PopU64()
}
inOffset, inSize := stack.Pop64(), stack.Pop64() // inputs
retOffset, retSize := stack.Pop64(), stack.Pop64() // outputs
inOffset, inSize := stack.PopBigInt(), stack.PopBigInt() // inputs
retOffset, retSize := stack.PopBigInt(), stack.Pop64() // outputs
vm.Debugf(" => %X\n", addr)
// Get the arguments from the memory
......@@ -868,7 +868,7 @@ func (vm *VM) call(caller, callee acm.MutableAccount, code, input []byte, value
vm.Debugf("resume %s (%v)\n", callee.Address(), gas)
case RETURN: // 0xF3
offset, size := stack.Pop64(), stack.Pop64()
offset, size := stack.PopBigInt(), stack.PopBigInt()
output, memErr := memory.Read(offset, size)
if memErr != nil {
vm.Debugf(" => Memory err: %s", memErr)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment