package blockdag import ( "fmt" "reflect" "sort" "testing" "time" "github.com/daglabs/btcd/dagconfig/daghash" "github.com/daglabs/btcd/dagconfig" ) 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 hashIDPair struct { hash *daghash.Hash id string } //TestPhantom iterate over several dag simulations, and checks //that the blue score, blue set and selected parent of each //block calculated as expected func TestPhantom(t *testing.T) { netParams := dagconfig.SimNetParams blockVersion := int32(0x10000000) tests := []struct { k uint32 dagData []*testBlockData virtualBlockID string expectedReds []string }{ { //Block hash order:AKJIHGFEDCB k: 1, virtualBlockID: "K", expectedReds: []string{"D"}, dagData: []*testBlockData{ { parents: []string{"A"}, id: "B", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"A"}, id: "C", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"B"}, id: "D", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"B"}, id: "E", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"C"}, id: "F", expectedScore: 2, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"C", "D"}, id: "G", expectedScore: 4, expectedSelectedParent: "C", expectedBlues: []string{"D", "B", "C"}, }, { parents: []string{"C", "E"}, id: "H", expectedScore: 4, expectedSelectedParent: "C", expectedBlues: []string{"E", "B", "C"}, }, { parents: []string{"E", "G"}, id: "I", expectedScore: 5, expectedSelectedParent: "E", expectedBlues: []string{"G", "D", "E"}, }, { parents: []string{"F"}, id: "J", expectedScore: 3, expectedSelectedParent: "F", expectedBlues: []string{"F"}, }, { parents: []string{"H", "I", "J"}, id: "K", expectedScore: 9, expectedSelectedParent: "H", expectedBlues: []string{"I", "J", "G", "F", "H"}, }, }, }, { //block hash order:AVUTSRQPONMLKJIHGFEDCB k: 2, virtualBlockID: "V", expectedReds: []string{"D", "J", "P"}, dagData: []*testBlockData{ { parents: []string{"A"}, id: "B", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"A"}, id: "C", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"B"}, id: "D", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"B"}, id: "E", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"C"}, id: "F", expectedScore: 2, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"C"}, id: "G", expectedScore: 2, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"G"}, id: "H", expectedScore: 3, expectedSelectedParent: "G", expectedBlues: []string{"G"}, }, { parents: []string{"E"}, id: "I", expectedScore: 3, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"E"}, id: "J", expectedScore: 3, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"I"}, id: "K", expectedScore: 4, expectedSelectedParent: "I", expectedBlues: []string{"I"}, }, { parents: []string{"K", "H"}, id: "L", expectedScore: 5, expectedSelectedParent: "K", expectedBlues: []string{"K"}, }, { parents: []string{"F", "L"}, id: "M", expectedScore: 10, expectedSelectedParent: "F", expectedBlues: []string{"L", "K", "I", "H", "G", "E", "B", "F"}, }, { parents: []string{"G", "K"}, id: "N", expectedScore: 7, expectedSelectedParent: "G", expectedBlues: []string{"K", "I", "E", "B", "G"}, }, { parents: []string{"J", "N"}, id: "O", expectedScore: 8, expectedSelectedParent: "N", expectedBlues: []string{"N"}, }, { parents: []string{"D"}, id: "P", expectedScore: 3, expectedSelectedParent: "D", expectedBlues: []string{"D"}, }, { parents: []string{"O", "P"}, id: "Q", expectedScore: 10, expectedSelectedParent: "P", expectedBlues: []string{"O", "N", "K", "J", "I", "E", "P"}, }, { parents: []string{"L", "Q"}, id: "R", expectedScore: 11, expectedSelectedParent: "Q", expectedBlues: []string{"Q"}, }, { parents: []string{"M", "R"}, id: "S", expectedScore: 15, expectedSelectedParent: "M", expectedBlues: []string{"R", "Q", "O", "N", "M"}, }, { parents: []string{"H", "F"}, id: "T", expectedScore: 5, expectedSelectedParent: "F", expectedBlues: []string{"H", "G", "F"}, }, { parents: []string{"M", "T"}, id: "U", expectedScore: 12, expectedSelectedParent: "M", expectedBlues: []string{"T", "M"}, }, { parents: []string{"S", "U"}, id: "V", expectedScore: 18, expectedSelectedParent: "S", expectedBlues: []string{"U", "T", "S"}, }, }, }, { //Block hash order:AXWVUTSRQPONMLKJIHGFEDCB k: 1, virtualBlockID: "X", expectedReds: []string{"D", "F", "G", "H", "J", "K", "L", "N", "O", "Q", "R", "S", "U", "V"}, dagData: []*testBlockData{ { parents: []string{"A"}, id: "B", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"A"}, id: "C", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"A"}, id: "D", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"A"}, id: "E", expectedScore: 1, expectedSelectedParent: "A", expectedBlues: []string{"A"}, }, { parents: []string{"B"}, id: "F", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"B"}, id: "G", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"C"}, id: "H", expectedScore: 2, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"C"}, id: "I", expectedScore: 2, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"B"}, id: "J", expectedScore: 2, expectedSelectedParent: "B", expectedBlues: []string{"B"}, }, { parents: []string{"D"}, id: "K", expectedScore: 2, expectedSelectedParent: "D", expectedBlues: []string{"D"}, }, { parents: []string{"D"}, id: "L", expectedScore: 2, expectedSelectedParent: "D", expectedBlues: []string{"D"}, }, { parents: []string{"E"}, id: "M", expectedScore: 2, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"E"}, id: "N", expectedScore: 2, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"F", "G", "J"}, id: "O", expectedScore: 5, expectedSelectedParent: "F", expectedBlues: []string{"J", "G", "F"}, }, { parents: []string{"B", "M", "I"}, id: "P", expectedScore: 6, expectedSelectedParent: "B", expectedBlues: []string{"M", "I", "E", "C", "B"}, }, { parents: []string{"K", "E"}, id: "Q", expectedScore: 4, expectedSelectedParent: "E", expectedBlues: []string{"K", "D", "E"}, }, { parents: []string{"L", "N"}, id: "R", expectedScore: 3, expectedSelectedParent: "L", expectedBlues: []string{"L"}, }, { parents: []string{"I", "Q"}, id: "S", expectedScore: 5, expectedSelectedParent: "Q", expectedBlues: []string{"Q"}, }, { parents: []string{"K", "P"}, id: "T", expectedScore: 7, expectedSelectedParent: "P", expectedBlues: []string{"P"}, }, { parents: []string{"K", "L"}, id: "U", expectedScore: 4, expectedSelectedParent: "K", expectedBlues: []string{"L", "K"}, }, { parents: []string{"U", "R"}, id: "V", expectedScore: 5, expectedSelectedParent: "R", expectedBlues: []string{"U", "R"}, }, { parents: []string{"S", "U", "T"}, id: "W", expectedScore: 8, expectedSelectedParent: "T", expectedBlues: []string{"T"}, }, { parents: []string{"V", "W", "H"}, id: "X", expectedScore: 9, expectedSelectedParent: "W", expectedBlues: []string{"W"}, }, }, }, { //Secret mining attack: The attacker is mining //blocks B,C,D,E,F,G,T in secret without propagating //them, so all blocks except T should be red, because //they don't follow the rules of PHANTOM that require //you to point to all the parents that you know, and //propagate your block as soon as it's mined //Block hash order:AYXWVUTSRQPONMLKJIHGFEDCB k: 1, virtualBlockID: "Y", expectedReds: []string{"B", "C", "D", "E", "F", "G", "L"}, 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{"C"}, id: "D", expectedScore: 3, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"D"}, id: "E", expectedScore: 4, expectedSelectedParent: "D", expectedBlues: []string{"D"}, }, { parents: []string{"E"}, id: "F", expectedScore: 5, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"F"}, id: "G", expectedScore: 6, 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{"H", "I"}, id: "J", expectedScore: 3, expectedSelectedParent: "H", expectedBlues: []string{"I", "H"}, }, { parents: []string{"H", "I"}, id: "K", expectedScore: 3, expectedSelectedParent: "H", expectedBlues: []string{"I", "H"}, }, { parents: []string{"I"}, id: "L", expectedScore: 2, expectedSelectedParent: "I", expectedBlues: []string{"I"}, }, { parents: []string{"J", "K", "L"}, id: "M", expectedScore: 5, expectedSelectedParent: "J", expectedBlues: []string{"K", "J"}, }, { parents: []string{"J", "K", "L"}, id: "N", expectedScore: 5, expectedSelectedParent: "J", expectedBlues: []string{"K", "J"}, }, { parents: []string{"N", "M"}, id: "O", expectedScore: 7, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"N", "M"}, id: "P", expectedScore: 7, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"N", "M"}, id: "Q", expectedScore: 7, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"O", "P", "Q"}, id: "R", expectedScore: 10, expectedSelectedParent: "O", expectedBlues: []string{"Q", "P", "O"}, }, { parents: []string{"O", "P", "Q"}, id: "S", expectedScore: 10, expectedSelectedParent: "O", expectedBlues: []string{"Q", "P", "O"}, }, { parents: []string{"G", "S", "R"}, id: "T", expectedScore: 12, expectedSelectedParent: "R", expectedBlues: []string{"S", "R"}, }, { parents: []string{"S", "R"}, id: "U", expectedScore: 12, expectedSelectedParent: "R", expectedBlues: []string{"S", "R"}, }, { parents: []string{"T", "U"}, id: "V", expectedScore: 14, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"T", "U"}, id: "W", expectedScore: 14, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"U", "T"}, id: "X", expectedScore: 14, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"V", "W", "X"}, id: "Y", expectedScore: 17, expectedSelectedParent: "V", expectedBlues: []string{"X", "W", "V"}, }, }, }, { //Censorship mining attack: The attacker is mining blocks B,C,D,E,F,G in secret without propagating them, //so all blocks except B and C should be red, because they don't follow the rules of //PHANTOM that require you to point to all the parents that you know //Block hash order:AYXWVUTSRQPONMLKJIHGFEDCB k: 1, virtualBlockID: "Y", expectedReds: []string{"D", "E", "F", "G", "L"}, 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{"C"}, id: "D", expectedScore: 3, expectedSelectedParent: "C", expectedBlues: []string{"C"}, }, { parents: []string{"D"}, id: "E", expectedScore: 4, expectedSelectedParent: "D", expectedBlues: []string{"D"}, }, { parents: []string{"E"}, id: "F", expectedScore: 5, expectedSelectedParent: "E", expectedBlues: []string{"E"}, }, { parents: []string{"F"}, id: "G", expectedScore: 6, 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{"H", "I", "B"}, id: "J", expectedScore: 4, expectedSelectedParent: "B", expectedBlues: []string{"I", "H", "B"}, }, { parents: []string{"H", "I", "B"}, id: "K", expectedScore: 4, expectedSelectedParent: "B", expectedBlues: []string{"I", "H", "B"}, }, { parents: []string{"I"}, id: "L", expectedScore: 2, expectedSelectedParent: "I", expectedBlues: []string{"I"}, }, { parents: []string{"J", "K", "L", "C"}, id: "M", expectedScore: 7, expectedSelectedParent: "J", expectedBlues: []string{"K", "C", "J"}, }, { parents: []string{"J", "K", "L", "C"}, id: "N", expectedScore: 7, expectedSelectedParent: "J", expectedBlues: []string{"K", "C", "J"}, }, { parents: []string{"N", "M", "D"}, id: "O", expectedScore: 9, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"N", "M", "D"}, id: "P", expectedScore: 9, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"N", "M", "D"}, id: "Q", expectedScore: 9, expectedSelectedParent: "M", expectedBlues: []string{"N", "M"}, }, { parents: []string{"O", "P", "Q", "E"}, id: "R", expectedScore: 12, expectedSelectedParent: "O", expectedBlues: []string{"Q", "P", "O"}, }, { parents: []string{"O", "P", "Q", "E"}, id: "S", expectedScore: 12, expectedSelectedParent: "O", expectedBlues: []string{"Q", "P", "O"}, }, { parents: []string{"G", "S", "R"}, id: "T", expectedScore: 14, expectedSelectedParent: "R", expectedBlues: []string{"S", "R"}, }, { parents: []string{"S", "R", "F"}, id: "U", expectedScore: 14, expectedSelectedParent: "R", expectedBlues: []string{"S", "R"}, }, { parents: []string{"T", "U"}, id: "V", expectedScore: 16, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"T", "U"}, id: "W", expectedScore: 16, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"T", "U"}, id: "X", expectedScore: 16, expectedSelectedParent: "T", expectedBlues: []string{"U", "T"}, }, { parents: []string{"V", "W", "X"}, id: "Y", expectedScore: 19, expectedSelectedParent: "V", expectedBlues: []string{"X", "W", "V"}, }, }, }, } for i, test := range tests { netParams.K = test.k // Generate enough synthetic blocks for the rest of the test blockDAG := newTestDAG(&netParams) genesisNode := blockDAG.genesis blockTime := genesisNode.Header().Timestamp blockByIDMap := make(map[string]*blockNode) idByBlockMap := make(map[*blockNode]string) blockByIDMap["A"] = genesisNode idByBlockMap[genesisNode] = "A" for _, blockData := range test.dagData { blockTime = blockTime.Add(time.Second) parents := blockSet{} for _, parentID := range blockData.parents { parent := blockByIDMap[parentID] parents.add(parent) } node := newTestNode(parents, blockVersion, 0, blockTime, test.k) node.hash = daghash.Hash{} //It helps to predict hash order for i, char := range blockData.id { node.hash[i] = byte(char) } blockDAG.index.AddNode(node) addNodeAsChildToParents(node) blockByIDMap[blockData.id] = node idByBlockMap[node] = blockData.id bluesIDs := make([]string, 0, len(node.blues)) for _, blue := range node.blues { bluesIDs = append(bluesIDs, idByBlockMap[blue]) } 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.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 !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) } } reds := make(map[string]bool) for id := range blockByIDMap { reds[id] = true } for tip := blockByIDMap[test.virtualBlockID]; tip.selectedParent != nil; tip = tip.selectedParent { tipID := idByBlockMap[tip] delete(reds, tipID) for _, blue := range tip.blues { blueID := idByBlockMap[blue] delete(reds, blueID) } } 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) } } } func checkReds(expectedReds []string, reds map[string]bool) bool { if len(expectedReds) != len(reds) { return false } for _, redID := range expectedReds { if !reds[redID] { return false } } return true }