mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-22 19:29:39 +00:00
core: implement NativeContract support
This commit is contained in:
parent
3831aec53f
commit
25354c44f9
6 changed files with 305 additions and 0 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
144
pkg/core/native/contract.go
Normal 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
|
||||
}
|
||||
}
|
16
pkg/core/native/interop.go
Normal file
16
pkg/core/native/interop.go
Normal 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
|
||||
}
|
106
pkg/core/native_contract_test.go
Normal file
106
pkg/core/native_contract_test.go
Normal 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")
|
||||
}
|
||||
}
|
14
pkg/smartcontract/call_flags.go
Normal file
14
pkg/smartcontract/call_flags.go
Normal 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
|
||||
)
|
Loading…
Reference in a new issue