Implement GHOST (#1819)

* 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.
This commit is contained in:
stasatdaglabs 2021-08-19 13:59:43 +03:00 committed by GitHub
parent 65b5a080e4
commit 7b5720a155
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 623245 additions and 1 deletions

View File

@ -0,0 +1,17 @@
package model
import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
// SubDAG represents a context-free representation of a partial DAG
type SubDAG struct {
RootHashes []*externalapi.DomainHash
TipHashes []*externalapi.DomainHash
Blocks map[externalapi.DomainHash]*SubDAGBlock
}
// SubDAGBlock represents a block in a SubDAG
type SubDAGBlock struct {
BlockHash *externalapi.DomainHash
ParentHashes []*externalapi.DomainHash
ChildHashes []*externalapi.DomainHash
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
package ghost
import (
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
)
// GHOST calculates the GHOST chain for the given `subDAG`
func GHOST(subDAG *model.SubDAG) ([]*externalapi.DomainHash, error) {
futureSizes, err := futureSizes(subDAG)
if err != nil {
return nil, err
}
ghostChain := []*externalapi.DomainHash{}
dagRootHashWithLargestFutureSize := blockHashWithLargestFutureSize(futureSizes, subDAG.RootHashes)
currentHash := dagRootHashWithLargestFutureSize
for {
ghostChain = append(ghostChain, currentHash)
currentBlock := subDAG.Blocks[*currentHash]
childHashes := currentBlock.ChildHashes
if len(childHashes) == 0 {
break
}
childHashWithLargestFutureSize := blockHashWithLargestFutureSize(futureSizes, childHashes)
currentHash = childHashWithLargestFutureSize
}
return ghostChain, nil
}
func blockHashWithLargestFutureSize(futureSizes map[externalapi.DomainHash]uint64,
blockHashes []*externalapi.DomainHash) *externalapi.DomainHash {
var blockHashWithLargestFutureSize *externalapi.DomainHash
largestFutureSize := uint64(0)
for _, blockHash := range blockHashes {
blockFutureSize := futureSizes[*blockHash]
if blockHashWithLargestFutureSize == nil || blockFutureSize > largestFutureSize ||
(blockFutureSize == largestFutureSize && blockHash.Less(blockHashWithLargestFutureSize)) {
largestFutureSize = blockFutureSize
blockHashWithLargestFutureSize = blockHash
}
}
return blockHashWithLargestFutureSize
}
func futureSizes(subDAG *model.SubDAG) (map[externalapi.DomainHash]uint64, error) {
heightMaps := buildHeightMaps(subDAG)
ghostReachabilityManager, err := newGHOSTReachabilityManager(subDAG, heightMaps)
if err != nil {
return nil, err
}
futureSizes := make(map[externalapi.DomainHash]uint64, len(subDAG.Blocks))
height := heightMaps.maxHeight
for {
for _, blockHash := range heightMaps.heightToBlockHashesMap[height] {
block := subDAG.Blocks[*blockHash]
currentBlockReverseMergeSetSize, err := calculateReverseMergeSetSize(subDAG, ghostReachabilityManager, block)
if err != nil {
return nil, err
}
futureSize := currentBlockReverseMergeSetSize
if currentBlockReverseMergeSetSize > 0 {
selectedChild := block.ChildHashes[0]
selectedChildFutureSize := futureSizes[*selectedChild]
futureSize += selectedChildFutureSize
}
futureSizes[*blockHash] = futureSize
}
if height == 0 {
break
}
height--
}
return futureSizes, nil
}
func calculateReverseMergeSetSize(subDAG *model.SubDAG,
ghostReachabilityManager *ghostReachabilityManager, block *model.SubDAGBlock) (uint64, error) {
if len(block.ChildHashes) == 0 {
return 0, nil
}
selectedChild := block.ChildHashes[0]
reverseMergeSetSize := uint64(1)
knownSelectedChildDescendants := hashset.NewFromSlice(selectedChild)
queue := append([]*externalapi.DomainHash{}, block.ChildHashes...)
addedToQueue := hashset.NewFromSlice(block.ChildHashes...)
for len(queue) > 0 {
var currentBlockHash *externalapi.DomainHash
currentBlockHash, queue = queue[0], queue[1:]
currentBlock := subDAG.Blocks[*currentBlockHash]
if knownSelectedChildDescendants.Contains(currentBlockHash) {
for _, childHash := range currentBlock.ChildHashes {
knownSelectedChildDescendants.Add(childHash)
}
continue
}
isCurrentBlockDescendantOfSelectedChild, err := ghostReachabilityManager.isDescendantOf(currentBlockHash, selectedChild)
if err != nil {
return 0, err
}
if isCurrentBlockDescendantOfSelectedChild {
knownSelectedChildDescendants.Add(currentBlockHash)
for _, childHash := range currentBlock.ChildHashes {
knownSelectedChildDescendants.Add(childHash)
}
continue
}
reverseMergeSetSize++
for _, childHash := range currentBlock.ChildHashes {
if addedToQueue.Contains(childHash) {
continue
}
queue = append(queue, childHash)
addedToQueue.Add(childHash)
}
}
return reverseMergeSetSize, nil
}

View File

@ -0,0 +1,296 @@
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)
}
}
}

View File

@ -0,0 +1,75 @@
package ghost
import (
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
)
type heightMaps struct {
blockHashToHeightMap map[externalapi.DomainHash]uint64
heightToBlockHashesMap map[uint64][]*externalapi.DomainHash
maxHeight uint64
}
func buildHeightMaps(subDAG *model.SubDAG) *heightMaps {
blockHashToHeightMap := make(map[externalapi.DomainHash]uint64, len(subDAG.Blocks))
heightToBlockHashesMap := make(map[uint64][]*externalapi.DomainHash)
maxHeight := uint64(0)
queue := append([]*externalapi.DomainHash{}, subDAG.RootHashes...)
addedToQueue := hashset.NewFromSlice(subDAG.RootHashes...)
for len(queue) > 0 {
var currentBlockHash *externalapi.DomainHash
currentBlockHash, queue = queue[0], queue[1:]
// Send the block to the back of the queue if one or more of its parents had not been processed yet
currentBlock := subDAG.Blocks[*currentBlockHash]
hasMissingParentData := false
for _, parentHash := range currentBlock.ParentHashes {
if _, ok := blockHashToHeightMap[*parentHash]; !ok {
hasMissingParentData = true
continue
}
}
if hasMissingParentData {
queue = append(queue, currentBlockHash)
continue
}
for _, childHash := range currentBlock.ChildHashes {
if addedToQueue.Contains(childHash) {
continue
}
queue = append(queue, childHash)
addedToQueue.Add(childHash)
}
currentBlockHeight := uint64(0)
if len(currentBlock.ParentHashes) > 0 {
highestParentHeight := uint64(0)
for _, parentHash := range currentBlock.ParentHashes {
parentHeight := blockHashToHeightMap[*parentHash]
if parentHeight > highestParentHeight {
highestParentHeight = parentHeight
}
}
currentBlockHeight = highestParentHeight + 1
}
blockHashToHeightMap[*currentBlockHash] = currentBlockHeight
if _, ok := heightToBlockHashesMap[currentBlockHeight]; !ok {
heightToBlockHashesMap[currentBlockHeight] = []*externalapi.DomainHash{}
}
heightToBlockHashesMap[currentBlockHeight] = append(heightToBlockHashesMap[currentBlockHeight], currentBlockHash)
if currentBlockHeight > maxHeight {
maxHeight = currentBlockHeight
}
}
return &heightMaps{
blockHashToHeightMap: blockHashToHeightMap,
heightToBlockHashesMap: heightToBlockHashesMap,
maxHeight: maxHeight,
}
}

View File

@ -0,0 +1,144 @@
package ghost
import (
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/processes/reachabilitymanager"
"github.com/pkg/errors"
)
type ghostReachabilityManager struct {
ghostdagDataStore *ghostdagDataStore
reachabilityDataStore *reachabilityDataStore
reachabilityManager model.ReachabilityManager
}
type ghostdagDataStore struct {
blockGHOSTDAGData map[externalapi.DomainHash]*externalapi.BlockGHOSTDAGData
}
func newGHOSTDAGDataStore() *ghostdagDataStore {
return &ghostdagDataStore{
blockGHOSTDAGData: map[externalapi.DomainHash]*externalapi.BlockGHOSTDAGData{},
}
}
func (gds *ghostdagDataStore) Stage(_ *model.StagingArea, blockHash *externalapi.DomainHash,
blockGHOSTDAGData *externalapi.BlockGHOSTDAGData, _ bool) {
gds.blockGHOSTDAGData[*blockHash] = blockGHOSTDAGData
}
func (gds *ghostdagDataStore) IsStaged(_ *model.StagingArea) bool {
return true
}
func (gds *ghostdagDataStore) Get(_ model.DBReader, _ *model.StagingArea,
blockHash *externalapi.DomainHash, _ bool) (*externalapi.BlockGHOSTDAGData, error) {
blockGHOSTDAGData, ok := gds.blockGHOSTDAGData[*blockHash]
if !ok {
return nil, errors.Errorf("ghostdag data not found for block hash %s", blockHash)
}
return blockGHOSTDAGData, nil
}
type reachabilityDataStore struct {
reachabilityData map[externalapi.DomainHash]model.ReachabilityData
reachabilityReindexRoot *externalapi.DomainHash
}
func newReachabilityDataStore() *reachabilityDataStore {
return &reachabilityDataStore{
reachabilityData: map[externalapi.DomainHash]model.ReachabilityData{},
reachabilityReindexRoot: nil,
}
}
func (rds *reachabilityDataStore) StageReachabilityData(_ *model.StagingArea,
blockHash *externalapi.DomainHash, reachabilityData model.ReachabilityData) {
rds.reachabilityData[*blockHash] = reachabilityData
}
func (rds *reachabilityDataStore) StageReachabilityReindexRoot(_ *model.StagingArea,
reachabilityReindexRoot *externalapi.DomainHash) {
rds.reachabilityReindexRoot = reachabilityReindexRoot
}
func (rds *reachabilityDataStore) IsStaged(_ *model.StagingArea) bool {
return true
}
func (rds *reachabilityDataStore) ReachabilityData(_ model.DBReader, _ *model.StagingArea,
blockHash *externalapi.DomainHash) (model.ReachabilityData, error) {
reachabilityData, ok := rds.reachabilityData[*blockHash]
if !ok {
return nil, errors.Errorf("reachability data not found for block hash %s", blockHash)
}
return reachabilityData, nil
}
func (rds *reachabilityDataStore) HasReachabilityData(_ model.DBReader, _ *model.StagingArea,
blockHash *externalapi.DomainHash) (bool, error) {
_, ok := rds.reachabilityData[*blockHash]
return ok, nil
}
func (rds *reachabilityDataStore) ReachabilityReindexRoot(_ model.DBReader,
_ *model.StagingArea) (*externalapi.DomainHash, error) {
return rds.reachabilityReindexRoot, nil
}
func newGHOSTReachabilityManager(subDAG *model.SubDAG, heightMaps *heightMaps) (*ghostReachabilityManager, error) {
ghostdagDataStore := newGHOSTDAGDataStore()
reachabilityDataStore := newReachabilityDataStore()
reachabilityManager := reachabilitymanager.New(nil, ghostdagDataStore, reachabilityDataStore)
ghostReachabilityManager := &ghostReachabilityManager{
ghostdagDataStore: ghostdagDataStore,
reachabilityDataStore: reachabilityDataStore,
reachabilityManager: reachabilityManager,
}
err := ghostReachabilityManager.initialize(subDAG, heightMaps)
if err != nil {
return nil, err
}
return ghostReachabilityManager, nil
}
func (grm *ghostReachabilityManager) initialize(subDAG *model.SubDAG, heightMaps *heightMaps) error {
for blockHash, block := range subDAG.Blocks {
blockHeight := heightMaps.blockHashToHeightMap[blockHash]
selectedParent := model.VirtualGenesisBlockHash
if len(block.ParentHashes) > 0 {
selectedParent = block.ParentHashes[0]
}
blockGHOSTDAGData := externalapi.NewBlockGHOSTDAGData(blockHeight, nil, selectedParent, nil, nil, nil)
grm.ghostdagDataStore.Stage(nil, &blockHash, blockGHOSTDAGData, false)
}
err := grm.reachabilityManager.Init(nil)
if err != nil {
return err
}
for height := uint64(0); height <= heightMaps.maxHeight; height++ {
for _, blockHash := range heightMaps.heightToBlockHashesMap[height] {
err := grm.reachabilityManager.AddBlock(nil, blockHash)
if err != nil {
return err
}
}
}
return nil
}
func (grm *ghostReachabilityManager) isDescendantOf(blockAHash *externalapi.DomainHash, blockBHash *externalapi.DomainHash) (bool, error) {
return grm.reachabilityManager.IsDAGAncestorOf(nil, blockBHash, blockAHash)
}

View File

@ -59,7 +59,7 @@ func TestRPCMaxInboundConnections(t *testing.T) {
if err != nil {
t.Fatalf("newTestRPCClient: %s", err)
}
case <-time.After(time.Second):
case <-time.After(time.Second * 5):
t.Fatalf("Timeout for connecting %d RPC connections elapsed", grpcserver.RPCMaxInboundConnections)
}