core: implement NativeContract support

This commit is contained in:
Evgenii Stratonikov 2020-03-19 18:52:37 +03:00
parent 3831aec53f
commit 25354c44f9
6 changed files with 305 additions and 0 deletions

View file

@ -14,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -123,6 +124,8 @@ type Blockchain struct {
log *zap.Logger
lastBatch *storage.MemBatch
contracts native.Contracts
}
type headersOpFunc func(headerList *HeaderHashList)
@ -167,6 +170,8 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
generationAmount: genAmount,
decrementInterval: decrementInterval,
contracts: *native.NewContracts(),
}
if err := bc.init(); err != nil {
@ -726,6 +731,13 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
bc.lastBatch = cache.DAO.GetBatch()
}
for i := range bc.contracts.Contracts {
systemInterop := bc.newInteropContext(trigger.Application, cache, block, nil)
if err := bc.contracts.Contracts[i].OnPersist(systemInterop); err != nil {
return err
}
}
_, err := cache.Persist()
if err != nil {
return err
@ -832,6 +844,11 @@ func (bc *Blockchain) LastBatch() *storage.MemBatch {
return bc.lastBatch
}
// RegisterNative registers native contract in the blockchain.
func (bc *Blockchain) RegisterNative(c native.Contract) {
bc.contracts.Add(c)
}
// processOutputs processes transaction outputs.
func processOutputs(tx *transaction.Transaction, dao *dao.Cached) error {
for index, output := range tx.Outputs {

View file

@ -15,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
@ -24,7 +25,12 @@ import (
// up for current blockchain.
func SpawnVM(ic *interop.Context) *vm.VM {
vm := vm.New()
bc := ic.Chain.(*Blockchain)
vm.SetScriptGetter(func(hash util.Uint160) ([]byte, bool) {
if c := bc.contracts.ByHash(hash); c != nil {
meta := c.Metadata()
return meta.Script, (meta.Manifest.Features&smartcontract.HasDynamicInvoke != 0)
}
cs, err := ic.DAO.GetContractState(hash)
if err != nil {
return nil, false
@ -34,6 +40,7 @@ func SpawnVM(ic *interop.Context) *vm.VM {
})
vm.RegisterInteropGetter(getSystemInterop(ic))
vm.RegisterInteropGetter(getNeoInterop(ic))
vm.RegisterInteropGetter(bc.contracts.GetNativeInterop(ic))
return vm
}
@ -161,6 +168,7 @@ var neoInterops = []interop.Function{
{Name: "Neo.Iterator.Key", Func: iterator.Key, Price: 1},
{Name: "Neo.Iterator.Keys", Func: iterator.Keys, Price: 1},
{Name: "Neo.Iterator.Values", Func: iterator.Values, Price: 1},
{Name: "Neo.Native.Deploy", Func: native.Deploy, Price: 1},
{Name: "Neo.Output.GetAssetId", Func: outputGetAssetID, Price: 1},
{Name: "Neo.Output.GetScriptHash", Func: outputGetScriptHash, Price: 1},
{Name: "Neo.Output.GetValue", Func: outputGetValue, Price: 1},

144
pkg/core/native/contract.go Normal file
View file

@ -0,0 +1,144 @@
package native
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/pkg/errors"
)
// Method is a signature for a native method.
type Method = func(ic *interop.Context, args []vm.StackItem) vm.StackItem
// MethodAndPrice is a native-contract method descriptor.
type MethodAndPrice struct {
Func Method
Price int64
RequiredFlags smartcontract.CallFlag
}
// Contract is an interface for all native contracts.
type Contract interface {
Metadata() *ContractMD
OnPersist(*interop.Context) error
}
// ContractMD represents native contract instance.
type ContractMD struct {
Manifest manifest.Manifest
ServiceName string
ServiceID uint32
Script []byte
Hash util.Uint160
Methods map[string]MethodAndPrice
}
// Contracts is a set of registered native contracts.
type Contracts struct {
Contracts []Contract
}
// NewContractMD returns Contract with the specified list of methods.
func NewContractMD(name string) *ContractMD {
c := &ContractMD{
ServiceName: name,
ServiceID: vm.InteropNameToID([]byte(name)),
Methods: make(map[string]MethodAndPrice),
}
w := io.NewBufBinWriter()
emit.Syscall(w.BinWriter, c.ServiceName)
c.Script = w.Bytes()
c.Hash = hash.Hash160(c.Script)
c.Manifest = *manifest.DefaultManifest(c.Hash)
return c
}
// ByHash returns native contract with the specified hash.
func (cs *Contracts) ByHash(h util.Uint160) Contract {
for _, ctr := range cs.Contracts {
if ctr.Metadata().Hash.Equals(h) {
return ctr
}
}
return nil
}
// ByID returns native contract with the specified id.
func (cs *Contracts) ByID(id uint32) Contract {
for _, ctr := range cs.Contracts {
if ctr.Metadata().ServiceID == id {
return ctr
}
}
return nil
}
// AddMethod adds new method to a native contract.
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method, safe bool) {
c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc)
c.Methods[desc.Name] = *md
if safe {
c.Manifest.SafeMethods.Add(desc.Name)
}
}
// AddEvent adds new event to a native contract.
func (c *ContractMD) AddEvent(name string, ps ...manifest.Parameter) {
c.Manifest.ABI.Events = append(c.Manifest.ABI.Events, manifest.Event{
Name: name,
Parameters: ps,
})
}
// NewContracts returns new empty set of native contracts.
func NewContracts() *Contracts {
return &Contracts{
Contracts: []Contract{},
}
}
// Add adds new native contracts to the list.
func (cs *Contracts) Add(c Contract) {
cs.Contracts = append(cs.Contracts, c)
}
// GetNativeInterop returns an interop getter for a given set of contracts.
func (cs *Contracts) GetNativeInterop(ic *interop.Context) func(uint32) *vm.InteropFuncPrice {
return func(id uint32) *vm.InteropFuncPrice {
if c := cs.ByID(id); c != nil {
return &vm.InteropFuncPrice{
Func: getNativeInterop(ic, c),
Price: 0, // TODO price func
}
}
return nil
}
}
// getNativeInterop returns native contract interop.
func getNativeInterop(ic *interop.Context, c Contract) func(v *vm.VM) error {
return func(v *vm.VM) error {
h := v.GetContextScriptHash(0)
if !h.Equals(c.Metadata().Hash) {
return errors.New("invalid hash")
}
name := string(v.Estack().Pop().Bytes())
args := v.Estack().Pop().Array()
m, ok := c.Metadata().Methods[name]
if !ok {
return fmt.Errorf("method %s not found", name)
}
result := m.Func(ic, args)
v.Estack().PushVal(result)
return nil
}
}

View file

@ -0,0 +1,16 @@
package native
import (
"errors"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/vm"
)
// Deploy deploys native contract.
func Deploy(ic *interop.Context, _ *vm.VM) error {
if ic.Block.Index != 0 {
return errors.New("native contracts can be deployed only at 0 block")
}
return nil
}

View file

@ -0,0 +1,106 @@
package core
import (
"errors"
"math/rand"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/stretchr/testify/require"
)
type testNative struct {
meta native.ContractMD
blocks chan uint32
}
func (tn *testNative) Metadata() *native.ContractMD {
return &tn.meta
}
func (tn *testNative) OnPersist(ic *interop.Context) error {
select {
case tn.blocks <- ic.Block.Index:
return nil
default:
return errors.New("error on persist")
}
}
var _ native.Contract = (*testNative)(nil)
func newTestNative() *testNative {
tn := &testNative{
meta: *native.NewContractMD("Test.Native.Sum"),
blocks: make(chan uint32, 1),
}
desc := &manifest.Method{
Name: "sum",
Parameters: []manifest.Parameter{
manifest.NewParameter("addend1", smartcontract.IntegerType),
manifest.NewParameter("addend2", smartcontract.IntegerType),
},
ReturnType: smartcontract.IntegerType,
}
md := &native.MethodAndPrice{
Func: tn.sum,
Price: 1,
RequiredFlags: smartcontract.NoneFlag,
}
tn.meta.AddMethod(md, desc, true)
return tn
}
func (tn *testNative) sum(_ *interop.Context, args []vm.StackItem) vm.StackItem {
s1, err := args[0].TryInteger()
if err != nil {
panic(err)
}
s2, err := args[1].TryInteger()
if err != nil {
panic(err)
}
return vm.NewBigIntegerItem(s1.Add(s1, s2))
}
func TestNativeContract_Invoke(t *testing.T) {
chain := newTestChain(t)
defer chain.Close()
tn := newTestNative()
chain.RegisterNative(tn)
w := io.NewBufBinWriter()
emit.AppCallWithOperationAndArgs(w.BinWriter, tn.Metadata().Hash, "sum", int64(14), int64(28))
script := w.Bytes()
tx := transaction.NewInvocationTX(script, 0)
mn := transaction.NewMinerTXWithNonce(rand.Uint32())
validUntil := chain.blockHeight + 1
tx.ValidUntilBlock = validUntil
mn.ValidUntilBlock = validUntil
b := chain.newBlock(mn, tx)
require.NoError(t, chain.AddBlock(b))
res, err := chain.GetAppExecResult(tx.Hash())
require.NoError(t, err)
require.Equal(t, "HALT", res.VMState)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, smartcontract.IntegerType, res.Stack[0].Type)
require.EqualValues(t, 42, res.Stack[0].Value)
require.NoError(t, chain.persist())
select {
case index := <-tn.blocks:
require.Equal(t, chain.blockHeight, index)
default:
require.Fail(t, "onPersist wasn't called")
}
}

View file

@ -0,0 +1,14 @@
package smartcontract
// CallFlag represents call flag.
type CallFlag byte
// Default flags.
const (
NoneFlag CallFlag = 0
AllowModifyStates CallFlag = 1 << iota
AllowCall
AllowNotify
ReadOnly = AllowCall | AllowNotify
All = AllowModifyStates | AllowCall | AllowNotify
)