*: add GenesisTransaction extension to the protocol configuration

Provide a script that should be deployed in the genesis block.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-10-19 15:29:41 +03:00
parent 065bd3f0be
commit 8cc32a91b6
8 changed files with 178 additions and 13 deletions

View file

@ -365,6 +365,9 @@ Genesis:
Oracle: Oracle:
- 03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0 - 03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0
- 0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30 - 0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30
Transaction:
Script: "DCECEDp/fdAWVYWX95YNJ8UWpDlP2Wi55lFV60sBPkBAQG5BVuezJw=="
SystemFee: 100000000
``` ```
where: where:
- `Roles` is a map from node roles that should be set at the moment of native - `Roles` is a map from node roles that should be set at the moment of native
@ -383,3 +386,15 @@ where:
with NativeUpdateHistory setting, which means that specified roles will be set with NativeUpdateHistory setting, which means that specified roles will be set
only during native RoleManagement contract initialisation (which may be only during native RoleManagement contract initialisation (which may be
performed in some non-genesis block). By default, no roles are designated. 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

@ -1,6 +1,7 @@
package config package config
import ( import (
"encoding/base64"
"fmt" "fmt"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
@ -14,13 +15,35 @@ type Genesis struct {
// Designation contract initialization. It is NeoGo extension and must be // Designation contract initialization. It is NeoGo extension and must be
// disabled on the public Neo N3 networks. // disabled on the public Neo N3 networks.
Roles map[noderoles.Role]keys.PublicKeys 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
} }
// genesisAux is an auxiliary structure for Genesis YAML marshalling. // GenesisTransaction is a placeholder for script that should be included into genesis
type genesisAux struct { // block as a transaction script with the given system fee. Provided
Roles map[string]keys.PublicKeys `yaml:"Roles"` // 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. // MarshalYAML implements the YAML marshaler interface.
func (e Genesis) MarshalYAML() (any, error) { func (e Genesis) MarshalYAML() (any, error) {
var aux genesisAux var aux genesisAux
@ -28,6 +51,12 @@ func (e Genesis) MarshalYAML() (any, error) {
for r, ks := range e.Roles { for r, ks := range e.Roles {
aux.Roles[r.String()] = ks 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 return aux, nil
} }
@ -47,5 +76,16 @@ func (e *Genesis) UnmarshalYAML(unmarshal func(any) error) error {
e.Roles[r] = ks 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 return nil
} }

View file

@ -1,6 +1,7 @@
package config package config
import ( import (
"encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"path/filepath" "path/filepath"
@ -293,6 +294,10 @@ func TestGenesisExtensionsMarshalYAML(t *testing.T) {
noderoles.NeoFSAlphabet: {pub}, noderoles.NeoFSAlphabet: {pub},
noderoles.P2PNotary: {pub}, noderoles.P2PNotary: {pub},
}, },
Transaction: &GenesisTransaction{
Script: []byte{1, 2, 3, 4},
SystemFee: 123,
},
} }
testserdes.MarshalUnmarshalYAML(t, g, new(Genesis)) testserdes.MarshalUnmarshalYAML(t, g, new(Genesis))
}) })
@ -300,20 +305,36 @@ func TestGenesisExtensionsMarshalYAML(t *testing.T) {
t.Run("unmarshal config", func(t *testing.T) { t.Run("unmarshal config", func(t *testing.T) {
t.Run("good", func(t *testing.T) { t.Run("good", func(t *testing.T) {
pubStr := hex.EncodeToString(pub.Bytes()) pubStr := hex.EncodeToString(pub.Bytes())
script := []byte{1, 2, 3, 4}
cfgYml := fmt.Sprintf(`ProtocolConfiguration: cfgYml := fmt.Sprintf(`ProtocolConfiguration:
Genesis: Genesis:
Transaction:
Script: "%s"
SystemFee: 123
Roles: Roles:
NeoFSAlphabet: NeoFSAlphabet:
- %s - %s
- %s - %s
Oracle: Oracle:
- %s - %s
- %s`, pubStr, pubStr, pubStr, pubStr) - %s`, base64.StdEncoding.EncodeToString(script), pubStr, pubStr, pubStr, pubStr)
cfg := new(Config) cfg := new(Config)
require.NoError(t, yaml.Unmarshal([]byte(cfgYml), cfg)) require.NoError(t, yaml.Unmarshal([]byte(cfgYml), cfg))
require.Equal(t, 2, len(cfg.ProtocolConfiguration.Genesis.Roles)) 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.NeoFSAlphabet])
require.Equal(t, keys.PublicKeys{pub, pub}, cfg.ProtocolConfiguration.Genesis.Roles[noderoles.Oracle]) 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) { t.Run("unknown role", func(t *testing.T) {

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

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