core/vm: implement contract storage and script retrieval
Fixes script invocations via the APPCALL instruction. Adjust contract state field types accordingly.
This commit is contained in:
parent
e83dc94744
commit
ceca9cdb67
6 changed files with 175 additions and 16 deletions
|
@ -313,6 +313,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
|
||||||
spentCoins = make(SpentCoins)
|
spentCoins = make(SpentCoins)
|
||||||
accounts = make(Accounts)
|
accounts = make(Accounts)
|
||||||
assets = make(Assets)
|
assets = make(Assets)
|
||||||
|
contracts = make(Contracts)
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := storeAsBlock(batch, block, 0); err != nil {
|
if err := storeAsBlock(batch, block, 0); err != nil {
|
||||||
|
@ -399,7 +400,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
|
||||||
Email: t.Email,
|
Email: t.Email,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
}
|
}
|
||||||
_ = contract
|
contracts[contract.ScriptHash()] = contract
|
||||||
|
|
||||||
case *transaction.InvocationTX:
|
case *transaction.InvocationTX:
|
||||||
}
|
}
|
||||||
|
@ -418,6 +419,9 @@ func (bc *Blockchain) storeBlock(block *Block) error {
|
||||||
if err := assets.commit(batch); err != nil {
|
if err := assets.commit(batch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := contracts.commit(batch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := bc.memStore.PutBatch(batch); err != nil {
|
if err := bc.memStore.PutBatch(batch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -643,6 +647,33 @@ func getAssetStateFromStore(s storage.Store, assetID util.Uint256) *AssetState {
|
||||||
return &a
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContractState returns contract by its script hash.
|
||||||
|
func (bc *Blockchain) GetContractState(hash util.Uint160) *ContractState {
|
||||||
|
cs := getContractStateFromStore(bc.memStore, hash)
|
||||||
|
if cs == nil {
|
||||||
|
cs = getContractStateFromStore(bc.Store, hash)
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContractStateFromStore returns contract state as recorded in the given
|
||||||
|
// store by the given script hash.
|
||||||
|
func getContractStateFromStore(s storage.Store, hash util.Uint160) *ContractState {
|
||||||
|
key := storage.AppendPrefix(storage.STContract, hash.Bytes())
|
||||||
|
contractBytes, err := s.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var c ContractState
|
||||||
|
r := io.NewBinReaderFromBuf(contractBytes)
|
||||||
|
c.DecodeBinary(r)
|
||||||
|
if r.Err != nil || c.ScriptHash() != hash {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
// GetAccountState returns the account state from its script hash
|
// GetAccountState returns the account state from its script hash
|
||||||
func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState {
|
func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState {
|
||||||
as, err := getAccountStateFromStore(bc.memStore, scriptHash)
|
as, err := getAccountStateFromStore(bc.memStore, scriptHash)
|
||||||
|
@ -1001,6 +1032,13 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error {
|
||||||
|
|
||||||
vm := vm.New(vm.ModeMute)
|
vm := vm.New(vm.ModeMute)
|
||||||
vm.SetCheckedHash(t.VerificationHash().Bytes())
|
vm.SetCheckedHash(t.VerificationHash().Bytes())
|
||||||
|
vm.SetScriptGetter(func(hash util.Uint160) []byte {
|
||||||
|
cs := bc.GetContractState(hash)
|
||||||
|
if cs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cs.Script
|
||||||
|
})
|
||||||
vm.LoadScript(verification)
|
vm.LoadScript(verification)
|
||||||
vm.LoadScript(witnesses[i].InvocationScript)
|
vm.LoadScript(witnesses[i].InvocationScript)
|
||||||
vm.Run()
|
vm.Run()
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/core/storage"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/io"
|
||||||
"github.com/CityOfZion/neo-go/pkg/smartcontract"
|
"github.com/CityOfZion/neo-go/pkg/smartcontract"
|
||||||
"github.com/CityOfZion/neo-go/pkg/util"
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Contracts is a mapping between scripthash and ContractState.
|
||||||
|
type Contracts map[util.Uint160]*ContractState
|
||||||
|
|
||||||
// ContractState holds information about a smart contract in the NEO blockchain.
|
// ContractState holds information about a smart contract in the NEO blockchain.
|
||||||
type ContractState struct {
|
type ContractState struct {
|
||||||
Script []byte
|
Script []byte
|
||||||
ParamList []smartcontract.ParamType
|
ParamList []smartcontract.ParamType
|
||||||
ReturnType smartcontract.ParamType
|
ReturnType smartcontract.ParamType
|
||||||
Properties []int
|
Properties []byte
|
||||||
Name string
|
Name string
|
||||||
CodeVersion string
|
CodeVersion string
|
||||||
Author string
|
Author string
|
||||||
|
@ -21,3 +27,69 @@ type ContractState struct {
|
||||||
|
|
||||||
scriptHash util.Uint160
|
scriptHash util.Uint160
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commit flushes all contracts to the given storage.Batch.
|
||||||
|
func (a Contracts) commit(b storage.Batch) error {
|
||||||
|
buf := io.NewBufBinWriter()
|
||||||
|
for hash, contract := range a {
|
||||||
|
contract.EncodeBinary(buf.BinWriter)
|
||||||
|
if buf.Err != nil {
|
||||||
|
return buf.Err
|
||||||
|
}
|
||||||
|
key := storage.AppendPrefix(storage.STContract, hash.Bytes())
|
||||||
|
b.Put(key, buf.Bytes())
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements Serializable interface.
|
||||||
|
func (a *ContractState) DecodeBinary(br *io.BinReader) {
|
||||||
|
a.Script = br.ReadBytes()
|
||||||
|
paramBytes := br.ReadBytes()
|
||||||
|
a.ParamList = make([]smartcontract.ParamType, len(paramBytes))
|
||||||
|
for k := range paramBytes {
|
||||||
|
a.ParamList[k] = smartcontract.ParamType(paramBytes[k])
|
||||||
|
}
|
||||||
|
br.ReadLE(&a.ReturnType)
|
||||||
|
a.Properties = br.ReadBytes()
|
||||||
|
a.Name = br.ReadString()
|
||||||
|
a.CodeVersion = br.ReadString()
|
||||||
|
a.Author = br.ReadString()
|
||||||
|
a.Email = br.ReadString()
|
||||||
|
a.Description = br.ReadString()
|
||||||
|
br.ReadLE(&a.HasStorage)
|
||||||
|
br.ReadLE(&a.HasDynamicInvoke)
|
||||||
|
a.createHash()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements Serializable interface.
|
||||||
|
func (a *ContractState) EncodeBinary(bw *io.BinWriter) {
|
||||||
|
bw.WriteBytes(a.Script)
|
||||||
|
bw.WriteVarUint(uint64(len(a.ParamList)))
|
||||||
|
for k := range a.ParamList {
|
||||||
|
bw.WriteLE(a.ParamList[k])
|
||||||
|
}
|
||||||
|
bw.WriteLE(a.ReturnType)
|
||||||
|
bw.WriteBytes(a.Properties)
|
||||||
|
bw.WriteString(a.Name)
|
||||||
|
bw.WriteString(a.CodeVersion)
|
||||||
|
bw.WriteString(a.Author)
|
||||||
|
bw.WriteString(a.Email)
|
||||||
|
bw.WriteString(a.Description)
|
||||||
|
bw.WriteLE(a.HasStorage)
|
||||||
|
bw.WriteLE(a.HasDynamicInvoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScriptHash returns a contract script hash.
|
||||||
|
func (a *ContractState) ScriptHash() util.Uint160 {
|
||||||
|
if a.scriptHash.Equals(util.Uint160{}) {
|
||||||
|
a.createHash()
|
||||||
|
}
|
||||||
|
return a.scriptHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHash creates contract script hash.
|
||||||
|
func (a *ContractState) createHash() {
|
||||||
|
a.scriptHash = hash.Hash160(a.Script)
|
||||||
|
}
|
||||||
|
|
39
pkg/core/contract_state_test.go
Normal file
39
pkg/core/contract_state_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/io"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeDecodeContractState(t *testing.T) {
|
||||||
|
script := []byte("testscript")
|
||||||
|
|
||||||
|
contract := &ContractState{
|
||||||
|
Script: script,
|
||||||
|
ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type},
|
||||||
|
ReturnType: smartcontract.BoolType,
|
||||||
|
Properties: []byte("smth"),
|
||||||
|
Name: "Contracto",
|
||||||
|
CodeVersion: "1.0.0",
|
||||||
|
Author: "Joe Random",
|
||||||
|
Email: "joe@example.com",
|
||||||
|
Description: "Test contract",
|
||||||
|
HasStorage: true,
|
||||||
|
HasDynamicInvoke: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, hash.Hash160(script), contract.ScriptHash())
|
||||||
|
buf := io.NewBufBinWriter()
|
||||||
|
contract.EncodeBinary(buf.BinWriter)
|
||||||
|
assert.Nil(t, buf.Err)
|
||||||
|
contractDecoded := &ContractState{}
|
||||||
|
r := io.NewBinReaderFromBuf(buf.Bytes())
|
||||||
|
contractDecoded.DecodeBinary(r)
|
||||||
|
assert.Nil(t, r.Err)
|
||||||
|
assert.Equal(t, contract, contractDecoded)
|
||||||
|
assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash())
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package smartcontract
|
||||||
import "github.com/CityOfZion/neo-go/pkg/util"
|
import "github.com/CityOfZion/neo-go/pkg/util"
|
||||||
|
|
||||||
// ParamType represent the Type of the contract parameter
|
// ParamType represent the Type of the contract parameter
|
||||||
type ParamType int
|
type ParamType byte
|
||||||
|
|
||||||
// A list of supported smart contract parameter types.
|
// A list of supported smart contract parameter types.
|
||||||
const (
|
const (
|
||||||
|
|
29
pkg/vm/vm.go
29
pkg/vm/vm.go
|
@ -35,8 +35,8 @@ type VM struct {
|
||||||
// registered interop hooks.
|
// registered interop hooks.
|
||||||
interop map[string]InteropFunc
|
interop map[string]InteropFunc
|
||||||
|
|
||||||
// scripts loaded in memory.
|
// callback to get scripts.
|
||||||
scripts map[util.Uint160][]byte
|
getScript func(util.Uint160) []byte
|
||||||
|
|
||||||
istack *Stack // invocation stack.
|
istack *Stack // invocation stack.
|
||||||
estack *Stack // execution stack.
|
estack *Stack // execution stack.
|
||||||
|
@ -51,12 +51,12 @@ type VM struct {
|
||||||
// New returns a new VM object ready to load .avm bytecode scripts.
|
// New returns a new VM object ready to load .avm bytecode scripts.
|
||||||
func New(mode Mode) *VM {
|
func New(mode Mode) *VM {
|
||||||
vm := &VM{
|
vm := &VM{
|
||||||
interop: make(map[string]InteropFunc),
|
interop: make(map[string]InteropFunc),
|
||||||
scripts: make(map[util.Uint160][]byte),
|
getScript: nil,
|
||||||
state: haltState,
|
state: haltState,
|
||||||
istack: NewStack("invocation"),
|
istack: NewStack("invocation"),
|
||||||
estack: NewStack("evaluation"),
|
estack: NewStack("evaluation"),
|
||||||
astack: NewStack("alt"),
|
astack: NewStack("alt"),
|
||||||
}
|
}
|
||||||
if mode == ModeMute {
|
if mode == ModeMute {
|
||||||
vm.mute = true
|
vm.mute = true
|
||||||
|
@ -248,6 +248,11 @@ func (v *VM) SetCheckedHash(h []byte) {
|
||||||
copy(v.checkhash, h)
|
copy(v.checkhash, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetScriptGetter sets the script getter for CALL instructions.
|
||||||
|
func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) {
|
||||||
|
v.getScript = gs
|
||||||
|
}
|
||||||
|
|
||||||
// execute performs an instruction cycle in the VM. Acting on the instruction (opcode).
|
// execute performs an instruction cycle in the VM. Acting on the instruction (opcode).
|
||||||
func (v *VM) execute(ctx *Context, op Instruction) {
|
func (v *VM) execute(ctx *Context, op Instruction) {
|
||||||
// Instead of polluting the whole VM logic with error handling, we will recover
|
// Instead of polluting the whole VM logic with error handling, we will recover
|
||||||
|
@ -850,8 +855,8 @@ func (v *VM) execute(ctx *Context, op Instruction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case APPCALL, TAILCALL:
|
case APPCALL, TAILCALL:
|
||||||
if len(v.scripts) == 0 {
|
if v.getScript == nil {
|
||||||
panic("script table is empty")
|
panic("no getScript callback is set up")
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := util.Uint160DecodeBytes(ctx.readBytes(20))
|
hash, err := util.Uint160DecodeBytes(ctx.readBytes(20))
|
||||||
|
@ -859,8 +864,8 @@ func (v *VM) execute(ctx *Context, op Instruction) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
script, ok := v.scripts[hash]
|
script := v.getScript(hash)
|
||||||
if !ok {
|
if script == nil {
|
||||||
panic("could not find script")
|
panic("could not find script")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1000,7 +1000,12 @@ func TestAppCall(t *testing.T) {
|
||||||
prog = append(prog, byte(RET))
|
prog = append(prog, byte(RET))
|
||||||
|
|
||||||
vm := load(prog)
|
vm := load(prog)
|
||||||
vm.scripts[hash] = makeProgram(DEPTH)
|
vm.SetScriptGetter(func(in util.Uint160) []byte {
|
||||||
|
if in.Equals(hash) {
|
||||||
|
return makeProgram(DEPTH)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
vm.estack.PushVal(2)
|
vm.estack.PushVal(2)
|
||||||
|
|
||||||
vm.Run()
|
vm.Run()
|
||||||
|
|
Loading…
Reference in a new issue