// Copyright 2018 The dexon-consensus Authors // This file is part of the dexon-consensus library. // // The dexon-consensus library is free software: you can redistribute it // and/or modify it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation, either version 3 of the License, // or (at your option) any later version. // // The dexon-consensus library is distributed in the hope that it will be // useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser // General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the dexon-consensus library. If not, see // . package test import ( "bytes" "context" "fmt" "testing" "time" "github.com/dexon-foundation/dexon-consensus/common" "github.com/dexon-foundation/dexon-consensus/core" "github.com/dexon-foundation/dexon-consensus/core/crypto" "github.com/dexon-foundation/dexon-consensus/core/crypto/dkg" "github.com/dexon-foundation/dexon-consensus/core/types" typesDKG "github.com/dexon-foundation/dexon-consensus/core/types/dkg" "github.com/dexon-foundation/dexon-consensus/core/utils" "github.com/stretchr/testify/suite" ) func getCRS(round, reset uint64) []byte { return []byte(fmt.Sprintf("r#%d,reset#%d", round, reset)) } type evtParamToCheck struct { round uint64 reset uint64 height uint64 crs common.Hash } type AppTestSuite struct { suite.Suite pubKeys []crypto.PublicKey signers []*utils.Signer logger common.Logger } func (s *AppTestSuite) SetupSuite() { prvKeys, pubKeys, err := NewKeys(4) s.Require().NoError(err) s.pubKeys = pubKeys for _, k := range prvKeys { s.signers = append(s.signers, utils.NewSigner(k)) } s.logger = &common.NullLogger{} } func (s *AppTestSuite) prepareGov() *Governance { gov, err := NewGovernance( NewState(1, s.pubKeys, 100*time.Millisecond, s.logger, true), core.ConfigRoundShift) s.Require().NoError(err) return gov } func (s *AppTestSuite) proposeMPK( gov *Governance, round, reset uint64, count int) { for idx, pubKey := range s.pubKeys[:count] { _, pubShare := dkg.NewPrivateKeyShares(utils.GetDKGThreshold( gov.Configuration(round))) mpk := &typesDKG.MasterPublicKey{ Round: round, Reset: reset, DKGID: typesDKG.NewID(types.NewNodeID(pubKey)), PublicKeyShares: *pubShare, } s.Require().NoError(s.signers[idx].SignDKGMasterPublicKey(mpk)) gov.AddDKGMasterPublicKey(mpk) } } func (s *AppTestSuite) proposeFinalize( gov *Governance, round, reset uint64, count int) { for idx, pubKey := range s.pubKeys[:count] { final := &typesDKG.Finalize{ ProposerID: types.NewNodeID(pubKey), Round: round, Reset: reset, } s.Require().NoError(s.signers[idx].SignDKGFinalize(final)) gov.AddDKGFinalize(final) } } func (s *AppTestSuite) deliverBlockWithTimeFromSequenceLength( app *App, hash common.Hash) { s.deliverBlock(app, hash, time.Time{}.Add( time.Duration(len(app.DeliverSequence))*time.Second), uint64(len(app.DeliverSequence)+1)) } func (s *AppTestSuite) deliverBlock( app *App, hash common.Hash, timestamp time.Time, height uint64) { app.BlockDelivered(hash, types.Position{}, types.FinalizationResult{ Timestamp: timestamp, Height: height, }) } func (s *AppTestSuite) TestCompare() { var ( now = time.Now().UTC() b0 = types.Block{Hash: common.Hash{}} b1 = types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Height: 1}, } ) // Prepare an OK App instance. app1 := NewApp(0, nil, nil) app1.BlockConfirmed(b0) app1.BlockConfirmed(b1) app1.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{ Height: 1, Timestamp: now, }) app1.BlockDelivered(b1.Hash, b1.Position, types.FinalizationResult{ Height: 2, Timestamp: now.Add(1 * time.Second), }) app2 := NewApp(0, nil, nil) s.Require().Equal(ErrEmptyDeliverSequence.Error(), app1.Compare(app2).Error()) app2.BlockConfirmed(b0) app2.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{ Height: 1, Timestamp: now, }) b1Bad := types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Height: 1}, } app2.BlockConfirmed(b1Bad) app2.BlockDelivered(b1Bad.Hash, b1Bad.Position, types.FinalizationResult{ Height: 1, Timestamp: now, }) s.Require().Equal(ErrMismatchBlockHashSequence.Error(), app1.Compare(app2).Error()) app2 = NewApp(0, nil, nil) app2.BlockConfirmed(b0) app2.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{ Height: 1, Timestamp: now.Add(1 * time.Second), }) s.Require().Equal(ErrMismatchConsensusTime.Error(), app1.Compare(app2).Error()) } func (s *AppTestSuite) TestVerify() { var ( now = time.Now().UTC() b0 = types.Block{Hash: common.Hash{}} b1 = types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Height: 1}, } ) app := NewApp(0, nil, nil) s.Require().Equal(ErrEmptyDeliverSequence.Error(), app.Verify().Error()) app.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{}) app.BlockDelivered(b1.Hash, b1.Position, types.FinalizationResult{Height: 1}) s.Require().Equal( ErrDeliveredBlockNotConfirmed.Error(), app.Verify().Error()) app = NewApp(0, nil, nil) app.BlockConfirmed(b0) app.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{ Height: 1, Timestamp: now, }) app.BlockConfirmed(b1) app.BlockDelivered(b1.Hash, b1.Position, types.FinalizationResult{ Height: 2, Timestamp: now.Add(-1 * time.Second), }) s.Require().Equal(ErrConsensusTimestampOutOfOrder.Error(), app.Verify().Error()) app = NewApp(0, nil, nil) app.BlockConfirmed(b0) app.BlockConfirmed(b1) app.BlockDelivered(b0.Hash, b0.Position, types.FinalizationResult{ Height: 1, Timestamp: now, }) app.BlockDelivered(b1.Hash, b1.Position, types.FinalizationResult{ Height: 1, Timestamp: now.Add(1 * time.Second), }) } func (s *AppTestSuite) TestWitness() { // Deliver several blocks, there is only one chain only. app := NewApp(0, nil, nil) deliver := func(b *types.Block) { app.BlockConfirmed(*b) app.BlockDelivered(b.Hash, b.Position, b.Finalization) } b00 := &types.Block{ Hash: common.NewRandomHash(), Finalization: types.FinalizationResult{ Height: 1, Timestamp: time.Now().UTC(), }} b01 := &types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Height: 1}, Finalization: types.FinalizationResult{ ParentHash: b00.Hash, Height: 2, Timestamp: time.Now().UTC(), }, Witness: types.Witness{ Height: 1, Data: b00.Hash.Bytes(), }} b02 := &types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Height: 2}, Finalization: types.FinalizationResult{ ParentHash: b01.Hash, Height: 3, Timestamp: time.Now().UTC(), }, Witness: types.Witness{ Height: 1, Data: b00.Hash.Bytes(), }} deliver(b00) deliver(b01) deliver(b02) // A block with higher witness height, should retry later. s.Require().Equal(types.VerifyRetryLater, app.VerifyBlock(&types.Block{ Witness: types.Witness{Height: 4}})) // Mismatched witness height and data, should return invalid. s.Require().Equal(types.VerifyInvalidBlock, app.VerifyBlock(&types.Block{ Witness: types.Witness{Height: 1, Data: b01.Hash.Bytes()}})) // We can only verify a block followed last confirmed block. s.Require().Equal(types.VerifyRetryLater, app.VerifyBlock(&types.Block{ Witness: types.Witness{Height: 2, Data: b01.Hash.Bytes()}, Position: types.Position{Height: 4}})) // It's the OK case. s.Require().Equal(types.VerifyOK, app.VerifyBlock(&types.Block{ Witness: types.Witness{Height: 2, Data: b01.Hash.Bytes()}, Position: types.Position{Height: 3}})) // Check current last pending height. s.Require().Equal(app.LastPendingHeight, uint64(3)) // We can only prepare witness for what've delivered. _, err := app.PrepareWitness(4) s.Require().Equal(err.Error(), ErrLowerPendingHeight.Error()) // It should be ok to prepare for height that already delivered. w, err := app.PrepareWitness(3) s.Require().NoError(err) s.Require().Equal(w.Height, b02.Finalization.Height) s.Require().Equal(0, bytes.Compare(w.Data, b02.Hash[:])) } func (s *AppTestSuite) TestAttachedWithRoundEvent() { // This test case is copied/modified from // integraion.RoundEventTestSuite.TestFromRoundN, the difference is the // calls to utils.RoundEvent.ValidateNextRound is not explicitly called but // triggered by App.BlockDelivered. var ( gov = s.prepareGov() roundLength = uint64(100) ) s.Require().NoError(gov.State().RequestChange(StateChangeRoundLength, roundLength)) for r := uint64(2); r <= uint64(20); r++ { gov.ProposeCRS(r, getCRS(r, 0)) } for r := uint64(0); r <= uint64(19); r++ { gov.NotifyRound(r, r*roundLength) } gov.NotifyRound(20, 2200) // Reset round#20 twice, then make it done DKG preparation. gov.ResetDKG(getCRS(20, 1)) gov.ResetDKG(getCRS(20, 2)) s.proposeMPK(gov, 20, 2, 3) s.proposeFinalize(gov, 20, 2, 3) s.Require().Equal(gov.DKGResetCount(20), uint64(2)) // Propose CRS for round#21, and it works without reset. gov.ProposeCRS(21, getCRS(21, 0)) s.proposeMPK(gov, 21, 0, 3) s.proposeFinalize(gov, 21, 0, 3) // Propose CRS for round#22, and it works without reset. gov.ProposeCRS(22, getCRS(22, 0)) s.proposeMPK(gov, 22, 0, 3) s.proposeFinalize(gov, 22, 0, 3) // Prepare utils.RoundEvent, starts from round#19, reset(for round#20)#1. rEvt, err := utils.NewRoundEvent(context.Background(), gov, s.logger, 19, 2019, core.ConfigRoundShift) s.Require().NoError(err) // Register a handler to collects triggered events. evts := make(chan evtParamToCheck, 3) rEvt.Register(func(params []utils.RoundEventParam) { for _, p := range params { evts <- evtParamToCheck{ round: p.Round, reset: p.Reset, height: p.BeginHeight, crs: p.CRS, } } }) // Setup App instance. app := NewApp(19, gov, rEvt) deliver := func(round, start, end uint64) { for i := start; i <= end; i++ { b := &types.Block{ Hash: common.NewRandomHash(), Position: types.Position{Round: round, Height: i}, Finalization: types.FinalizationResult{Height: i}, } app.BlockConfirmed(*b) app.BlockDelivered(b.Hash, b.Position, b.Finalization) } } // Deliver blocks from height=2020 to height=2081. for r := uint64(0); r <= uint64(19); r++ { deliver(r, r*roundLength, (r+1)*roundLength-1) } deliver(19, 2000, 2091) s.Require().Equal(<-evts, evtParamToCheck{19, 1, 2000, gov.CRS(19)}) s.Require().Equal(<-evts, evtParamToCheck{19, 2, 2100, gov.CRS(19)}) s.Require().Equal(<-evts, evtParamToCheck{20, 0, 2200, gov.CRS(20)}) // Deliver blocks from height=2082 to height=2281. deliver(19, 2092, 2199) deliver(20, 2200, 2291) s.Require().Equal(<-evts, evtParamToCheck{21, 0, 2300, gov.CRS(21)}) // Deliver blocks from height=2282 to height=2381. deliver(20, 2292, 2299) deliver(21, 2300, 2391) s.Require().Equal(<-evts, evtParamToCheck{22, 0, 2400, gov.CRS(22)}) } func TestApp(t *testing.T) { suite.Run(t, new(AppTestSuite)) }