[NOD-191] Added .acceptingBlock and .confirmations methods to blockNode (#305)

* [NOD-191] Added selectedPathChainSlice to virtualBlock.

* [NOD-191] Implemented acceptingBlock().

* [NOD-191] Implemented confirmations().

* [NOD-191] Added selectedPathChainSlice tests to TestSelectedPath.

* [NOD-191] Fixed a bug in acceptingBlock(). Written tests for confirmations().

* [NOD-191] Written tests for acceptingBlock().

* [NOD-191] Added test to make sure that acceptingBlock(tip) returns the virtual block.

* [NOD-191] Added a panic if we somehow feed a childless block that isn't the virtual to acceptingBlock.

* [NOD-191] Fixed comments.

* [NOD-191] Fixed a bug in acceptingBlock. Added red block tests for acceptingBlock.

* [NOD-191] Added red block tests for confirmations.

* [NOD-191] Fixed misleading comment and error message.
This commit is contained in:
stasatdaglabs 2019-05-23 10:57:03 +03:00 committed by Svarog
parent ec10346e79
commit da7c9c7dfb
4 changed files with 361 additions and 2 deletions

View File

@ -1182,6 +1182,84 @@ func (dag *BlockDAG) GetUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) {
return dag.virtual.utxoSet.get(outPoint)
}
// confirmations returns the current confirmations number of the given node
// The confirmations number is defined as follows:
// * If the node is red -> 0
// * Otherwise -> virtual.blueScore - acceptingBlock.blueScore + 1
func (dag *BlockDAG) confirmations(node *blockNode) (uint64, error) {
acceptingBlock, err := dag.acceptingBlock(node)
if err != nil {
return 0, err
}
// if acceptingBlock is nil, the node is red
if acceptingBlock == nil {
return 0, nil
}
return dag.virtual.blueScore - acceptingBlock.blueScore + 1, nil
}
// acceptingBlock finds the node in the selected-parent chain that had accepted
// the given node
func (dag *BlockDAG) acceptingBlock(node *blockNode) (*blockNode, error) {
// Explicitly handle the DAG tips
if dag.virtual.tips().contains(node) {
// Return the virtual block if the node is one of the DAG blues
for _, tip := range dag.virtual.blues {
if tip == node {
return &dag.virtual.blockNode, nil
}
}
// Otherwise, this tip is red and doesn't have an accepting block
return nil, nil
}
// Return an error if the node is the virtual block
if len(node.children) == 0 {
if node == &dag.virtual.blockNode {
return nil, errors.New("cannot get acceptingBlock for virtual")
}
// A childless block that isn't a tip or the virtual can never happen. Panicking
panic(fmt.Errorf("got childless block %s that is neither a tip nor the virtual", node.hash))
}
// If the node is a chain-block itself, the accepting block is its chain-child
if dag.IsInSelectedPathChain(node.hash) {
for _, child := range node.children {
if dag.IsInSelectedPathChain(child.hash) {
return child, nil
}
}
return nil, fmt.Errorf("chain block %s does not have a chain child", node.hash)
}
// Find the only chain block that may contain the node in its blues
candidateAcceptingBlock := dag.oldestChainBlockWithBlueScoreGreaterThan(node.blueScore)
// candidateAcceptingBlock is the accepting block only if it actually contains
// the node in its blues
for _, blue := range candidateAcceptingBlock.blues {
if blue == node {
return candidateAcceptingBlock, nil
}
}
// Otherwise, the node is red and doesn't have an accepting block
return nil, nil
}
// oldestChainBlockWithBlueScoreGreaterThan finds the oldest chain block with a blue score
// greater than blueScore
func (dag *BlockDAG) oldestChainBlockWithBlueScoreGreaterThan(blueScore uint64) *blockNode {
chainBlockIndex := sort.Search(len(dag.virtual.selectedPathChainSlice), func(i int) bool {
selectedPathNode := dag.virtual.selectedPathChainSlice[i]
return selectedPathNode.blueScore > blueScore
})
return dag.virtual.selectedPathChainSlice[chainBlockIndex]
}
// IsInSelectedPathChain returns whether or not a block hash is found in the selected path
func (dag *BlockDAG) IsInSelectedPathChain(blockHash *daghash.Hash) bool {
return dag.virtual.selectedPathChainSet.containsHash(blockHash)

View File

@ -1248,6 +1248,243 @@ func TestValidateFeeTransaction(t *testing.T) {
buildBlock("block5WithRedBlockFees", block5ParentHashes, block5Txs, ErrBadFeeTransaction)
}
func TestConfirmations(t *testing.T) {
// Create a new database and DAG instance to run tests against.
dag, teardownFunc, err := DAGSetup("TestBlockCount", Config{
DAGParams: &dagconfig.SimNetParams,
})
if err != nil {
t.Fatalf("Failed to setup DAG instance: %v", err)
}
defer teardownFunc()
dag.TestSetBlockRewardMaturity(1)
// Check that the genesis block of a DAG with only the genesis block in it has confirmations = 1.
genesisConfirmations, err := dag.confirmations(dag.genesis)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for genesis block unexpectedly failed: %s", err)
}
if genesisConfirmations != 1 {
t.Fatalf("TestConfirmations: unexpected confirmations for genesis block. Want: 1, got: %d", genesisConfirmations)
}
processBlocks := func(blocks []*util.Block) {
for _, block := range blocks {
isOrphan, err := dag.ProcessBlock(block, BFNone)
if err != nil {
t.Fatalf("ProcessBlock fail on block %s: %v\n", block.Hash(), err)
}
if isOrphan {
t.Fatalf("ProcessBlock incorrectly returned block %s is an orphan\n", block.Hash())
}
}
}
// Add a chain of blocks
loadedBlocks, err := loadBlocks("blk_0_to_4.dat")
if err != nil {
t.Fatalf("Error loading file: %v\n", err)
}
chainBlocks := loadedBlocks[1:]
processBlocks(chainBlocks)
// Make sure that each one of the chain blocks has the expected confirmations number
for i, block := range chainBlocks {
node := dag.index.LookupNode(block.Hash())
confirmations, err := dag.confirmations(node)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for node 1 unexpectedly failed: %s", err)
}
expectedConfirmations := uint64(len(chainBlocks) - i)
if confirmations != expectedConfirmations {
t.Fatalf("TestConfirmations: unexpected confirmations for node 1. "+
"Want: %d, got: %d", expectedConfirmations, confirmations)
}
}
// Add a branching block
loadedBlocks, err = loadBlocks("blk_3B.dat")
if err != nil {
t.Fatalf("Error loading file: %v\n", err)
}
processBlocks(loadedBlocks)
// Check that the genesis has a confirmations number == blockCount
genesisConfirmations, err = dag.confirmations(dag.genesis)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for genesis block unexpectedly failed: %s", err)
}
expectedGenesisConfirmations := dag.blockCount
if genesisConfirmations != expectedGenesisConfirmations {
t.Fatalf("TestConfirmations: unexpected confirmations for genesis block. "+
"Want: %d, got: %d", expectedGenesisConfirmations, genesisConfirmations)
}
// Check that each of the tips had a confirmation number of 1.
tips := dag.virtual.tips()
for _, tip := range tips {
tipConfirmations, err := dag.confirmations(tip)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for tip unexpectedly failed: %s", err)
}
if tipConfirmations != 1 {
t.Fatalf("TestConfirmations: unexpected confirmations for tip. "+
"Want: 1, got: %d", tipConfirmations)
}
}
// Generate K blocks to force the "main" chain to become red
nodeGenerator := buildNodeGenerator(dag.dagParams.K, false)
branchingChainTip := dag.index.LookupNode(loadedBlocks[0].Hash())
for i := uint32(0); i < dag.dagParams.K; i++ {
nextBranchingChainTip := nodeGenerator(setFromSlice(branchingChainTip))
dag.virtual.AddTip(nextBranchingChainTip)
branchingChainTip = nextBranchingChainTip
}
// Make sure that a red block has confirmation number = 0
redChainBlock := dag.index.LookupNode(chainBlocks[3].Hash())
redChainBlockConfirmations, err := dag.confirmations(redChainBlock)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for red chain block unexpectedly failed: %s", err)
}
if redChainBlockConfirmations != 0 {
t.Fatalf("TestConfirmations: unexpected confirmations for red chain block. "+
"Want: 0, got: %d", redChainBlockConfirmations)
}
// Make sure that the red tip has confirmation number = 0
redChainTip := dag.index.LookupNode(chainBlocks[len(chainBlocks)-1].Hash())
redChainTipConfirmations, err := dag.confirmations(redChainTip)
if err != nil {
t.Fatalf("TestConfirmations: confirmations for red chain tip unexpectedly failed: %s", err)
}
if redChainTipConfirmations != 0 {
t.Fatalf("TestConfirmations: unexpected confirmations for red tip block. "+
"Want: 0, got: %d", redChainTipConfirmations)
}
}
func TestAcceptingBlock(t *testing.T) {
// Create a new database and DAG instance to run tests against.
dag, teardownFunc, err := DAGSetup("TestAcceptingBlock", Config{
DAGParams: &dagconfig.SimNetParams,
})
if err != nil {
t.Fatalf("Failed to setup DAG instance: %v", err)
}
defer teardownFunc()
dag.TestSetBlockRewardMaturity(1)
// Check that the genesis block of a DAG with only the genesis block in it is accepted by the virtual.
genesisAcceptingBlock, err := dag.acceptingBlock(dag.genesis)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for genesis block unexpectedly failed: %s", err)
}
if genesisAcceptingBlock != &dag.virtual.blockNode {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for genesis block. "+
"Want: virtual, got: %s", genesisAcceptingBlock.hash)
}
processBlocks := func(blocks []*util.Block) {
for _, block := range blocks {
isOrphan, err := dag.ProcessBlock(block, BFNone)
if err != nil {
t.Fatalf("ProcessBlock fail on block %s: %v\n", block.Hash(), err)
}
if isOrphan {
t.Fatalf("ProcessBlock incorrectly returned block %s is an orphan\n", block.Hash())
}
}
}
// Add a chain of blocks
chainBlocks, err := loadBlocks("blk_0_to_4.dat")
if err != nil {
t.Fatalf("Error loading file: %v\n", err)
}
processBlocks(chainBlocks[1:])
// Make sure that each chain block (including the genesis) is accepted by its child
for i, chainBlock := range chainBlocks[:1] {
expectedAcceptingBlock := chainBlocks[i+1]
expectedAcceptingBlockNode := dag.index.LookupNode(expectedAcceptingBlock.Hash())
chainBlockNode := dag.index.LookupNode(chainBlock.Hash())
chainAcceptingBlockNode, err := dag.acceptingBlock(chainBlockNode)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for chain block unexpectedly failed: %s", err)
}
if expectedAcceptingBlockNode != chainAcceptingBlockNode {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for chain block. "+
"Want: %s, got: %s", expectedAcceptingBlockNode.hash, chainAcceptingBlockNode.hash)
}
}
// Add a branching block
branchingBlock, err := loadBlocks("blk_3B.dat")
if err != nil {
t.Fatalf("Error loading file: %v\n", err)
}
processBlocks(branchingBlock)
// Make sure that the accepting block of the parent of the branching block didn't change
expectedAcceptingBlock := dag.index.LookupNode(chainBlocks[3].Hash())
intersectionBlock := dag.index.LookupNode(chainBlocks[2].Hash())
intersectionAcceptingBlock, err := dag.acceptingBlock(intersectionBlock)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for intersection block unexpectedly failed: %s", err)
}
if expectedAcceptingBlock != intersectionAcceptingBlock {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for intersection block. "+
"Want: %s, got: %s", expectedAcceptingBlock.hash, intersectionAcceptingBlock.hash)
}
// Make sure that the accepting block of all the tips in the virtual block
for _, tip := range dag.virtual.tips() {
tipAcceptingBlock, err := dag.acceptingBlock(tip)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for tip unexpectedly failed: %s", err)
}
if tipAcceptingBlock != &dag.virtual.blockNode {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for tip. "+
"Want: Virtual, got: %s", tipAcceptingBlock.hash)
}
}
// Generate K blocks to force the "main" chain to become red
nodeGenerator := buildNodeGenerator(dag.dagParams.K, false)
branchingChainTip := dag.index.LookupNode(branchingBlock[0].Hash())
for i := uint32(0); i < dag.dagParams.K; i++ {
nextBranchingChainTip := nodeGenerator(setFromSlice(branchingChainTip))
dag.virtual.AddTip(nextBranchingChainTip)
branchingChainTip = nextBranchingChainTip
}
// Make sure that a red block returns nil
redChainBlock := dag.index.LookupNode(chainBlocks[3].Hash())
redChainBlockAcceptionBlock, err := dag.acceptingBlock(redChainBlock)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for red chain block unexpectedly failed: %s", err)
}
if redChainBlockAcceptionBlock != nil {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for red chain block. "+
"Want: nil, got: %s", redChainBlockAcceptionBlock.hash)
}
// Make sure that a red tip returns nil
redChainTip := dag.index.LookupNode(chainBlocks[len(chainBlocks)-1].Hash())
redChainTipAcceptingBlock, err := dag.acceptingBlock(redChainTip)
if err != nil {
t.Fatalf("TestAcceptingBlock: acceptingBlock for red chain tip unexpectedly failed: %s", err)
}
if redChainTipAcceptingBlock != nil {
t.Fatalf("TestAcceptingBlock: unexpected acceptingBlock for red tip block. "+
"Want: nil, got: %s", redChainTipAcceptingBlock.hash)
}
}
// payToPubKeyHashScript creates a new script to pay a transaction
// output to a 20-byte pubkey hash. It is expected that the input is a valid
// hash.

View File

@ -14,8 +14,15 @@ type virtualBlock struct {
phantomK uint32
utxoSet *FullUTXOSet
blockNode
// selectedPathChainSet is a block set that includes all the blocks that belong to the chain of selected parents from the virtual block.
// selectedPathChainSet is a block set that includes all the blocks
// that belong to the chain of selected parents from the virtual block.
selectedPathChainSet blockSet
// selectedPathChainSlice is an ordered slice that includes all the
// blocks that belong the the chain of selected parents from the
// virtual block.
selectedPathChainSlice []*blockNode
}
// newVirtualBlock creates and returns a new VirtualBlock.
@ -25,6 +32,7 @@ func newVirtualBlock(tips blockSet, phantomK uint32) *virtualBlock {
virtual.phantomK = phantomK
virtual.utxoSet = NewFullUTXOSet()
virtual.selectedPathChainSet = newSet()
virtual.selectedPathChainSlice = nil
virtual.setTips(tips)
return &virtual
@ -61,11 +69,12 @@ func (v *virtualBlock) setTips(tips blockSet) {
// and aren't selected ancestors of the old one.
func (v *virtualBlock) updateSelectedPathSet(oldSelectedParent *blockNode) {
var intersectionNode *blockNode
nodesToAdd := make([]*blockNode, 0)
for node := v.blockNode.selectedParent; intersectionNode == nil && node != nil; node = node.selectedParent {
if v.selectedPathChainSet.contains(node) {
intersectionNode = node
} else {
v.selectedPathChainSet.add(node)
nodesToAdd = append(nodesToAdd, node)
}
}
@ -73,11 +82,26 @@ func (v *virtualBlock) updateSelectedPathSet(oldSelectedParent *blockNode) {
panic("updateSelectedPathSet: Cannot find intersection node. The block index may be corrupted.")
}
// Remove the nodes in the set from the oldSelectedParent down to the intersectionNode
removeCount := 0
if intersectionNode != nil {
for node := oldSelectedParent; !node.hash.IsEqual(intersectionNode.hash); node = node.selectedParent {
v.selectedPathChainSet.remove(node)
removeCount++
}
}
// Remove the last removeCount nodes from the slice
v.selectedPathChainSlice = v.selectedPathChainSlice[:len(v.selectedPathChainSlice)-removeCount]
// Reverse nodesToAdd, since we collected them in reverse order
for left, right := 0, len(nodesToAdd)-1; left < right; left, right = left+1, right-1 {
nodesToAdd[left], nodesToAdd[right] = nodesToAdd[right], nodesToAdd[left]
}
// Add the nodes to the set and to the slice
for _, node := range nodesToAdd {
v.selectedPathChainSet.add(node)
}
v.selectedPathChainSlice = append(v.selectedPathChainSlice, nodesToAdd...)
}
// SetTips replaces the tips of the virtual block with the blocks in the

View File

@ -126,6 +126,12 @@ func TestSelectedPath(t *testing.T) {
if !reflect.DeepEqual(virtual.selectedPathChainSet, firstPath) {
t.Fatalf("TestSelectedPath: selectedPathSet doesn't include the expected values. got %v, want %v", virtual.selectedParent, firstPath)
}
// We expect that selectedPathChainSlice should have all the blocks we've added so far
wantLen := 11
gotLen := len(virtual.selectedPathChainSlice)
if wantLen != gotLen {
t.Fatalf("TestSelectedPath: selectedPathChainSlice doesn't have the expected length. got %d, want %d", gotLen, wantLen)
}
secondPath := initialPath.clone()
tip = initialTip
@ -138,6 +144,13 @@ func TestSelectedPath(t *testing.T) {
if !reflect.DeepEqual(virtual.selectedPathChainSet, secondPath) {
t.Fatalf("TestSelectedPath: selectedPathSet didn't handle the re-org as expected. got %v, want %v", virtual.selectedParent, firstPath)
}
// We expect that selectedPathChainSlice should have all the blocks we've added so far except the old chain
wantLen = 106
gotLen = len(virtual.selectedPathChainSlice)
if wantLen != gotLen {
t.Fatalf("TestSelectedPath: selectedPathChainSlice doesn't have"+
"the expected length, possibly because it didn't handle the re-org as expected. got %d, want %d", gotLen, wantLen)
}
tip = initialTip
for i := 0; i < 3; i++ {
@ -148,6 +161,13 @@ func TestSelectedPath(t *testing.T) {
if !reflect.DeepEqual(virtual.selectedPathChainSet, secondPath) {
t.Fatalf("TestSelectedPath: selectedPathSet did an unexpected re-org. got %v, want %v", virtual.selectedParent, firstPath)
}
// We expect that selectedPathChainSlice not to change
wantLen = 106
gotLen = len(virtual.selectedPathChainSlice)
if wantLen != gotLen {
t.Fatalf("TestSelectedPath: selectedPathChainSlice doesn't"+
"have the expected length, possibly due to unexpected did an unexpected re-org. got %d, want %d", gotLen, wantLen)
}
// We call updateSelectedPathSet manually without updating the tips, to check if it panics
virtual2 := newVirtualBlock(nil, phantomK)