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 VerifyBlocks: true
VerifyTransactions: false VerifyTransactions: false
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -11,6 +11,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -17,6 +17,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -21,6 +21,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: false VerifyTransactions: false
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -9,6 +9,17 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -18,6 +18,18 @@ ProtocolConfiguration:
VerifyBlocks: true VerifyBlocks: true
VerifyTransactions: true VerifyTransactions: true
P2PSigExtensions: 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: ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file. # 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) { 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(neo.Hash), cs.NEO.Hash.BytesBE())
require.Equal(t, []byte(gas.Hash), cs.GAS.Hash.BytesBE()) require.Equal(t, []byte(gas.Hash), cs.GAS.Hash.BytesBE())
require.Equal(t, []byte(oracle.Hash), cs.Oracle.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. // Here we test that corresponding method does exist, is invoked and correct value is returned.
func TestNativeHelpersCompile(t *testing.T) { func TestNativeHelpersCompile(t *testing.T) {
cs := native.NewContracts(true) cs := native.NewContracts(true, map[string][]uint32{})
u160 := `interop.Hash160("aaaaaaaaaaaaaaaaaaaa")` u160 := `interop.Hash160("aaaaaaaaaaaaaaaaaaaa")`
u256 := `interop.Hash256("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")` u256 := `interop.Hash256("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")`
pub := `interop.PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")` pub := `interop.PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")`

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"gopkg.in/yaml.v2" "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) 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 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"` MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"`
// MaxTransactionsPerBlock is the maximum amount of transactions per block. // MaxTransactionsPerBlock is the maximum amount of transactions per block.
MaxTransactionsPerBlock uint16 `yaml:"MaxTransactionsPerBlock"` 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 enables additional signature-related logic.
P2PSigExtensions bool `yaml:"P2PSigExtensions"` P2PSigExtensions bool `yaml:"P2PSigExtensions"`
// ReservedAttributes allows to have reserved attributes range for experimental or private purposes. // 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 { if err != nil {
return nil, err return nil, err
} }
if len(cfg.NativeUpdateHistories) == 0 {
cfg.NativeUpdateHistories = map[string][]uint32{}
log.Info("NativeActivations are not set, using default values")
}
bc := &Blockchain{ bc := &Blockchain{
config: cfg, config: cfg,
dao: dao.NewSimple(s, cfg.Magic, cfg.StateRootInHeader), 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{}), subCh: make(chan interface{}),
unsubCh: 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) bc.stateRoot = stateroot.NewModule(bc, bc.log, bc.dao.Store)

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"math/rand" "math/rand"
"path"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -1614,3 +1615,40 @@ func TestMPTDeleteNoKey(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, vm.HaltState, aer.VMState) 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. // Sort sorts interop functions by id.
func Sort(fs []Function) { func Sort(fs []Function) {
sort.Slice(fs, func(i, j int) bool { return fs[i].ID < fs[j].ID }) 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. // "C" and "O" can easily be typed by accident.
func TestNamesASCII(t *testing.T) { func TestNamesASCII(t *testing.T) {
cs := NewContracts(true) cs := NewContracts(true, map[string][]uint32{})
for _, c := range cs.Contracts { for _, c := range cs.Contracts {
require.True(t, isASCII(c.Metadata().Name)) require.True(t, isASCII(c.Metadata().Name))
for _, m := range c.Metadata().Methods { 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, // NewContracts returns new set of native contracts with new GAS, NEO, Policy, Oracle,
// Designate and (optional) Notary contracts. // Designate and (optional) Notary contracts.
func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { func NewContracts(p2pSigExtensionsEnabled bool, nativeUpdateHistories map[string][]uint32) *Contracts {
cs := new(Contracts) cs := new(Contracts)
mgmt := newManagement() mgmt := newManagement()
@ -117,6 +117,13 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
cs.Contracts = append(cs.Contracts, notary) 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 return cs
} }

View file

@ -26,6 +26,13 @@ func Call(ic *interop.Context) error {
if c == nil { if c == nil {
return fmt.Errorf("native contract %d not found", version) 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()) m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP())
if !ok { if !ok {
return fmt.Errorf("method not found") 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") return errors.New("onPersist must be trigered by system")
} }
for _, c := range ic.Natives { for _, c := range ic.Natives {
if !c.Metadata().IsActive(ic.Block.Index) {
continue
}
err := c.OnPersist(ic) err := c.OnPersist(ic)
if err != nil { if err != nil {
return err return err
@ -71,6 +81,9 @@ func PostPersist(ic *interop.Context) error {
return errors.New("postPersist must be trigered by system") return errors.New("postPersist must be trigered by system")
} }
for _, c := range ic.Natives { for _, c := range ic.Natives {
if !c.Metadata().IsActive(ic.Block.Index) {
continue
}
err := c.PostPersist(ic) err := c.PostPersist(ic)
if err != nil { if err != nil {
return err return err

View file

@ -446,12 +446,12 @@ func (m *Management) Metadata() *interop.ContractMD {
// OnPersist implements Contract interface. // OnPersist implements Contract interface.
func (m *Management) OnPersist(ic *interop.Context) error { 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 { for _, native := range ic.Natives {
md := native.Metadata() md := native.Metadata()
history := md.UpdateHistory
if len(history) == 0 || history[0] != ic.Block.Index {
continue
}
cs := &state.Contract{ cs := &state.Contract{
ContractBase: md.ContractBase, ContractBase: md.ContractBase,

View file

@ -14,3 +14,18 @@ const (
CryptoLib = "CryptoLib" CryptoLib = "CryptoLib"
StdLib = "StdLib" 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. // registerNative registers native contract in the blockchain.
func (bc *Blockchain) registerNative(c interop.Contract) { func (bc *Blockchain) registerNative(c interop.Contract) {
bc.contracts.Contracts = append(bc.contracts.Contracts, c) bc.contracts.Contracts = append(bc.contracts.Contracts, c)
bc.config.NativeUpdateHistories[c.Metadata().Name] = c.Metadata().UpdateHistory
} }
const ( const (
@ -62,8 +63,10 @@ const (
) )
func newTestNative() *testNative { func newTestNative() *testNative {
cMD := interop.NewContractMD("Test.Native.Sum", 0)
cMD.UpdateHistory = []uint32{0}
tn := &testNative{ tn := &testNative{
meta: *interop.NewContractMD("Test.Native.Sum", 0), meta: *cMD,
blocks: make(chan uint32, 1), blocks: make(chan uint32, 1),
} }
defer tn.meta.UpdateHash() defer tn.meta.UpdateHash()
@ -246,6 +249,21 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
require.Error(t, v.Run()) 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) { t.Run("success", func(t *testing.T) {
v := ic.SpawnVM() v := ic.SpawnVM()
v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) 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. // NativeContract holds information about native contract.
type NativeContract struct { type NativeContract struct {
ContractBase ContractBase
ActiveBlockIndex uint32 `json:"activeblockindex"` UpdateHistory []uint32 `json:"updatehistory"`
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.

View file

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