From ceca9cdb67e1fc38d5b76d1289054c5144162d0c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 30 Sep 2019 19:52:16 +0300 Subject: [PATCH] core/vm: implement contract storage and script retrieval Fixes script invocations via the APPCALL instruction. Adjust contract state field types accordingly. --- pkg/core/blockchain.go | 40 +++++++++++++++- pkg/core/contract_state.go | 74 +++++++++++++++++++++++++++++- pkg/core/contract_state_test.go | 39 ++++++++++++++++ pkg/smartcontract/param_context.go | 2 +- pkg/vm/vm.go | 29 +++++++----- pkg/vm/vm_test.go | 7 ++- 6 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 pkg/core/contract_state_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3f955fd06..59e4bc5b5 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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() diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go index f33946d26..1ba19cbaa 100644 --- a/pkg/core/contract_state.go +++ b/pkg/core/contract_state.go @@ -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) +} diff --git a/pkg/core/contract_state_test.go b/pkg/core/contract_state_test.go new file mode 100644 index 000000000..a34039aae --- /dev/null +++ b/pkg/core/contract_state_test.go @@ -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()) +} diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index b23d95b2d..b4b75c45c 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -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 ( diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 49f20259b..5754a7436 100644 --- a/pkg/vm/vm.go +++ b/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. @@ -51,12 +51,12 @@ type VM struct { // New returns a new VM object ready to load .avm bytecode scripts. func New(mode Mode) *VM { vm := &VM{ - interop: make(map[string]InteropFunc), - scripts: make(map[util.Uint160][]byte), - state: haltState, - istack: NewStack("invocation"), - estack: NewStack("evaluation"), - astack: NewStack("alt"), + interop: make(map[string]InteropFunc), + getScript: nil, + state: haltState, + istack: NewStack("invocation"), + estack: NewStack("evaluation"), + astack: NewStack("alt"), } if mode == ModeMute { vm.mute = true @@ -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") } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 428a24735..b1cc8cd65 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -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()