Merge pull request #3168 from nspcc-dev/roles-init

Introduce Genesis protocol setting extensions
This commit is contained in:
Roman Khimov 2023-10-19 18:49:45 +03:00 committed by GitHub
commit 890f64007a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 590 additions and 36 deletions

View file

@ -325,6 +325,7 @@ protocol-related settings described in the table below.
| Section | Type | Default value | Description | Notes | | 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. | | 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. |
| Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. | | Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. |
| MaxBlockSize | `uint32` | `262144` | Maximum block size in bytes. | | 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. | | 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. | | 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. | | 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.

View file

@ -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
}

View file

@ -10,16 +10,26 @@ func _() {
var x [1]struct{} var x [1]struct{}
_ = x[HFAspidochelone-1] _ = x[HFAspidochelone-1]
_ = x[HFBasilisk-2] _ = 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 { func (i Hardfork) String() string {
i -= 1 switch {
if i >= Hardfork(len(_Hardfork_index)-1) { case 1 <= i && i <= 2:
return "Hardfork(" + strconv.FormatInt(int64(i+1), 10) + ")" 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]]
} }

View file

@ -16,6 +16,10 @@ type (
ProtocolConfiguration struct { ProtocolConfiguration struct {
// CommitteeHistory stores committee size change history (height: size). // CommitteeHistory stores committee size change history (height: size).
CommitteeHistory map[uint32]uint32 `yaml:"CommitteeHistory"` 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"` Magic netmode.Magic `yaml:"Magic"`
MemPoolSize int `yaml:"MemPoolSize"` MemPoolSize int `yaml:"MemPoolSize"`

View file

@ -1,11 +1,18 @@
package config package config
import ( import (
"encoding/base64"
"encoding/hex"
"fmt"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "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" "github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
) )
func TestProtocolConfigurationValidation(t *testing.T) { func TestProtocolConfigurationValidation(t *testing.T) {
@ -275,3 +282,85 @@ func TestProtocolConfigurationEquals(t *testing.T) {
p.ValidatorsHistory = map[uint32]uint32{112: 0} p.ValidatorsHistory = map[uint32]uint32{112: 0}
require.False(t, p.Equals(o)) 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")
})
})
}

View file

@ -47,7 +47,9 @@ import (
const ( const (
version = "0.2.10" 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 defaultGCPeriod = 10000
defaultMemPoolSize = 50000 defaultMemPoolSize = 50000
defaultP2PNotaryRequestPayloadPoolSize = 1000 defaultP2PNotaryRequestPayloadPoolSize = 1000
@ -228,7 +230,7 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger) (*Bl
// Protocol configuration fixups/checks. // Protocol configuration fixups/checks.
if cfg.InitialGASSupply <= 0 { 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)) log.Info("initial gas supply is not set or wrong, setting default value", zap.Stringer("InitialGASSupply", cfg.InitialGASSupply))
} }
if cfg.MemPoolSize <= 0 { if cfg.MemPoolSize <= 0 {

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random" "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/compiler"
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "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"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "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"
"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/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/emit"
@ -2438,3 +2440,36 @@ func TestBlockchain_ResetState(t *testing.T) {
} }
require.Equal(t, expectedLUB, lub) 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))
}

View file

@ -80,7 +80,7 @@ func newBlock(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block), func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block),
txs ...*transaction.Transaction) *block.Block { txs ...*transaction.Transaction) *block.Block {
validators, _ := validatorsFromConfig(cfg) validators, _, _ := validatorsFromConfig(cfg)
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators) valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
witness := transaction.Witness{ witness := transaction.Witness{
VerificationScript: valScript, VerificationScript: valScript,

View file

@ -87,7 +87,7 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts {
cs.Policy = policy cs.Policy = policy
cs.Contracts = append(cs.Contracts, neo, gas, policy) cs.Contracts = append(cs.Contracts, neo, gas, policy)
desig := newDesignate(cfg.P2PSigExtensions) desig := newDesignate(cfg.P2PSigExtensions, cfg.Genesis.Roles)
desig.NEO = neo desig.NEO = neo
cs.Designate = desig cs.Designate = desig
cs.Contracts = append(cs.Contracts, desig) cs.Contracts = append(cs.Contracts, desig)

View file

@ -21,6 +21,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "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/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "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 defines whether the P2P signature extensions logic is relevant.
p2pSigExtensionsEnabled bool 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 OracleService atomic.Value
// NotaryService represents a Notary node module. // 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) 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 := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)}
s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled
s.initialNodeRoles = initialNodeRoles
defer s.UpdateHash() defer s.UpdateHash()
desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType,
@ -127,6 +132,19 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
func (s *Designate) Initialize(ic *interop.Context) error { func (s *Designate) Initialize(ic *interop.Context) error {
cache := &DesignationCache{} cache := &DesignationCache{}
ic.DAO.SetCache(s.ID, cache) 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 return nil
} }
@ -355,10 +373,14 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
if !s.isValidRole(r) { if !s.isValidRole(r) {
return ErrInvalidRole return ErrInvalidRole
} }
h := s.NEO.GetCommitteeAddress(ic.DAO)
if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { if ic.Trigger != trigger.OnPersist {
return ErrInvalidWitness h := s.NEO.GetCommitteeAddress(ic.DAO)
if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok {
return ErrInvalidWitness
}
} }
if ic.Block == nil { if ic.Block == nil {
return ErrNoBlock return ErrNoBlock
} }

View file

@ -1,14 +1,17 @@
package native_test package native_test
import ( import (
"sort"
"testing" "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/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest" "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/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/emit"
@ -137,3 +140,23 @@ func TestDesignate_Cache(t *testing.T) {
require.Nil(t, updatedNodes) require.Nil(t, updatedNodes)
require.False(t, updateCalled) 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)
}

View file

@ -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
}

View file

@ -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) + ")"
}
}

View file

@ -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)
}
}

View file

@ -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
)

View file

@ -24,7 +24,7 @@ func TestDesignate_DesignateAsRole(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
bl := block.New(bc.config.StateRootInHeader) bl := block.New(bc.config.StateRootInHeader)
bl.Index = bc.BlockHeight() + 1 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.SpawnVM()
ic.VM.LoadScript([]byte{byte(opcode.RET)}) ic.VM.LoadScript([]byte{byte(opcode.RET)})

View file

@ -30,7 +30,7 @@ var (
func (i WitnessScope) String() string { func (i WitnessScope) String() string {
switch { switch {
case 0 <= i && i <= 1: case i <= 1:
return _WitnessScope_name_0[_WitnessScope_index_0[i]:_WitnessScope_index_0[i+1]] return _WitnessScope_name_0[_WitnessScope_index_0[i]:_WitnessScope_index_0[i+1]]
case i == 16: case i == 16:
return _WitnessScope_name_1 return _WitnessScope_name_1

View file

@ -34,7 +34,7 @@ var (
func (i WitnessConditionType) String() string { func (i WitnessConditionType) String() string {
switch { switch {
case 0 <= i && i <= 3: case i <= 3:
return _WitnessConditionType_name_0[_WitnessConditionType_index_0[i]:_WitnessConditionType_index_0[i+1]] return _WitnessConditionType_name_0[_WitnessConditionType_index_0[i]:_WitnessConditionType_index_0[i+1]]
case 24 <= i && i <= 25: case 24 <= i && i <= 25:
i -= 24 i -= 24

View file

@ -1,6 +1,7 @@
package core package core
import ( import (
"fmt"
"time" "time"
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
@ -15,7 +16,7 @@ import (
// CreateGenesisBlock creates a genesis block based on the given configuration. // CreateGenesisBlock creates a genesis block based on the given configuration.
func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) { func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) {
validators, err := validatorsFromConfig(cfg) validators, committee, err := validatorsFromConfig(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -25,6 +26,49 @@ func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error)
return nil, err 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{ base := block.Header{
Version: 0, Version: 0,
PrevHash: util.Uint256{}, PrevHash: util.Uint256{},
@ -41,19 +85,19 @@ func CreateGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error)
b := &block.Block{ b := &block.Block{
Header: base, Header: base,
Transactions: []*transaction.Transaction{}, Transactions: txs,
} }
b.RebuildMerkleRoot() b.RebuildMerkleRoot()
return b, nil 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) vs, err := keys.NewPublicKeysFromStrings(cfg.StandbyCommittee)
if err != nil { 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) { 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 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. // hashSliceReverse reverses the given slice of util.Uint256.
func hashSliceReverse(dest []util.Uint256) { func hashSliceReverse(dest []util.Uint256) {
for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 {

View file

@ -30,7 +30,7 @@ func TestGetConsensusAddressMainNet(t *testing.T) {
cfg, err := config.Load("../../config", netmode.MainNet) cfg, err := config.Load("../../config", netmode.MainNet)
require.NoError(t, err) require.NoError(t, err)
validators, err := validatorsFromConfig(cfg.ProtocolConfiguration) validators, _, err := validatorsFromConfig(cfg.ProtocolConfiguration)
require.NoError(t, err) require.NoError(t, err)
script, err := getNextConsensusAddress(validators) script, err := getNextConsensusAddress(validators)

View file

@ -390,3 +390,23 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error {
return nil 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)
}

View file

@ -10,6 +10,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
) )
func TestEncodeDecodeInfinity(t *testing.T) { func TestEncodeDecodeInfinity(t *testing.T) {
@ -246,3 +247,61 @@ func BenchmarkPublicDecodeBytes(t *testing.B) {
require.NoError(t, k.DecodeBytes(keyBytes)) 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))
}

View file

@ -62,7 +62,7 @@ var (
func (i CommandType) String() string { func (i CommandType) String() string {
switch { switch {
case 0 <= i && i <= 1: case i <= 1:
return _CommandType_name_0[_CommandType_index_0[i]:_CommandType_index_0[i+1]] return _CommandType_name_0[_CommandType_index_0[i]:_CommandType_index_0[i+1]]
case 16 <= i && i <= 17: case 16 <= i && i <= 17:
i -= 16 i -= 16