cmd/vm: code segmenting

This commit is contained in:
Jeffrey Wilcke 2015-06-23 19:04:05 +02:00
parent be935bff84
commit cb7cd07ed0
9 changed files with 237 additions and 81 deletions

View file

@ -22,79 +22,153 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log"
"math/big" "math/big"
"os" "os"
"runtime" "runtime"
"time" "time"
"github.com/codegangsta/cli"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/logger"
) )
var ( var (
code = flag.String("code", "", "evm code") app *cli.App
loglevel = flag.Int("log", 4, "log level") DebugFlag = cli.BoolFlag{
gas = flag.String("gas", "1000000000", "gas amount") Name: "debug",
price = flag.String("price", "0", "gas price") Usage: "output full trace logs",
value = flag.String("value", "0", "tx value") }
dump = flag.Bool("dump", false, "dump state after run") CodeFlag = cli.StringFlag{
data = flag.String("data", "", "data") Name: "code",
Usage: "EVM code",
}
GasFlag = cli.StringFlag{
Name: "gas",
Usage: "gas limit for the evm",
Value: "10000000000",
}
PriceFlag = cli.StringFlag{
Name: "price",
Usage: "price set for the evm",
Value: "0",
}
ValueFlag = cli.StringFlag{
Name: "value",
Usage: "value set for the evm",
Value: "0",
}
DumpFlag = cli.BoolFlag{
Name: "dump",
Usage: "dumps the state after the run",
}
InputFlag = cli.StringFlag{
Name: "input",
Usage: "input for the EVM",
}
SysStatFlag = cli.BoolFlag{
Name: "sysstat",
Usage: "display system stats",
}
) )
func perr(v ...interface{}) { func init() {
fmt.Println(v...) app = utils.NewApp("0.2", "the evm command line interface")
//os.Exit(1) app.Flags = []cli.Flag{
DebugFlag,
SysStatFlag,
CodeFlag,
GasFlag,
PriceFlag,
ValueFlag,
DumpFlag,
InputFlag,
}
app.Action = run
} }
func main() { func run(ctx *cli.Context) {
flag.Parse() vm.Debug = ctx.GlobalBool(DebugFlag.Name)
logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, log.LstdFlags, logger.LogLevel(*loglevel)))
vm.Debug = true
db, _ := ethdb.NewMemDatabase() db, _ := ethdb.NewMemDatabase()
statedb := state.New(common.Hash{}, db) statedb := state.New(common.Hash{}, db)
sender := statedb.CreateAccount(common.StringToAddress("sender")) sender := statedb.CreateAccount(common.StringToAddress("sender"))
receiver := statedb.CreateAccount(common.StringToAddress("receiver")) receiver := statedb.CreateAccount(common.StringToAddress("receiver"))
receiver.SetCode(common.Hex2Bytes(*code)) receiver.SetCode(common.Hex2Bytes(ctx.GlobalString(CodeFlag.Name)))
vmenv := NewEnv(statedb, common.StringToAddress("evmuser"), common.Big(*value)) vmenv := NewEnv(statedb, common.StringToAddress("evmuser"), common.Big(ctx.GlobalString(ValueFlag.Name)))
tstart := time.Now() tstart := time.Now()
ret, e := vmenv.Call(sender, receiver.Address(), common.Hex2Bytes(*data), common.Big(*gas), common.Big(*price), common.Big(*value)) vmstart := time.Now()
ret, e := vmenv.Call(
sender,
receiver.Address(),
common.Hex2Bytes(ctx.GlobalString(InputFlag.Name)),
common.Big(ctx.GlobalString(GasFlag.Name)),
common.Big(ctx.GlobalString(PriceFlag.Name)),
common.Big(ctx.GlobalString(ValueFlag.Name)),
)
logger.Flush()
if e != nil { if e != nil {
perr(e) fmt.Println(e)
os.Exit(1)
} }
fmt.Println("no analysis", time.Since(vmstart))
if *dump { vmstart = time.Now()
ret, e = vmenv.Call(
sender,
receiver.Address(),
common.Hex2Bytes(ctx.GlobalString(InputFlag.Name)),
common.Big(ctx.GlobalString(GasFlag.Name)),
common.Big(ctx.GlobalString(PriceFlag.Name)),
common.Big(ctx.GlobalString(ValueFlag.Name)),
)
if e != nil {
fmt.Println(e)
os.Exit(1)
}
fmt.Println("with analysis", time.Since(vmstart))
if ctx.GlobalBool(DumpFlag.Name) {
fmt.Println(string(statedb.Dump())) fmt.Println(string(statedb.Dump()))
} }
vm.StdErrFormat(vmenv.StructLogs()) vm.StdErrFormat(vmenv.StructLogs())
var mem runtime.MemStats if ctx.GlobalBool(SysStatFlag.Name) {
runtime.ReadMemStats(&mem) var mem runtime.MemStats
fmt.Printf("vm took %v\n", time.Since(tstart)) runtime.ReadMemStats(&mem)
fmt.Printf(`alloc: %d fmt.Printf("vm took %v\n", time.Since(tstart))
fmt.Printf(`alloc: %d
tot alloc: %d tot alloc: %d
no. malloc: %d no. malloc: %d
heap alloc: %d heap alloc: %d
heap objs: %d heap objs: %d
num gc: %d num gc: %d
`, mem.Alloc, mem.TotalAlloc, mem.Mallocs, mem.HeapAlloc, mem.HeapObjects, mem.NumGC) `, mem.Alloc, mem.TotalAlloc, mem.Mallocs, mem.HeapAlloc, mem.HeapObjects, mem.NumGC)
}
fmt.Printf("%x\n", ret) fmt.Printf("OUT: 0x%x\n", ret)
for codehash, chunks := range vm.Chunks {
for _, chunk := range chunks {
fmt.Printf("%x => %v\n", codehash, chunk)
}
}
}
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} }
type VMEnv struct { type VMEnv struct {

View file

@ -243,7 +243,7 @@ func (self *StateObject) AddGas(gas, price *big.Int) {
} }
func (self *StateObject) Copy() *StateObject { func (self *StateObject) Copy() *StateObject {
stateObject := NewStateObject(self.Address(), self.db) stateObject := &StateObject{db: self.db, address: self.address, balance: new(big.Int), gasPool: new(big.Int), dirty: true}
stateObject.balance.Set(self.balance) stateObject.balance.Set(self.balance)
stateObject.codeHash = common.CopyBytes(self.codeHash) stateObject.codeHash = common.CopyBytes(self.codeHash)
stateObject.nonce = self.nonce stateObject.nonce = self.nonce

View file

@ -26,16 +26,25 @@ type Context struct {
Args []byte Args []byte
} }
var dests destinations
func init() {
dests = make(destinations)
}
// Create a new context for the given data items. // Create a new context for the given data items.
func NewContext(caller ContextRef, object ContextRef, value, gas, price *big.Int) *Context { func NewContext(caller ContextRef, object ContextRef, value, gas, price *big.Int) *Context {
c := &Context{caller: caller, self: object, Args: nil} c := &Context{caller: caller, self: object, Args: nil}
if parent, ok := caller.(*Context); ok { c.jumpdests = dests
// Reuse JUMPDEST analysis from parent context if available. /*
c.jumpdests = parent.jumpdests if parent, ok := caller.(*Context); ok {
} else { // Reuse JUMPDEST analysis from parent context if available.
c.jumpdests = make(destinations) c.jumpdests = parent.jumpdests
} } else {
c.jumpdests = make(destinations)
}
*/
// Gas should be a pointer so it can safely be reduced through the run // Gas should be a pointer so it can safely be reduced through the run
// This pointer will be off the state transition // This pointer will be off the state transition

View file

@ -38,8 +38,8 @@ func baseCheck(op OpCode, stack *stack, gas *big.Int) error {
return err return err
} }
if r.stackPush > 0 && len(stack.data)-r.stackPop+r.stackPush > int(params.StackLimit.Int64())+1 { if r.stackPush > 0 && stack.len()-r.stackPop+r.stackPush > int(params.StackLimit.Int64()) {
return fmt.Errorf("stack limit reached %d (%d)", len(stack.data), params.StackLimit.Int64()) return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit.Int64())
} }
gas.Add(gas, r.gas) gas.Add(gas, r.gas)

View file

@ -332,3 +332,15 @@ func (o OpCode) String() string {
return str return str
} }
func isDynamic(op OpCode) bool {
switch op {
case CREATE, CALL, CALLCODE, JUMP, JUMPI, SUICIDE, STOP, RETURN, EXTCODECOPY, CODECOPY, MSTORE, MSTORE8:
return true
}
if _, ok := opCodeToString[op]; !ok {
return true
}
return false
}

View file

@ -11,32 +11,26 @@ func newstack() *stack {
type stack struct { type stack struct {
data []*big.Int data []*big.Int
ptr int
} }
func (st *stack) Data() []*big.Int { func (st *stack) Data() []*big.Int {
return st.data[:st.ptr] return st.data
} }
func (st *stack) push(d *big.Int) { func (st *stack) push(d *big.Int) {
// NOTE push limit (1024) is checked in baseCheck // NOTE push limit (1024) is checked in baseCheck
stackItem := new(big.Int).Set(d) stackItem := new(big.Int).Set(d)
if len(st.data) > st.ptr { st.data = append(st.data, stackItem)
st.data[st.ptr] = stackItem
} else {
st.data = append(st.data, stackItem)
}
st.ptr++
} }
func (st *stack) pop() (ret *big.Int) { func (st *stack) pop() (ret *big.Int) {
st.ptr-- ret = st.data[len(st.data)-1]
ret = st.data[st.ptr] st.data = st.data[:len(st.data)-1]
return return
} }
func (st *stack) len() int { func (st *stack) len() int {
return st.ptr return len(st.data)
} }
func (st *stack) swap(n int) { func (st *stack) swap(n int) {

View file

@ -8,8 +8,27 @@ import (
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/hashicorp/golang-lru"
) )
type segment struct {
cstart, cend uint64
msize uint64
gas *big.Int
}
func (c *segment) String() string {
return fmt.Sprintf("{%d %d %d %v}", c.cstart, c.cend, c.msize, c.gas)
}
type codeSegments map[uint64]*segment
var segments *lru.Cache
func init() {
segments, _ = lru.New(256)
}
// Vm implements VirtualMachine // Vm implements VirtualMachine
type Vm struct { type Vm struct {
env Environment env Environment
@ -44,14 +63,19 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
value = context.value value = context.value
price = context.Price price = context.Price
op OpCode // current opcode op OpCode // current opcode
codehash = crypto.Sha3Hash(code) // codehash is used when doing jump dest caching codehash = crypto.FnvHash(code) // codehash is used when doing jump dest caching
mem = NewMemory() // bound memory mem = NewMemory() // bound memory
stack = newstack() // local stack stack = newstack() // local stack
statedb = self.env.State() // current state statedb = self.env.State() // current state
// For optimisation reason we're using uint64 as the program counter. // For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC to be uint256. Pratically much less so feasible. // It's theoretically possible to go above 2^64. The YP defines the PC to be uint256. Pratically much less so feasible.
pc = uint64(0) // program counter pc = uint64(0) // program counter
ppc = uint64(0) // previous program counter
csegment *segment // current code segment
csegments codeSegments // program segments
nopay uint64 = 0
disableOptim bool
// jump evaluates and checks whether the given jump destination is a valid one // jump evaluates and checks whether the given jump destination is a valid one
// if valid move the `pc` otherwise return an error. // if valid move the `pc` otherwise return an error.
@ -69,13 +93,15 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
newMemSize *big.Int newMemSize *big.Int
cost *big.Int cost *big.Int
) )
if c, ok := segments.Get(codehash); ok {
csegments = c.(codeSegments)
} else {
csegments = make(codeSegments)
segments.Add(codehash, csegments)
}
// User defer pattern to check for an error and, based on the error being nil or not, use all gas and return. // User defer pattern to check for an error and, based on the error being nil or not, use all gas and return.
defer func() { defer func() {
if self.After != nil {
self.After(context, err)
}
if err != nil { if err != nil {
self.log(pc, op, context.Gas, cost, mem, stack, context, err) self.log(pc, op, context.Gas, cost, mem, stack, context, err)
@ -84,6 +110,13 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
ret = context.Return(nil) ret = context.Return(nil)
} }
if csegment != nil && err != nil && csegment.cend == 0 {
// delete this tracking segment. We couldn't fully determine the stats
delete(csegments, csegment.cstart)
} else if csegment != nil && err == nil && csegment.cend == 0 {
csegment.cend = pc
}
}() }()
if context.CodeAddr != nil { if context.CodeAddr != nil {
@ -98,31 +131,57 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
} }
for { for {
// The base for all big integer arithmetic // Get the current operation location of pc
base := new(big.Int)
// Get the memory location of pc
op = context.GetOp(pc) op = context.GetOp(pc)
if isDynamic(op) && csegment != nil {
csegment.cend = ppc
csegment = nil
nopay = 0
} else if csegment == nil && pc != 0 {
// allocate a new segment for tracking information
csegment = &segment{pc, 0, 0, new(big.Int)}
csegments[pc] = csegment
} else {
if segment, ok := csegments[pc]; ok && !disableOptim {
nopay = segment.cend
if !context.UseGas(segment.gas) {
context.UseGas(context.Gas)
// calculate the new memory size and gas price for the current executing opcode return context.Return(nil), OutOfGasError{}
newMemSize, cost, err = self.calculateGasAndSize(context, caller, op, statedb, mem, stack) }
if err != nil { }
return nil, err
} }
// Use the calculated gas. When insufficient gas is present, use all gas and return an if pc > nopay || pc == 0 || disableOptim {
// Out Of Gas error // calculate the new memory size and gas price for the current executing opcode
if !context.UseGas(cost) { newMemSize, cost, err = self.calculateGasAndSize(context, caller, op, statedb, mem, stack)
if err != nil {
return nil, err
}
context.UseGas(context.Gas) // Use the calculated gas. When insufficient gas is present, use all gas and return an
// Out Of Gas error
if !context.UseGas(cost) {
context.UseGas(context.Gas)
return context.Return(nil), OutOfGasError{} return context.Return(nil), OutOfGasError{}
}
// Resize the memory calculated previously
mem.Resize(newMemSize.Uint64())
if csegment != nil {
csegment.msize = uint64(len(mem.store))
csegment.gas.Add(csegment.gas, cost)
}
} }
// Resize the memory calculated previously
mem.Resize(newMemSize.Uint64())
// Add a log message // Add a log message
self.log(pc, op, context.Gas, cost, mem, stack, context, nil) self.log(pc, op, context.Gas, cost, mem, stack, context, nil)
ppc = pc
// The base for all big integer arithmetic
base := new(big.Int)
switch op { switch op {
case ADD: case ADD:
x, y := stack.pop(), stack.pop() x, y := stack.pop(), stack.pop()
@ -348,7 +407,7 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
case SHA3: case SHA3:
offset, size := stack.pop(), stack.pop() offset, size := stack.pop(), stack.pop()
data := crypto.Sha3(mem.Get(offset.Int64(), size.Int64())) data := crypto.Sha3(mem.GetPtr(offset.Int64(), size.Int64()))
stack.push(common.BigD(data)) stack.push(common.BigD(data))
@ -491,7 +550,7 @@ func (self *Vm) Run(context *Context, input []byte) (ret []byte, err error) {
case MLOAD: case MLOAD:
offset := stack.pop() offset := stack.pop()
val := common.BigD(mem.Get(offset.Int64(), 32)) val := common.BigD(mem.GetPtr(offset.Int64(), 32))
stack.push(val) stack.push(val)
case MSTORE: case MSTORE:

View file

@ -8,6 +8,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"hash/fnv"
"io" "io"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
@ -35,6 +36,15 @@ func init() {
secp256k1n = common.String2Big("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") secp256k1n = common.String2Big("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")
} }
func FnvHash(data ...[]byte) (h common.Hash) {
fnvHash := fnv.New32()
for _, b := range data {
fnvHash.Write(b)
}
fnvHash.Sum(h[:0])
return h
}
func Sha3(data ...[]byte) []byte { func Sha3(data ...[]byte) []byte {
d := sha3.NewKeccak256() d := sha3.NewKeccak256()
for _, b := range data { for _, b := range data {

View file

@ -91,9 +91,7 @@ func runStateTest(test VmTest) error {
} }
var ( var (
ret []byte ret []byte
// gas *big.Int
// err error
logs state.Logs logs state.Logs
) )
@ -113,7 +111,7 @@ func runStateTest(test VmTest) error {
} }
if obj.Balance().Cmp(common.Big(account.Balance)) != 0 { if obj.Balance().Cmp(common.Big(account.Balance)) != 0 {
return fmt.Errorf("(%x) balance failed. Expected %v, got %v => %v\n", obj.Address().Bytes()[:4], account.Balance, obj.Balance(), new(big.Int).Sub(common.Big(account.Balance), obj.Balance())) return fmt.Errorf("(%x) balance failed. Expected %v, got %v => %v\n", obj.Address().Bytes()[:4], common.Big(account.Balance), obj.Balance(), new(big.Int).Sub(common.Big(account.Balance), obj.Balance()))
} }
if obj.Nonce() != common.String2Big(account.Nonce).Uint64() { if obj.Nonce() != common.String2Big(account.Nonce).Uint64() {