mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-06-05 21:56:50 +00:00

* Implement GHOST. * Implement TestGHOST. * Make GHOST() take arbitrary subDAGs. * Hold RootHashes in SubDAG rather than one GenesisHash. * Select which root the GHOST chain starts with instead of passing a lowHash. * If two child hashes have the same future size, decide which one is larger using the block hash. * Extract blockHashWithLargestFutureSize to a separate function. * Calculate future size for each block individually. * Make TestGHOST deterministic. * Increase the timeout for connecting 128 connections in TestRPCMaxInboundConnections. * Implement BenchmarkGHOST. * Fix an infinite loop. * Use much larger benchmark data. * Optimize `futureSizes` using reverse merge sets. * Temporarily make the benchmark data smaller while GHOST is being optimized. * Fix a bug in futureSizes. * Fix a bug in populateReverseMergeSet. * Choose a selectedChild at random instead of the one with the largest reverse merge set size. * Rename populateReverseMergeSet to calculateReverseMergeSet. * Use reachability to resolve isDescendantOf. * Extract heightMaps to a separate object. * Iterate using height maps in futureSizes. * Don't store reverse merge sets in memory. * Change calculateReverseMergeSet to calculateReverseMergeSetSize. * Fix bad initial reverseMergeSetSize. * Optimize calculateReverseMergeSetSize. * Enlarge the benchmark data to 86k blocks.
297 lines
8.2 KiB
Go
297 lines
8.2 KiB
Go
package ghost
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/kaspanet/kaspad/domain/consensus"
|
|
"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/utils/consensushashing"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestGHOST(t *testing.T) {
|
|
testChain := []struct {
|
|
parents []string
|
|
id string
|
|
expectedGHOSTChain []string
|
|
}{
|
|
{
|
|
parents: []string{"A"},
|
|
id: "B",
|
|
expectedGHOSTChain: []string{"A", "B"},
|
|
},
|
|
{
|
|
parents: []string{"B"},
|
|
id: "C",
|
|
expectedGHOSTChain: []string{"A", "B", "C"},
|
|
},
|
|
{
|
|
parents: []string{"B"},
|
|
id: "D",
|
|
expectedGHOSTChain: []string{"A", "B", "D"},
|
|
},
|
|
{
|
|
parents: []string{"C", "D"},
|
|
id: "E",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "E"},
|
|
},
|
|
{
|
|
parents: []string{"C", "D"},
|
|
id: "F",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "F"},
|
|
},
|
|
{
|
|
parents: []string{"A"},
|
|
id: "G",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "F"},
|
|
},
|
|
{
|
|
parents: []string{"G"},
|
|
id: "H",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "F"},
|
|
},
|
|
{
|
|
parents: []string{"H", "F"},
|
|
id: "I",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "F", "I"},
|
|
},
|
|
{
|
|
parents: []string{"I"},
|
|
id: "J",
|
|
expectedGHOSTChain: []string{"A", "B", "D", "F", "I", "J"},
|
|
},
|
|
}
|
|
|
|
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
|
factory := consensus.NewFactory()
|
|
tc, tearDown, err := factory.NewTestConsensus(consensusConfig, "TestBlockWindow")
|
|
if err != nil {
|
|
t.Fatalf("NewTestConsensus: %s", err)
|
|
}
|
|
defer tearDown(false)
|
|
|
|
blockByIDMap := make(map[string]*externalapi.DomainHash)
|
|
idByBlockMap := make(map[externalapi.DomainHash]string)
|
|
blockByIDMap["A"] = consensusConfig.GenesisHash
|
|
idByBlockMap[*consensusConfig.GenesisHash] = "A"
|
|
mostRecentHash := consensusConfig.GenesisHash
|
|
|
|
for _, blockData := range testChain {
|
|
parents := hashset.New()
|
|
for _, parentID := range blockData.parents {
|
|
parent := blockByIDMap[parentID]
|
|
parents.Add(parent)
|
|
}
|
|
|
|
blockHash := addBlockWithHashSmallerThan(t, tc, parents.ToSlice(), mostRecentHash)
|
|
if err != nil {
|
|
t.Fatalf("AddBlock: %+v", err)
|
|
}
|
|
blockByIDMap[blockData.id] = blockHash
|
|
idByBlockMap[*blockHash] = blockData.id
|
|
mostRecentHash = blockHash
|
|
|
|
subDAG := convertDAGtoSubDAG(t, consensusConfig, tc)
|
|
ghostChainHashes, err := GHOST(subDAG)
|
|
if err != nil {
|
|
t.Fatalf("GHOST: %+v", err)
|
|
}
|
|
ghostChainIDs := make([]string, len(ghostChainHashes))
|
|
for i, ghostChainHash := range ghostChainHashes {
|
|
ghostChainIDs[i] = idByBlockMap[*ghostChainHash]
|
|
}
|
|
if !reflect.DeepEqual(ghostChainIDs, blockData.expectedGHOSTChain) {
|
|
t.Errorf("After adding block ID %s, GHOST chain expected to have IDs %s but got IDs %s",
|
|
blockData.id, blockData.expectedGHOSTChain, ghostChainIDs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// addBlockWithHashSmallerThan adds a block to the DAG with the given parents such that its
|
|
// hash is smaller than `maxHash`. This ensures that the GHOST chain calculated from the
|
|
// DAG is deterministic
|
|
func addBlockWithHashSmallerThan(t *testing.T, tc testapi.TestConsensus,
|
|
parentHashes []*externalapi.DomainHash, maxHash *externalapi.DomainHash) *externalapi.DomainHash {
|
|
|
|
var block *externalapi.DomainBlock
|
|
blockHash := maxHash
|
|
for maxHash.LessOrEqual(blockHash) {
|
|
var err error
|
|
block, _, err = tc.BuildBlockWithParents(parentHashes, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildBlockWithParents: %+v", err)
|
|
}
|
|
blockHash = consensushashing.BlockHash(block)
|
|
}
|
|
|
|
_, err := tc.ValidateAndInsertBlock(block, true)
|
|
if err != nil {
|
|
t.Fatalf("ValidateAndInsertBlock: %+v", err)
|
|
}
|
|
|
|
return blockHash
|
|
}
|
|
|
|
func convertDAGtoSubDAG(t *testing.T, consensusConfig *consensus.Config, tc testapi.TestConsensus) *model.SubDAG {
|
|
genesisHash := consensusConfig.GenesisHash
|
|
|
|
stagingArea := model.NewStagingArea()
|
|
tipHashes, err := tc.ConsensusStateStore().Tips(stagingArea, tc.DatabaseContext())
|
|
if err != nil {
|
|
t.Fatalf("Tips: %+v", err)
|
|
}
|
|
|
|
subDAG := &model.SubDAG{
|
|
RootHashes: []*externalapi.DomainHash{genesisHash},
|
|
TipHashes: tipHashes,
|
|
Blocks: map[externalapi.DomainHash]*model.SubDAGBlock{},
|
|
}
|
|
|
|
visited := hashset.New()
|
|
queue := tc.DAGTraversalManager().NewDownHeap(stagingArea)
|
|
err = queue.PushSlice(tipHashes)
|
|
if err != nil {
|
|
t.Fatalf("PushSlice: %+v", err)
|
|
}
|
|
for queue.Len() > 0 {
|
|
blockHash := queue.Pop()
|
|
visited.Add(blockHash)
|
|
dagChildHashes, err := tc.DAGTopologyManager().Children(stagingArea, blockHash)
|
|
if err != nil {
|
|
t.Fatalf("Children: %+v", err)
|
|
}
|
|
childHashes := []*externalapi.DomainHash{}
|
|
for _, dagChildHash := range dagChildHashes {
|
|
if dagChildHash.Equal(model.VirtualBlockHash) {
|
|
continue
|
|
}
|
|
childHashes = append(childHashes, dagChildHash)
|
|
}
|
|
|
|
dagParentHashes, err := tc.DAGTopologyManager().Parents(stagingArea, blockHash)
|
|
if err != nil {
|
|
t.Fatalf("Parents: %+v", err)
|
|
}
|
|
parentHashes := []*externalapi.DomainHash{}
|
|
for _, dagParentHash := range dagParentHashes {
|
|
if dagParentHash.Equal(model.VirtualGenesisBlockHash) {
|
|
continue
|
|
}
|
|
parentHashes = append(parentHashes, dagParentHash)
|
|
if !visited.Contains(dagParentHash) {
|
|
err := queue.Push(dagParentHash)
|
|
if err != nil {
|
|
t.Fatalf("Push: %+v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
subDAG.Blocks[*blockHash] = &model.SubDAGBlock{
|
|
BlockHash: blockHash,
|
|
ParentHashes: parentHashes,
|
|
ChildHashes: childHashes,
|
|
}
|
|
}
|
|
return subDAG
|
|
}
|
|
|
|
type jsonBlock struct {
|
|
ID string `json:"ID"`
|
|
Parents []string `json:"Parents"`
|
|
}
|
|
|
|
type testJSON struct {
|
|
Blocks []*jsonBlock `json:"blocks"`
|
|
}
|
|
|
|
func BenchmarkGHOST(b *testing.B) {
|
|
b.StopTimer()
|
|
|
|
// Load JSON
|
|
b.Logf("Loading JSON data")
|
|
jsonFile, err := os.Open("benchmark_data.json")
|
|
if err != nil {
|
|
b.Fatalf("Open: %+v", err)
|
|
}
|
|
defer jsonFile.Close()
|
|
|
|
test := &testJSON{}
|
|
decoder := json.NewDecoder(jsonFile)
|
|
decoder.DisallowUnknownFields()
|
|
err = decoder.Decode(&test)
|
|
if err != nil {
|
|
b.Fatalf("Decode: %+v", err)
|
|
}
|
|
|
|
// Convert JSON data to a SubDAG
|
|
b.Logf("Converting JSON data to SubDAG")
|
|
subDAG := &model.SubDAG{
|
|
RootHashes: []*externalapi.DomainHash{},
|
|
TipHashes: []*externalapi.DomainHash{},
|
|
Blocks: make(map[externalapi.DomainHash]*model.SubDAGBlock, len(test.Blocks)),
|
|
}
|
|
blockIDToHash := func(blockID string) *externalapi.DomainHash {
|
|
blockHashHex := fmt.Sprintf("%064s", blockID)
|
|
blockHash, err := externalapi.NewDomainHashFromString(blockHashHex)
|
|
if err != nil {
|
|
b.Fatalf("NewDomainHashFromString: %+v", err)
|
|
}
|
|
return blockHash
|
|
}
|
|
for _, block := range test.Blocks {
|
|
blockHash := blockIDToHash(block.ID)
|
|
|
|
parentHashes := []*externalapi.DomainHash{}
|
|
for _, parentID := range block.Parents {
|
|
parentHash := blockIDToHash(parentID)
|
|
parentHashes = append(parentHashes, parentHash)
|
|
}
|
|
|
|
subDAG.Blocks[*blockHash] = &model.SubDAGBlock{
|
|
BlockHash: blockHash,
|
|
ParentHashes: parentHashes,
|
|
ChildHashes: []*externalapi.DomainHash{},
|
|
}
|
|
}
|
|
for _, block := range subDAG.Blocks {
|
|
for _, parentHash := range block.ParentHashes {
|
|
parentBlock := subDAG.Blocks[*parentHash]
|
|
parentAlreadyHasBlockAsChild := false
|
|
for _, childHash := range parentBlock.ChildHashes {
|
|
if block.BlockHash.Equal(childHash) {
|
|
parentAlreadyHasBlockAsChild = true
|
|
break
|
|
}
|
|
}
|
|
if !parentAlreadyHasBlockAsChild {
|
|
parentBlock.ChildHashes = append(parentBlock.ChildHashes, block.BlockHash)
|
|
}
|
|
}
|
|
}
|
|
for _, block := range subDAG.Blocks {
|
|
if len(block.ParentHashes) == 0 {
|
|
subDAG.RootHashes = append(subDAG.RootHashes, block.BlockHash)
|
|
}
|
|
if len(block.ChildHashes) == 0 {
|
|
subDAG.TipHashes = append(subDAG.TipHashes, block.BlockHash)
|
|
}
|
|
}
|
|
|
|
b.Logf("Running benchmark")
|
|
b.ResetTimer()
|
|
b.StartTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := GHOST(subDAG)
|
|
if err != nil {
|
|
b.Fatalf("GHOST: %+v", err)
|
|
}
|
|
}
|
|
}
|