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:
Roman Khimov 2019-09-30 19:52:16 +03:00
parent e83dc94744
commit ceca9cdb67
6 changed files with 175 additions and 16 deletions

View file

@ -313,6 +313,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
spentCoins = make(SpentCoins)
accounts = make(Accounts)
assets = make(Assets)
contracts = make(Contracts)
)
if err := storeAsBlock(batch, block, 0); err != nil {
@ -399,7 +400,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
Email: t.Email,
Description: t.Description,
}
_ = contract
contracts[contract.ScriptHash()] = contract
case *transaction.InvocationTX:
}
@ -418,6 +419,9 @@ func (bc *Blockchain) storeBlock(block *Block) error {
if err := assets.commit(batch); err != nil {
return err
}
if err := contracts.commit(batch); err != nil {
return err
}
if err := bc.memStore.PutBatch(batch); err != nil {
return err
}
@ -643,6 +647,33 @@ func getAssetStateFromStore(s storage.Store, assetID util.Uint256) *AssetState {
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
func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState {
as, err := getAccountStateFromStore(bc.memStore, scriptHash)
@ -1001,6 +1032,13 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error {
vm := vm.New(vm.ModeMute)
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(witnesses[i].InvocationScript)
vm.Run()

View file

@ -1,16 +1,22 @@
package core
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/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.
type ContractState struct {
Script []byte
ParamList []smartcontract.ParamType
ReturnType smartcontract.ParamType
Properties []int
Properties []byte
Name string
CodeVersion string
Author string
@ -21,3 +27,69 @@ type ContractState struct {
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)
}

View 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())
}

View file

@ -3,7 +3,7 @@ package smartcontract
import "github.com/CityOfZion/neo-go/pkg/util"
// ParamType represent the Type of the contract parameter
type ParamType int
type ParamType byte
// A list of supported smart contract parameter types.
const (

View file

@ -35,8 +35,8 @@ type VM struct {
// registered interop hooks.
interop map[string]InteropFunc
// scripts loaded in memory.
scripts map[util.Uint160][]byte
// callback to get scripts.
getScript func(util.Uint160) []byte
istack *Stack // invocation stack.
estack *Stack // execution stack.
@ -52,7 +52,7 @@ type VM struct {
func New(mode Mode) *VM {
vm := &VM{
interop: make(map[string]InteropFunc),
scripts: make(map[util.Uint160][]byte),
getScript: nil,
state: haltState,
istack: NewStack("invocation"),
estack: NewStack("evaluation"),
@ -248,6 +248,11 @@ func (v *VM) SetCheckedHash(h []byte) {
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).
func (v *VM) execute(ctx *Context, op Instruction) {
// 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:
if len(v.scripts) == 0 {
panic("script table is empty")
if v.getScript == nil {
panic("no getScript callback is set up")
}
hash, err := util.Uint160DecodeBytes(ctx.readBytes(20))
@ -859,8 +864,8 @@ func (v *VM) execute(ctx *Context, op Instruction) {
panic(err)
}
script, ok := v.scripts[hash]
if !ok {
script := v.getScript(hash)
if script == nil {
panic("could not find script")
}

View file

@ -1000,7 +1000,12 @@ func TestAppCall(t *testing.T) {
prog = append(prog, byte(RET))
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.Run()