diff --git a/docs/node-configuration.md b/docs/node-configuration.md
index c5ebcf5fa..666e3c1cd 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,54 @@ 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
+ Transaction:
+ Script: "DCECEDp/fdAWVYWX95YNJ8UWpDlP2Wi55lFV60sBPkBAQG5BVuezJw=="
+ SystemFee: 100000000
+```
+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.
+
+- `Transaction` is a container for transaction script that should be deployed in
+ the genesis block if provided. `Transaction` includes `Script` which is a
+ base64-encoded transaction script and `SystemFee` which is a transaction's
+ system fee value (in GAS) that will be spent during transaction execution.
+ Transaction generated from the provided parameters has two signers at max with
+ CalledByEntry witness scope: the first one is standby validators multisignature
+ signer and the second one (if differs from the first) is committee
+ multisignature signer.
+
+ Note that `Transaction` is a NeoGo extension that isn't supported by the NeoC#
+ node and must be disabled on the public Neo N3 networks.
diff --git a/pkg/config/genesis_extensions.go b/pkg/config/genesis_extensions.go
new file mode 100644
index 000000000..ca4eeb588
--- /dev/null
+++ b/pkg/config/genesis_extensions.go
@@ -0,0 +1,91 @@
+package config
+
+import (
+ "encoding/base64"
+ "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
+ // Transaction contains transaction script that should be deployed in the
+ // genesis block. It is NeoGo extension and must be disabled on the public
+ // Neo N3 networks.
+ Transaction *GenesisTransaction
+}
+
+// GenesisTransaction is a placeholder for script that should be included into genesis
+// block as a transaction script with the given system fee. Provided
+// system fee value will be taken from the standby validators account which is
+// added to the list of Signers as a sender with CalledByEntry scope.
+type GenesisTransaction struct {
+ Script []byte
+ SystemFee int64
+}
+
+type (
+ // genesisAux is an auxiliary structure for Genesis YAML marshalling.
+ genesisAux struct {
+ Roles map[string]keys.PublicKeys `yaml:"Roles"`
+ Transaction *genesisTransactionAux `yaml:"Transaction"`
+ }
+ // genesisTransactionAux is an auxiliary structure for GenesisTransaction YAML
+ // marshalling.
+ genesisTransactionAux struct {
+ Script string `yaml:"Script"`
+ SystemFee int64 `yaml:"SystemFee"`
+ }
+)
+
+// 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
+ }
+ if e.Transaction != nil {
+ aux.Transaction = &genesisTransactionAux{
+ Script: base64.StdEncoding.EncodeToString(e.Transaction.Script),
+ SystemFee: e.Transaction.SystemFee,
+ }
+ }
+ 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
+ }
+
+ if aux.Transaction != nil {
+ script, err := base64.StdEncoding.DecodeString(aux.Transaction.Script)
+ if err != nil {
+ return fmt.Errorf("failed to decode script of genesis transaction: %w", err)
+ }
+ e.Transaction = &GenesisTransaction{
+ Script: script,
+ SystemFee: aux.Transaction.SystemFee,
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/config/hardfork_string.go b/pkg/config/hardfork_string.go
index f73923119..48f2349f0 100644
--- a/pkg/config/hardfork_string.go
+++ b/pkg/config/hardfork_string.go
@@ -10,16 +10,26 @@ func _() {
var x [1]struct{}
_ = x[HFAspidochelone-1]
_ = x[HFBasilisk-2]
+ _ = x[hfLast-4]
}
-const _Hardfork_name = "AspidocheloneBasilisk"
+const (
+ _Hardfork_name_0 = "AspidocheloneBasilisk"
+ _Hardfork_name_1 = "hfLast"
+)
-var _Hardfork_index = [...]uint8{0, 13, 21}
+var (
+ _Hardfork_index_0 = [...]uint8{0, 13, 21}
+)
func (i Hardfork) String() string {
- i -= 1
- if i >= Hardfork(len(_Hardfork_index)-1) {
- return "Hardfork(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ switch {
+ case 1 <= i && i <= 2:
+ i -= 1
+ return _Hardfork_name_0[_Hardfork_index_0[i]:_Hardfork_index_0[i+1]]
+ case i == 4:
+ return _Hardfork_name_1
+ default:
+ return "Hardfork(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _Hardfork_name[_Hardfork_index[i]:_Hardfork_index[i+1]]
}
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..1b17fec49 100644
--- a/pkg/config/protocol_config_test.go
+++ b/pkg/config/protocol_config_test.go
@@ -1,11 +1,18 @@
package config
import (
+ "encoding/base64"
+ "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 +282,85 @@ 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},
+ },
+ Transaction: &GenesisTransaction{
+ Script: []byte{1, 2, 3, 4},
+ SystemFee: 123,
+ },
+ }
+ 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())
+ script := []byte{1, 2, 3, 4}
+ cfgYml := fmt.Sprintf(`ProtocolConfiguration:
+ Genesis:
+ Transaction:
+ Script: "%s"
+ SystemFee: 123
+ Roles:
+ NeoFSAlphabet:
+ - %s
+ - %s
+ Oracle:
+ - %s
+ - %s`, base64.StdEncoding.EncodeToString(script), 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])
+ require.Equal(t, &GenesisTransaction{
+ Script: script,
+ SystemFee: 123,
+ }, cfg.ProtocolConfiguration.Genesis.Transaction)
+ })
+
+ t.Run("empty", func(t *testing.T) {
+ cfgYml := `ProtocolConfiguration:`
+ cfg := new(Config)
+ require.NoError(t, yaml.Unmarshal([]byte(cfgYml), cfg))
+ require.Nil(t, cfg.ProtocolConfiguration.Genesis.Transaction)
+ require.Empty(t, cfg.ProtocolConfiguration.Genesis.Roles)
+ })
+
+ 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/blockchain.go b/pkg/core/blockchain.go
index 96410b051..40667c3d4 100644
--- a/pkg/core/blockchain.go
+++ b/pkg/core/blockchain.go
@@ -47,7 +47,9 @@ import (
const (
version = "0.2.10"
- defaultInitialGAS = 52000000_00000000
+ // DefaultInitialGAS is the default amount of GAS emitted to the standby validators
+ // multisignature account during native GAS contract initialization.
+ DefaultInitialGAS = 52000000_00000000
defaultGCPeriod = 10000
defaultMemPoolSize = 50000
defaultP2PNotaryRequestPayloadPoolSize = 1000
@@ -228,7 +230,7 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger) (*Bl
// Protocol configuration fixups/checks.
if cfg.InitialGASSupply <= 0 {
- cfg.InitialGASSupply = fixedn.Fixed8(defaultInitialGAS)
+ cfg.InitialGASSupply = fixedn.Fixed8(DefaultInitialGAS)
log.Info("initial gas supply is not set or wrong, setting default value", zap.Stringer("InitialGASSupply", cfg.InitialGASSupply))
}
if cfg.MemPoolSize <= 0 {
diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go
index f26eb777d..f95d634b3 100644
--- a/pkg/core/blockchain_neotest_test.go
+++ b/pkg/core/blockchain_neotest_test.go
@@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random"
+ "github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
@@ -37,6 +38,7 @@ import (
"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"
+ "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/emit"
@@ -2438,3 +2440,36 @@ func TestBlockchain_ResetState(t *testing.T) {
}
require.Equal(t, expectedLUB, lub)
}
+
+func TestBlockchain_GenesisTransactionExtension(t *testing.T) {
+ priv0 := testchain.PrivateKeyByID(0)
+ acc0 := wallet.NewAccountFromPrivateKey(priv0)
+ require.NoError(t, acc0.ConvertMultisig(1, []*keys.PublicKey{priv0.PublicKey()}))
+ from := acc0.ScriptHash()
+ to := util.Uint160{1, 2, 3}
+ amount := 1
+
+ script := io.NewBufBinWriter()
+ emit.Bytes(script.BinWriter, from.BytesBE())
+ emit.Syscall(script.BinWriter, interopnames.SystemRuntimeCheckWitness)
+ emit.Bytes(script.BinWriter, to.BytesBE())
+ emit.Syscall(script.BinWriter, interopnames.SystemRuntimeCheckWitness)
+ emit.AppCall(script.BinWriter, state.CreateNativeContractHash(nativenames.Neo), "transfer", callflag.All, from, to, amount, nil)
+ emit.Opcodes(script.BinWriter, opcode.ASSERT)
+
+ var sysFee int64 = 1_0000_0000
+ bc, acc := chain.NewSingleWithCustomConfig(t, func(blockchain *config.Blockchain) {
+ blockchain.Genesis.Transaction = &config.GenesisTransaction{
+ Script: script.Bytes(),
+ SystemFee: sysFee,
+ }
+ })
+ e := neotest.NewExecutor(t, bc, acc, acc)
+ b := e.GetBlockByIndex(t, 0)
+ tx := b.Transactions[0]
+ e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true), stackitem.NewBool(false))
+ e.CheckGASBalance(t, e.Validator.ScriptHash(), big.NewInt(core.DefaultInitialGAS-sysFee))
+ actualNeo, lub := e.Chain.GetGoverningTokenBalance(to)
+ require.Equal(t, int64(amount), actualNeo.Int64())
+ require.Equal(t, 0, int(lub))
+}
diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go
index 7e2c7b0ac..a2466d6e5 100644
--- a/pkg/core/helper_test.go
+++ b/pkg/core/helper_test.go
@@ -80,7 +80,7 @@ func newBlock(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block),
txs ...*transaction.Transaction) *block.Block {
- validators, _ := validatorsFromConfig(cfg)
+ validators, _, _ := validatorsFromConfig(cfg)
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
witness := transaction.Witness{
VerificationScript: valScript,
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
new file mode 100644
index 000000000..0998a67ae
--- /dev/null
+++ b/pkg/core/native/noderoles/role.go
@@ -0,0 +1,39 @@
+package noderoles
+
+//go:generate stringer -type=Role
+
+// Role represents the type of the participant.
+type Role byte
+
+// Role enumeration.
+const (
+ _ Role = 1 << iota
+ _
+ StateValidator
+ Oracle
+ NeoFSAlphabet
+ P2PNotary
+ // last denotes the end of roles enum. Consider adding new roles before the last.
+ 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
+
+func init() {
+ roles = make(map[string]Role)
+ for i := StateValidator; i < last; i = i << 1 {
+ roles[i.String()] = i
+ Roles = append(Roles, i)
+ }
+}
+
+// FromString returns a node role parsed from its string representation and a
+// boolean value denoting whether the conversion was OK and the role exists.
+func FromString(s string) (Role, bool) {
+ r, ok := roles[s]
+ return r, ok
+}
diff --git a/pkg/core/native/noderoles/role_string.go b/pkg/core/native/noderoles/role_string.go
new file mode 100644
index 000000000..b1e99b230
--- /dev/null
+++ b/pkg/core/native/noderoles/role_string.go
@@ -0,0 +1,41 @@
+// Code generated by "stringer -type=Role"; DO NOT EDIT.
+
+package noderoles
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[StateValidator-4]
+ _ = x[Oracle-8]
+ _ = x[NeoFSAlphabet-16]
+ _ = x[P2PNotary-32]
+ _ = x[last-64]
+}
+
+const (
+ _Role_name_0 = "StateValidator"
+ _Role_name_1 = "Oracle"
+ _Role_name_2 = "NeoFSAlphabet"
+ _Role_name_3 = "P2PNotary"
+ _Role_name_4 = "last"
+)
+
+func (i Role) String() string {
+ switch {
+ case i == 4:
+ return _Role_name_0
+ case i == 8:
+ return _Role_name_1
+ case i == 16:
+ return _Role_name_2
+ case i == 32:
+ return _Role_name_3
+ case i == 64:
+ return _Role_name_4
+ default:
+ return "Role(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+}
diff --git a/pkg/core/native/noderoles/role_test.go b/pkg/core/native/noderoles/role_test.go
new file mode 100644
index 000000000..487dacfd1
--- /dev/null
+++ b/pkg/core/native/noderoles/role_test.go
@@ -0,0 +1,27 @@
+package noderoles
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestFromString(t *testing.T) {
+ valid := map[string]Role{
+ "StateValidator": StateValidator,
+ "Oracle": Oracle,
+ "NeoFSAlphabet": NeoFSAlphabet,
+ "P2PNotary": P2PNotary,
+ }
+ for s, expected := range valid {
+ actual, ok := FromString(s)
+ require.True(t, ok)
+ require.Equal(t, expected, actual)
+ }
+
+ invalid := []string{"last", "InvalidRole"}
+ for _, s := range invalid {
+ _, ok := FromString(s)
+ require.False(t, ok)
+ }
+}
diff --git a/pkg/core/native/noderoles/roles.go b/pkg/core/native/noderoles/roles.go
deleted file mode 100644
index 26c5f9e1d..000000000
--- a/pkg/core/native/noderoles/roles.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package noderoles
-
-// Role represents the type of the participant.
-type Role byte
-
-// Role enumeration.
-const (
- StateValidator Role = 4
- Oracle Role = 8
- NeoFSAlphabet Role = 16
- P2PNotary Role = 32
-)
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/core/transaction/witness_scope_string.go b/pkg/core/transaction/witness_scope_string.go
index 805ac1473..f62b96baf 100644
--- a/pkg/core/transaction/witness_scope_string.go
+++ b/pkg/core/transaction/witness_scope_string.go
@@ -30,7 +30,7 @@ var (
func (i WitnessScope) String() string {
switch {
- case 0 <= i && i <= 1:
+ case i <= 1:
return _WitnessScope_name_0[_WitnessScope_index_0[i]:_WitnessScope_index_0[i+1]]
case i == 16:
return _WitnessScope_name_1
diff --git a/pkg/core/transaction/witnessconditiontype_string.go b/pkg/core/transaction/witnessconditiontype_string.go
index 96e0d8a38..bbbc6b378 100644
--- a/pkg/core/transaction/witnessconditiontype_string.go
+++ b/pkg/core/transaction/witnessconditiontype_string.go
@@ -34,7 +34,7 @@ var (
func (i WitnessConditionType) String() string {
switch {
- case 0 <= i && i <= 3:
+ case i <= 3:
return _WitnessConditionType_name_0[_WitnessConditionType_index_0[i]:_WitnessConditionType_index_0[i+1]]
case 24 <= i && i <= 25:
i -= 24
diff --git a/pkg/core/util.go b/pkg/core/util.go
index 2694e8885..196a2c2f4 100644
--- a/pkg/core/util.go
+++ b/pkg/core/util.go
@@ -1,6 +1,7 @@
package core
import (
+ "fmt"
"time"
"github.com/nspcc-dev/neo-go/pkg/config"
@@ -15,7 +16,7 @@ import (
// CreateGenesisBlock creates a genesis block based on the given configuration.
func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) {
- validators, err := validatorsFromConfig(cfg)
+ validators, committee, err := validatorsFromConfig(cfg)
if err != nil {
return nil, err
}
@@ -25,6 +26,49 @@ func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error)
return nil, err
}
+ txs := []*transaction.Transaction{}
+ if cfg.Genesis.Transaction != nil {
+ committeeH, err := getCommitteeAddress(committee)
+ if err != nil {
+ return nil, fmt.Errorf("failed to calculate committee address: %w", err)
+ }
+ tx := cfg.Genesis.Transaction
+ signers := []transaction.Signer{
+ {
+ Account: nextConsensus,
+ Scopes: transaction.CalledByEntry,
+ },
+ }
+ scripts := []transaction.Witness{
+ {
+ InvocationScript: []byte{},
+ VerificationScript: []byte{byte(opcode.PUSH1)},
+ },
+ }
+ if !committeeH.Equals(nextConsensus) {
+ signers = append(signers, []transaction.Signer{
+ {
+ Account: committeeH,
+ Scopes: transaction.CalledByEntry,
+ },
+ }...)
+ scripts = append(scripts, []transaction.Witness{
+ {
+ InvocationScript: []byte{},
+ VerificationScript: []byte{byte(opcode.PUSH1)},
+ },
+ }...)
+ }
+
+ txs = append(txs, &transaction.Transaction{
+ SystemFee: tx.SystemFee,
+ ValidUntilBlock: 1,
+ Script: tx.Script,
+ Signers: signers,
+ Scripts: scripts,
+ })
+ }
+
base := block.Header{
Version: 0,
PrevHash: util.Uint256{},
@@ -41,19 +85,19 @@ func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error)
b := &block.Block{
Header: base,
- Transactions: []*transaction.Transaction{},
+ Transactions: txs,
}
b.RebuildMerkleRoot()
return b, nil
}
-func validatorsFromConfig(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, error) {
+func validatorsFromConfig(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, []*keys.PublicKey, error) {
vs, err := keys.NewPublicKeysFromStrings(cfg.StandbyCommittee)
if err != nil {
- return nil, err
+ return nil, nil, err
}
- return vs[:cfg.GetNumOfCNs(0)], nil
+ return vs.Copy()[:cfg.GetNumOfCNs(0)], vs, nil
}
func getNextConsensusAddress(validators []*keys.PublicKey) (val util.Uint160, err error) {
@@ -64,6 +108,14 @@ func getNextConsensusAddress(validators []*keys.PublicKey) (val util.Uint160, er
return hash.Hash160(raw), nil
}
+func getCommitteeAddress(committee []*keys.PublicKey) (val util.Uint160, err error) {
+ raw, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee)
+ if err != nil {
+ return val, err
+ }
+ return hash.Hash160(raw), nil
+}
+
// hashSliceReverse reverses the given slice of util.Uint256.
func hashSliceReverse(dest []util.Uint256) {
for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 {
diff --git a/pkg/core/util_test.go b/pkg/core/util_test.go
index 7925db1a8..6becf5ff0 100644
--- a/pkg/core/util_test.go
+++ b/pkg/core/util_test.go
@@ -30,7 +30,7 @@ func TestGetConsensusAddressMainNet(t *testing.T) {
cfg, err := config.Load("../../config", netmode.MainNet)
require.NoError(t, err)
- validators, err := validatorsFromConfig(cfg.ProtocolConfiguration)
+ validators, _, err := validatorsFromConfig(cfg.ProtocolConfiguration)
require.NoError(t, err)
script, err := getNextConsensusAddress(validators)
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))
+}
diff --git a/pkg/network/message_string.go b/pkg/network/message_string.go
index 8e516f377..2ebdacd9b 100644
--- a/pkg/network/message_string.go
+++ b/pkg/network/message_string.go
@@ -62,7 +62,7 @@ var (
func (i CommandType) String() string {
switch {
- case 0 <= i && i <= 1:
+ case i <= 1:
return _CommandType_name_0[_CommandType_index_0[i]:_CommandType_index_0[i+1]]
case 16 <= i && i <= 17:
i -= 16