[DEV-74] Added infrastructure for Diff-based UTXOs (#39)

* [DEV-74] Implemented and written tests for utxoIterator.

* [DEV-74] Improved utxoIterator tests.

* [DEV-74] Implemented utxoCollection tests.

* [DEV-74] Implemented utxoDiff and its tests.

* [DEV-74] Implemented utxoSet.

* [DEV -74] Added tests for fullUTXOSet.

* [DEV-74] Added some tests for diffUTXOSet.

* [DEV-74] Wrote tests for diffUTXOSet iterator.

* [DEV-74] Added a negative test for diffUTXOSet.withDiff.

* [DEV-74] Wrote tests for addTx.

* [DEV-74] Wrote a toRemove test for addTx.

* [DEV-74] Changed blockNode.utxoDiff to be of type utxoDiff.

* [DEV-74] Removed superfluous whitespace.

* [DEV-74] Renamed confusing "previousHash" to "hash".

* [DEV-74] Fixed bad test and corrected failing test.

* [DEV-74] Moved confusing "negatives" test to be part of the general utxoCollection test.

* [DEV-74] Removed utxoDiff.inverted.

* [DEV-74] Renamed blockNode.utxoDiff to blockNode.diff.

* [DEV-74] Renamed diff to diffFrom for clarity's sake.

* [DEV-74] Converted the type of utxoCollection from map[daghash.Hash]map[uint32]*wire.TxOut to map[wire.OutPoint]*UtxoEntry.

* [DEV-74] Corrected test names in utxoCollection_test.

* [DEV-74] Removed superfluous utxoCollection iterator and moved utxoIterator into utxoset.go.

* [DEV-74] Renamed variables in utxoset.go.

* [DEV-74] Renamed verifyTx to areInputsInUTXO and removed a superfulous test.

* [DEV-74] Fixed bad test logic in TestDiffUTXOSet_addTx.

* [DEV-74] Added a few comments. Added reference-equals checks to clone functions.

* [DEV-74] Moved utxoCollection and utxoDiff into utxoset.go.

* [DEV-74] Wrote explanations for utxoCollection and utxoDiff tests.

* [DEV-74] Wrote explanations for all utxoSet tests besides addTx.

* [DEV-74] Wrote explanations for TestDiffUTXOSet_addTx.

* [DEV-74] Moved the documentation for utxoDiff into utxoset.go.

* [DEV-74] Wrote an explanation on utxoSet.

* [DEV-74] Moved diffChild next to diff, improved their comments, moved the explanations for diffFrom and withDiff to the appropriate methods, got rid of utxoIterator, and renamed areInputsInUTXO to containsInputs.

* [DEV-74] Replaced boring old-fashioned reference equality with special, fancy reference equality for maps, slices, and channels.

* [DEV-74] Wrote additional explanations for test cases.
This commit is contained in:
stasatdaglabs 2018-08-08 11:06:09 +03:00 committed by Svarog
parent ed5df3bf7d
commit f6eb9edecf
3 changed files with 1227 additions and 8 deletions

View File

@ -61,9 +61,8 @@ func (status blockStatus) KnownInvalid() bool {
return status&(statusValidateFailed|statusInvalidAncestor) != 0
}
// blockNode represents a block within the block chain and is primarily used to
// aid in selecting the best chain to be the main chain. The main chain is
// stored into the block database.
// blockNode represents a block within the block DAG. The DAG is stored into
// the block database.
type blockNode struct {
// NOTE: Additions, deletions, or modifications to the order of the
// definitions in this struct should not be changed without considering
@ -82,17 +81,19 @@ type blockNode struct {
// children are all the blocks that refer to this block as a parent
children blockSet
// diffChild is the child that UTXODiff will be built from
diffChild *blockNode
// blues are all blue blocks in this block's worldview that are in its selected parent anticone
blues []*blockNode
// blueScore is the count of all the blue blocks in this block's past
blueScore uint64
// utxoDiff is the UTXO of the block represented as a diff to the virtual block
utxoDiff UtxoViewpoint
// diff is the UTXO representation of the block
// A block's UTXO is reconstituted by applying diffWith on every block in the chain of diffChildren
// from the virtual block down to the block. See diffChild
diff utxoDiff
// diffChild is the child that diff will be built from. See diff
diffChild *blockNode
// hash is the double sha 256 of the block.
hash daghash.Hash

434
blockdag/utxoset.go Normal file
View File

@ -0,0 +1,434 @@
package blockdag
import (
"fmt"
"github.com/daglabs/btcd/wire"
"errors"
"sort"
"strings"
)
// utxoCollection represents a set of UTXOs indexed by their outPoints
type utxoCollection map[wire.OutPoint]*UtxoEntry
func (uc utxoCollection) String() string {
utxoStrings := make([]string, len(uc))
i := 0
for outPoint, utxoEntry := range uc {
utxoStrings[i] = fmt.Sprintf("(%s, %d) => %d", outPoint.Hash, outPoint.Index, utxoEntry.amount)
i++
}
// Sort strings for determinism.
sort.Strings(utxoStrings)
return fmt.Sprintf("[ %s ]", strings.Join(utxoStrings, ", "))
}
// clone returns a clone of this collection
func (uc utxoCollection) clone() utxoCollection {
clone := utxoCollection{}
for outPoint, entry := range uc {
clone[outPoint] = entry
}
return clone
}
// utxoDiff represents a diff between two UTXO Sets.
type utxoDiff struct {
toAdd utxoCollection
toRemove utxoCollection
}
// newUTXODiff creates a new, empty utxoDiff
func newUTXODiff() *utxoDiff {
return &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
}
}
// diffFrom returns a new utxoDiff with the difference between this utxoDiff and another
// Assumes that:
// Both utxoDiffs are from the same base
// If a txOut exists in both utxoDiffs, its underlying values would be the same
//
// diffFrom follows a set of rules represented by the following 3 by 3 table:
//
// | | this | |
// ---------+-----------+-----------+-----------+-----------
// | | toAdd | toRemove | None
// ---------+-----------+-----------+-----------+-----------
// other | toAdd | - | X | toAdd
// ---------+-----------+-----------+-----------+-----------
// | toRemove | X | - | toRemove
// ---------+-----------+-----------+-----------+-----------
// | None | toRemove | toAdd | -
//
// Key:
// - Don't add anything to the result
// X Return an error
// toAdd Add the UTXO into the toAdd collection of the result
// toRemove Add the UTXO into the toRemove collection of the result
//
// Examples:
// 1. This diff contains a UTXO in toAdd, and the other diff contains it in toRemove
// diffFrom results in an error
// 2. This diff contains a UTXO in toRemove, and the other diff does not contain it
// diffFrom results in the UTXO being added to toAdd
func (d *utxoDiff) diffFrom(other *utxoDiff) (*utxoDiff, error) {
result := newUTXODiff()
// Note that the following cases are not accounted for, as they are impossible
// as long as the base utxoSet is the same:
// - if utxoEntry is in d.toAdd and other.toRemove
// - if utxoEntry is in d.toRemove and other.toAdd
// All transactions in d.toAdd:
// If they are not in other.toAdd - should be added in result.toRemove
// If they are in other.toRemove - base utxoSet is not the same
for outPoint, utxoEntry := range d.toAdd {
if _, ok := other.toAdd[outPoint]; !ok {
result.toRemove[outPoint] = utxoEntry
}
if _, ok := other.toRemove[outPoint]; ok {
return nil, errors.New("diffFrom: transaction both in d.toAdd and in other.toRemove")
}
}
// All transactions in d.toRemove:
// If they are not in other.toRemove - should be added in result.toAdd
// If they are in other.toAdd - base utxoSet is not the same
for outPoint, utxoEntry := range d.toRemove {
if _, ok := other.toRemove[outPoint]; !ok {
result.toAdd[outPoint] = utxoEntry
}
if _, ok := other.toAdd[outPoint]; ok {
return nil, errors.New("diffFrom: transaction both in d.toRemove and in other.toAdd")
}
}
// All transactions in other.toAdd:
// If they are not in d.toAdd - should be added in result.toAdd
for outPoint, utxoEntry := range other.toAdd {
if _, ok := d.toAdd[outPoint]; !ok {
result.toAdd[outPoint] = utxoEntry
}
}
// All transactions in other.toRemove:
// If they are not in d.toRemove - should be added in result.toRemove
for outPoint, utxoEntry := range other.toRemove {
if _, ok := d.toRemove[outPoint]; !ok {
result.toRemove[outPoint] = utxoEntry
}
}
return result, nil
}
// withDiff applies provided diff to this diff, creating a new utxoDiff, that would be the result if
// first d, and than diff were applied to the same base
//
// withDiff follows a set of rules represented by the following 3 by 3 table:
//
// | | this | |
// ---------+-----------+-----------+-----------+-----------
// | | toAdd | toRemove | None
// ---------+-----------+-----------+-----------+-----------
// other | toAdd | X | - | toAdd
// ---------+-----------+-----------+-----------+-----------
// | toRemove | - | X | toRemove
// ---------+-----------+-----------+-----------+-----------
// | None | toAdd | toRemove | -
//
// Key:
// - Don't add anything to the result
// X Return an error
// toAdd Add the UTXO into the toAdd collection of the result
// toRemove Add the UTXO into the toRemove collection of the result
//
// Examples:
// 1. This diff contains a UTXO in toAdd, and the other diff contains it in toRemove
// withDiff results in nothing being added
// 2. This diff contains a UTXO in toRemove, and the other diff does not contain it
// withDiff results in the UTXO being added to toRemove
func (d *utxoDiff) withDiff(diff *utxoDiff) (*utxoDiff, error) {
result := newUTXODiff()
// All transactions in d.toAdd:
// If they are not in diff.toRemove - should be added in result.toAdd
// If they are in diff.toAdd - should throw an error
// Otherwise - should be ignored
for outPoint, utxoEntry := range d.toAdd {
if _, ok := diff.toRemove[outPoint]; !ok {
result.toAdd[outPoint] = utxoEntry
}
if _, ok := diff.toAdd[outPoint]; ok {
return nil, errors.New("withDiff: transaction both in d.toAdd and in other.toAdd")
}
}
// All transactions in d.toRemove:
// If they are not in diff.toAdd - should be added in result.toRemove
// If they are in diff.toRemove - should throw an error
// Otherwise - should be ignored
for outPoint, utxoEntry := range d.toRemove {
if _, ok := diff.toAdd[outPoint]; !ok {
result.toRemove[outPoint] = utxoEntry
}
if _, ok := diff.toRemove[outPoint]; ok {
return nil, errors.New("withDiff: transaction both in d.toRemove and in other.toRemove")
}
}
// All transactions in diff.toAdd:
// If they are not in d.toRemove - should be added in result.toAdd
for outPoint, utxoEntry := range diff.toAdd {
if _, ok := d.toRemove[outPoint]; !ok {
result.toAdd[outPoint] = utxoEntry
}
}
// All transactions in diff.toRemove:
// If they are not in d.toAdd - should be added in result.toRemove
for outPoint, utxoEntry := range diff.toRemove {
if _, ok := d.toAdd[outPoint]; !ok {
result.toRemove[outPoint] = utxoEntry
}
}
return result, nil
}
// clone returns a clone of this utxoDiff
func (d *utxoDiff) clone() *utxoDiff {
return &utxoDiff{
toAdd: d.toAdd.clone(),
toRemove: d.toRemove.clone(),
}
}
func (d utxoDiff) String() string {
return fmt.Sprintf("toAdd: %s; toRemove: %s", d.toAdd, d.toRemove)
}
// newUTXOEntry creates a new utxoEntry representing the given txOut
func newUTXOEntry(txOut *wire.TxOut) *UtxoEntry {
entry := new(UtxoEntry)
entry.amount = txOut.Value
entry.pkScript = txOut.PkScript
return entry
}
// utxoSet represents a set of unspent transaction outputs
// Every DAG has exactly one fullUTXOSet.
// When a new block arrives, it is validated and applied to the fullUTXOSet in the following manner:
// 1. Get the block's PastUTXO:
// 2. Add all the block's transactions to the block's PastUTXO
// 3. For each of the block's parents,
// 3.1. Rebuild their utxoDiff
// 3.2. Set the block as their diffChild
// 4. Create and initialize a new virtual block
// 5. Get the new virtual's PastUTXO
// 6. Rebuild the utxoDiff for all the tips
// 7. Convert (meld) the new virtual's diffUTXOSet into a fullUTXOSet. This updates the DAG's fullUTXOSet
type utxoSet interface {
fmt.Stringer
diffFrom(other utxoSet) (*utxoDiff, error)
withDiff(utxoDiff *utxoDiff) (utxoSet, error)
addTx(tx *wire.MsgTx) (ok bool)
clone() utxoSet
}
// fullUTXOSet represents a full list of transaction outputs and their values
type fullUTXOSet struct {
utxoCollection
}
// newFullUTXOSet creates a new utxoSet with full list of transaction outputs and their values
func newFullUTXOSet() *fullUTXOSet {
return &fullUTXOSet{
utxoCollection: utxoCollection{},
}
}
// diffFrom returns the difference between this utxoSet and another
// diffFrom can only work when other is a diffUTXOSet, and its base utxoSet is this.
func (fus *fullUTXOSet) diffFrom(other utxoSet) (*utxoDiff, error) {
otherDiffSet, ok := other.(*diffUTXOSet)
if !ok {
return nil, errors.New("can't diffFrom two fullUTXOSets")
}
if otherDiffSet.base != fus {
return nil, errors.New("can diffFrom only with diffUTXOSet where this fullUTXOSet is the base")
}
return otherDiffSet.utxoDiff, nil
}
// withDiff returns a utxoSet which is a diff between this and another utxoSet
func (fus *fullUTXOSet) withDiff(other *utxoDiff) (utxoSet, error) {
return newDiffUTXOSet(fus, other.clone()), nil
}
// addTx adds a transaction to this utxoSet and returns true iff it's valid in this UTXO's context
func (fus *fullUTXOSet) addTx(tx *wire.MsgTx) bool {
if !fus.containsInputs(tx) {
return false
}
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
delete(fus.utxoCollection, outPoint)
}
for i, txOut := range tx.TxOut {
hash := tx.TxHash()
outPoint := *wire.NewOutPoint(&hash, uint32(i))
entry := newUTXOEntry(txOut)
fus.utxoCollection[outPoint] = entry
}
return true
}
func (fus *fullUTXOSet) containsInputs(tx *wire.MsgTx) bool {
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
if _, ok := fus.utxoCollection[outPoint]; !ok {
return false
}
}
return true
}
// collection returns a collection of all UTXOs in this set
func (fus *fullUTXOSet) collection() utxoCollection {
return fus.utxoCollection.clone()
}
// clone returns a clone of this utxoSet
func (fus *fullUTXOSet) clone() utxoSet {
return &fullUTXOSet{utxoCollection: fus.utxoCollection.clone()}
}
// diffUTXOSet represents a utxoSet with a base fullUTXOSet and a UTXODiff
type diffUTXOSet struct {
base *fullUTXOSet
utxoDiff *utxoDiff
}
// newDiffUTXOSet Creates a new utxoSet based on a base fullUTXOSet and a UTXODiff
func newDiffUTXOSet(base *fullUTXOSet, diff *utxoDiff) *diffUTXOSet {
return &diffUTXOSet{
base: base,
utxoDiff: diff,
}
}
// diffFrom returns the difference between this utxoSet and another.
// diffFrom can work if other is this's base fullUTXOSet, or a diffUTXOSet with the same base as this
func (dus *diffUTXOSet) diffFrom(other utxoSet) (*utxoDiff, error) {
otherDiffSet, ok := other.(*diffUTXOSet)
if !ok {
return nil, errors.New("can't diffFrom diffUTXOSet with fullUTXOSet")
}
if otherDiffSet.base != dus.base {
return nil, errors.New("can't diffFrom with another diffUTXOSet with a different base")
}
return dus.utxoDiff.diffFrom(otherDiffSet.utxoDiff)
}
// withDiff return a new utxoSet which is a diffFrom between this and another utxoSet
func (dus *diffUTXOSet) withDiff(other *utxoDiff) (utxoSet, error) {
diff, err := dus.utxoDiff.withDiff(other)
if err != nil {
return nil, err
}
return newDiffUTXOSet(dus.base, diff), nil
}
// addTx adds a transaction to this utxoSet and returns true iff it's valid in this UTXO's context
func (dus *diffUTXOSet) addTx(tx *wire.MsgTx) bool {
if !dus.containsInputs(tx) {
return false
}
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
if _, ok := dus.utxoDiff.toAdd[outPoint]; ok {
delete(dus.utxoDiff.toAdd, outPoint)
} else {
prevUTXOEntry := dus.base.utxoCollection[outPoint]
dus.utxoDiff.toRemove[outPoint] = prevUTXOEntry
}
}
for i, txOut := range tx.TxOut {
hash := tx.TxHash()
outPoint := *wire.NewOutPoint(&hash, uint32(i))
entry := newUTXOEntry(txOut)
if _, ok := dus.utxoDiff.toRemove[outPoint]; ok {
delete(dus.utxoDiff.toRemove, outPoint)
} else {
dus.utxoDiff.toAdd[outPoint] = entry
}
}
return true
}
func (dus *diffUTXOSet) containsInputs(tx *wire.MsgTx) bool {
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
_, isInBase := dus.base.utxoCollection[outPoint]
_, isInDiffToAdd := dus.utxoDiff.toAdd[outPoint]
_, isInDiffToRemove := dus.utxoDiff.toRemove[outPoint]
if (!isInBase && !isInDiffToAdd) || isInDiffToRemove {
return false
}
}
return true
}
// meldToBase updates the base fullUTXOSet with all changes in diff
func (dus *diffUTXOSet) meldToBase() {
for outPoint := range dus.utxoDiff.toRemove {
delete(dus.base.utxoCollection, outPoint)
}
for outPoint, utxoEntry := range dus.utxoDiff.toAdd {
dus.base.utxoCollection[outPoint] = utxoEntry
}
dus.utxoDiff = newUTXODiff()
}
func (dus *diffUTXOSet) String() string {
return fmt.Sprintf("{Base: %s, To Add: %s, To Remove: %s}", dus.base, dus.utxoDiff.toAdd, dus.utxoDiff.toRemove)
}
// collection returns a collection of all UTXOs in this set
func (dus *diffUTXOSet) collection() utxoCollection {
clone := dus.clone().(*diffUTXOSet)
clone.meldToBase()
return clone.base.collection()
}
// clone returns a clone of this UTXO Set
func (dus *diffUTXOSet) clone() utxoSet {
return newDiffUTXOSet(dus.base.clone().(*fullUTXOSet), dus.utxoDiff.clone())
}

784
blockdag/utxoset_test.go Normal file
View File

@ -0,0 +1,784 @@
package blockdag
import (
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/wire"
"reflect"
"testing"
)
// TestUTXOCollection makes sure that utxoCollection cloning and string representations work as expected.
func TestUTXOCollection(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
outPoint0 := *wire.NewOutPoint(hash0, 0)
outPoint1 := *wire.NewOutPoint(hash1, 0)
utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10})
utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20})
// For each of the following test cases, we will:
// .String() the given collection and compare it to expectedString
// .clone() the given collection and compare its value to itself (expected: equals) and its reference to itself (expected: not equal)
tests := []struct {
name string
collection utxoCollection
expectedString string
}{
{
name: "empty collection",
collection: utxoCollection{},
expectedString: "[ ]",
},
{
name: "one member",
collection: utxoCollection{
outPoint0: utxoEntry1,
},
expectedString: "[ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 20 ]",
},
{
name: "two members",
collection: utxoCollection{
outPoint0: utxoEntry0,
outPoint1: utxoEntry1,
},
expectedString: "[ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10, (1111111111111111111111111111111111111111111111111111111111111111, 0) => 20 ]",
},
}
for _, test := range tests {
// Test utxoCollection string representation
collectionString := test.collection.String()
if collectionString != test.expectedString {
t.Errorf("unexpected string in test \"%s\". "+
"Expected: \"%s\", got: \"%s\".", test.name, test.expectedString, collectionString)
}
// Test utxoCollection cloning
collectionClone := test.collection.clone()
if reflect.ValueOf(collectionClone).Pointer() == reflect.ValueOf(test.collection).Pointer() {
t.Errorf("collection is reference-equal to its clone in test \"%s\". ", test.name)
}
if !reflect.DeepEqual(test.collection, collectionClone) {
t.Errorf("collection is not equal to its clone in test \"%s\". "+
"Expected: \"%s\", got: \"%s\".", test.name, collectionString, collectionClone.String())
}
}
}
// TestUTXODiff makes sure that utxoDiff creation, cloning, and string representations work as expected.
func TestUTXODiff(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
outPoint0 := *wire.NewOutPoint(hash0, 0)
outPoint1 := *wire.NewOutPoint(hash1, 0)
utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10})
utxoEntry1 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 20})
diff := utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{outPoint1: utxoEntry1},
}
// Test utxoDiff creation
newDiff := newUTXODiff()
if len(newDiff.toAdd) != 0 || len(newDiff.toRemove) != 0 {
t.Errorf("new diff is not empty")
}
// Test utxoDiff cloning
clonedDiff := *diff.clone()
if &clonedDiff == &diff {
t.Errorf("cloned diff is reference-equal to the original")
}
if !reflect.DeepEqual(clonedDiff, diff) {
t.Errorf("cloned diff not equal to the original"+
"Original: \"%v\", cloned: \"%v\".", diff, clonedDiff)
}
// Test utxoDiff string representation
expectedDiffString := "toAdd: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ]; toRemove: [ (1111111111111111111111111111111111111111111111111111111111111111, 0) => 20 ]"
diffString := clonedDiff.String()
if diffString != expectedDiffString {
t.Errorf("unexpected diff string. "+
"Expected: \"%s\", got: \"%s\".", expectedDiffString, diffString)
}
}
// TestUTXODiffRules makes sure that all diffFrom and withDiff rules are followed.
// Each test case represents a cell in the two tables outlined in the documentation for utxoDiff.
func TestUTXODiffRules(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
outPoint0 := *wire.NewOutPoint(hash0, 0)
utxoEntry0 := newUTXOEntry(&wire.TxOut{PkScript: []byte{}, Value: 10})
// For each of the following test cases, we will:
// this.diffFrom(other) and compare it to expectedDiffFromResult
// this.withDiff(other) and compare it to expectedWithDiffResult
//
// Note: an expected nil result means that we expect the respective operation to fail
tests := []struct {
name string
this *utxoDiff
other *utxoDiff
expectedDiffFromResult *utxoDiff
expectedWithDiffResult *utxoDiff
}{
{
name: "one toAdd in this, one toAdd in other",
this: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedWithDiffResult: nil,
},
{
name: "one toAdd in this, one toRemove in other",
this: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
expectedDiffFromResult: nil,
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
{
name: "one toAdd in this, empty other",
this: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
},
{
name: "one toRemove in this, one toAdd in other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
other: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
expectedDiffFromResult: nil,
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
{
name: "one toRemove in this, one toRemove in other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedWithDiffResult: nil,
},
{
name: "one toRemove in this, empty other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
},
{
name: "empty this, one toAdd in other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
},
{
name: "empty this, one toRemove in other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
},
{
name: "empty this, empty other",
this: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
other: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedDiffFromResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
expectedWithDiffResult: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
}
for _, test := range tests {
// diffFrom from this to other
diffResult, err := test.this.diffFrom(test.other)
// Test whether diffFrom returned an error
isDiffFromOk := err == nil
expectedIsDiffFromOk := test.expectedDiffFromResult != nil
if isDiffFromOk != expectedIsDiffFromOk {
t.Errorf("unexpected diffFrom error in test \"%s\". "+
"Expected: \"%t\", got: \"%t\".", test.name, expectedIsDiffFromOk, isDiffFromOk)
}
// If not error, test the diffFrom result
if isDiffFromOk && !reflect.DeepEqual(diffResult, test.expectedDiffFromResult) {
t.Errorf("unexpected diffFrom result in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.expectedDiffFromResult, diffResult)
}
// withDiff from this to other
withDiffResult, err := test.this.withDiff(test.other)
// Test whether withDiff returned an error
isWithDiffOk := err == nil
expectedIsWithDiffOk := test.expectedWithDiffResult != nil
if isWithDiffOk != expectedIsWithDiffOk {
t.Errorf("unexpected withDiff error in test \"%s\". "+
"Expected: \"%t\", got: \"%t\".", test.name, expectedIsWithDiffOk, isWithDiffOk)
}
// Ig not error, test the withDiff result
if isWithDiffOk && !reflect.DeepEqual(withDiffResult, test.expectedWithDiffResult) {
t.Errorf("unexpected withDiff result in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.expectedWithDiffResult, withDiffResult)
}
}
}
// TestFullUTXOSet makes sure that fullUTXOSet is working as expected.
func TestFullUTXOSet(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
outPoint0 := *wire.NewOutPoint(hash0, 0)
outPoint1 := *wire.NewOutPoint(hash1, 0)
txOut0 := &wire.TxOut{PkScript: []byte{}, Value: 10}
txOut1 := &wire.TxOut{PkScript: []byte{}, Value: 20}
utxoEntry0 := newUTXOEntry(txOut0)
utxoEntry1 := newUTXOEntry(txOut1)
diff := &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{outPoint1: utxoEntry1},
}
// Test fullUTXOSet creation
emptySet := newFullUTXOSet()
if len(emptySet.collection()) != 0 {
t.Errorf("new set is not empty")
}
// Test fullUTXOSet withDiff
withDiffResult, err := emptySet.withDiff(diff)
if err != nil {
t.Errorf("withDiff unexpectedly failed")
}
withDiffUTXOSet, ok := withDiffResult.(*diffUTXOSet)
if !ok {
t.Errorf("withDiff is of unexpected type")
}
if !reflect.DeepEqual(withDiffUTXOSet.base, emptySet) || !reflect.DeepEqual(withDiffUTXOSet.utxoDiff, diff) {
t.Errorf("withDiff is of unexpected composition")
}
// Test fullUTXOSet addTx
txIn0 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: *hash0, Index: 0}, Sequence: 0}
transaction0 := wire.NewMsgTx(1)
transaction0.TxIn = []*wire.TxIn{txIn0}
transaction0.TxOut = []*wire.TxOut{txOut0}
if ok = emptySet.addTx(transaction0); ok {
t.Errorf("addTx unexpectedly succeeded")
}
emptySet = &fullUTXOSet{utxoCollection: utxoCollection{outPoint0: utxoEntry0}}
if ok = emptySet.addTx(transaction0); !ok {
t.Errorf("addTx unexpectedly failed")
}
// Test fullUTXOSet collection
if !reflect.DeepEqual(emptySet.collection(), emptySet.utxoCollection) {
t.Errorf("collection does not equal the set's utxoCollection")
}
// Test fullUTXOSet cloning
clonedEmptySet := emptySet.clone().(*fullUTXOSet)
if !reflect.DeepEqual(clonedEmptySet, emptySet) {
t.Errorf("clone does not equal the original set")
}
if clonedEmptySet == emptySet {
t.Errorf("cloned set is reference-equal to the original")
}
}
// TestDiffUTXOSet makes sure that diffUTXOSet is working as expected.
func TestDiffUTXOSet(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
hash1, _ := daghash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
outPoint0 := *wire.NewOutPoint(hash0, 0)
outPoint1 := *wire.NewOutPoint(hash1, 0)
txOut0 := &wire.TxOut{PkScript: []byte{}, Value: 10}
txOut1 := &wire.TxOut{PkScript: []byte{}, Value: 20}
utxoEntry0 := newUTXOEntry(txOut0)
utxoEntry1 := newUTXOEntry(txOut1)
diff := &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{outPoint1: utxoEntry1},
}
// Test diffUTXOSet creation
emptySet := newDiffUTXOSet(newFullUTXOSet(), newUTXODiff())
if len(emptySet.collection()) != 0 {
t.Errorf("new set is not empty")
}
// Test diffUTXOSet withDiff
withDiffResult, err := emptySet.withDiff(diff)
if err != nil {
t.Errorf("withDiff unexpectedly failed")
}
withDiffUTXOSet, ok := withDiffResult.(*diffUTXOSet)
if !ok {
t.Errorf("withDiff is of unexpected type")
}
withDiff, _ := newUTXODiff().withDiff(diff)
if !reflect.DeepEqual(withDiffUTXOSet.base, emptySet.base) || !reflect.DeepEqual(withDiffUTXOSet.utxoDiff, withDiff) {
t.Errorf("withDiff is of unexpected composition")
}
_, err = newDiffUTXOSet(newFullUTXOSet(), diff).withDiff(diff)
if err == nil {
t.Errorf("withDiff unexpectedly succeeded")
}
// Given a diffSet, each case tests that meldToBase, String, collection, and cloning work as expected
// For each of the following test cases, we will:
// .meldToBase() the given diffSet and compare it to expectedMeldSet
// .String() the given diffSet and compare it to expectedString
// .collection() the given diffSet and compare it to expectedCollection
// .clone() the given diffSet and compare its value to itself (expected: equals) and its reference to itself (expected: not equal)
tests := []struct {
name string
diffSet *diffUTXOSet
expectedMeldSet *diffUTXOSet
expectedString string
expectedCollection utxoCollection
}{
{
name: "empty base, empty diff",
diffSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedMeldSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedString: "{Base: [ ], To Add: [ ], To Remove: [ ]}",
expectedCollection: utxoCollection{},
},
{
name: "empty base, one member in diff toAdd",
diffSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint0: utxoEntry0},
toRemove: utxoCollection{},
},
},
expectedMeldSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint0: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedString: "{Base: [ ], To Add: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ], To Remove: [ ]}",
expectedCollection: utxoCollection{outPoint0: utxoEntry0},
},
{
name: "empty base, one member in diff toRemove",
diffSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
},
expectedMeldSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedString: "{Base: [ ], To Add: [ ], To Remove: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ]}",
expectedCollection: utxoCollection{},
},
{
name: "one member in base toAdd, one member in diff toAdd",
diffSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint0: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint1: utxoEntry1},
toRemove: utxoCollection{},
},
},
expectedMeldSet: &diffUTXOSet{
base: &fullUTXOSet{
utxoCollection: utxoCollection{
outPoint0: utxoEntry0,
outPoint1: utxoEntry1,
},
},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedString: "{Base: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ], To Add: [ (1111111111111111111111111111111111111111111111111111111111111111, 0) => 20 ], To Remove: [ ]}",
expectedCollection: utxoCollection{
outPoint0: utxoEntry0,
outPoint1: utxoEntry1,
},
},
{
name: "one member in base toAdd, same one member in diff toRemove",
diffSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint0: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{outPoint0: utxoEntry0},
},
},
expectedMeldSet: &diffUTXOSet{
base: &fullUTXOSet{
utxoCollection: utxoCollection{},
},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
expectedString: "{Base: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ], To Add: [ ], To Remove: [ (0000000000000000000000000000000000000000000000000000000000000000, 0) => 10 ]}",
expectedCollection: utxoCollection{},
},
}
for _, test := range tests {
// Test meldToBase
meldSet := test.diffSet.clone().(*diffUTXOSet)
meldSet.meldToBase()
if !reflect.DeepEqual(meldSet, test.expectedMeldSet) {
t.Errorf("unexpected melded set in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.expectedMeldSet, meldSet)
}
// Test string representation
setString := test.diffSet.String()
if setString != test.expectedString {
t.Errorf("unexpected string in test \"%s\". "+
"Expected: \"%s\", got: \"%s\".", test.name, test.expectedString, setString)
}
// Test collection
setCollection := test.diffSet.collection()
if !reflect.DeepEqual(setCollection, test.expectedCollection) {
t.Errorf("unexpected set collection in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.expectedCollection, setCollection)
}
// Test cloning
clonedSet := test.diffSet.clone().(*diffUTXOSet)
if !reflect.DeepEqual(clonedSet, test.diffSet) {
t.Errorf("unexpected set clone in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.diffSet, clonedSet)
}
if clonedSet == test.diffSet {
t.Errorf("cloned set is reference-equal to the original")
}
}
}
// TestUTXOSetDiffRules makes sure that utxoSet diffFrom rules are followed.
// The rules are:
// 1. Neither fullUTXOSet nor diffUTXOSet can diffFrom a fullUTXOSet.
// 2. fullUTXOSet cannot diffFrom a diffUTXOSet with a base other that itself.
// 3. diffUTXOSet cannot diffFrom a diffUTXOSet with a different base.
func TestUTXOSetDiffRules(t *testing.T) {
fullSet := newFullUTXOSet()
diffSet := newDiffUTXOSet(fullSet, newUTXODiff())
// For each of the following test cases, we will call utxoSet.diffFrom(diffSet) and compare
// whether the function succeeded with expectedSuccess
//
// Note: since test cases are similar for both fullUTXOSet and diffUTXOSet, we test both using the same test cases
run := func(set utxoSet) {
tests := []struct {
name string
diffSet utxoSet
expectedSuccess bool
}{
{
name: "diff from fullSet",
diffSet: newFullUTXOSet(),
expectedSuccess: false,
},
{
name: "diff from diffSet with different base",
diffSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()),
expectedSuccess: false,
},
{
name: "diff from diffSet with same base",
diffSet: newDiffUTXOSet(fullSet, newUTXODiff()),
expectedSuccess: true,
},
}
for _, test := range tests {
_, err := set.diffFrom(test.diffSet)
success := err == nil
if success != test.expectedSuccess {
t.Errorf("unexpected diffFrom success in test \"%s\". "+
"Expected: \"%t\", got: \"%t\".", test.name, test.expectedSuccess, success)
}
}
}
run(fullSet) // Perform the test cases above on a fullUTXOSet
run(diffSet) // Perform the test cases above on a diffUTXOSet
}
// TestDiffUTXOSet_addTx makes sure that diffUTXOSet addTx works as expected
func TestDiffUTXOSet_addTx(t *testing.T) {
// transaction0 is coinbase. As such, it does not have any inputs
txOut0 := &wire.TxOut{PkScript: []byte{0}, Value: 10}
utxoEntry0 := newUTXOEntry(txOut0)
transaction0 := wire.NewMsgTx(1)
transaction0.TxIn = []*wire.TxIn{}
transaction0.TxOut = []*wire.TxOut{txOut0}
// transaction1 spends transaction0
hash1 := transaction0.TxHash()
outPoint1 := *wire.NewOutPoint(&hash1, 0)
txIn1 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: hash1, Index: 0}, Sequence: 0}
txOut1 := &wire.TxOut{PkScript: []byte{1}, Value: 20}
utxoEntry1 := newUTXOEntry(txOut1)
transaction1 := wire.NewMsgTx(1)
transaction1.TxIn = []*wire.TxIn{txIn1}
transaction1.TxOut = []*wire.TxOut{txOut1}
// transaction2 spends transaction1
hash2 := transaction1.TxHash()
outPoint2 := *wire.NewOutPoint(&hash2, 0)
txIn2 := &wire.TxIn{SignatureScript: []byte{}, PreviousOutPoint: wire.OutPoint{Hash: hash2, Index: 0}, Sequence: 0}
txOut2 := &wire.TxOut{PkScript: []byte{2}, Value: 30}
utxoEntry2 := newUTXOEntry(txOut2)
transaction2 := wire.NewMsgTx(1)
transaction2.TxIn = []*wire.TxIn{txIn2}
transaction2.TxOut = []*wire.TxOut{txOut2}
// outpoint3 is the outpoint for transaction2
hash3 := transaction2.TxHash()
outPoint3 := *wire.NewOutPoint(&hash3, 0)
// For each of the following test cases, we will:
// 1. startSet.addTx() all the transactions in toAdd, in order
// 2. Compare the result set with expectedSet
tests := []struct {
name string
startSet *diffUTXOSet
toAdd []*wire.MsgTx
expectedSet *diffUTXOSet
}{
{
name: "add coinbase transaction to empty set",
startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()),
toAdd: []*wire.MsgTx{transaction0},
expectedSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint1: utxoEntry0},
toRemove: utxoCollection{},
},
},
},
{
name: "add regular transaction to empty set",
startSet: newDiffUTXOSet(newFullUTXOSet(), newUTXODiff()),
toAdd: []*wire.MsgTx{transaction1},
expectedSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
},
{
name: "add transaction to set with its input in base",
startSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
toAdd: []*wire.MsgTx{transaction1},
expectedSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint2: utxoEntry1},
toRemove: utxoCollection{outPoint1: utxoEntry0},
},
},
},
{
name: "add transaction to set with its input in diff toAdd",
startSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint1: utxoEntry0},
toRemove: utxoCollection{},
},
},
toAdd: []*wire.MsgTx{transaction1},
expectedSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint2: utxoEntry1},
toRemove: utxoCollection{},
},
},
},
{
name: "add transaction to set with its input in diff toAdd and its output in diff toRemove",
startSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint1: utxoEntry0},
toRemove: utxoCollection{outPoint2: utxoEntry1},
},
},
toAdd: []*wire.MsgTx{transaction1},
expectedSet: &diffUTXOSet{
base: newFullUTXOSet(),
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
},
{
name: "add two transactions, one spending the other, to set with the first input in base",
startSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
},
},
toAdd: []*wire.MsgTx{transaction1, transaction2},
expectedSet: &diffUTXOSet{
base: &fullUTXOSet{utxoCollection: utxoCollection{outPoint1: utxoEntry0}},
utxoDiff: &utxoDiff{
toAdd: utxoCollection{outPoint3: utxoEntry2},
toRemove: utxoCollection{outPoint1: utxoEntry0},
},
},
},
}
for _, test := range tests {
diffSet := test.startSet.clone()
// Apply all transactions, in order, to diffSet
for _, transaction := range test.toAdd {
diffSet.addTx(transaction)
}
// Make sure that the result diffSet equals to the expectedSet
if !reflect.DeepEqual(diffSet, test.expectedSet) {
t.Errorf("unexpected diffSet in test \"%s\". "+
"Expected: \"%v\", got: \"%v\".", test.name, test.expectedSet, diffSet)
}
}
}