[DEV-303] Implement block finality (#139)

* [DEV-303] Implement block finality

* [DEV-303] Add finality tests

* [DEV-303] Make finality tests use maybeAcceptBlock

* [DEV-303] Only check finality rules if we are fastAdd mode

* [DEV-303] replace hasBiggerFinalityScoreThan checks with .finalityScore()
This commit is contained in:
Ori Newman 2019-01-03 13:22:05 +02:00 committed by Mike Zak
parent f0f79b1fcc
commit c0aafdf7e1
13 changed files with 303 additions and 86 deletions

View File

@ -244,6 +244,10 @@ func (node *blockNode) isGenesis() bool {
return len(node.parents) == 0 return len(node.parents) == 0
} }
func (node *blockNode) finalityScore() uint64 {
return node.blueScore / finalityInterval
}
// String returns a string that contains the block hash and height. // String returns a string that contains the block hash and height.
func (node blockNode) String() string { func (node blockNode) String() string {
return fmt.Sprintf("%s (%d)", node.hash, node.height) return fmt.Sprintf("%s (%d)", node.hash, node.height)

View File

@ -6,24 +6,19 @@ import (
func TestChainHeight(t *testing.T) { func TestChainHeight(t *testing.T) {
phantomK := uint32(2) phantomK := uint32(2)
buildNode := buildNodeGenerator(phantomK) buildNode := buildNodeGenerator(phantomK, true)
buildWithChildren := func(parents blockSet) *blockNode {
node := buildNode(parents)
addNodeAsChildToParents(node)
return node
}
node0 := buildWithChildren(setFromSlice()) node0 := buildNode(setFromSlice())
node1 := buildWithChildren(setFromSlice(node0)) node1 := buildNode(setFromSlice(node0))
node2 := buildWithChildren(setFromSlice(node0)) node2 := buildNode(setFromSlice(node0))
node3 := buildWithChildren(setFromSlice(node0)) node3 := buildNode(setFromSlice(node0))
node4 := buildWithChildren(setFromSlice(node1, node2, node3)) node4 := buildNode(setFromSlice(node1, node2, node3))
node5 := buildWithChildren(setFromSlice(node1, node2, node3)) node5 := buildNode(setFromSlice(node1, node2, node3))
node6 := buildWithChildren(setFromSlice(node1, node2, node3)) node6 := buildNode(setFromSlice(node1, node2, node3))
node7 := buildWithChildren(setFromSlice(node0)) node7 := buildNode(setFromSlice(node0))
node8 := buildWithChildren(setFromSlice(node7)) node8 := buildNode(setFromSlice(node7))
node9 := buildWithChildren(setFromSlice(node8)) node9 := buildNode(setFromSlice(node8))
node10 := buildWithChildren(setFromSlice(node9, node6)) node10 := buildNode(setFromSlice(node9, node6))
// Because nodes 7 & 8 were mined secretly, node10's selected // Because nodes 7 & 8 were mined secretly, node10's selected
// parent will be node6, although node9 is higher. So in this // parent will be node6, although node9 is higher. So in this

View File

@ -200,15 +200,23 @@ func addNodeAsChildToParents(node *blockNode) {
} }
} }
func buildNodeGenerator(phantomK uint32) func(parents blockSet) *blockNode { func buildNodeGenerator(phantomK uint32, withChildren bool) func(parents blockSet) *blockNode {
// For the purposes of these tests, we'll create blockNodes whose hashes are a // For the purposes of these tests, we'll create blockNodes whose hashes are a
// series of numbers from 0 to n. // series of numbers from 1 to 255.
hashCounter := byte(0) hashCounter := byte(1)
return func(parents blockSet) *blockNode { buildNode := func(parents blockSet) *blockNode {
block := newBlockNode(nil, parents, phantomK) block := newBlockNode(nil, parents, phantomK)
block.hash = daghash.Hash{hashCounter} block.hash = daghash.Hash{hashCounter}
hashCounter++ hashCounter++
return block return block
} }
if withChildren {
return func(parents blockSet) *blockNode {
node := buildNode(parents)
addNodeAsChildToParents(node)
return node
}
}
return buildNode
} }

View File

@ -23,6 +23,8 @@ const (
// maxOrphanBlocks is the maximum number of orphan blocks that can be // maxOrphanBlocks is the maximum number of orphan blocks that can be
// queued. // queued.
maxOrphanBlocks = 100 maxOrphanBlocks = 100
finalityInterval = 100
) )
// BlockLocator is used to help locate a specific block. The algorithm for // BlockLocator is used to help locate a specific block. The algorithm for
@ -138,6 +140,8 @@ type BlockDAG struct {
// certain blockchain events. // certain blockchain events.
notificationsLock sync.RWMutex notificationsLock sync.RWMutex
notifications []NotificationCallback notifications []NotificationCallback
lastFinalityPoint *blockNode
} }
// HaveBlock returns whether or not the DAG instance has the block represented // HaveBlock returns whether or not the DAG instance has the block represented
@ -503,12 +507,26 @@ func (dag *BlockDAG) connectBlock(node *blockNode, block *util.Block, fastAdd bo
} }
} }
var finalityPointCandidate *blockNode
if !fastAdd {
var err error
finalityPointCandidate, err = dag.checkFinalityRulesAndGetFinalityPointCandidate(node)
if err != nil {
return err
}
}
// Add the node to the virtual and update the UTXO set of the DAG. // Add the node to the virtual and update the UTXO set of the DAG.
utxoDiff, acceptedTxsData, err := dag.applyUTXOChanges(node, block, fastAdd) utxoDiff, acceptedTxsData, err := dag.applyUTXOChanges(node, block, fastAdd)
if err != nil { if err != nil {
return err return err
} }
if finalityPointCandidate != nil {
dag.lastFinalityPoint = finalityPointCandidate
}
// Write any block status changes to DB before updating the DAG state. // Write any block status changes to DB before updating the DAG state.
err = dag.index.flushToDB() err = dag.index.flushToDB()
if err != nil { if err != nil {
@ -518,7 +536,11 @@ func (dag *BlockDAG) connectBlock(node *blockNode, block *util.Block, fastAdd bo
// Atomically insert info into the database. // Atomically insert info into the database.
err = dag.db.Update(func(dbTx database.Tx) error { err = dag.db.Update(func(dbTx database.Tx) error {
// Update best block state. // Update best block state.
err := dbPutDAGTipHashes(dbTx, dag.TipHashes()) state := &dagState{
TipHashes: dag.TipHashes(),
LastFinalityPoint: dag.lastFinalityPoint.hash,
}
err := dbPutDAGState(dbTx, state)
if err != nil { if err != nil {
return err return err
} }
@ -563,6 +585,27 @@ func (dag *BlockDAG) connectBlock(node *blockNode, block *util.Block, fastAdd bo
return nil return nil
} }
func (dag *BlockDAG) checkFinalityRulesAndGetFinalityPointCandidate(node *blockNode) (*blockNode, error) {
var finalityPointCandidate *blockNode
finalityErr := ruleError(ErrFinality, "The last finality point is not in the selected chain of this block")
if node.blueScore <= dag.lastFinalityPoint.blueScore {
return nil, finalityErr
}
shouldFindFinalityPointCandidate := node.finalityScore() > dag.lastFinalityPoint.finalityScore()
for currentNode := node.selectedParent; currentNode != dag.lastFinalityPoint; currentNode = currentNode.selectedParent {
if currentNode.blueScore <= dag.lastFinalityPoint.blueScore {
return nil, finalityErr
}
if shouldFindFinalityPointCandidate && currentNode.finalityScore() > currentNode.selectedParent.finalityScore() {
finalityPointCandidate = currentNode
}
}
return finalityPointCandidate, nil
}
// applyUTXOChanges does the following: // applyUTXOChanges does the following:
// 1. Verifies that each transaction within the new block could spend an existing UTXO. // 1. Verifies that each transaction within the new block could spend an existing UTXO.
// 2. Connects each of the new block's parents to the block. // 2. Connects each of the new block's parents to the block.
@ -1528,7 +1571,7 @@ func New(config *Config) (*BlockDAG, error) {
} }
// Initialize the chain state from the passed database. When the db // Initialize the chain state from the passed database. When the db
// does not yet contain any chain state, both it and the chain state // does not yet contain any DAG state, both it and the DAG state
// will be initialized to contain only the genesis block. // will be initialized to contain only the genesis block.
if err := dag.initDAGState(); err != nil { if err := dag.initDAGState(); err != nil {
return nil, err return nil, err

View File

@ -40,16 +40,16 @@ func TestBlockCount(t *testing.T) {
blocks = append(blocks, blockTmp...) blocks = append(blocks, blockTmp...)
} }
// Create a new database and chain instance to run tests against. // Create a new database and DAG instance to run tests against.
dag, teardownFunc, err := DAGSetup("haveblock", Config{ dag, teardownFunc, err := DAGSetup("haveblock", Config{
DAGParams: &dagconfig.SimNetParams, DAGParams: &dagconfig.SimNetParams,
}) })
if err != nil { if err != nil {
t.Fatalf("Failed to setup chain instance: %v", err) t.Fatalf("Failed to setup DAG instance: %v", err)
} }
defer teardownFunc() defer teardownFunc()
// Since we're not dealing with the real block chain, set the coinbase // Since we're not dealing with the real block DAG, set the coinbase
// maturity to 1. // maturity to 1.
dag.TstSetCoinbaseMaturity(1) dag.TstSetCoinbaseMaturity(1)
@ -849,3 +849,132 @@ func testErrorThroughPatching(t *testing.T, expectedErrorMessage string, targetF
"Want: %s, got: %s", expectedErrorMessage, err) "Want: %s, got: %s", expectedErrorMessage, err)
} }
} }
// TestFinality checks that the finality mechanism works as expected.
// This is how the flow goes:
// 1) We build a chain of finalityInterval blocks and call its tip altChainTip.
// 2) We build another chain (let's call it mainChain) of 2 * finalityInterval
// blocks, which points to genesis, and then we check that the block in that
// chain with height of finalityInterval is marked as finality point (This is
// very predictable, because the blue score of each new block in a chain is the
// parents plus one).
// 3) We make a new child to block with height (2 * finalityInterval - 1)
// in mainChain, and we check that connecting it to the DAG
// doesn't affect the last finality point.
// 4) We make a block that points to genesis, and check that it
// gets rejected because its blue score is lower then the last finality
// point.
// 5) We make a block that points to altChainTip, and check that it
// gets rejected because it doesn't have the last finality point in
// its selected parent chain.
func TestFinality(t *testing.T) {
params := dagconfig.SimNetParams
params.K = 1
dag, teardownFunc, err := DAGSetup("TestFinality", Config{
DAGParams: &params,
})
if err != nil {
t.Fatalf("Failed to setup DAG instance: %v", err)
}
defer teardownFunc()
blockTime := time.Unix(dag.genesis.timestamp, 0)
extraNonce := int64(0)
buildNodeToDag := func(parents blockSet) (*blockNode, error) {
// We need to change the blockTime to keep all block hashes unique
blockTime = blockTime.Add(time.Second)
// We need to change the extraNonce to keep coinbase hashes unique
extraNonce++
bh := &wire.BlockHeader{
Version: 1,
Bits: dag.genesis.bits,
ParentHashes: parents.hashes(),
Timestamp: blockTime,
}
msgBlock := wire.NewMsgBlock(bh)
blockHeight := parents.maxHeight() + 1
coinbaseTx, err := createCoinbaseTx(blockHeight, 1, extraNonce, dag.dagParams)
if err != nil {
return nil, err
}
msgBlock.AddTransaction(coinbaseTx)
block := util.NewBlock(msgBlock)
dag.dagLock.Lock()
defer dag.dagLock.Unlock()
err = dag.maybeAcceptBlock(block, BFNone)
if err != nil {
return nil, err
}
return dag.index.LookupNode(block.Hash()), nil
}
currentNode := dag.genesis
// First we build a chain of finalityInterval blocks for future use
for currentNode.blueScore < finalityInterval {
currentNode, err = buildNodeToDag(setFromSlice(currentNode))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
}
altChainTip := currentNode
// Now we build a new chain of 2 * finalityInterval blocks, pointed to genesis, and
// we expect the block with height 1 * finalityInterval to be the last finality point
currentNode = dag.genesis
for currentNode.blueScore < finalityInterval {
currentNode, err = buildNodeToDag(setFromSlice(currentNode))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
}
expectedFinalityPoint := currentNode
for currentNode.blueScore < 2*finalityInterval {
currentNode, err = buildNodeToDag(setFromSlice(currentNode))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
}
if dag.lastFinalityPoint != expectedFinalityPoint {
t.Errorf("TestFinality: dag.lastFinalityPoint expected to be %v but got %v", expectedFinalityPoint, dag.lastFinalityPoint)
}
// Here we check that even if we create a parallel tip (a new tip with
// the same parents as the current one) with the same blue score as the
// current tip, it still won't affect the last finality point.
_, err = buildNodeToDag(setFromSlice(currentNode.selectedParent))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
if dag.lastFinalityPoint != expectedFinalityPoint {
t.Errorf("TestFinality: dag.lastFinalityPoint was unexpectly changed")
}
// Here we check that a block with lower blue score than the last finality
// point will get rejected
nodeWithLowBlueScore, err := buildNodeToDag(setFromSlice(dag.genesis))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
if !dag.index.NodeStatus(nodeWithLowBlueScore).KnownInvalid() {
t.Errorf("TestFinality: nodeWithLowBlueScore was expected to be invalid, but got valid instead")
}
// Here we check that a block that doesn't have the last finality point in
// its selected parent chain will get rejected
nodeWithFinalityPointInAnticone, err := buildNodeToDag(setFromSlice(altChainTip))
if err != nil {
t.Fatalf("TestFinality: buildNodeToDag unexpectedly returned an error: %v", err)
}
if !dag.index.NodeStatus(nodeWithFinalityPointInAnticone).KnownInvalid() {
t.Errorf("TestFinality: invalidNode was expected to be invalid, but got valid instead")
}
}

View File

@ -41,9 +41,9 @@ var (
// the block height -> block hash index. // the block height -> block hash index.
heightIndexBucketName = []byte("heightidx") heightIndexBucketName = []byte("heightidx")
// dagTipHashesKeyName is the name of the db key used to store the DAG // dagStateKeyName is the name of the db key used to store the DAG
// tip hashes. // tip hashes.
dagTipHashesKeyName = []byte("dagtiphashes") dagStateKeyName = []byte("dagstate")
// utxoSetVersionKeyName is the name of the db key used to store the // utxoSetVersionKeyName is the name of the db key used to store the
// version of the utxo set currently in the database. // version of the utxo set currently in the database.
@ -697,38 +697,43 @@ func dbFetchHeightByHash(dbTx database.Tx, hash *daghash.Hash) (int32, error) {
return int32(byteOrder.Uint32(serializedHeight)), nil return int32(byteOrder.Uint32(serializedHeight)), nil
} }
// serializeDAGTipHashes returns the serialization of the DAG tip hashes. type dagState struct {
// This is data to be stored in the DAG tip hashes bucket. TipHashes []daghash.Hash
func serializeDAGTipHashes(tipHashes []daghash.Hash) ([]byte, error) { LastFinalityPoint daghash.Hash
return json.Marshal(tipHashes)
} }
// deserializeDAGTipHashes deserializes the passed serialized DAG tip hashes. // serializeDAGState returns the serialization of the DAG state.
// This is data stored in the DAG tip hashes bucket and is updated after // This is data to be stored in the DAG state bucket.
func serializeDAGState(state *dagState) ([]byte, error) {
return json.Marshal(state)
}
// deserializeDAGState deserializes the passed serialized DAG state.
// This is data stored in the DAG state bucket and is updated after
// every block is connected to the DAG. // every block is connected to the DAG.
func deserializeDAGTipHashes(serializedData []byte) ([]daghash.Hash, error) { func deserializeDAGState(serializedData []byte) (*dagState, error) {
var tipHashes []daghash.Hash var state *dagState
err := json.Unmarshal(serializedData, &tipHashes) err := json.Unmarshal(serializedData, &state)
if err != nil { if err != nil {
return nil, database.Error{ return nil, database.Error{
ErrorCode: database.ErrCorruption, ErrorCode: database.ErrCorruption,
Description: "corrupt DAG tip hashes", Description: "corrupt DAG state",
} }
} }
return tipHashes, nil return state, nil
} }
// dbPutDAGTipHashes uses an existing database transaction to store the latest // dbPutDAGState uses an existing database transaction to store the latest
// tip hashes of the DAG. // tip hashes of the DAG.
func dbPutDAGTipHashes(dbTx database.Tx, tipHashes []daghash.Hash) error { func dbPutDAGState(dbTx database.Tx, state *dagState) error {
serializedData, err := serializeDAGTipHashes(tipHashes) serializedData, err := serializeDAGState(state)
if err != nil { if err != nil {
return err return err
} }
return dbTx.Metadata().Put(dagTipHashesKeyName, serializedData) return dbTx.Metadata().Put(dagStateKeyName, serializedData)
} }
// createDAGState initializes both the database and the DAG state to the // createDAGState initializes both the database and the DAG state to the
@ -758,6 +763,9 @@ func (dag *BlockDAG) createDAGState() error {
// Add the new node to the index which is used for faster lookups. // Add the new node to the index which is used for faster lookups.
dag.index.addNode(node) dag.index.addNode(node)
// Initiate the last finality point to the genesis block
dag.lastFinalityPoint = node
// Create the initial the database chain state including creating the // Create the initial the database chain state including creating the
// necessary index buckets and inserting the genesis block. // necessary index buckets and inserting the genesis block.
err := dag.db.Update(func(dbTx database.Tx) error { err := dag.db.Update(func(dbTx database.Tx) error {
@ -810,8 +818,12 @@ func (dag *BlockDAG) createDAGState() error {
return err return err
} }
// Store the current DAG tip hashes into the database. // Store the current DAG state into the database.
err = dbPutDAGTipHashes(dbTx, dag.TipHashes()) state := &dagState{
TipHashes: dag.TipHashes(),
LastFinalityPoint: *genesisBlock.Hash(),
}
err = dbPutDAGState(dbTx, state)
if err != nil { if err != nil {
return err return err
} }
@ -830,7 +842,7 @@ func (dag *BlockDAG) initDAGState() error {
// everything from scratch or upgrade certain buckets. // everything from scratch or upgrade certain buckets.
var initialized bool var initialized bool
err := dag.db.View(func(dbTx database.Tx) error { err := dag.db.View(func(dbTx database.Tx) error {
initialized = dbTx.Metadata().Get(dagTipHashesKeyName) != nil initialized = dbTx.Metadata().Get(dagStateKeyName) != nil
return nil return nil
}) })
if err != nil { if err != nil {
@ -849,9 +861,9 @@ func (dag *BlockDAG) initDAGState() error {
// When it doesn't exist, it means the database hasn't been // When it doesn't exist, it means the database hasn't been
// initialized for use with the DAG yet, so break out now to allow // initialized for use with the DAG yet, so break out now to allow
// that to happen under a writable database transaction. // that to happen under a writable database transaction.
serializedData := dbTx.Metadata().Get(dagTipHashesKeyName) serializedData := dbTx.Metadata().Get(dagStateKeyName)
log.Tracef("Serialized DAG tip hashes: %x", serializedData) log.Tracef("Serialized DAG tip hashes: %x", serializedData)
tipHashes, err := deserializeDAGTipHashes(serializedData) state, err := deserializeDAGState(serializedData)
if err != nil { if err != nil {
return err return err
} }
@ -977,16 +989,19 @@ func (dag *BlockDAG) initDAGState() error {
// Apply the stored tips to the virtual block. // Apply the stored tips to the virtual block.
tips := newSet() tips := newSet()
for _, tipHash := range tipHashes { for _, tipHash := range state.TipHashes {
tip := dag.index.LookupNode(&tipHash) tip := dag.index.LookupNode(&tipHash)
if tip == nil { if tip == nil {
return AssertError(fmt.Sprintf("initDAGState: cannot find "+ return AssertError(fmt.Sprintf("initDAGState: cannot find "+
"DAG tip %s in block index", tipHashes)) "DAG tip %s in block index", state.TipHashes))
} }
tips.add(tip) tips.add(tip)
} }
dag.virtual.SetTips(tips) dag.virtual.SetTips(tips)
// Set the last finality point
dag.lastFinalityPoint = dag.index.LookupNode(&state.LastFinalityPoint)
return nil return nil
}) })
} }

View File

@ -532,64 +532,70 @@ func TestUtxoEntryDeserializeErrors(t *testing.T) {
} }
} }
// TestDAGTipHashesSerialization ensures serializing and deserializing the // TestDAGStateSerialization ensures serializing and deserializing the
// DAG tip hashes works as expected. // DAG state works as expected.
func TestDAGTipHashesSerialization(t *testing.T) { func TestDAGStateSerialization(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
tipHashes []daghash.Hash state *dagState
serialized []byte serialized []byte
}{ }{
{ {
name: "genesis", name: "genesis",
tipHashes: []daghash.Hash{*newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}, state: &dagState{
serialized: []byte("[[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]]"), LastFinalityPoint: *newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
TipHashes: []daghash.Hash{*newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")},
},
serialized: []byte("{\"TipHashes\":[[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]],\"LastFinalityPoint\":[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]}"),
}, },
{ {
name: "block 1", name: "block 1",
tipHashes: []daghash.Hash{*newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048")}, state: &dagState{
serialized: []byte("[[72,96,235,24,191,27,22,32,227,126,148,144,252,138,66,117,20,65,111,215,81,89,171,134,104,142,154,131,0,0,0,0]]"), LastFinalityPoint: *newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
TipHashes: []daghash.Hash{*newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048")},
},
serialized: []byte("{\"TipHashes\":[[72,96,235,24,191,27,22,32,227,126,148,144,252,138,66,117,20,65,111,215,81,89,171,134,104,142,154,131,0,0,0,0]],\"LastFinalityPoint\":[111,226,140,10,182,241,179,114,193,166,162,70,174,99,247,79,147,30,131,101,225,90,8,156,104,214,25,0,0,0,0,0]}"),
}, },
} }
for i, test := range tests { for i, test := range tests {
gotBytes, err := serializeDAGTipHashes(test.tipHashes) gotBytes, err := serializeDAGState(test.state)
if err != nil { if err != nil {
t.Errorf("serializeDAGTipHashes #%d (%s) "+ t.Errorf("serializeDAGState #%d (%s) "+
"unexpected error: %v", i, test.name, err) "unexpected error: %v", i, test.name, err)
continue continue
} }
// Ensure the tipHashes serializes to the expected value. // Ensure the dagState serializes to the expected value.
if !bytes.Equal(gotBytes, test.serialized) { if !bytes.Equal(gotBytes, test.serialized) {
t.Errorf("serializeDAGTipHashes #%d (%s): mismatched "+ t.Errorf("serializeDAGState #%d (%s): mismatched "+
"bytes - got %s, want %s", i, test.name, "bytes - got %s, want %s", i, test.name,
string(gotBytes), string(test.serialized)) string(gotBytes), string(test.serialized))
continue continue
} }
// Ensure the serialized bytes are decoded back to the expected // Ensure the serialized bytes are decoded back to the expected
// tipHashes. // dagState.
tipHashes, err := deserializeDAGTipHashes(test.serialized) state, err := deserializeDAGState(test.serialized)
if err != nil { if err != nil {
t.Errorf("deserializeDAGTipHashes #%d (%s) "+ t.Errorf("deserializeDAGState #%d (%s) "+
"unexpected error: %v", i, test.name, err) "unexpected error: %v", i, test.name, err)
continue continue
} }
if !reflect.DeepEqual(tipHashes, test.tipHashes) { if !reflect.DeepEqual(state, test.state) {
t.Errorf("deserializeDAGTipHashes #%d (%s) "+ t.Errorf("deserializeDAGState #%d (%s) "+
"mismatched tipHashes - got %v, want %v", i, "mismatched state - got %v, want %v", i,
test.name, tipHashes, test.tipHashes) test.name, state, test.state)
continue continue
} }
} }
} }
// TestDAGTipHashesDeserializeErrors performs negative tests against // TestDAGStateDeserializeErrors performs negative tests against
// deserializing the DAG tip hashes to ensure error paths work as expected. // deserializing the DAG state to ensure error paths work as expected.
func TestDAGTipHashesDeserializeErrors(t *testing.T) { func TestDAGStateDeserializeErrors(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
@ -611,9 +617,9 @@ func TestDAGTipHashesDeserializeErrors(t *testing.T) {
for _, test := range tests { for _, test := range tests {
// Ensure the expected error type and code is returned. // Ensure the expected error type and code is returned.
_, err := deserializeDAGTipHashes(test.serialized) _, err := deserializeDAGState(test.serialized)
if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { if reflect.TypeOf(err) != reflect.TypeOf(test.errType) {
t.Errorf("deserializeDAGTipHashes (%s): expected "+ t.Errorf("deserializeDAGState (%s): expected "+
"error type does not match - got %T, want %T", "error type does not match - got %T, want %T",
test.name, err, test.errType) test.name, err, test.errType)
continue continue
@ -621,7 +627,7 @@ func TestDAGTipHashesDeserializeErrors(t *testing.T) {
if derr, ok := err.(database.Error); ok { if derr, ok := err.(database.Error); ok {
tderr := test.errType.(database.Error) tderr := test.errType.(database.Error)
if derr.ErrorCode != tderr.ErrorCode { if derr.ErrorCode != tderr.ErrorCode {
t.Errorf("deserializeDAGTipHashes (%s): "+ t.Errorf("deserializeDAGState (%s): "+
"wrong error code got: %v, want: %v", "wrong error code got: %v, want: %v",
test.name, derr.ErrorCode, test.name, derr.ErrorCode,
tderr.ErrorCode) tderr.ErrorCode)

View File

@ -209,6 +209,8 @@ const (
// ErrWithDiff indicates that there was an error with UTXOSet.WithDiff // ErrWithDiff indicates that there was an error with UTXOSet.WithDiff
ErrWithDiff ErrWithDiff
// ErrFinality indicates that a block doesn't adhere to the finality rules
ErrFinality
// ErrTransactionsNotSorted indicates that transactions in block are not // ErrTransactionsNotSorted indicates that transactions in block are not
// sorted by subnetwork // sorted by subnetwork
ErrTransactionsNotSorted ErrTransactionsNotSorted
@ -256,6 +258,7 @@ var errorCodeStrings = map[ErrorCode]string{
ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock", ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock",
ErrParentBlockNotCurrentTips: "ErrParentBlockNotCurrentTips", ErrParentBlockNotCurrentTips: "ErrParentBlockNotCurrentTips",
ErrWithDiff: "ErrWithDiff", ErrWithDiff: "ErrWithDiff",
ErrFinality: "ErrFinality",
ErrTransactionsNotSorted: "ErrTransactionsNotSorted", ErrTransactionsNotSorted: "ErrTransactionsNotSorted",
} }

View File

@ -55,6 +55,7 @@ func TestErrorCodeStringer(t *testing.T) {
{ErrInvalidAncestorBlock, "ErrInvalidAncestorBlock"}, {ErrInvalidAncestorBlock, "ErrInvalidAncestorBlock"},
{ErrParentBlockNotCurrentTips, "ErrParentBlockNotCurrentTips"}, {ErrParentBlockNotCurrentTips, "ErrParentBlockNotCurrentTips"},
{ErrWithDiff, "ErrWithDiff"}, {ErrWithDiff, "ErrWithDiff"},
{ErrFinality, "ErrFinality"},
{ErrTransactionsNotSorted, "ErrTransactionsNotSorted"}, {ErrTransactionsNotSorted, "ErrTransactionsNotSorted"},
{0xffff, "Unknown ErrorCode (65535)"}, {0xffff, "Unknown ErrorCode (65535)"},
} }

View File

@ -84,10 +84,24 @@ func traverseCandidates(newBlock *blockNode, candidates blockSet, selectedParent
queue.Push(parent) queue.Push(parent)
} }
// TODO (Ori): This is temporary until children bug is fixed. After it we should return it to `selectedParentPast.anyChildInSet(current)`
isInSelectedParentPast := func(block *blockNode) bool {
if selectedParent.parents.contains(block) {
return true
}
for _, child := range block.children {
if selectedParentPast.contains(child) {
return true
}
}
return false
}
for queue.Len() > 0 { for queue.Len() > 0 {
current := queue.pop() current := queue.pop()
if candidates.contains(current) { if candidates.contains(current) {
if current == selectedParent || selectedParentPast.anyChildInSet(current) { if current == selectedParent || isInSelectedParentPast(current) {
selectedParentPast.add(current) selectedParentPast.add(current)
} else { } else {
blues = append(blues, current) blues = append(blues, current)

View File

@ -184,7 +184,7 @@ func (d *UTXODiff) diffFrom(other *UTXODiff) (*UTXODiff, error) {
result.toRemove.add(outPoint, utxoEntry) result.toRemove.add(outPoint, utxoEntry)
} }
if other.toRemove.contains(outPoint) { if other.toRemove.contains(outPoint) {
return nil, errors.New("diffFrom: transaction both in d.toAdd and in other.toRemove") return nil, fmt.Errorf("diffFrom: outpoint %v both in d.toAdd and in other.toRemove", outPoint)
} }
} }

View File

@ -803,9 +803,8 @@ func TestDiffUTXOSet_addTx(t *testing.T) {
// outputs paying an appropriate subsidy based on the passed block height to the // outputs paying an appropriate subsidy based on the passed block height to the
// address associated with the harness. It automatically uses a standard // address associated with the harness. It automatically uses a standard
// signature script that starts with the block height // signature script that starts with the block height
func createCoinbaseTx(blockHeight int32, numOutputs uint32) (*wire.MsgTx, error) { func createCoinbaseTx(blockHeight int32, numOutputs uint32, extraNonce int64, params *dagconfig.Params) (*wire.MsgTx, error) {
// Create standard coinbase script. // Create standard coinbase script.
extraNonce := int64(0)
coinbaseScript, err := txscript.NewScriptBuilder(). coinbaseScript, err := txscript.NewScriptBuilder().
AddInt64(int64(blockHeight)).AddInt64(extraNonce).Script() AddInt64(int64(blockHeight)).AddInt64(extraNonce).Script()
if err != nil { if err != nil {
@ -821,7 +820,7 @@ func createCoinbaseTx(blockHeight int32, numOutputs uint32) (*wire.MsgTx, error)
SignatureScript: coinbaseScript, SignatureScript: coinbaseScript,
Sequence: wire.MaxTxInSequenceNum, Sequence: wire.MaxTxInSequenceNum,
}) })
totalInput := CalcBlockSubsidy(blockHeight, &dagconfig.MainNetParams) totalInput := CalcBlockSubsidy(blockHeight, params)
amountPerOutput := totalInput / uint64(numOutputs) amountPerOutput := totalInput / uint64(numOutputs)
remainder := totalInput - amountPerOutput*uint64(numOutputs) remainder := totalInput - amountPerOutput*uint64(numOutputs)
for i := uint32(0); i < numOutputs; i++ { for i := uint32(0); i < numOutputs; i++ {
@ -858,7 +857,7 @@ func TestApplyUTXOChanges(t *testing.T) {
}, },
} }
cbTx, err := createCoinbaseTx(1, 1) cbTx, err := createCoinbaseTx(1, 1, 0, dag.dagParams)
if err != nil { if err != nil {
t.Errorf("createCoinbaseTx: %v", err) t.Errorf("createCoinbaseTx: %v", err)
} }
@ -925,7 +924,7 @@ func TestDiffFromTx(t *testing.T) {
fus := &FullUTXOSet{ fus := &FullUTXOSet{
utxoCollection: utxoCollection{}, utxoCollection: utxoCollection{},
} }
cbTx, err := createCoinbaseTx(1, 1) cbTx, err := createCoinbaseTx(1, 1, 0, &dagconfig.SimNetParams)
if err != nil { if err != nil {
t.Errorf("createCoinbaseTx: %v", err) t.Errorf("createCoinbaseTx: %v", err)
} }

View File

@ -12,7 +12,7 @@ import (
// TestVirtualBlock ensures that VirtualBlock works as expected. // TestVirtualBlock ensures that VirtualBlock works as expected.
func TestVirtualBlock(t *testing.T) { func TestVirtualBlock(t *testing.T) {
phantomK := uint32(1) phantomK := uint32(1)
buildNode := buildNodeGenerator(phantomK) buildNode := buildNodeGenerator(phantomK, false)
// Create a DAG as follows: // Create a DAG as follows:
// 0 <- 1 <- 2 // 0 <- 1 <- 2
@ -101,7 +101,7 @@ func TestVirtualBlock(t *testing.T) {
func TestSelectedPath(t *testing.T) { func TestSelectedPath(t *testing.T) {
phantomK := uint32(1) phantomK := uint32(1)
buildNode := buildNodeGenerator(phantomK) buildNode := buildNodeGenerator(phantomK, false)
// Create an empty VirtualBlock // Create an empty VirtualBlock
virtual := newVirtualBlock(nil, phantomK) virtual := newVirtualBlock(nil, phantomK)