From f9c213734473ecf3ea6b99b60a508ff0f80f50de Mon Sep 17 00:00:00 2001 From: Elichai Turkel Date: Wed, 25 Nov 2020 13:42:55 +0200 Subject: [PATCH] [RES-65] Add a test for BoundedMergeDepth - new (#1131) * Test bounded merge depth * Fix a bug in GetBlockInfo, where trying to use reachability on an invalid block * Add a test to reproduce and test the GetBlockInfo bug --- domain/consensus/consensus.go | 5 + domain/consensus/consensus_test.go | 75 +++++++++ domain/consensus/finality_test.go | 251 +++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 domain/consensus/consensus_test.go diff --git a/domain/consensus/consensus.go b/domain/consensus/consensus.go index 3a8903ee3..fa07ae484 100644 --- a/domain/consensus/consensus.go +++ b/domain/consensus/consensus.go @@ -124,6 +124,11 @@ func (s *consensus) GetBlockInfo(blockHash *externalapi.DomainHash) (*externalap } blockInfo.BlockStatus = blockStatus + // If the status is invalid, then we don't have the necessary reachability data to check if it's in PruningPoint.Future. + if blockStatus == externalapi.StatusInvalid { + return blockInfo, nil + } + isBlockInHeaderPruningPointFuture, err := s.syncManager.IsBlockInHeaderPruningPointFuture(blockHash) if err != nil { return nil, err diff --git a/domain/consensus/consensus_test.go b/domain/consensus/consensus_test.go new file mode 100644 index 000000000..af64f73e9 --- /dev/null +++ b/domain/consensus/consensus_test.go @@ -0,0 +1,75 @@ +package consensus + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/dagconfig" + "github.com/pkg/errors" + "testing" +) + +func TestConsensus_GetBlockInfo(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := NewFactory() + consensus, teardown, err := factory.NewTestConsensus(params, "TestConsensus_GetBlockInfo") + if err != nil { + t.Fatalf("Error setting up consensus: %+v", err) + } + defer teardown() + + invalidBlock, err := consensus.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, nil, nil) + if err != nil { + t.Fatal(err) + } + invalidBlock.Header.TimeInMilliseconds = 0 + err = consensus.ValidateAndInsertBlock(invalidBlock) + if !errors.Is(err, ruleerrors.ErrTimeTooOld) { + t.Fatalf("Expected block to be invalid with err: %v, instead found: %v", ruleerrors.ErrTimeTooOld, err) + } + + info, err := consensus.GetBlockInfo(consensusserialization.BlockHash(invalidBlock)) + if err != nil { + t.Fatalf("Failed to get block info: %v", err) + } + + if !info.Exists { + t.Fatal("The block is missing") + } + if info.BlockStatus != externalapi.StatusInvalid { + t.Fatalf("Expected block status: %s, instead got: %s", externalapi.StatusInvalid, info.BlockStatus) + } + if info.IsBlockInHeaderPruningPointFuture != false { + t.Fatalf("Expected IsBlockInHeaderPruningPointFuture=false, instead found: %t", info.IsBlockInHeaderPruningPointFuture) + } + + emptyCoinbase := externalapi.DomainCoinbaseData{} + validBlock, err := consensus.BuildBlock(&emptyCoinbase, nil) + if err != nil { + t.Fatalf("consensus.BuildBlock with an empty coinbase shouldn't fail: %v", err) + } + + err = consensus.ValidateAndInsertBlock(validBlock) + if err != nil { + t.Fatalf("consensus.ValidateAndInsertBlock with a block straight from consensus.BuildBlock should not fail: %v", err) + } + + info, err = consensus.GetBlockInfo(consensusserialization.BlockHash(validBlock)) + if err != nil { + t.Fatalf("Failed to get block info: %v", err) + } + + if !info.Exists { + t.Fatal("The block is missing") + } + if info.BlockStatus != externalapi.StatusValid { + t.Fatalf("Expected block status: %s, instead got: %s", externalapi.StatusValid, info.BlockStatus) + } + if info.IsBlockInHeaderPruningPointFuture != true { + t.Fatalf("Expected IsBlockInHeaderPruningPointFuture=true, instead found: %t", info.IsBlockInHeaderPruningPointFuture) + } + + }) +} diff --git a/domain/consensus/finality_test.go b/domain/consensus/finality_test.go index 24af2edb9..5cf6532e6 100644 --- a/domain/consensus/finality_test.go +++ b/domain/consensus/finality_test.go @@ -1,11 +1,16 @@ package consensus import ( + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/model/testapi" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization" "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" "github.com/kaspanet/kaspad/domain/dagconfig" + "github.com/pkg/errors" + "fmt" "testing" ) @@ -167,3 +172,249 @@ func TestFinality(t *testing.T) { } }) } + +func TestBoundedMergeDepth(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + // Set finalityInterval to 50 blocks, so that test runs quickly + params.FinalityDuration = 50 * params.TargetTimePerBlock + finalityInterval := int(params.FinalityDepth()) + + if int(params.K) >= finalityInterval { + t.Fatal("K must be smaller than finality duration for this test to run") + } + + checkViolatingMergeDepth := func(consensus testapi.TestConsensus, parents []*externalapi.DomainHash) (*externalapi.DomainBlock, bool) { + block, err := consensus.BuildBlockWithParents(parents, nil, nil) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: BuildBlockWithParents failed: %v", err) + return nil, false // fo some reason go doesn't recognize that t.Fatalf never returns + } + + err = consensus.ValidateAndInsertBlock(block) + if err == nil { + return block, false + } else if errors.Is(err, ruleerrors.ErrViolatingBoundedMergeDepth) { + return block, true + } else { + t.Fatalf("TestBoundedMergeDepth: expected err: %v, found err: %v", ruleerrors.ErrViolatingBoundedMergeDepth, err) + return nil, false // fo some reason go doesn't recognize that t.Fatalf never returns + } + } + + processBlock := func(consensus testapi.TestConsensus, block *externalapi.DomainBlock, name string) { + err := consensus.ValidateAndInsertBlock(block) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: %s got unexpected error from ProcessBlock: %+v", name, err) + + } + } + + buildAndInsertBlock := func(consensus testapi.TestConsensus, parentHashes []*externalapi.DomainHash) *externalapi.DomainBlock { + block, err := consensus.BuildBlockWithParents(parentHashes, nil, nil) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed building block: %v", err) + } + err = consensus.ValidateAndInsertBlock(block) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed Inserting block to consensus: %v", err) + } + return block + } + + getStatus := func(consensus testapi.TestConsensus, block *externalapi.DomainBlock) externalapi.BlockStatus { + blockInfo, err := consensus.GetBlockInfo(consensusserialization.BlockHash(block)) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed to get block info: %v", err) + } else if !blockInfo.Exists { + t.Fatalf("TestBoundedMergeDepth: Failed to get block info, block doesn't exists") + } + return blockInfo.BlockStatus + } + + factory := NewFactory() + consensusBuild, teardownFunc1, err := factory.NewTestConsensus(params, "BoundedMergeTestBuild") + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Error setting up consensus: %+v", err) + } + + consensusReal, teardownFunc2, err := factory.NewTestConsensus(params, "BoundedMergeTestReal") + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Error setting up consensus: %+v", err) + } + defer teardownFunc2() + + // Create a block on top on genesis + block1 := buildAndInsertBlock(consensusBuild, []*externalapi.DomainHash{params.GenesisHash}) + + // Create a chain + selectedChain := make([]*externalapi.DomainBlock, 0, finalityInterval+1) + parent := consensusserialization.BlockHash(block1) + // Make sure this is always bigger than `blocksChain2` so it will stay the selected chain + for i := 0; i < finalityInterval+2; i++ { + block := buildAndInsertBlock(consensusBuild, []*externalapi.DomainHash{parent}) + selectedChain = append(selectedChain, block) + parent = consensusserialization.BlockHash(block) + } + + // Create another chain + blocksChain2 := make([]*externalapi.DomainBlock, 0, finalityInterval+1) + parent = consensusserialization.BlockHash(block1) + for i := 0; i < finalityInterval+1; i++ { + block := buildAndInsertBlock(consensusBuild, []*externalapi.DomainHash{parent}) + blocksChain2 = append(blocksChain2, block) + parent = consensusserialization.BlockHash(block) + } + + // Teardown and assign nil to make sure we use the right DAG from here on. + teardownFunc1() + consensusBuild = nil + + // Now test against the real DAG + // submit block1 + processBlock(consensusReal, block1, "block1") + + // submit chain1 + for i, block := range selectedChain { + processBlock(consensusReal, block, fmt.Sprintf("selectedChain block No %d", i)) + } + + // submit chain2 + for i, block := range blocksChain2 { + processBlock(consensusReal, block, fmt.Sprintf("blocksChain2 block No %d", i)) + } + + // submit a block pointing at tip(chain1) and on first block in chain2 directly + mergeDepthViolatingBlockBottom, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{consensusserialization.BlockHash(blocksChain2[0]), consensusserialization.BlockHash(selectedChain[len(selectedChain)-1])}) + if !isViolatingMergeDepth { + t.Fatalf("TestBoundedMergeDepth: Expected mergeDepthViolatingBlockBottom to violate merge depth") + } + + // submit a block pointing at tip(chain1) and tip(chain2) should also obviously violate merge depth (this points at first block in chain2 indirectly) + mergeDepthViolatingTop, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{consensusserialization.BlockHash(blocksChain2[len(blocksChain2)-1]), consensusserialization.BlockHash(selectedChain[len(selectedChain)-1])}) + if !isViolatingMergeDepth { + t.Fatalf("TestBoundedMergeDepth: Expected mergeDepthViolatingTop to violate merge depth") + } + + // the location of the parents in the slices need to be both `-X` so the `selectedChain` one will have higher blueScore (it's a chain longer by 1) + kosherizingBlock, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{consensusserialization.BlockHash(blocksChain2[len(blocksChain2)-3]), consensusserialization.BlockHash(selectedChain[len(selectedChain)-3])}) + kosherizingBlockHash := consensusserialization.BlockHash(kosherizingBlock) + if isViolatingMergeDepth { + t.Fatalf("TestBoundedMergeDepth: Expected blueKosherizingBlock to not violate merge depth") + } + + virtualGhotDagData, err := consensusReal.GHOSTDAGDataStore().Get(consensusReal.DatabaseContext(), model.VirtualBlockHash) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed getting the ghostdag data of the virtual: %v", err) + } + // Make sure it's actually blue + found := false + for _, blue := range virtualGhotDagData.MergeSetBlues { + if *blue == *kosherizingBlockHash { + found = true + break + } + } + if !found { + t.Fatalf("TestBoundedMergeDepth: Expected kosherizingBlock to be blue by the virtual") + } + + pointAtBlueKosherizing, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{kosherizingBlockHash, consensusserialization.BlockHash(selectedChain[len(selectedChain)-1])}) + if isViolatingMergeDepth { + + t.Fatalf("TestBoundedMergeDepth: Expected selectedTip to not violate merge depth") + } + + virtualSelectedParent, err := consensusReal.GetVirtualSelectedParent() + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed getting the virtual selected parent %v", err) + } + + if *consensusserialization.BlockHash(virtualSelectedParent) != *consensusserialization.BlockHash(pointAtBlueKosherizing) { + t.Fatalf("TestBoundedMergeDepth: Expected %s to be the selectedTip but found %s instead", consensusserialization.BlockHash(pointAtBlueKosherizing), consensusserialization.BlockHash(virtualSelectedParent)) + } + + // Now let's make the kosherizing block red and try to merge again + tip := consensusserialization.BlockHash(selectedChain[len(selectedChain)-1]) + // we use k-1 because `kosherizingBlock` points at tip-2, so 2+k-1 = k+1 anticone. + for i := 0; i < int(params.K)-1; i++ { + block := buildAndInsertBlock(consensusReal, []*externalapi.DomainHash{tip}) + tip = consensusserialization.BlockHash(block) + } + + virtualSelectedParent, err = consensusReal.GetVirtualSelectedParent() + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed getting the virtual selected parent %v", err) + } + + if *consensusserialization.BlockHash(virtualSelectedParent) != *tip { + t.Fatalf("TestBoundedMergeDepth: Expected %s to be the selectedTip but found %s instead", tip, consensusserialization.BlockHash(virtualSelectedParent)) + } + + virtualGhotDagData, err = consensusReal.GHOSTDAGDataStore().Get(consensusReal.DatabaseContext(), model.VirtualBlockHash) + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed getting the ghostdag data of the virtual: %v", err) + } + // Make sure it's actually blue + found = false + for _, blue := range virtualGhotDagData.MergeSetBlues { + if *blue == *kosherizingBlockHash { + found = true + break + } + } + if found { + t.Fatalf("expected kosherizingBlock to be red by the virtual") + } + + pointAtRedKosherizing, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{kosherizingBlockHash, tip}) + if !isViolatingMergeDepth { + t.Fatalf("TestBoundedMergeDepth: Expected selectedTipRedKosherize to violate merge depth") + } + + // Now `pointAtBlueKosherizing` itself is actually still blue, so we can still point at that even though we can't point at kosherizing directly anymore + transitiveBlueKosherizing, isViolatingMergeDepth := checkViolatingMergeDepth(consensusReal, []*externalapi.DomainHash{consensusserialization.BlockHash(pointAtBlueKosherizing), tip}) + if isViolatingMergeDepth { + t.Fatalf("TestBoundedMergeDepth: Expected transitiveBlueKosherizing to not violate merge depth") + } + + virtualSelectedParent, err = consensusReal.GetVirtualSelectedParent() + if err != nil { + t.Fatalf("TestBoundedMergeDepth: Failed getting the virtual selected parent %v", err) + } + + if *consensusserialization.BlockHash(virtualSelectedParent) != *consensusserialization.BlockHash(transitiveBlueKosherizing) { + t.Fatalf("TestBoundedMergeDepth: Expected %s to be the selectedTip but found %s instead", consensusserialization.BlockHash(transitiveBlueKosherizing), consensusserialization.BlockHash(virtualSelectedParent)) + } + + // Lets validate the status of all the interesting blocks + if getStatus(consensusReal, pointAtBlueKosherizing) != externalapi.StatusValid { + t.Fatalf("TestBoundedMergeDepth: pointAtBlueKosherizing expected status '%s' but got '%s'", externalapi.StatusValid, getStatus(consensusReal, pointAtBlueKosherizing)) + } + if getStatus(consensusReal, pointAtRedKosherizing) != externalapi.StatusInvalid { + t.Fatalf("TestBoundedMergeDepth: pointAtRedKosherizing expected status '%s' but got '%s'", externalapi.StatusInvalid, getStatus(consensusReal, pointAtRedKosherizing)) + } + if getStatus(consensusReal, transitiveBlueKosherizing) != externalapi.StatusValid { + t.Fatalf("TestBoundedMergeDepth: transitiveBlueKosherizing expected status '%s' but got '%s'", externalapi.StatusValid, getStatus(consensusReal, transitiveBlueKosherizing)) + } + if getStatus(consensusReal, mergeDepthViolatingBlockBottom) != externalapi.StatusInvalid { + t.Fatalf("TestBoundedMergeDepth: mergeDepthViolatingBlockBottom expected status '%s' but got '%s'", externalapi.StatusInvalid, getStatus(consensusReal, mergeDepthViolatingBlockBottom)) + } + if getStatus(consensusReal, mergeDepthViolatingTop) != externalapi.StatusInvalid { + t.Fatalf("TestBoundedMergeDepth: mergeDepthViolatingTop expected status '%s' but got '%s'", externalapi.StatusInvalid, getStatus(consensusReal, mergeDepthViolatingTop)) + } + if getStatus(consensusReal, kosherizingBlock) != externalapi.StatusUTXOPendingVerification { + t.Fatalf("kosherizingBlock expected status '%s' but got '%s'", externalapi.StatusUTXOPendingVerification, getStatus(consensusReal, kosherizingBlock)) + } + + for i, b := range blocksChain2 { + if getStatus(consensusReal, b) != externalapi.StatusUTXOPendingVerification { + t.Fatalf("blocksChain2[%d] expected status '%s' but got '%s'", i, externalapi.StatusUTXOPendingVerification, getStatus(consensusReal, b)) + } + } + for i, b := range selectedChain { + if getStatus(consensusReal, b) != externalapi.StatusValid { + t.Fatalf("selectedChain[%d] expected status '%s' but got '%s'", i, externalapi.StatusValid, getStatus(consensusReal, b)) + } + } + }) +}