From c82a951a245e41498a1be8c55090b9fc0350aeb4 Mon Sep 17 00:00:00 2001 From: Elichai Turkel Date: Tue, 25 Aug 2020 14:00:43 +0300 Subject: [PATCH] [NOD-1316] Refactor TestGHOSTDAG to enable arbitrary DAGs (#899) * Add VirtualBlueHashes to BlockDAG * Refactor TestGHOSTDAG to read DAGs from json files * Added a new DAG for the ghostdag test suite * Pass BehaviorFlags to delayed blocks --- domain/blockdag/dag.go | 11 + domain/blockdag/delayed_blocks.go | 6 +- domain/blockdag/ghostdag_test.go | 239 ++++----------- domain/blockdag/process.go | 4 +- domain/blockdag/testdata/dags/dag0.json | 233 ++++++++++++++ domain/blockdag/testdata/dags/dag1.json | 386 ++++++++++++++++++++++++ 6 files changed, 698 insertions(+), 181 deletions(-) create mode 100644 domain/blockdag/testdata/dags/dag0.json create mode 100644 domain/blockdag/testdata/dags/dag1.json diff --git a/domain/blockdag/dag.go b/domain/blockdag/dag.go index f01464143..b23694e89 100644 --- a/domain/blockdag/dag.go +++ b/domain/blockdag/dag.go @@ -285,6 +285,17 @@ func (dag *BlockDAG) SelectedTipBlueScore() uint64 { return dag.selectedTip().blueScore } +// VirtualBlueHashes returns the blue of the current virtual block +func (dag *BlockDAG) VirtualBlueHashes() []*daghash.Hash { + dag.RLock() + defer dag.RUnlock() + hashes := make([]*daghash.Hash, len(dag.virtual.blues)) + for i, blue := range dag.virtual.blues { + hashes[i] = blue.hash + } + return hashes +} + // VirtualBlueScore returns the blue score of the current virtual block func (dag *BlockDAG) VirtualBlueScore() uint64 { return dag.virtual.blueScore diff --git a/domain/blockdag/delayed_blocks.go b/domain/blockdag/delayed_blocks.go index ade054dc3..2376a053b 100644 --- a/domain/blockdag/delayed_blocks.go +++ b/domain/blockdag/delayed_blocks.go @@ -12,6 +12,7 @@ import ( type delayedBlock struct { block *util.Block processTime mstime.Time + flags BehaviorFlags } func (dag *BlockDAG) isKnownDelayedBlock(hash *daghash.Hash) bool { @@ -19,12 +20,13 @@ func (dag *BlockDAG) isKnownDelayedBlock(hash *daghash.Hash) bool { return exists } -func (dag *BlockDAG) addDelayedBlock(block *util.Block, delay time.Duration) error { +func (dag *BlockDAG) addDelayedBlock(block *util.Block, flags BehaviorFlags, delay time.Duration) error { processTime := dag.Now().Add(delay) log.Debugf("Adding block to delayed blocks queue (block hash: %s, process time: %s)", block.Hash().String(), processTime) delayedBlock := &delayedBlock{ block: block, processTime: processTime, + flags: flags, } dag.delayedBlocks[*block.Hash()] = delayedBlock @@ -42,7 +44,7 @@ func (dag *BlockDAG) processDelayedBlocks() error { break } delayedBlock := dag.popDelayedBlock() - _, _, err := dag.processBlockNoLock(delayedBlock.block, BFAfterDelay) + _, _, err := dag.processBlockNoLock(delayedBlock.block, delayedBlock.flags|BFAfterDelay) if err != nil { log.Errorf("Error while processing delayed block (block %s): %s", delayedBlock.block.Hash().String(), err) // Rule errors should not be propagated as they refer only to the delayed block, diff --git a/domain/blockdag/ghostdag_test.go b/domain/blockdag/ghostdag_test.go index 8126e5193..ee616b20c 100644 --- a/domain/blockdag/ghostdag_test.go +++ b/domain/blockdag/ghostdag_test.go @@ -1,7 +1,10 @@ package blockdag import ( + "encoding/json" "fmt" + "os" + "path/filepath" "reflect" "sort" "strings" @@ -13,12 +16,19 @@ import ( "github.com/kaspanet/kaspad/util/daghash" ) -type testBlockData struct { - parents []string - id string // id is a virtual entity that is used only for tests so we can define relations between blocks without knowing their hash - expectedScore uint64 - expectedSelectedParent string - expectedBlues []string +type block struct { + ID string // id is a virtual entity that is used only for tests so we can define relations between blocks without knowing their hash + ExpectedScore uint64 + ExpectedSelectedParent string + ExpectedBlues []string + Parents []string +} + +type testData struct { + K dagconfig.KType + GenesisID string + ExpectedReds []string + Blocks []block } // TestGHOSTDAG iterates over several dag simulations, and checks @@ -26,158 +36,26 @@ type testBlockData struct { // block are calculated as expected. func TestGHOSTDAG(t *testing.T) { dagParams := dagconfig.SimnetParams + err := filepath.Walk("./testdata/dags/", func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + var test testData + file, err := os.Open(path) + if err != nil { + t.Fatalf("TestGHOSTDAG: failed opening file: %s", path) + } + decoder := json.NewDecoder(file) + decoder.DisallowUnknownFields() + err = decoder.Decode(&test) + if err != nil { + t.Fatalf("TestGHOSTDAG: test: %s, failed decoding json: %v", info.Name(), err) + } - tests := []struct { - k dagconfig.KType - expectedReds []string - dagData []*testBlockData - }{ - { - k: 3, - expectedReds: []string{"F", "G", "H", "I", "O", "P"}, - dagData: []*testBlockData{ - { - parents: []string{"A"}, - id: "B", - expectedScore: 1, - expectedSelectedParent: "A", - expectedBlues: []string{"A"}, - }, - { - parents: []string{"B"}, - id: "C", - expectedScore: 2, - expectedSelectedParent: "B", - expectedBlues: []string{"B"}, - }, - { - parents: []string{"A"}, - id: "D", - expectedScore: 1, - expectedSelectedParent: "A", - expectedBlues: []string{"A"}, - }, - { - parents: []string{"C", "D"}, - id: "E", - expectedScore: 4, - expectedSelectedParent: "C", - expectedBlues: []string{"C", "D"}, - }, - { - parents: []string{"A"}, - id: "F", - expectedScore: 1, - expectedSelectedParent: "A", - expectedBlues: []string{"A"}, - }, - { - parents: []string{"F"}, - id: "G", - expectedScore: 2, - expectedSelectedParent: "F", - expectedBlues: []string{"F"}, - }, - { - parents: []string{"A"}, - id: "H", - expectedScore: 1, - expectedSelectedParent: "A", - expectedBlues: []string{"A"}, - }, - { - parents: []string{"A"}, - id: "I", - expectedScore: 1, - expectedSelectedParent: "A", - expectedBlues: []string{"A"}, - }, - { - parents: []string{"E", "G"}, - id: "J", - expectedScore: 5, - expectedSelectedParent: "E", - expectedBlues: []string{"E"}, - }, - { - parents: []string{"J"}, - id: "K", - expectedScore: 6, - expectedSelectedParent: "J", - expectedBlues: []string{"J"}, - }, - { - parents: []string{"I", "K"}, - id: "L", - expectedScore: 7, - expectedSelectedParent: "K", - expectedBlues: []string{"K"}, - }, - { - parents: []string{"L"}, - id: "M", - expectedScore: 8, - expectedSelectedParent: "L", - expectedBlues: []string{"L"}, - }, - { - parents: []string{"M"}, - id: "N", - expectedScore: 9, - expectedSelectedParent: "M", - expectedBlues: []string{"M"}, - }, - { - parents: []string{"M"}, - id: "O", - expectedScore: 9, - expectedSelectedParent: "M", - expectedBlues: []string{"M"}, - }, - { - parents: []string{"M"}, - id: "P", - expectedScore: 9, - expectedSelectedParent: "M", - expectedBlues: []string{"M"}, - }, - { - parents: []string{"M"}, - id: "Q", - expectedScore: 9, - expectedSelectedParent: "M", - expectedBlues: []string{"M"}, - }, - { - parents: []string{"M"}, - id: "R", - expectedScore: 9, - expectedSelectedParent: "M", - expectedBlues: []string{"M"}, - }, - { - parents: []string{"R"}, - id: "S", - expectedScore: 10, - expectedSelectedParent: "R", - expectedBlues: []string{"R"}, - }, - { - parents: []string{"N", "O", "P", "Q", "S"}, - id: "T", - expectedScore: 13, - expectedSelectedParent: "S", - expectedBlues: []string{"S", "Q", "N"}, - }, - }, - }, - } - - for i, test := range tests { func() { resetExtraNonceForTest() - dagParams.K = test.k - dag, teardownFunc, err := DAGSetup(fmt.Sprintf("TestGHOSTDAG%d", i), true, Config{ + dagParams.K = test.K + dag, teardownFunc, err := DAGSetup(fmt.Sprintf("TestGHOSTDAG %s", info.Name()), true, Config{ DAGParams: &dagParams, }) if err != nil { @@ -188,32 +66,33 @@ func TestGHOSTDAG(t *testing.T) { genesisNode := dag.genesis blockByIDMap := make(map[string]*blockNode) idByBlockMap := make(map[*blockNode]string) - blockByIDMap["A"] = genesisNode - idByBlockMap[genesisNode] = "A" + blockByIDMap[test.GenesisID] = genesisNode + idByBlockMap[genesisNode] = test.GenesisID - for _, blockData := range test.dagData { + for _, blockData := range test.Blocks { parents := blockSet{} - for _, parentID := range blockData.parents { + for _, parentID := range blockData.Parents { parent := blockByIDMap[parentID] parents.add(parent) } block, err := PrepareBlockForTest(dag, parents.hashes(), nil) if err != nil { - t.Fatalf("TestGHOSTDAG: block %v got unexpected error from PrepareBlockForTest: %v", blockData.id, err) + t.Fatalf("TestGHOSTDAG: block %s got unexpected error from PrepareBlockForTest: %v", blockData.ID, + err) } utilBlock := util.NewBlock(block) isOrphan, isDelayed, err := dag.ProcessBlock(utilBlock, BFNoPoWCheck) if err != nil { - t.Fatalf("TestGHOSTDAG: dag.ProcessBlock got unexpected error for block %v: %v", blockData.id, err) + t.Fatalf("TestGHOSTDAG: dag.ProcessBlock got unexpected error for block %s: %v", blockData.ID, err) } if isDelayed { t.Fatalf("TestGHOSTDAG: block %s "+ - "is too far in the future", blockData.id) + "is too far in the future", blockData.ID) } if isOrphan { - t.Fatalf("TestGHOSTDAG: block %v was unexpectedly orphan", blockData.id) + t.Fatalf("TestGHOSTDAG: block %s was unexpectedly orphan", blockData.ID) } node, ok := dag.index.LookupNode(utilBlock.Hash()) @@ -221,8 +100,8 @@ func TestGHOSTDAG(t *testing.T) { t.Fatalf("block %s does not exist in the DAG", utilBlock.Hash()) } - blockByIDMap[blockData.id] = node - idByBlockMap[node] = blockData.id + blockByIDMap[blockData.ID] = node + idByBlockMap[node] = blockData.ID bluesIDs := make([]string, 0, len(node.blues)) for _, blue := range node.blues { @@ -231,17 +110,17 @@ func TestGHOSTDAG(t *testing.T) { selectedParentID := idByBlockMap[node.selectedParent] fullDataStr := fmt.Sprintf("blues: %v, selectedParent: %v, score: %v", bluesIDs, selectedParentID, node.blueScore) - if blockData.expectedScore != node.blueScore { - t.Errorf("Test %d: Block %v expected to have score %v but got %v (fulldata: %v)", - i, blockData.id, blockData.expectedScore, node.blueScore, fullDataStr) + if blockData.ExpectedScore != node.blueScore { + t.Errorf("Test %s: Block %s expected to have score %v but got %v (fulldata: %v)", + info.Name(), blockData.ID, blockData.ExpectedScore, node.blueScore, fullDataStr) } - if blockData.expectedSelectedParent != selectedParentID { - t.Errorf("Test %d: Block %v expected to have selected parent %v but got %v (fulldata: %v)", - i, blockData.id, blockData.expectedSelectedParent, selectedParentID, fullDataStr) + if blockData.ExpectedSelectedParent != selectedParentID { + t.Errorf("Test %s: Block %s expected to have selected parent %v but got %v (fulldata: %v)", + info.Name(), blockData.ID, blockData.ExpectedSelectedParent, selectedParentID, fullDataStr) } - if !reflect.DeepEqual(blockData.expectedBlues, bluesIDs) { - t.Errorf("Test %d: Block %v expected to have blues %v but got %v (fulldata: %v)", - i, blockData.id, blockData.expectedBlues, bluesIDs, fullDataStr) + if !reflect.DeepEqual(blockData.ExpectedBlues, bluesIDs) { + t.Errorf("Test %s: Block %s expected to have blues %v but got %v (fulldata: %v)", + info.Name(), blockData.ID, blockData.ExpectedBlues, bluesIDs, fullDataStr) } } @@ -259,16 +138,22 @@ func TestGHOSTDAG(t *testing.T) { delete(reds, blueID) } } - if !checkReds(test.expectedReds, reds) { + if !checkReds(test.ExpectedReds, reds) { redsIDs := make([]string, 0, len(reds)) for id := range reds { redsIDs = append(redsIDs, id) } sort.Strings(redsIDs) - sort.Strings(test.expectedReds) - t.Errorf("Test %d: Expected reds %v but got %v", i, test.expectedReds, redsIDs) + sort.Strings(test.ExpectedReds) + t.Errorf("Test %s: Expected reds %v but got %v", info.Name(), test.ExpectedReds, redsIDs) } }() + + return nil + }) + + if err != nil { + t.Fatal(err) } } diff --git a/domain/blockdag/process.go b/domain/blockdag/process.go index d0b401a0e..18b278dcf 100644 --- a/domain/blockdag/process.go +++ b/domain/blockdag/process.go @@ -86,7 +86,7 @@ func (dag *BlockDAG) checkBlockDelay(block *util.Block, flags BehaviorFlags) (is } if isDelayed { - err := dag.addDelayedBlock(block, delay) + err := dag.addDelayedBlock(block, flags, delay) if err != nil { return false, err } @@ -114,7 +114,7 @@ func (dag *BlockDAG) checkMissingParents(block *util.Block, flags BehaviorFlags) if isParentDelayed { // Add Millisecond to ensure that parent process time will be after its child. delay += time.Millisecond - err := dag.addDelayedBlock(block, delay) + err := dag.addDelayedBlock(block, flags, delay) if err != nil { return false, false, err } diff --git a/domain/blockdag/testdata/dags/dag0.json b/domain/blockdag/testdata/dags/dag0.json new file mode 100644 index 000000000..86e795bd1 --- /dev/null +++ b/domain/blockdag/testdata/dags/dag0.json @@ -0,0 +1,233 @@ +{ + "K": 4, + "GenesisID": "A", + "ExpectedReds": [ + "Q", + "H", + "I" + ], + "Blocks": [ + { + "ID": "B", + "ExpectedScore": 1, + "ExpectedSelectedParent": "A", + "ExpectedBlues": [ + "A" + ], + "Parents": [ + "A" + ] + }, + { + "ID": "C", + "ExpectedScore": 2, + "ExpectedSelectedParent": "B", + "ExpectedBlues": [ + "B" + ], + "Parents": [ + "B" + ] + }, + { + "ID": "D", + "ExpectedScore": 1, + "ExpectedSelectedParent": "A", + "ExpectedBlues": [ + "A" + ], + "Parents": [ + "A" + ] + }, + { + "ID": "E", + "ExpectedScore": 4, + "ExpectedSelectedParent": "C", + "ExpectedBlues": [ + "C", + "D" + ], + "Parents": [ + "C", + "D" + ] + }, + { + "ID": "F", + "ExpectedScore": 1, + "ExpectedSelectedParent": "A", + "ExpectedBlues": [ + "A" + ], + "Parents": [ + "A" + ] + }, + { + "ID": "G", + "ExpectedScore": 2, + "ExpectedSelectedParent": "F", + "ExpectedBlues": [ + "F" + ], + "Parents": [ + "F" + ] + }, + { + "ID": "H", + "ExpectedScore": 1, + "ExpectedSelectedParent": "A", + "ExpectedBlues": [ + "A" + ], + "Parents": [ + "A" + ] + }, + { + "ID": "I", + "ExpectedScore": 1, + "ExpectedSelectedParent": "A", + "ExpectedBlues": [ + "A" + ], + "Parents": [ + "A" + ] + }, + { + "ID": "J", + "ExpectedScore": 7, + "ExpectedSelectedParent": "E", + "ExpectedBlues": [ + "E", + "F", + "G" + ], + "Parents": [ + "E", + "G" + ] + }, + { + "ID": "K", + "ExpectedScore": 8, + "ExpectedSelectedParent": "J", + "ExpectedBlues": [ + "J" + ], + "Parents": [ + "J" + ] + }, + { + "ID": "L", + "ExpectedScore": 9, + "ExpectedSelectedParent": "K", + "ExpectedBlues": [ + "K" + ], + "Parents": [ + "I", + "K" + ] + }, + { + "ID": "M", + "ExpectedScore": 10, + "ExpectedSelectedParent": "L", + "ExpectedBlues": [ + "L" + ], + "Parents": [ + "L" + ] + }, + { + "ID": "N", + "ExpectedScore": 11, + "ExpectedSelectedParent": "M", + "ExpectedBlues": [ + "M" + ], + "Parents": [ + "M" + ] + }, + { + "ID": "O", + "ExpectedScore": 11, + "ExpectedSelectedParent": "M", + "ExpectedBlues": [ + "M" + ], + "Parents": [ + "M" + ] + }, + { + "ID": "P", + "ExpectedScore": 11, + "ExpectedSelectedParent": "M", + "ExpectedBlues": [ + "M" + ], + "Parents": [ + "M" + ] + }, + { + "ID": "Q", + "ExpectedScore": 11, + "ExpectedSelectedParent": "M", + "ExpectedBlues": [ + "M" + ], + "Parents": [ + "M" + ] + }, + { + "ID": "R", + "ExpectedScore": 11, + "ExpectedSelectedParent": "M", + "ExpectedBlues": [ + "M" + ], + "Parents": [ + "M" + ] + }, + { + "ID": "S", + "ExpectedScore": 12, + "ExpectedSelectedParent": "R", + "ExpectedBlues": [ + "R" + ], + "Parents": [ + "R" + ] + }, + { + "ID": "T", + "ExpectedScore": 16, + "ExpectedSelectedParent": "S", + "ExpectedBlues": [ + "S", + "P", + "N", + "O" + ], + "Parents": [ + "N", + "O", + "P", + "Q", + "S" + ] + } + ] +} diff --git a/domain/blockdag/testdata/dags/dag1.json b/domain/blockdag/testdata/dags/dag1.json new file mode 100644 index 000000000..b0066ff3e --- /dev/null +++ b/domain/blockdag/testdata/dags/dag1.json @@ -0,0 +1,386 @@ +{ + "K": 4, + "GenesisID": "0", + "ExpectedReds": [ + "12", + "30", + "6", + "27", + "4", + "16", + "7", + "23", + "24", + "11", + "15", + "19", + "9" + ], + "Blocks": [ + { + "ID": "1", + "ExpectedScore": 1, + "ExpectedSelectedParent": "0", + "ExpectedBlues": [ + "0" + ], + "Parents": [ + "0" + ] + }, + { + "ID": "2", + "ExpectedScore": 1, + "ExpectedSelectedParent": "0", + "ExpectedBlues": [ + "0" + ], + "Parents": [ + "0" + ] + }, + { + "ID": "3", + "ExpectedScore": 1, + "ExpectedSelectedParent": "0", + "ExpectedBlues": [ + "0" + ], + "Parents": [ + "0" + ] + }, + { + "ID": "4", + "ExpectedScore": 2, + "ExpectedSelectedParent": "1", + "ExpectedBlues": [ + "1" + ], + "Parents": [ + "1" + ] + }, + { + "ID": "5", + "ExpectedScore": 3, + "ExpectedSelectedParent": "2", + "ExpectedBlues": [ + "2", + "3" + ], + "Parents": [ + "2", + "3" + ] + }, + { + "ID": "6", + "ExpectedScore": 2, + "ExpectedSelectedParent": "3", + "ExpectedBlues": [ + "3" + ], + "Parents": [ + "3" + ] + }, + { + "ID": "7", + "ExpectedScore": 3, + "ExpectedSelectedParent": "6", + "ExpectedBlues": [ + "6" + ], + "Parents": [ + "6" + ] + }, + { + "ID": "8", + "ExpectedScore": 3, + "ExpectedSelectedParent": "2", + "ExpectedBlues": [ + "2", + "1" + ], + "Parents": [ + "1", + "2" + ] + }, + { + "ID": "9", + "ExpectedScore": 5, + "ExpectedSelectedParent": "5", + "ExpectedBlues": [ + "5", + "6" + ], + "Parents": [ + "5", + "6" + ] + }, + { + "ID": "10", + "ExpectedScore": 5, + "ExpectedSelectedParent": "8", + "ExpectedBlues": [ + "8", + "4" + ], + "Parents": [ + "8", + "4" + ] + }, + { + "ID": "11", + "ExpectedScore": 7, + "ExpectedSelectedParent": "9", + "ExpectedBlues": [ + "9", + "7" + ], + "Parents": [ + "7", + "9" + ] + }, + { + "ID": "12", + "ExpectedScore": 8, + "ExpectedSelectedParent": "9", + "ExpectedBlues": [ + "9", + "8", + "10" + ], + "Parents": [ + "10", + "9" + ] + }, + { + "ID": "13", + "ExpectedScore": 6, + "ExpectedSelectedParent": "8", + "ExpectedBlues": [ + "8", + "3", + "5" + ], + "Parents": [ + "5", + "8" + ] + }, + { + "ID": "14", + "ExpectedScore": 8, + "ExpectedSelectedParent": "13", + "ExpectedBlues": [ + "13", + "10" + ], + "Parents": [ + "13", + "10" + ] + }, + { + "ID": "15", + "ExpectedScore": 9, + "ExpectedSelectedParent": "11", + "ExpectedBlues": [ + "11", + "13" + ], + "Parents": [ + "11", + "13" + ] + }, + { + "ID": "16", + "ExpectedScore": 8, + "ExpectedSelectedParent": "11", + "ExpectedBlues": [ + "11" + ], + "Parents": [ + "11" + ] + }, + { + "ID": "17", + "ExpectedScore": 9, + "ExpectedSelectedParent": "14", + "ExpectedBlues": [ + "14" + ], + "Parents": [ + "14" + ] + }, + { + "ID": "18", + "ExpectedScore": 7, + "ExpectedSelectedParent": "13", + "ExpectedBlues": [ + "13" + ], + "Parents": [ + "13" + ] + }, + { + "ID": "19", + "ExpectedScore": 10, + "ExpectedSelectedParent": "15", + "ExpectedBlues": [ + "15" + ], + "Parents": [ + "18", + "15" + ] + }, + { + "ID": "20", + "ExpectedScore": 10, + "ExpectedSelectedParent": "17", + "ExpectedBlues": [ + "17" + ], + "Parents": [ + "16", + "17" + ] + }, + { + "ID": "21", + "ExpectedScore": 12, + "ExpectedSelectedParent": "20", + "ExpectedBlues": [ + "20", + "18" + ], + "Parents": [ + "18", + "20" + ] + }, + { + "ID": "22", + "ExpectedScore": 13, + "ExpectedSelectedParent": "21", + "ExpectedBlues": [ + "21" + ], + "Parents": [ + "19", + "21" + ] + }, + { + "ID": "23", + "ExpectedScore": 11, + "ExpectedSelectedParent": "17", + "ExpectedBlues": [ + "17", + "12" + ], + "Parents": [ + "12", + "17" + ] + }, + { + "ID": "24", + "ExpectedScore": 13, + "ExpectedSelectedParent": "23", + "ExpectedBlues": [ + "23", + "20" + ], + "Parents": [ + "20", + "23" + ] + }, + { + "ID": "25", + "ExpectedScore": 13, + "ExpectedSelectedParent": "21", + "ExpectedBlues": [ + "21" + ], + "Parents": [ + "21" + ] + }, + { + "ID": "26", + "ExpectedScore": 15, + "ExpectedSelectedParent": "22", + "ExpectedBlues": [ + "22", + "25" + ], + "Parents": [ + "22", + "24", + "25" + ] + }, + { + "ID": "27", + "ExpectedScore": 9, + "ExpectedSelectedParent": "16", + "ExpectedBlues": [ + "16" + ], + "Parents": [ + "16" + ] + }, + { + "ID": "28", + "ExpectedScore": 14, + "ExpectedSelectedParent": "25", + "ExpectedBlues": [ + "25" + ], + "Parents": [ + "23", + "25" + ] + }, + { + "ID": "29", + "ExpectedScore": 17, + "ExpectedSelectedParent": "26", + "ExpectedBlues": [ + "26", + "28" + ], + "Parents": [ + "26", + "28" + ] + }, + { + "ID": "30", + "ExpectedScore": 10, + "ExpectedSelectedParent": "27", + "ExpectedBlues": [ + "27" + ], + "Parents": [ + "27" + ] + } + ] +}