native: change OnPersist/PostPersist handling

Every contract now has these and they're always invoked. See
neo-project/neo#1913 and neo-project/neo#2119.
This commit is contained in:
Roman Khimov 2020-12-13 21:36:06 +03:00
parent fc361213a7
commit 090bee8624
13 changed files with 95 additions and 143 deletions

View file

@ -89,6 +89,8 @@ type MethodAndPrice struct {
type Contract interface {
Initialize(*Context) error
Metadata() *ContractMD
OnPersist(*Context) error
PostPersist(*Context) error
}
// ContractMD represents native contract instance.

View file

@ -27,6 +27,8 @@ const (
SystemContractDestroy = "System.Contract.Destroy"
SystemContractIsStandard = "System.Contract.IsStandard"
SystemContractGetCallFlags = "System.Contract.GetCallFlags"
SystemContractNativeOnPersist = "System.Contract.NativeOnPersist"
SystemContractNativePostPersist = "System.Contract.NativePostPersist"
SystemContractUpdate = "System.Contract.Update"
SystemEnumeratorConcat = "System.Enumerator.Concat"
SystemEnumeratorCreate = "System.Enumerator.Create"
@ -96,6 +98,8 @@ var names = []string{
SystemContractDestroy,
SystemContractIsStandard,
SystemContractGetCallFlags,
SystemContractNativeOnPersist,
SystemContractNativePostPersist,
SystemContractUpdate,
SystemEnumeratorConcat,
SystemEnumeratorCreate,

View file

@ -67,6 +67,8 @@ var systemInterops = []interop.Function{
{Name: interopnames.SystemContractDestroy, Func: contractDestroy, Price: 1000000, RequiredFlags: smartcontract.WriteStates, DisallowCallback: true},
{Name: interopnames.SystemContractIsStandard, Func: contractIsStandard, Price: 30000, RequiredFlags: smartcontract.ReadStates, ParamCount: 1},
{Name: interopnames.SystemContractGetCallFlags, Func: contractGetCallFlags, Price: 30000, DisallowCallback: true},
{Name: interopnames.SystemContractNativeOnPersist, Func: native.OnPersist, Price: 0, DisallowCallback: true},
{Name: interopnames.SystemContractNativePostPersist, Func: native.PostPersist, Price: 0, DisallowCallback: true},
{Name: interopnames.SystemContractUpdate, Func: contractUpdate, Price: 0,
RequiredFlags: smartcontract.WriteStates, ParamCount: 2, DisallowCallback: true},
{Name: interopnames.SystemEnumeratorConcat, Func: enumerator.Concat, Price: 400, ParamCount: 2, DisallowCallback: true},

View file

@ -1,15 +1,13 @@
package native
import (
"errors"
"strings"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)
// reservedContractID represents the upper bound of the reserved IDs for native contracts.
@ -62,9 +60,9 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
gas.NEO = neo
cs.GAS = gas
cs.Contracts = append(cs.Contracts, gas)
cs.NEO = neo
cs.Contracts = append(cs.Contracts, neo)
cs.Contracts = append(cs.Contracts, gas)
policy := newPolicy()
cs.Policy = policy
@ -93,62 +91,24 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
return cs
}
// GetPersistScript returns VM script calling "onPersist" method of every native contract.
// GetPersistScript returns VM script calling "onPersist" syscall for native contracts.
func (cs *Contracts) GetPersistScript() []byte {
if cs.persistScript != nil {
return cs.persistScript
}
w := io.NewBufBinWriter()
for i := range cs.Contracts {
md := cs.Contracts[i].Metadata()
// Not every contract is persisted:
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L90
if md.ContractID == policyContractID || md.ContractID == oracleContractID || md.ContractID == designateContractID {
continue
}
emit.Int(w.BinWriter, 0)
emit.Opcodes(w.BinWriter, opcode.NEWARRAY)
emit.String(w.BinWriter, "onPersist")
emit.AppCall(w.BinWriter, md.Hash)
emit.Opcodes(w.BinWriter, opcode.DROP)
}
emit.Syscall(w.BinWriter, interopnames.SystemContractNativeOnPersist)
cs.persistScript = w.Bytes()
return cs.persistScript
}
// GetPostPersistScript returns VM script calling "postPersist" method of some native contracts.
// GetPostPersistScript returns VM script calling "postPersist" syscall for native contracts.
func (cs *Contracts) GetPostPersistScript() []byte {
if cs.postPersistScript != nil {
return cs.postPersistScript
}
w := io.NewBufBinWriter()
for i := range cs.Contracts {
md := cs.Contracts[i].Metadata()
// Not every contract is persisted:
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103
if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID || md.ContractID == notaryContractID {
continue
}
emit.Int(w.BinWriter, 0)
emit.Opcodes(w.BinWriter, opcode.NEWARRAY)
emit.String(w.BinWriter, "postPersist")
emit.AppCall(w.BinWriter, md.Hash)
emit.Opcodes(w.BinWriter, opcode.DROP)
}
emit.Syscall(w.BinWriter, interopnames.SystemContractNativePostPersist)
cs.postPersistScript = w.Bytes()
return cs.postPersistScript
}
func postPersistBase(ic *interop.Context) error {
if ic.Trigger != trigger.PostPersist {
return errors.New("postPersist must be trigered by system")
}
return nil
}
func onPersistBase(ic *interop.Context) error {
if ic.Trigger != trigger.OnPersist {
return errors.New("onPersist must be trigered by system")
}
return nil
}

View file

@ -87,14 +87,6 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
md = newMethodAndPrice(s.designateAsRole, 0, smartcontract.WriteStates)
s.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.WriteStates)
s.AddMethod(md, desc)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.WriteStates)
s.AddMethod(md, desc)
return s
}
@ -103,6 +95,16 @@ func (s *Designate) Initialize(ic *interop.Context) error {
return nil
}
// OnPersist implements Contract interface.
func (s *Designate) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements Contract interface.
func (s *Designate) PostPersist(ic *interop.Context) error {
return nil
}
// OnPersistEnd updates cached values if they've been changed.
func (s *Designate) OnPersistEnd(d dao.DAO) error {
if !s.rolesChanged() {

View file

@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
)
// Deploy deploys native contract.
@ -70,3 +71,31 @@ func Call(ic *interop.Context) error {
}
return nil
}
// OnPersist calls OnPersist methods for all native contracts.
func OnPersist(ic *interop.Context) error {
if ic.Trigger != trigger.OnPersist {
return errors.New("onPersist must be trigered by system")
}
for _, c := range ic.Natives {
err := c.OnPersist(ic)
if err != nil {
return err
}
}
return nil
}
// PostPersist calls PostPersist methods for all native contracts.
func PostPersist(ic *interop.Context) error {
if ic.Trigger != trigger.PostPersist {
return errors.New("postPersist must be trigered by system")
}
for _, c := range ic.Natives {
err := c.PostPersist(ic)
if err != nil {
return err
}
}
return nil
}

View file

@ -31,16 +31,11 @@ func newGAS() *GAS {
nep17.symbol = "gas"
nep17.decimals = 8
nep17.factor = GASFactor
nep17.onPersist = chainOnPersist(onPersistBase, g.OnPersist)
nep17.incBalance = g.increaseBalance
nep17.ContractID = gasContractID
g.nep17TokenNative = *nep17
onp := g.Methods["onPersist"]
onp.Func = getOnPersistWrapper(g.onPersist)
g.Methods["onPersist"] = onp
return g
}
@ -98,6 +93,11 @@ func (g *GAS) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements Contract interface.
func (g *GAS) PostPersist(ic *interop.Context) error {
return nil
}
func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) {
s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators())
if err != nil {
@ -105,16 +105,3 @@ func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) {
}
return hash.Hash160(s), nil
}
func chainOnPersist(fs ...func(*interop.Context) error) func(*interop.Context) error {
return func(ic *interop.Context) error {
for i := range fs {
if fs[i] != nil {
if err := fs[i](ic); err != nil {
return err
}
}
}
return nil
}
}

View file

@ -97,8 +97,6 @@ func newNEO() *NEO {
nep17.symbol = "neo"
nep17.decimals = 0
nep17.factor = 1
nep17.onPersist = chainOnPersist(onPersistBase, n.OnPersist)
nep17.postPersist = chainOnPersist(nep17.postPersist, n.PostPersist)
nep17.incBalance = n.increaseBalance
nep17.ContractID = neoContractID
@ -109,14 +107,6 @@ func newNEO() *NEO {
n.committee.Store(keysWithVotes(nil))
n.committeeHash.Store(util.Uint160{})
onp := n.Methods["onPersist"]
onp.Func = getOnPersistWrapper(n.onPersist)
n.Methods["onPersist"] = onp
pp := n.Methods["postPersist"]
pp.Func = getOnPersistWrapper(n.postPersist)
n.Methods["postPersist"] = pp
desc := newDescriptor("unclaimedGas", smartcontract.IntegerType,
manifest.NewParameter("account", smartcontract.Hash160Type),
manifest.NewParameter("end", smartcontract.IntegerType))

View file

@ -2,7 +2,6 @@ package native
import (
"errors"
"fmt"
"math"
"math/big"
@ -36,8 +35,6 @@ type nep17TokenNative struct {
symbol string
decimals int64
factor int64
onPersist func(*interop.Context) error
postPersist func(*interop.Context) error
incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int) error
}
@ -48,8 +45,6 @@ func (c *nep17TokenNative) Metadata() *interop.ContractMD {
return &c.ContractMD
}
var _ interop.Contract = (*nep17TokenNative)(nil)
func newNEP17Native(name string) *nep17TokenNative {
n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name)}
n.Manifest.SupportedStandards = []string{manifest.NEP17StandardName}
@ -82,14 +77,6 @@ func newNEP17Native(name string) *nep17TokenNative {
md = newMethodAndPrice(n.Transfer, 8000000, smartcontract.WriteStates|smartcontract.AllowCall|smartcontract.AllowNotify)
n.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.WriteStates)
n.AddMethod(md, desc)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.WriteStates)
n.AddMethod(md, desc)
n.AddEvent("Transfer", transferParams...)
return n
@ -333,13 +320,3 @@ func toUint32(s stackitem.Item) uint32 {
}
return uint32(int64Value)
}
func getOnPersistWrapper(f func(ic *interop.Context) error) interop.Method {
return func(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
err := f(ic)
if err != nil {
panic(fmt.Errorf("OnPersist for native contract: %w", err))
}
return stackitem.Null{}
}
}

View file

@ -98,14 +98,6 @@ func newNotary() *Notary {
md = newMethodAndPrice(n.setMaxNotValidBeforeDelta, 300_0000, smartcontract.WriteStates)
n.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.WriteStates)
n.AddMethod(md, desc)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.WriteStates)
n.AddMethod(md, desc)
return n
}
@ -179,6 +171,11 @@ func (n *Notary) OnPersistEnd(dao dao.DAO) error {
return nil
}
// PostPersist implements Contract interface.
func (n *Notary) PostPersist(ic *interop.Context) error {
return nil
}
// onPayment records deposited amount as belonging to "from" address with a lock
// till the specified chain's height.
func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item {

View file

@ -122,15 +122,6 @@ func newOracle() *Oracle {
md = newMethodAndPrice(o.verify, 100_0000, smartcontract.NoneFlag)
o.AddMethod(md, desc)
pp := chainOnPersist(postPersistBase, o.PostPersist)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.WriteStates)
o.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.WriteStates)
o.AddMethod(md, desc)
o.AddEvent("OracleRequest", manifest.NewParameter("Id", smartcontract.IntegerType),
manifest.NewParameter("RequestContract", smartcontract.Hash160Type),
manifest.NewParameter("Url", smartcontract.StringType),
@ -141,6 +132,11 @@ func newOracle() *Oracle {
return o
}
// OnPersist implements Contract interface.
func (o *Oracle) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist represents `postPersist` method.
func (o *Oracle) PostPersist(ic *interop.Context) error {
var nodes keys.PublicKeys

View file

@ -124,13 +124,6 @@ func newPolicy() *Policy {
md = newMethodAndPrice(p.unblockAccount, 3000000, smartcontract.WriteStates)
p.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(p.OnPersist), 0, smartcontract.WriteStates)
p.AddMethod(md, desc)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.WriteStates)
p.AddMethod(md, desc)
return p
}
@ -157,6 +150,11 @@ func (p *Policy) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements Contract interface.
func (p *Policy) PostPersist(ic *interop.Context) error {
return nil
}
// OnPersistEnd updates cached Policy values if they've been changed
func (p *Policy) OnPersistEnd(dao dao.DAO) error {
if p.isValid {

View file

@ -1,6 +1,7 @@
package core
import (
"errors"
"math/big"
"testing"
@ -33,18 +34,19 @@ func (tn *testNative) Metadata() *interop.ContractMD {
return &tn.meta
}
func (tn *testNative) OnPersist(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
if ic.Trigger != trigger.OnPersist {
panic("invalid trigger")
}
func (tn *testNative) OnPersist(ic *interop.Context) error {
select {
case tn.blocks <- ic.Block.Index:
return stackitem.NewBool(true)
return nil
default:
return stackitem.NewBool(false)
return errors.New("can't send index")
}
}
func (tn *testNative) PostPersist(ic *interop.Context) error {
return nil
}
var _ interop.Contract = (*testNative)(nil)
// registerNative registers native contract in the blockchain.
@ -106,10 +108,6 @@ func newTestNative() *testNative {
RequiredFlags: smartcontract.NoneFlag}
tn.meta.AddMethod(md, desc)
desc = &manifest.Method{Name: "onPersist", ReturnType: smartcontract.BoolType}
md = &interop.MethodAndPrice{Func: tn.OnPersist, RequiredFlags: smartcontract.WriteStates}
tn.meta.AddMethod(md, desc)
return tn
}
@ -252,18 +250,28 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) {
})
require.NoError(t, err)
var drainTN = func(t *testing.T) {
select {
case <-tn.blocks:
default:
require.Fail(t, "testNative didn't send us block")
}
}
cs, _ := getTestContractState()
require.NoError(t, chain.dao.PutContractState(cs))
t.Run("non-native, no return", func(t *testing.T) {
res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.Hash, "justReturn", []interface{}{})
require.NoError(t, err)
drainTN(t)
checkResult(t, res, stackitem.Null{}) // simple call is done with EnsureNotEmpty
})
t.Run("non-native, with return", func(t *testing.T) {
res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash,
"callOtherContractWithReturn", cs.Hash, "ret7", []interface{}{})
require.NoError(t, err)
drainTN(t)
checkResult(t, res, stackitem.Make(8))
})
}