Sort unmined transactions in their dependency order.

This changes the wtxmgr.Store.UnminedTxs method to sort transactions,
using the Kahn topological sort algorithm, before returning
transactions to the caller.  This is possible because transactions
form a sort of directed acyclic graph (DAG) where transactions
reference other transactions to spend their outputs (multiple
referenced outputs from a single transaction spent by the same
transaction count as a single graph edge).

This prevents the possibility of orphan rejection errors when sending
unmined transactions to a full node at startup.  As these transactions
are sent using the sendrawtransaction RPC, which does not permit
orphans, this topological sort is required.

Fixes #156.
This commit is contained in:
Josh Rickmar 2016-03-16 19:07:38 -04:00
parent c2ed8ffc2b
commit e12d23716f
2 changed files with 135 additions and 15 deletions

115
wtxmgr/kahnsort.go Normal file
View file

@ -0,0 +1,115 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package wtxmgr
import (
"github.com/btcsuite/btcd/wire"
)
type graphNode struct {
value *TxRecord
outEdges []*wire.ShaHash
inDegree int
}
type hashGraph map[wire.ShaHash]graphNode
func makeGraph(set map[wire.ShaHash]*TxRecord) hashGraph {
graph := make(hashGraph)
for _, rec := range set {
// Add a node for every transaction record. The output edges
// and input degree are set by iterating over each record's
// inputs below.
if _, ok := graph[rec.Hash]; !ok {
graph[rec.Hash] = graphNode{value: rec}
}
inputLoop:
for _, input := range rec.MsgTx.TxIn {
// Transaction inputs that reference transactions not
// included in the set do not create any (local) graph
// edges.
if _, ok := set[input.PreviousOutPoint.Hash]; !ok {
continue
}
inputNode := graph[input.PreviousOutPoint.Hash]
// Skip duplicate edges.
for _, outEdge := range inputNode.outEdges {
if *outEdge == input.PreviousOutPoint.Hash {
continue inputLoop
}
}
// Mark a directed edge from the previous transaction
// hash to this transaction record and increase the
// input degree for this record's node.
inputRec := inputNode.value
if inputRec == nil {
inputRec = set[input.PreviousOutPoint.Hash]
}
graph[input.PreviousOutPoint.Hash] = graphNode{
value: inputRec,
outEdges: append(inputNode.outEdges, &rec.Hash),
inDegree: inputNode.inDegree,
}
node := graph[rec.Hash]
graph[rec.Hash] = graphNode{
value: rec,
outEdges: node.outEdges,
inDegree: node.inDegree + 1,
}
}
}
return graph
}
// graphRoots returns the roots of the graph. That is, it returns the node's
// values for all nodes which contain an input degree of 0.
func graphRoots(graph hashGraph) []*TxRecord {
roots := make([]*TxRecord, 0, len(graph))
for _, node := range graph {
if node.inDegree == 0 {
roots = append(roots, node.value)
}
}
return roots
}
// dependencySort topologically sorts a set of transaction records by their
// dependency order. It is implemented using Kahn's algorithm.
func dependencySort(txs map[wire.ShaHash]*TxRecord) []*TxRecord {
graph := makeGraph(txs)
s := graphRoots(graph)
// If there are no edges (no transactions from the map reference each
// other), then Kahn's algorithm is unnecessary.
if len(s) == len(txs) {
return s
}
sorted := make([]*TxRecord, 0, len(txs))
for len(s) != 0 {
rec := s[0]
s = s[1:]
sorted = append(sorted, rec)
n := graph[rec.Hash]
for _, mHash := range n.outEdges {
m := graph[*mHash]
if m.inDegree != 0 {
m.inDegree--
graph[*mHash] = m
if m.inDegree == 0 {
s = append(s, m.value)
}
}
}
}
return sorted
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2013-2015 The btcsuite developers
// Copyright (c) 2013-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -123,37 +123,42 @@ func (s *Store) removeConflict(ns walletdb.Bucket, rec *TxRecord) error {
}
// UnminedTxs returns the underlying transactions for all unmined transactions
// which are not known to have been mined in a block.
// which are not known to have been mined in a block. Transactions are
// guaranteed to be sorted by their dependency order.
func (s *Store) UnminedTxs() ([]*wire.MsgTx, error) {
var txs []*wire.MsgTx
var recSet map[wire.ShaHash]*TxRecord
err := scopedView(s.namespace, func(ns walletdb.Bucket) error {
var err error
txs, err = s.unminedTxs(ns)
recSet, err = s.unminedTxRecords(ns)
return err
})
return txs, err
if err != nil {
return nil, err
}
recs := dependencySort(recSet)
txs := make([]*wire.MsgTx, 0, len(recs))
for _, rec := range recs {
txs = append(txs, &rec.MsgTx)
}
return txs, nil
}
func (s *Store) unminedTxs(ns walletdb.Bucket) ([]*wire.MsgTx, error) {
var unmined []*wire.MsgTx
func (s *Store) unminedTxRecords(ns walletdb.Bucket) (map[wire.ShaHash]*TxRecord, error) {
unmined := make(map[wire.ShaHash]*TxRecord)
err := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error {
// TODO: Parsing transactions from the db may be a little
// expensive. It's possible the caller only wants the
// serialized transactions.
var txHash wire.ShaHash
err := readRawUnminedHash(k, &txHash)
if err != nil {
return err
}
var rec TxRecord
err = readRawTxRecord(&txHash, v, &rec)
rec := new(TxRecord)
err = readRawTxRecord(&txHash, v, rec)
if err != nil {
return err
}
tx := rec.MsgTx
unmined = append(unmined, &tx)
unmined[rec.Hash] = rec
return nil
})
return unmined, err