Merge pull request #1827 from nspcc-dev/native/update_history

config: add NativeUpdateHistory
This commit is contained in:
Roman Khimov 2021-03-16 12:47:22 +03:00 committed by GitHub
commit a18fbc7bb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 270 additions and 11 deletions

View file

@ -21,6 +21,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: false
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -11,6 +11,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -21,6 +21,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: false
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -9,6 +9,17 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: false
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -18,6 +18,18 @@ ProtocolConfiguration:
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: true
NativeActivations:
ContractManagement: [0]
StdLib: [0]
CryptoLib: [0]
LedgerContract: [0]
NeoToken: [0]
GasToken: [0]
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
Notary: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -29,7 +29,7 @@ import (
)
func TestContractHashes(t *testing.T) {
cs := native.NewContracts(true)
cs := native.NewContracts(true, map[string][]uint32{})
require.Equal(t, []byte(neo.Hash), cs.NEO.Hash.BytesBE())
require.Equal(t, []byte(gas.Hash), cs.GAS.Hash.BytesBE())
require.Equal(t, []byte(oracle.Hash), cs.Oracle.Hash.BytesBE())
@ -93,7 +93,7 @@ type nativeTestCase struct {
// Here we test that corresponding method does exist, is invoked and correct value is returned.
func TestNativeHelpersCompile(t *testing.T) {
cs := native.NewContracts(true)
cs := native.NewContracts(true, map[string][]uint32{})
u160 := `interop.Hash160("aaaaaaaaaaaaaaaaaaaa")`
u256 := `interop.Hash256("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")`
pub := `interop.PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")`

View file

@ -6,6 +6,7 @@ import (
"os"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"gopkg.in/yaml.v2"
)
@ -56,5 +57,11 @@ func LoadFile(configPath string) (Config, error) {
return Config{}, fmt.Errorf("failed to unmarshal config YAML: %w", err)
}
for name := range config.ProtocolConfiguration.NativeUpdateHistories {
if !nativenames.IsValid(name) {
return Config{}, fmt.Errorf("NativeActivations configuration section contains unexpected native contract name: %s", name)
}
}
return config, nil
}

14
pkg/config/config_test.go Normal file
View file

@ -0,0 +1,14 @@
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
const testConfigPath = "./testdata/protocol.test.yml"
func TestUnexpectedNativeUpdateHistoryContract(t *testing.T) {
_, err := LoadFile(testConfigPath)
require.Error(t, err)
}

View file

@ -22,6 +22,8 @@ type (
MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"`
// MaxTransactionsPerBlock is the maximum amount of transactions per block.
MaxTransactionsPerBlock uint16 `yaml:"MaxTransactionsPerBlock"`
// NativeUpdateHistories is the list of histories of native contracts updates.
NativeUpdateHistories map[string][]uint32 `yaml:"NativeActivations"`
// P2PSigExtensions enables additional signature-related logic.
P2PSigExtensions bool `yaml:"P2PSigExtensions"`
// ReservedAttributes allows to have reserved attributes range for experimental or private purposes.

4
pkg/config/testdata/protocol.test.yml vendored Normal file
View file

@ -0,0 +1,4 @@
ProtocolConfiguration:
NativeActivations:
ContractManagement: [0]
UnexpectedContractName: [0]

View file

@ -180,6 +180,10 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
if err != nil {
return nil, err
}
if len(cfg.NativeUpdateHistories) == 0 {
cfg.NativeUpdateHistories = map[string][]uint32{}
log.Info("NativeActivations are not set, using default values")
}
bc := &Blockchain{
config: cfg,
dao: dao.NewSimple(s, cfg.Magic, cfg.StateRootInHeader),
@ -192,7 +196,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
subCh: make(chan interface{}),
unsubCh: make(chan interface{}),
contracts: *native.NewContracts(cfg.P2PSigExtensions),
contracts: *native.NewContracts(cfg.P2PSigExtensions, cfg.NativeUpdateHistories),
}
bc.stateRoot = stateroot.NewModule(bc, bc.log, bc.dao.Store)

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math/big"
"math/rand"
"path"
"strings"
"testing"
"time"
@ -1614,3 +1615,40 @@ func TestMPTDeleteNoKey(t *testing.T) {
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer.VMState)
}
// Test that UpdateHistory is added to ProtocolConfiguration for all native contracts
// for all default configurations. If UpdateHistory is not added to config, then
// native contract is disabled. It's easy to forget about config while adding new
// native contract.
func TestConfigNativeUpdateHistory(t *testing.T) {
const prefixPath = "../../config"
check := func(t *testing.T, cfgFileSuffix interface{}) {
cfgPath := path.Join(prefixPath, fmt.Sprintf("protocol.%s.yml", cfgFileSuffix))
cfg, err := config.LoadFile(cfgPath)
require.NoError(t, err, fmt.Errorf("failed to load %s", cfgPath))
natives := native.NewContracts(cfg.ProtocolConfiguration.P2PSigExtensions, map[string][]uint32{})
assert.Equal(t, len(natives.Contracts),
len(cfg.ProtocolConfiguration.NativeUpdateHistories),
fmt.Errorf("protocol configuration file %s: extra or missing NativeUpdateHistory in NativeActivations section", cfgPath))
for _, c := range natives.Contracts {
assert.NotNil(t, cfg.ProtocolConfiguration.NativeUpdateHistories[c.Metadata().Name],
fmt.Errorf("protocol configuration file %s: configuration for %s native contract is missing in NativeActivations section; "+
"edit the test if the contract should be disabled", cfgPath, c.Metadata().Name))
}
}
testCases := []interface{}{
netmode.MainNet,
netmode.PrivNet,
netmode.TestNet,
netmode.UnitTestNet,
"privnet.docker.one",
"privnet.docker.two",
"privnet.docker.three",
"privnet.docker.four",
"privnet.docker.single",
"unit_testnet.single",
}
for _, tc := range testCases {
check(t, tc)
}
}

View file

@ -209,6 +209,12 @@ func (c *ContractMD) AddEvent(name string, ps ...manifest.Parameter) {
})
}
// IsActive returns true iff the contract was deployed by the specified height.
func (c *ContractMD) IsActive(height uint32) bool {
history := c.UpdateHistory
return len(history) != 0 && history[0] <= height
}
// Sort sorts interop functions by id.
func Sort(fs []Function) {
sort.Slice(fs, func(i, j int) bool { return fs[i].ID < fs[j].ID })

View file

@ -9,7 +9,7 @@ import (
// "C" and "O" can easily be typed by accident.
func TestNamesASCII(t *testing.T) {
cs := NewContracts(true)
cs := NewContracts(true, map[string][]uint32{})
for _, c := range cs.Contracts {
require.True(t, isASCII(c.Metadata().Name))
for _, m := range c.Metadata().Methods {

View file

@ -56,7 +56,7 @@ func (cs *Contracts) ByName(name string) interop.Contract {
// NewContracts returns new set of native contracts with new GAS, NEO, Policy, Oracle,
// Designate and (optional) Notary contracts.
func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
func NewContracts(p2pSigExtensionsEnabled bool, nativeUpdateHistories map[string][]uint32) *Contracts {
cs := new(Contracts)
mgmt := newManagement()
@ -117,6 +117,13 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
cs.Contracts = append(cs.Contracts, notary)
}
setDefaultHistory := len(nativeUpdateHistories) == 0
for _, c := range cs.Contracts {
if setDefaultHistory {
nativeUpdateHistories[c.Metadata().Name] = []uint32{0}
}
c.Metadata().NativeContract.UpdateHistory = nativeUpdateHistories[c.Metadata().Name]
}
return cs
}

View file

@ -26,6 +26,13 @@ func Call(ic *interop.Context) error {
if c == nil {
return fmt.Errorf("native contract %d not found", version)
}
history := c.Metadata().UpdateHistory
if len(history) == 0 {
return fmt.Errorf("native contract %s is disabled", c.Metadata().Name)
}
if history[0] > ic.Chain.BlockHeight() {
return fmt.Errorf("native contract %s is active after height = %d", c.Metadata().Name, history[0])
}
m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP())
if !ok {
return fmt.Errorf("method not found")
@ -57,6 +64,9 @@ func OnPersist(ic *interop.Context) error {
return errors.New("onPersist must be trigered by system")
}
for _, c := range ic.Natives {
if !c.Metadata().IsActive(ic.Block.Index) {
continue
}
err := c.OnPersist(ic)
if err != nil {
return err
@ -71,6 +81,9 @@ func PostPersist(ic *interop.Context) error {
return errors.New("postPersist must be trigered by system")
}
for _, c := range ic.Natives {
if !c.Metadata().IsActive(ic.Block.Index) {
continue
}
err := c.PostPersist(ic)
if err != nil {
return err

View file

@ -446,12 +446,12 @@ func (m *Management) Metadata() *interop.ContractMD {
// OnPersist implements Contract interface.
func (m *Management) OnPersist(ic *interop.Context) error {
if ic.Block.Index != 0 { // We're only deploying at 0 at the moment.
return nil
}
for _, native := range ic.Natives {
md := native.Metadata()
history := md.UpdateHistory
if len(history) == 0 || history[0] != ic.Block.Index {
continue
}
cs := &state.Contract{
ContractBase: md.ContractBase,

View file

@ -14,3 +14,18 @@ const (
CryptoLib = "CryptoLib"
StdLib = "StdLib"
)
// IsValid checks that name is a valid native contract's name.
func IsValid(name string) bool {
return name == Management ||
name == Ledger ||
name == Neo ||
name == Gas ||
name == Policy ||
name == Oracle ||
name == Designation ||
name == Notary ||
name == NameService ||
name == CryptoLib ||
name == StdLib
}

View file

@ -0,0 +1,19 @@
package native
import (
"fmt"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/stretchr/testify/require"
)
func TestNativenamesIsValid(t *testing.T) {
// test that all native names has been added to IsValid
contracts := NewContracts(true, map[string][]uint32{})
for _, c := range contracts.Contracts {
require.True(t, nativenames.IsValid(c.Metadata().Name), fmt.Errorf("add %s to nativenames.IsValid(...)", c))
}
require.False(t, nativenames.IsValid("unkonwn"))
}

View file

@ -54,6 +54,7 @@ var _ interop.Contract = (*testNative)(nil)
// registerNative registers native contract in the blockchain.
func (bc *Blockchain) registerNative(c interop.Contract) {
bc.contracts.Contracts = append(bc.contracts.Contracts, c)
bc.config.NativeUpdateHistories[c.Metadata().Name] = c.Metadata().UpdateHistory
}
const (
@ -62,8 +63,10 @@ const (
)
func newTestNative() *testNative {
cMD := interop.NewContractMD("Test.Native.Sum", 0)
cMD.UpdateHistory = []uint32{0}
tn := &testNative{
meta: *interop.NewContractMD("Test.Native.Sum", 0),
meta: *cMD,
blocks: make(chan uint32, 1),
}
defer tn.meta.UpdateHash()
@ -246,6 +249,21 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
require.Error(t, v.Run())
})
t.Run("fail, bad NativeUpdateHistory height", func(t *testing.T) {
tn.Metadata().UpdateHistory = []uint32{chain.blockHeight + 1}
v := ic.SpawnVM()
v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All)
v.Estack().PushVal(14)
v.Estack().PushVal(28)
v.Jump(v.Context(), sumOffset)
// it's prohibited to call natives before NativeUpdateHistory[0] height
require.Error(t, v.Run())
// set the value back to 0
tn.Metadata().UpdateHistory = []uint32{0}
})
t.Run("success", func(t *testing.T) {
v := ic.SpawnVM()
v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All)

View file

@ -32,7 +32,7 @@ type ContractBase struct {
// NativeContract holds information about native contract.
type NativeContract struct {
ContractBase
ActiveBlockIndex uint32 `json:"activeblockindex"`
UpdateHistory []uint32 `json:"updatehistory"`
}
// DecodeBinary implements Serializable interface.

View file

@ -566,6 +566,7 @@ var rpcTestCases = map[string][]rpcTestCase{
cs := e.chain.GetContractState((*lst)[i].Hash)
require.NotNil(t, cs)
require.True(t, cs.ID <= 0)
require.Equal(t, []uint32{0}, (*lst)[i].UpdateHistory)
}
},
},