From 25354c44f93a7851e7a2d45882d7448eed7f0672 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 19 Mar 2020 18:52:37 +0300 Subject: [PATCH] core: implement NativeContract support --- pkg/core/blockchain.go | 17 ++++ pkg/core/interops.go | 8 ++ pkg/core/native/contract.go | 144 +++++++++++++++++++++++++++++++ pkg/core/native/interop.go | 16 ++++ pkg/core/native_contract_test.go | 106 +++++++++++++++++++++++ pkg/smartcontract/call_flags.go | 14 +++ 6 files changed, 305 insertions(+) create mode 100644 pkg/core/native/contract.go create mode 100644 pkg/core/native/interop.go create mode 100644 pkg/core/native_contract_test.go create mode 100644 pkg/smartcontract/call_flags.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 20ebc9f65..d8a9509ea 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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 { diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 3d4d8355c..05443042e 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -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}, diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go new file mode 100644 index 000000000..f68556ded --- /dev/null +++ b/pkg/core/native/contract.go @@ -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 + } +} diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go new file mode 100644 index 000000000..6b6cec16e --- /dev/null +++ b/pkg/core/native/interop.go @@ -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 +} diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go new file mode 100644 index 000000000..414f160e1 --- /dev/null +++ b/pkg/core/native_contract_test.go @@ -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") + } +} diff --git a/pkg/smartcontract/call_flags.go b/pkg/smartcontract/call_flags.go new file mode 100644 index 000000000..9bc042fd5 --- /dev/null +++ b/pkg/smartcontract/call_flags.go @@ -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 +)