Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

native: implement HF-based update #3402

Merged
merged 12 commits into from
May 10, 2024
Merged
8 changes: 7 additions & 1 deletion cli/vm/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ func newTestVMClIWithState(t *testing.T) *executor {
}
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, store)
require.NoError(t, err)

// Save config for future usage.
protoCfg := bc.GetConfig()

go bc.Run()
e := neotest.NewExecutor(t, bc, validators, committee)
basicchain.InitSimple(t, "../../", e)
Expand All @@ -145,7 +149,9 @@ func newTestVMClIWithState(t *testing.T) *executor {
require.NoError(t, err)
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions = opts
cfg.ProtocolConfiguration.StateRootInHeader = true
cfg.ProtocolConfiguration.StateRootInHeader = protoCfg.StateRootInHeader
cfg.ProtocolConfiguration.P2PStateExchangeExtensions = protoCfg.P2PStateExchangeExtensions
cfg.ProtocolConfiguration.Hardforks = protoCfg.Hardforks
return newTestVMCLIWithLogoAndCustomConfig(t, false, &cfg)
}

Expand Down
2 changes: 1 addition & 1 deletion docs/node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ protocol-related settings described in the table below.
| --- | --- | --- | --- | --- |
| CommitteeHistory | map[uint32]uint32 | none | Number of committee members after the given height, for example `{0: 1, 20: 4}` sets up a chain with one committee member since the genesis and then changes the setting to 4 committee members at the height of 20. `StandbyCommittee` committee setting must have the number of keys equal or exceeding the highest value in this option. Blocks numbers where the change happens must be divisible by the old and by the new values simultaneously. If not set, committee size is derived from the `StandbyCommittee` setting and never changes. |
| Genesis | [Genesis](#Genesis-Configuration) | none | The set of genesis block settings including NeoGo-specific protocol extensions that should be enabled at the genesis block or during native contracts initialisation. |
| Hardforks | `map[string]uint32` | [] | The set of incompatible changes that affect node behaviour starting from the specified height. The default value is an empty set which should be interpreted as "each known hard-fork is applied from the zero blockchain height". The list of valid hard-fork names:<br>• `Aspidochelone` represents hard-fork introduced in [#2469](https://github.com/nspcc-dev/neo-go/pull/2469) (ported from the [reference](https://github.com/neo-project/neo/pull/2712)). It adjusts the prices of `System.Contract.CreateStandardAccount` and `System.Contract.CreateMultisigAccount` interops so that the resulting prices are in accordance with `sha256` method of native `CryptoLib` contract. It also includes [#2519](https://github.com/nspcc-dev/neo-go/pull/2519) (ported from the [reference](https://github.com/neo-project/neo/pull/2749)) that adjusts the price of `System.Runtime.GetRandom` interop and fixes its vulnerability. A special NeoGo-specific change is included as well for ContractManagement's update/deploy call flags behaviour to be compatible with pre-0.99.0 behaviour that was changed because of the [3.2.0 protocol change](https://github.com/neo-project/neo/pull/2653).<br>• `Basilisk` represents hard-fork introduced in [#3056](https://github.com/nspcc-dev/neo-go/pull/3056) (ported from the [reference](https://github.com/neo-project/neo/pull/2881)). It enables strict smart contract script check against a set of JMP instructions and against method boundaries enabled on contract deploy or update. It also includes [#3080](https://github.com/nspcc-dev/neo-go/pull/3080) (ported from the [reference](https://github.com/neo-project/neo/pull/2883)) that increases `stackitem.Integer` JSON parsing precision up to the maximum value supported by the NeoVM. It also includes [#3085](https://github.com/nspcc-dev/neo-go/pull/3085) (ported from the [reference](https://github.com/neo-project/neo/pull/2810)) that enables strict check for notifications emitted by a contract to precisely match the events specified in the contract manifest. |
| Hardforks | `map[string]uint32` | [] | The set of incompatible changes that affect node behaviour starting from the specified height. The default value is an empty set which should be interpreted as "each known hard-fork is applied from the zero blockchain height". The list of valid hard-fork names:<br>• `Aspidochelone` represents hard-fork introduced in [#2469](https://github.com/nspcc-dev/neo-go/pull/2469) (ported from the [reference](https://github.com/neo-project/neo/pull/2712)). It adjusts the prices of `System.Contract.CreateStandardAccount` and `System.Contract.CreateMultisigAccount` interops so that the resulting prices are in accordance with `sha256` method of native `CryptoLib` contract. It also includes [#2519](https://github.com/nspcc-dev/neo-go/pull/2519) (ported from the [reference](https://github.com/neo-project/neo/pull/2749)) that adjusts the price of `System.Runtime.GetRandom` interop and fixes its vulnerability. A special NeoGo-specific change is included as well for ContractManagement's update/deploy call flags behaviour to be compatible with pre-0.99.0 behaviour that was changed because of the [3.2.0 protocol change](https://github.com/neo-project/neo/pull/2653).<br>• `Basilisk` represents hard-fork introduced in [#3056](https://github.com/nspcc-dev/neo-go/pull/3056) (ported from the [reference](https://github.com/neo-project/neo/pull/2881)). It enables strict smart contract script check against a set of JMP instructions and against method boundaries enabled on contract deploy or update. It also includes [#3080](https://github.com/nspcc-dev/neo-go/pull/3080) (ported from the [reference](https://github.com/neo-project/neo/pull/2883)) that increases `stackitem.Integer` JSON parsing precision up to the maximum value supported by the NeoVM. It also includes [#3085](https://github.com/nspcc-dev/neo-go/pull/3085) (ported from the [reference](https://github.com/neo-project/neo/pull/2810)) that enables strict check for notifications emitted by a contract to precisely match the events specified in the contract manifest. <br>• `Cockatrice` represents hard-fork introduced in [#3402](https://github.com/nspcc-dev/neo-go/pull/3402) (ported from the [reference](https://github.com/neo-project/neo/pull/2942)). Initially it is introduced along with the ability to update native contracts. This hard-fork also includes a couple of new native smart contract APIs: `keccak256` of native CryptoLib contract introduced in [#3301](https://github.com/nspcc-dev/neo-go/pull/3301) (ported from the [reference](https://github.com/neo-project/neo/pull/2925)) and `getCommitteeAddress` of native NeoToken contract inctroduced in [#3362](https://github.com/nspcc-dev/neo-go/pull/3362) (ported from the [reference](https://github.com/neo-project/neo/pull/3154)). |
| Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. |
| MaxBlockSize | `uint32` | `262144` | Maximum block size in bytes. |
| MaxBlockSystemFee | `int64` | `900000000000` | Maximum overall transactions system fee per block. |
Expand Down
4 changes: 2 additions & 2 deletions pkg/compiler/interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes"
"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"
Expand Down Expand Up @@ -610,7 +610,7 @@ func TestCallWithVersion(t *testing.T) {
e.DeployContract(t, ctr, nil)
c := e.CommitteeInvoker(ctr.Hash)

policyH := state.CreateNativeContractHash(nativenames.Policy)
policyH := nativehashes.Policy
t.Run("good", func(t *testing.T) {
c.Invoke(t, e.Chain.GetBaseExecFee(), "callWithVersion", policyH.BytesBE(), 0, "getExecFeeFactor")
})
Expand Down
8 changes: 5 additions & 3 deletions pkg/compiler/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func runNativeTestCases(t *testing.T, ctr interop.ContractMD, name string, nativ
})
}

func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.MethodAndPrice {
func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []string) interop.HFSpecificMethodAndPrice {
paramLen := len(params)

switch {
Expand All @@ -308,8 +308,10 @@ func getMethod(t *testing.T, ctr interop.ContractMD, name string, params []strin
name = strings.TrimSuffix(name, "WithData")
}

md, ok := ctr.GetMethod(name, paramLen)
require.True(t, ok, ctr.Manifest.Name, name, paramLen)
latestHF := config.LatestHardfork()
cMD := ctr.HFSpecificContractMD(&latestHF)
md, ok := cMD.GetMethod(name, paramLen)
require.True(t, ok, cMD.Manifest.Name, name, paramLen)
return md
}

Expand Down
41 changes: 41 additions & 0 deletions pkg/config/hardfork.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
// Hardfork represents the application hard-fork identifier.
type Hardfork byte

// HFDefault is a default value of Hardfork enum. It's a special constant
// aimed to denote the node code enabled by default starting from the
// genesis block. HFDefault is not a hard-fork, but this constant can be used for
// convenient hard-forks comparison and to refer to the default hard-fork-less
// node behaviour.
const HFDefault Hardfork = 0 // Default

const (
// HFAspidochelone represents hard-fork introduced in #2469 (ported from
// https://github.com/neo-project/neo/pull/2712) and #2519 (ported from
Expand All @@ -15,6 +22,11 @@
// https://github.com/neo-project/neo/pull/2883) and #3085 (ported from
// https://github.com/neo-project/neo/pull/2810).
HFBasilisk // Basilisk
// HFCockatrice represents hard-fork introduced in #3402 (ported from
// https://github.com/neo-project/neo/pull/2942), #3301 (ported from
// https://github.com/neo-project/neo/pull/2925) and #3362 (ported from
// https://github.com/neo-project/neo/pull/3154).
HFCockatrice // Cockatrice
// hfLast denotes the end of hardforks enum. Consider adding new hardforks
// before hfLast.
hfLast
Expand All @@ -36,9 +48,38 @@
}
}

// Cmp returns the result of hardforks comparison. It returns:
//
// -1 if hf < other
// 0 if hf == other
// +1 if hf > other
func (hf Hardfork) Cmp(other Hardfork) int {
switch {
case hf == other:
return 0
case hf < other:
return -1

Check warning on line 61 in pkg/config/hardfork.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/hardfork.go#L60-L61

Added lines #L60 - L61 were not covered by tests
default:
return 1
}
}

// Prev returns the previous hardfork for the given one. Calling Prev for the default hardfork is a no-op.
func (hf Hardfork) Prev() Hardfork {
if hf == HFDefault {
panic("unexpected call to Prev for the default hardfork")

Check warning on line 70 in pkg/config/hardfork.go

View check run for this annotation

Codecov / codecov/patch

pkg/config/hardfork.go#L70

Added line #L70 was not covered by tests
}
return hf >> 1
}

// IsHardforkValid denotes whether the provided string represents a valid
// Hardfork name.
func IsHardforkValid(s string) bool {
_, ok := hardforks[s]
return ok
}

// LatestHardfork returns latest known hardfork.
func LatestHardfork() Hardfork {
return hfLast >> 1
}
16 changes: 10 additions & 6 deletions pkg/config/hardfork_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 41 additions & 8 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"sync/atomic"
"time"

json "github.com/nspcc-dev/go-ordered-json"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/limits"
"github.com/nspcc-dev/neo-go/pkg/core/block"
Expand Down Expand Up @@ -44,7 +45,7 @@

// Tuning parameters.
const (
version = "0.2.10"
version = "0.2.11"

// DefaultInitialGAS is the default amount of GAS emitted to the standby validators
// multisignature account during native GAS contract initialization.
Expand Down Expand Up @@ -288,7 +289,7 @@
cfg.Hardforks[hf.String()] = 0
}
log.Info("Hardforks are not set, using default value")
} else {
} else if len(cfg.Hardforks) != 0 {
// Explicitly set the height of all old omitted hardforks to 0 for proper
// IsHardforkEnabled behaviour.
for _, hf := range config.Hardforks {
Expand Down Expand Up @@ -341,16 +342,36 @@
return res, h, err
}

// getCurrentHF returns the latest currently enabled hardfork. In case if no hardforks are enabled, the
// default config.Hardfork(0) value is returned.
func (bc *Blockchain) getCurrentHF() config.Hardfork {
var (
height = bc.BlockHeight()
current config.Hardfork
)
// Rely on the fact that hardforks list is continuous.
for _, hf := range config.Hardforks {
enableHeight, ok := bc.config.Hardforks[hf.String()]
if !ok || height < enableHeight {
break
}
current = hf
}
return current
}

// SetOracle sets oracle module. It can safely be called on the running blockchain.
// To unregister Oracle service use SetOracle(nil).
func (bc *Blockchain) SetOracle(mod native.OracleService) {
orc := bc.contracts.Oracle
currentHF := bc.getCurrentHF()
if mod != nil {
md, ok := orc.GetMethod(manifest.MethodVerify, -1)
orcMd := orc.HFSpecificContractMD(&currentHF)
md, ok := orcMd.GetMethod(manifest.MethodVerify, -1)
if !ok {
panic(fmt.Errorf("%s method not found", manifest.MethodVerify))
}
mod.UpdateNativeContract(orc.NEF.Script, orc.GetOracleResponseScript(),
mod.UpdateNativeContract(orcMd.NEF.Script, orc.GetOracleResponseScript(),
orc.Hash, md.MD.Offset)
keys, _, err := bc.GetDesignatedByRole(noderoles.Oracle)
if err != nil {
Expand Down Expand Up @@ -487,6 +508,7 @@
// Check autogenerated native contracts' manifests and NEFs against the stored ones.
// Need to be done after native Management cache initialization to be able to get
// contract state from DAO via high-level bc API.
var current = bc.getCurrentHF()
for _, c := range bc.contracts.Contracts {
md := c.Metadata()
storedCS := bc.GetContractState(md.Hash)
Expand All @@ -504,17 +526,20 @@
if err != nil {
return fmt.Errorf("failed to check native %s state against autogenerated one: %w", md.Name, err)
}
hfMD := md.HFSpecificContractMD(&current)
autogenCS := &state.Contract{
ContractBase: md.ContractBase,
ContractBase: hfMD.ContractBase,
UpdateCounter: storedCS.UpdateCounter, // it can be restored only from the DB, so use the stored value.
}
autogenCSBytes, err := stackitem.SerializeConvertible(autogenCS)
if err != nil {
return fmt.Errorf("failed to check native %s state against autogenerated one: %w", md.Name, err)
}
if !bytes.Equal(storedCSBytes, autogenCSBytes) {
return fmt.Errorf("native %s: version mismatch (stored contract state differs from autogenerated one), "+
"try to resynchronize the node from the genesis", md.Name)
storedJ, _ := json.Marshal(storedCS)
autogenJ, _ := json.Marshal(autogenCS)
return fmt.Errorf("native %s: version mismatch for the latest hardfork %s (stored contract state differs from autogenerated one), "+
"try to resynchronize the node from the genesis: %s vs %s", md.Name, current, string(storedJ), string(autogenJ))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's a friendly error message. Many users may even not notice the real message in these JSON dumps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dumps are not indented, so they are quite compact, I've tested it. I may add some indent. Or is it better to remove dumps at all?

}
}

Expand Down Expand Up @@ -2269,8 +2294,16 @@
// GetNatives returns list of native contracts.
func (bc *Blockchain) GetNatives() []state.NativeContract {
res := make([]state.NativeContract, 0, len(bc.contracts.Contracts))
current := bc.getCurrentHF()
for _, c := range bc.contracts.Contracts {
res = append(res, c.Metadata().NativeContract)
activeIn := c.ActiveIn()
if !(activeIn == nil || activeIn.Cmp(current) <= 0) {
continue

Check warning on line 2301 in pkg/core/blockchain.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/blockchain.go#L2301

Added line #L2301 was not covered by tests
}
md := c.Metadata().HFSpecificContractMD(&current)
res = append(res, state.NativeContract{
ContractBase: md.ContractBase,
})
}
return res
}
Expand Down
15 changes: 12 additions & 3 deletions pkg/core/blockchain_core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,24 @@ func TestBlockchain_IsRunning(t *testing.T) {
}

func TestNewBlockchain_InitHardforks(t *testing.T) {
t.Run("empty set", func(t *testing.T) {
t.Run("nil set", func(t *testing.T) {
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
c.ProtocolConfiguration.Hardforks = map[string]uint32{}
c.ProtocolConfiguration.Hardforks = nil
require.NoError(t, c.ProtocolConfiguration.Validate())
})
require.Equal(t, map[string]uint32{
config.HFAspidochelone.String(): 0,
config.HFBasilisk.String(): 0,
config.HFCockatrice.String(): 0,
}, bc.GetConfig().Hardforks)
})
t.Run("empty set", func(t *testing.T) {
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
c.ProtocolConfiguration.Hardforks = map[string]uint32{}
require.NoError(t, c.ProtocolConfiguration.Validate())
})
require.Equal(t, map[string]uint32{}, bc.GetConfig().Hardforks)
})
t.Run("missing old", func(t *testing.T) {
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
c.ProtocolConfiguration.Hardforks = map[string]uint32{config.HFBasilisk.String(): 5}
Expand All @@ -392,12 +400,13 @@ func TestNewBlockchain_InitHardforks(t *testing.T) {
})
t.Run("all present", func(t *testing.T) {
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
c.ProtocolConfiguration.Hardforks = map[string]uint32{config.HFAspidochelone.String(): 5, config.HFBasilisk.String(): 10}
c.ProtocolConfiguration.Hardforks = map[string]uint32{config.HFAspidochelone.String(): 5, config.HFBasilisk.String(): 10, config.HFCockatrice.String(): 15}
require.NoError(t, c.ProtocolConfiguration.Validate())
})
require.Equal(t, map[string]uint32{
config.HFAspidochelone.String(): 5,
config.HFBasilisk.String(): 10,
config.HFCockatrice.String(): 15,
}, bc.GetConfig().Hardforks)
})
}
Loading
Loading