diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 9d5638eca..3cd091d0f 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -14,6 +14,7 @@ import ( coreb "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -584,7 +585,13 @@ func (s *service) newBlockFromContext(ctx *dbft.Context) block.Block { block.Block.Timestamp = ctx.Timestamp / nsInMs block.Block.Index = ctx.BlockIndex - validators, err := s.Chain.GetValidators() + var validators keys.PublicKeys + var err error + if native.ShouldUpdateCommittee(ctx.BlockIndex, s.Chain) { + validators, err = s.Chain.GetValidators() + } else { + validators, err = s.Chain.GetNextBlockValidators() + } if err != nil { return nil } diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index 1d31b9089..ccb28e928 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/dbft/block" "github.com/nspcc-dev/dbft/payload" + "github.com/nspcc-dev/dbft/timer" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core" @@ -13,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/io" @@ -20,6 +22,7 @@ import ( "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/opcode" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) @@ -39,6 +42,122 @@ func TestNewService(t *testing.T) { srv.Chain.Close() } +func initServiceNextConsensus(t *testing.T, newAcc *wallet.Account, offset uint32) (*service, *wallet.Account) { + acc, err := wallet.NewAccountFromWIF(testchain.WIF(testchain.IDToOrder(0))) + require.NoError(t, err) + priv := acc.PrivateKey() + require.NoError(t, acc.ConvertMultisig(1, keys.PublicKeys{priv.PublicKey()})) + + bc := newSingleTestChain(t) + newPriv := newAcc.PrivateKey() + + // Transfer funds to new validator. + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.GoverningTokenHash(), "transfer", + acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(native.NEOTotalSupply)) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.UtilityTokenHash(), "transfer", + acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(1_000_000_000)) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + require.NoError(t, w.Err) + + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 20_000_000) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 10_000_000 + tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: acc.Contract.ScriptHash()}} + require.NoError(t, acc.SignTx(tx)) + require.NoError(t, bc.PoolTx(tx)) + + srv := newTestServiceWithChain(t, bc) + srv.dbft.Start() + + // Register new candidate. + w.Reset() + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.GoverningTokenHash(), "registerCandidate", newPriv.PublicKey().Bytes()) + require.NoError(t, w.Err) + + tx = transaction.New(netmode.UnitTestNet, w.Bytes(), 20_000_000) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 20_000_000 + tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}} + require.NoError(t, newAcc.SignTx(tx)) + + require.NoError(t, bc.PoolTx(tx)) + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.Context.BlockIndex}) + + for i := srv.dbft.BlockIndex; !native.ShouldUpdateCommittee(i+offset, bc); i++ { + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.Context.BlockIndex}) + } + + // Vote for new candidate. + w.Reset() + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.GoverningTokenHash(), "vote", + newPriv.GetScriptHash(), newPriv.PublicKey().Bytes()) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + require.NoError(t, w.Err) + + tx = transaction.New(netmode.UnitTestNet, w.Bytes(), 20_000_000) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 20_000_000 + tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}} + require.NoError(t, newAcc.SignTx(tx)) + + require.NoError(t, bc.PoolTx(tx)) + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex}) + + return srv, acc +} + +func TestService_NextConsensus(t *testing.T) { + newAcc, err := wallet.NewAccount() + require.NoError(t, err) + script, err := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys{newAcc.PrivateKey().PublicKey()}) + require.NoError(t, err) + + checkNextConsensus := func(t *testing.T, bc *core.Blockchain, height uint32, h util.Uint160) { + hdrHash := bc.GetHeaderHash(int(height)) + hdr, err := bc.GetHeader(hdrHash) + require.NoError(t, err) + require.Equal(t, h, hdr.NextConsensus) + } + + t.Run("vote 1 block before update", func(t *testing.T) { + srv, acc := initServiceNextConsensus(t, newAcc, 1) + bc := srv.Chain.(*core.Blockchain) + defer bc.Close() + + height := bc.BlockHeight() + checkNextConsensus(t, bc, height, acc.Contract.ScriptHash()) + // Reset <- we are here, update NextConsensus + // OnPersist <- update committee + // Block <- + + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex}) + checkNextConsensus(t, bc, height+1, hash.Hash160(script)) + }) + + t.Run("vote 2 blocks before update", func(t *testing.T) { + srv, acc := initServiceNextConsensus(t, newAcc, 2) + bc := srv.Chain.(*core.Blockchain) + defer bc.Close() + + height := bc.BlockHeight() + checkNextConsensus(t, bc, height, acc.Contract.ScriptHash()) + // Reset <- we are here + // OnPersist <- nothing to do + // Block <- + // + // Reset <- update next consensus + // OnPersist <- update committee + // Block <- + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex}) + checkNextConsensus(t, bc, height+1, acc.Contract.ScriptHash()) + + srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex}) + checkNextConsensus(t, bc, height+2, hash.Hash160(script)) + }) +} + func TestService_GetVerified(t *testing.T) { srv := newTestService(t) srv.dbft.Start() @@ -289,11 +408,16 @@ func shouldNotReceive(t *testing.T, ch chan Payload) { } func newTestService(t *testing.T) *service { + return newTestServiceWithChain(t, newTestChain(t)) +} + +func newTestServiceWithChain(t *testing.T, bc *core.Blockchain) *service { srv, err := NewService(Config{ - Logger: zaptest.NewLogger(t), - Broadcast: func(*Payload) {}, - Chain: newTestChain(t), - RequestTx: func(...util.Uint256) {}, + Logger: zaptest.NewLogger(t), + Broadcast: func(*Payload) {}, + Chain: bc, + RequestTx: func(...util.Uint256) {}, + TimePerBlock: time.Duration(bc.GetConfig().SecondsPerBlock) * time.Second, Wallet: &config.Wallet{ Path: "./testdata/wallet1.json", Password: "one", @@ -309,6 +433,18 @@ func getTestValidator(i int) (*privateKey, *publicKey) { return &privateKey{PrivateKey: key}, &publicKey{PublicKey: key.PublicKey()} } +func newSingleTestChain(t *testing.T) *core.Blockchain { + configPath := "../../config/protocol.unit_testnet.single.yml" + cfg, err := config.LoadFile(configPath) + require.NoError(t, err, "could not load config") + + chain, err := core.NewBlockchain(storage.NewMemoryStore(), cfg.ProtocolConfiguration, zaptest.NewLogger(t)) + require.NoError(t, err, "could not create chain") + + go chain.Run() + return chain +} + func newTestChain(t *testing.T) *core.Blockchain { unitTestNetCfg, err := config.Load("../../config", netmode.UnitTestNet) require.NoError(t, err) diff --git a/pkg/consensus/testdata/wallet1.json b/pkg/consensus/testdata/wallet1.json index 27a48a3ed..1612925d6 100644 --- a/pkg/consensus/testdata/wallet1.json +++ b/pkg/consensus/testdata/wallet1.json @@ -42,6 +42,23 @@ }, "lock": false, "isdefault": false + }, + { + "address": "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "EQwhArNiK/QBe9/jF8WK7V9MdT8ga324lgRvp9d0u8S/f43CEQtBE43vrw==", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false } ], "scrypt": { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index adef9e3b3..b6698c8e8 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -266,7 +266,8 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si) } -func shouldUpdateCommittee(h uint32, bc blockchainer.Blockchainer) bool { +// ShouldUpdateCommittee returns true if committee is updated at block h. +func ShouldUpdateCommittee(h uint32, bc blockchainer.Blockchainer) bool { cfg := bc.GetConfig() r := cfg.ValidatorsCount + len(cfg.StandbyCommittee) return h%uint32(r) == 0 @@ -274,7 +275,7 @@ func shouldUpdateCommittee(h uint32, bc blockchainer.Blockchainer) bool { // OnPersist implements Contract interface. func (n *NEO) OnPersist(ic *interop.Context) error { - if shouldUpdateCommittee(ic.Block.Index, ic.Chain) { + if ShouldUpdateCommittee(ic.Block.Index, ic.Chain) { if err := n.updateCommittee(ic); err != nil { return err }