diff --git a/docs/node-configuration.md b/docs/node-configuration.md index c5ebcf5fa..fd7307f64 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -325,6 +325,7 @@ protocol-related settings described in the table below. | Section | Type | Default value | Description | Notes | | --- | --- | --- | --- | --- | | 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:
• `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).
• `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. | | Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. | | MaxBlockSize | `uint32` | `262144` | Maximum block size in bytes. | @@ -346,3 +347,39 @@ protocol-related settings described in the table below. | ValidatorsCount | `uint32` | `0` | Number of validators set for the whole network lifetime, can't be set if `ValidatorsHistory` setting is used. | | ValidatorsHistory | map[uint32]uint32 | none | Number of consensus nodes to use after given height (see `CommitteeHistory` also). Heights where the change occurs must be divisible by the number of committee members at that height. Can't be used with `ValidatorsCount` not equal to zero. | | VerifyTransactions | `bool` | `false` | Denotes whether to verify transactions in the received blocks. | + +### Genesis Configuration + +`Genesis` subsection of protocol configuration section contains a set of settings +specific for genesis block including NeoGo node extensions that should be enabled +during genesis block persist or at the moment of native contracts initialisation. +`Genesis` has the following structure: +``` +Genesis: + Roles: + NeoFSAlphabet: + - 033238fa63bd08115ebf442d4af897eea2f6866e4c2001cd1f6e7656acdd91a5d3 + - 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c + - 02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e + - 03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050 + Oracle: + - 03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0 + - 0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30 +``` +where: +- `Roles` is a map from node roles that should be set at the moment of native + RoleManagement contract initialisation to the list of hex-encoded public keys + corresponding to this role. The set of valid roles includes: + - `StateValidator` + - `Oracle` + - `NeoFSAlphabet` + - `P2PNotary` + + Roles designation order follows the enumeration above. Designation + notifications will be emitted after each configured role designation. + + Note that Roles is a NeoGo extension that isn't supported by the NeoC# node and + must be disabled on the public Neo N3 networks. Roles extension is compatible + with NativeUpdateHistory setting, which means that specified roles will be set + only during native RoleManagement contract initialisation (which may be + performed in some non-genesis block). By default, no roles are designated. diff --git a/pkg/config/genesis_extensions.go b/pkg/config/genesis_extensions.go new file mode 100644 index 000000000..6e3685f2b --- /dev/null +++ b/pkg/config/genesis_extensions.go @@ -0,0 +1,51 @@ +package config + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" +) + +// Genesis represents a set of genesis block settings including the extensions +// enabled in the genesis block or during native contracts initialization. +type Genesis struct { + // Roles contains the set of roles that should be designated during native + // Designation contract initialization. It is NeoGo extension and must be + // disabled on the public Neo N3 networks. + Roles map[noderoles.Role]keys.PublicKeys +} + +// genesisAux is an auxiliary structure for Genesis YAML marshalling. +type genesisAux struct { + Roles map[string]keys.PublicKeys `yaml:"Roles"` +} + +// MarshalYAML implements the YAML marshaler interface. +func (e Genesis) MarshalYAML() (any, error) { + var aux genesisAux + aux.Roles = make(map[string]keys.PublicKeys, len(e.Roles)) + for r, ks := range e.Roles { + aux.Roles[r.String()] = ks + } + return aux, nil +} + +// UnmarshalYAML implements the YAML unmarshaler interface. +func (e *Genesis) UnmarshalYAML(unmarshal func(any) error) error { + var aux genesisAux + if err := unmarshal(&aux); err != nil { + return err + } + + e.Roles = make(map[noderoles.Role]keys.PublicKeys) + for s, ks := range aux.Roles { + r, ok := noderoles.FromString(s) + if !ok { + return fmt.Errorf("unknown node role: %s", s) + } + e.Roles[r] = ks + } + + return nil +} diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 3b2b59c06..2e9f8aad8 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -16,6 +16,10 @@ type ( ProtocolConfiguration struct { // CommitteeHistory stores committee size change history (height: size). CommitteeHistory map[uint32]uint32 `yaml:"CommitteeHistory"` + // Genesis stores genesis-related settings including a set of NeoGo + // extensions that should be included into genesis block or be enabled + // at the moment of native contracts initialization. + Genesis Genesis `yaml:"Genesis"` Magic netmode.Magic `yaml:"Magic"` MemPoolSize int `yaml:"MemPoolSize"` diff --git a/pkg/config/protocol_config_test.go b/pkg/config/protocol_config_test.go index 1b44c202d..fb008d002 100644 --- a/pkg/config/protocol_config_test.go +++ b/pkg/config/protocol_config_test.go @@ -1,11 +1,17 @@ package config import ( + "encoding/hex" + "fmt" "path/filepath" "testing" "time" + "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestProtocolConfigurationValidation(t *testing.T) { @@ -275,3 +281,65 @@ func TestProtocolConfigurationEquals(t *testing.T) { p.ValidatorsHistory = map[uint32]uint32{112: 0} require.False(t, p.Equals(o)) } + +func TestGenesisExtensionsMarshalYAML(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := pk.PublicKey() + + t.Run("MarshalUnmarshalYAML", func(t *testing.T) { + g := &Genesis{ + Roles: map[noderoles.Role]keys.PublicKeys{ + noderoles.NeoFSAlphabet: {pub}, + noderoles.P2PNotary: {pub}, + }, + } + testserdes.MarshalUnmarshalYAML(t, g, new(Genesis)) + }) + + t.Run("unmarshal config", func(t *testing.T) { + t.Run("good", func(t *testing.T) { + pubStr := hex.EncodeToString(pub.Bytes()) + cfgYml := fmt.Sprintf(`ProtocolConfiguration: + Genesis: + Roles: + NeoFSAlphabet: + - %s + - %s + Oracle: + - %s + - %s`, pubStr, pubStr, pubStr, pubStr) + cfg := new(Config) + require.NoError(t, yaml.Unmarshal([]byte(cfgYml), cfg)) + require.Equal(t, 2, len(cfg.ProtocolConfiguration.Genesis.Roles)) + require.Equal(t, keys.PublicKeys{pub, pub}, cfg.ProtocolConfiguration.Genesis.Roles[noderoles.NeoFSAlphabet]) + require.Equal(t, keys.PublicKeys{pub, pub}, cfg.ProtocolConfiguration.Genesis.Roles[noderoles.Oracle]) + }) + + t.Run("unknown role", func(t *testing.T) { + pubStr := hex.EncodeToString(pub.Bytes()) + cfgYml := fmt.Sprintf(`ProtocolConfiguration: + Genesis: + Roles: + BadRole: + - %s`, pubStr) + cfg := new(Config) + err := yaml.Unmarshal([]byte(cfgYml), cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown node role: BadRole") + }) + + t.Run("last role", func(t *testing.T) { + pubStr := hex.EncodeToString(pub.Bytes()) + cfgYml := fmt.Sprintf(`ProtocolConfiguration: + Genesis: + Roles: + last: + - %s`, pubStr) + cfg := new(Config) + err := yaml.Unmarshal([]byte(cfgYml), cfg) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown node role: last") + }) + }) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index a763b925e..19321f887 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -87,7 +87,7 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts { cs.Policy = policy cs.Contracts = append(cs.Contracts, neo, gas, policy) - desig := newDesignate(cfg.P2PSigExtensions) + desig := newDesignate(cfg.P2PSigExtensions, cfg.Genesis.Roles) desig.NEO = neo cs.Designate = desig cs.Contracts = append(cs.Contracts, desig) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 12d8fd79a..9cac6c7c1 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "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/stackitem" ) @@ -32,6 +33,9 @@ type Designate struct { // p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant. p2pSigExtensionsEnabled bool + // initialNodeRoles defines a set of node roles that should be defined at the contract + // deployment (initialization). + initialNodeRoles map[noderoles.Role]keys.PublicKeys OracleService atomic.Value // NotaryService represents a Notary node module. @@ -97,9 +101,10 @@ func (s *Designate) isValidRole(r noderoles.Role) bool { r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary) } -func newDesignate(p2pSigExtensionsEnabled bool) *Designate { +func newDesignate(p2pSigExtensionsEnabled bool, initialNodeRoles map[noderoles.Role]keys.PublicKeys) *Designate { s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)} s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled + s.initialNodeRoles = initialNodeRoles defer s.UpdateHash() desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, @@ -127,6 +132,19 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { func (s *Designate) Initialize(ic *interop.Context) error { cache := &DesignationCache{} ic.DAO.SetCache(s.ID, cache) + + if len(s.initialNodeRoles) != 0 { + for _, r := range noderoles.Roles { + pubs, ok := s.initialNodeRoles[r] + if !ok { + continue + } + err := s.DesignateAsRole(ic, r, pubs) + if err != nil { + return fmt.Errorf("failed to initialize Designation role data for role %s: %w", r, err) + } + } + } return nil } @@ -355,10 +373,14 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs if !s.isValidRole(r) { return ErrInvalidRole } - h := s.NEO.GetCommitteeAddress(ic.DAO) - if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { - return ErrInvalidWitness + + if ic.Trigger != trigger.OnPersist { + h := s.NEO.GetCommitteeAddress(ic.DAO) + if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { + return ErrInvalidWitness + } } + if ic.Block == nil { return ErrNoBlock } diff --git a/pkg/core/native/native_test/designate_test.go b/pkg/core/native/native_test/designate_test.go index 5bb7e47d0..2c89f9fe8 100644 --- a/pkg/core/native/native_test/designate_test.go +++ b/pkg/core/native/native_test/designate_test.go @@ -1,14 +1,17 @@ package native_test import ( + "sort" "testing" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -137,3 +140,23 @@ func TestDesignate_Cache(t *testing.T) { require.Nil(t, updatedNodes) require.False(t, updateCalled) } + +func TestDesignate_GenesisRolesExtension(t *testing.T) { + pk1, err := keys.NewPrivateKey() + require.NoError(t, err) + pk2, err := keys.NewPrivateKey() + require.NoError(t, err) + pubs := keys.PublicKeys{pk1.PublicKey(), pk2.PublicKey()} + + bc, acc := chain.NewSingleWithCustomConfig(t, func(blockchain *config.Blockchain) { + blockchain.Genesis.Roles = map[noderoles.Role]keys.PublicKeys{ + noderoles.StateValidator: pubs, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + c := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation)) + + // Check designated node in a separate block. + sort.Sort(pubs) + checkNodeRoles(t, c, true, noderoles.StateValidator, e.Chain.BlockHeight()+1, pubs) +} diff --git a/pkg/core/native/noderoles/role.go b/pkg/core/native/noderoles/role.go index 77e5d058d..0998a67ae 100644 --- a/pkg/core/native/noderoles/role.go +++ b/pkg/core/native/noderoles/role.go @@ -17,6 +17,9 @@ const ( last ) +// Roles is a set of all available roles sorted by values. +var Roles []Role + // roles is a map of valid Role string representation to its type. var roles map[string]Role @@ -24,6 +27,7 @@ func init() { roles = make(map[string]Role) for i := StateValidator; i < last; i = i << 1 { roles[i.String()] = i + Roles = append(Roles, i) } } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index 27685b671..f2c4048e5 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -24,7 +24,7 @@ func TestDesignate_DesignateAsRole(t *testing.T) { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) bl := block.New(bc.config.StateRootInHeader) bl.Index = bc.BlockHeight() + 1 - ic := bc.newInteropContext(trigger.OnPersist, bc.dao, bl, tx) + ic := bc.newInteropContext(trigger.Application, bc.dao, bl, tx) ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index c1bf5c3b0..241c26570 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -390,3 +390,23 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error { return nil } + +// MarshalYAML implements the YAML marshaler interface. +func (p *PublicKey) MarshalYAML() (any, error) { + return hex.EncodeToString(p.Bytes()), nil +} + +// UnmarshalYAML implements the YAML unmarshaler interface. +func (p *PublicKey) UnmarshalYAML(unmarshal func(any) error) error { + var s string + err := unmarshal(&s) + if err != nil { + return err + } + + b, err := hex.DecodeString(s) + if err != nil { + return fmt.Errorf("failed to decode public key from hex bytes: %w", err) + } + return p.DecodeBytes(b) +} diff --git a/pkg/crypto/keys/publickey_test.go b/pkg/crypto/keys/publickey_test.go index 13b37ffda..3fc8bbeb3 100644 --- a/pkg/crypto/keys/publickey_test.go +++ b/pkg/crypto/keys/publickey_test.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestEncodeDecodeInfinity(t *testing.T) { @@ -246,3 +247,61 @@ func BenchmarkPublicDecodeBytes(t *testing.B) { require.NoError(t, k.DecodeBytes(keyBytes)) } } + +func TestMarshallYAML(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + pubKey, err := NewPublicKeyFromString(str) + require.NoError(t, err) + + bytes, err := yaml.Marshal(&pubKey) + require.NoError(t, err) + + expected := []byte(str + "\n") // YAML marshaller adds new line in the end which is expected. + require.Equal(t, expected, bytes) +} + +func TestUnmarshallYAML(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + expected, err := NewPublicKeyFromString(str) + require.NoError(t, err) + + actual := &PublicKey{} + err = yaml.Unmarshal([]byte(str), actual) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +func TestUnmarshallYAMLBadCompresed(t *testing.T) { + str := `"02ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"` + actual := &PublicKey{} + err := yaml.Unmarshal([]byte(str), actual) + require.Error(t, err) + require.Contains(t, err.Error(), "error computing Y for compressed point") +} + +func TestUnmarshallYAMLNotAHex(t *testing.T) { + str := `"04Tb17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"` + actual := &PublicKey{} + err := yaml.Unmarshal([]byte(str), actual) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to decode public key from hex bytes") +} + +func TestUnmarshallYAMLUncompressed(t *testing.T) { + str := "046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5" + expected, err := NewPublicKeyFromString(str) + require.NoError(t, err) + + actual := &PublicKey{} + err = yaml.Unmarshal([]byte(str), actual) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +func TestMarshalUnmarshalYAML(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + expected, err := NewPublicKeyFromString(str) + require.NoError(t, err) + + testserdes.MarshalUnmarshalYAML(t, expected, new(PublicKey)) +}