diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index 39e856cbb..d602bb43d 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -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. diff --git a/config/protocol.privnet.docker.four.yml b/config/protocol.privnet.docker.four.yml index 0d801a219..3fb17befd 100644 --- a/config/protocol.privnet.docker.four.yml +++ b/config/protocol.privnet.docker.four.yml @@ -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. diff --git a/config/protocol.privnet.docker.one.yml b/config/protocol.privnet.docker.one.yml index fda02afe5..f23b01f28 100644 --- a/config/protocol.privnet.docker.one.yml +++ b/config/protocol.privnet.docker.one.yml @@ -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. diff --git a/config/protocol.privnet.docker.single.yml b/config/protocol.privnet.docker.single.yml index b091cc272..7401609b4 100644 --- a/config/protocol.privnet.docker.single.yml +++ b/config/protocol.privnet.docker.single.yml @@ -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. diff --git a/config/protocol.privnet.docker.three.yml b/config/protocol.privnet.docker.three.yml index 883c21c7f..e4c9ebfd0 100644 --- a/config/protocol.privnet.docker.three.yml +++ b/config/protocol.privnet.docker.three.yml @@ -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. diff --git a/config/protocol.privnet.docker.two.yml b/config/protocol.privnet.docker.two.yml index 5a2835531..d32e79a56 100644 --- a/config/protocol.privnet.docker.two.yml +++ b/config/protocol.privnet.docker.two.yml @@ -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. diff --git a/config/protocol.privnet.yml b/config/protocol.privnet.yml index dd27fc1e4..0ca78c3dc 100644 --- a/config/protocol.privnet.yml +++ b/config/protocol.privnet.yml @@ -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. diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index 9e5fe247e..1668e7c93 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -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. diff --git a/config/protocol.unit_testnet.single.yml b/config/protocol.unit_testnet.single.yml index be3daf0dc..dc47603ad 100644 --- a/config/protocol.unit_testnet.single.yml +++ b/config/protocol.unit_testnet.single.yml @@ -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. diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index 5c7fcdb39..c56b71090 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -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. diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index af49507c9..8d8771f55 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -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")` diff --git a/pkg/config/config.go b/pkg/config/config.go index dbea5bac7..0248e057a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..16a59b317 --- /dev/null +++ b/pkg/config/config_test.go @@ -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) +} diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 0dafadb6e..bda04c28f 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -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. diff --git a/pkg/config/testdata/protocol.test.yml b/pkg/config/testdata/protocol.test.yml new file mode 100644 index 000000000..31619903f --- /dev/null +++ b/pkg/config/testdata/protocol.test.yml @@ -0,0 +1,4 @@ +ProtocolConfiguration: + NativeActivations: + ContractManagement: [0] + UnexpectedContractName: [0] diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5af463146..43fe9f6fd 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 59f6c3a5e..207aaeac9 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -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) + } +} diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 6768c5324..9f7471ab7 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -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 }) diff --git a/pkg/core/native/compatibility_test.go b/pkg/core/native/compatibility_test.go index 1b8e3c37c..40754114d 100644 --- a/pkg/core/native/compatibility_test.go +++ b/pkg/core/native/compatibility_test.go @@ -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 { diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 20566f969..a81264523 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -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 } diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 084a3f2de..c112ba539 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -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 diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 75eed6978..00a7270e9 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -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, diff --git a/pkg/core/native/nativenames_test.go b/pkg/core/native/nativenames_test.go index 24770ab2e..7fd02b2d5 100644 --- a/pkg/core/native/nativenames_test.go +++ b/pkg/core/native/nativenames_test.go @@ -10,7 +10,7 @@ import ( func TestNativenamesIsValid(t *testing.T) { // test that all native names has been added to IsValid - contracts := NewContracts(true) + 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)) } diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 358641c52..17bd076f2 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -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) diff --git a/pkg/core/state/contract.go b/pkg/core/state/contract.go index 8d918dd90..6c2fd0310 100644 --- a/pkg/core/state/contract.go +++ b/pkg/core/state/contract.go @@ -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. diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 2a5c18335..d296cef39 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -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) } }, },