forked from TrueCloudLab/neoneo-go
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)
|
||||
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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
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"
|
||||
|
||||
// ParamType represent the Type of the contract parameter
|
||||
type ParamType int
|
||||
type ParamType byte
|
||||
|
||||
// A list of supported smart contract parameter types.
|
||||
const (
|
||||
|
|
19
pkg/vm/vm.go
19
pkg/vm/vm.go
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue