diff --git a/docs/node-configuration.md b/docs/node-configuration.md index fd7307f64..666e3c1cd 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -365,6 +365,9 @@ Genesis: 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 @@ -383,3 +386,15 @@ where: 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 index 6e3685f2b..ca4eeb588 100644 --- a/pkg/config/genesis_extensions.go +++ b/pkg/config/genesis_extensions.go @@ -1,6 +1,7 @@ package config import ( + "encoding/base64" "fmt" "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 // 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 } -// genesisAux is an auxiliary structure for Genesis YAML marshalling. -type genesisAux struct { - Roles map[string]keys.PublicKeys `yaml:"Roles"` +// 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 @@ -28,6 +51,12 @@ func (e Genesis) MarshalYAML() (any, error) { 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 } @@ -47,5 +76,16 @@ func (e *Genesis) UnmarshalYAML(unmarshal func(any) error) error { 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/protocol_config_test.go b/pkg/config/protocol_config_test.go index fb008d002..1b17fec49 100644 --- a/pkg/config/protocol_config_test.go +++ b/pkg/config/protocol_config_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/base64" "encoding/hex" "fmt" "path/filepath" @@ -293,6 +294,10 @@ func TestGenesisExtensionsMarshalYAML(t *testing.T) { noderoles.NeoFSAlphabet: {pub}, noderoles.P2PNotary: {pub}, }, + Transaction: &GenesisTransaction{ + Script: []byte{1, 2, 3, 4}, + SystemFee: 123, + }, } 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("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`, pubStr, pubStr, pubStr, pubStr) + - %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) { 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/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)