diff --git a/blockdag/dag.go b/blockdag/dag.go index 5ef25a519..c4eae269d 100644 --- a/blockdag/dag.go +++ b/blockdag/dag.go @@ -1189,6 +1189,8 @@ func (node *blockNode) acceptSelectedParentTransactions(selectedParent *util.Blo func (dag *BlockDAG) restoreUTXO(node *blockNode) (UTXOSet, error) { stack := []*blockNode{} + // Iterate over the chain of diff-childs from node till virtual and add them + // all into a stack for current := node; current != nil; { stack = append(stack, current) var err error @@ -1198,20 +1200,28 @@ func (dag *BlockDAG) restoreUTXO(node *blockNode) (UTXOSet, error) { } } - utxo := UTXOSet(dag.virtual.utxoSet) + // Start with the top item in the stack, going over it top-to-bottom, + // applying the UTXO-diff one-by-one. + topNode, stack := stack[len(stack)-1], stack[:len(stack)-1] // pop the top item in the stack + topNodeDiff, err := dag.utxoDiffStore.diffByNode(topNode) + if err != nil { + return nil, err + } + accumulatedDiff := topNodeDiff.clone() for i := len(stack) - 1; i >= 0; i-- { diff, err := dag.utxoDiffStore.diffByNode(stack[i]) if err != nil { return nil, err } - utxo, err = utxo.WithDiff(diff) + // Use WithDiffInPlace, otherwise copying the diffs again and again create a polynomial overhead + err = accumulatedDiff.WithDiffInPlace(diff) if err != nil { return nil, err } } - return utxo, nil + return NewDiffUTXOSet(dag.virtual.utxoSet, accumulatedDiff), nil } // updateTipsUTXO builds and applies new diff UTXOs for all the DAG's tips diff --git a/blockdag/utxoset.go b/blockdag/utxoset.go index cfa942f34..fbf32417d 100644 --- a/blockdag/utxoset.go +++ b/blockdag/utxoset.go @@ -2,11 +2,12 @@ package blockdag import ( "fmt" - "github.com/pkg/errors" "math" "sort" "strings" + "github.com/pkg/errors" + "github.com/kaspanet/kaspad/ecc" "github.com/kaspanet/kaspad/wire" ) @@ -224,6 +225,10 @@ func (d *UTXODiff) diffFrom(other *UTXODiff) (*UTXODiff, error) { for outpoint, utxoEntry := range d.toAdd { if !other.toAdd.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore) { result.toRemove.add(outpoint, utxoEntry) + } else if (d.toRemove.contains(outpoint) && !other.toRemove.contains(outpoint)) || + (!d.toRemove.contains(outpoint) && other.toRemove.contains(outpoint)) { + return nil, errors.New( + "diffFrom: outpoint both in d.toAdd, other.toAdd, and only one of d.toRemove and other.toRemove") } if diffEntry, ok := other.toRemove.get(outpoint); ok { // An exception is made for entries with unequal blue scores @@ -243,6 +248,18 @@ func (d *UTXODiff) diffFrom(other *UTXODiff) (*UTXODiff, error) { // 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 { + diffEntry, ok := other.toRemove.get(outpoint) + if ok { + // if have the same entry in d.toRemove - simply don't copy. + // unless existing entry is with different blue score, in this case - this is an error + if utxoEntry.blockBlueScore != diffEntry.blockBlueScore { + return nil, errors.New("diffFrom: outpoint both in d.toRemove and other.toRemove with different " + + "blue scores, with no corresponding entry in d.toAdd") + } + } else { // if no existing entry - add to result.toAdd + result.toAdd.add(outpoint, utxoEntry) + } + if !other.toRemove.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore) { result.toAdd.add(outpoint, utxoEntry) } @@ -256,7 +273,7 @@ func (d *UTXODiff) diffFrom(other *UTXODiff) (*UTXODiff, error) { other.toRemove.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore)) { continue } - return nil, errors.New("diffFrom: transaction both in d.toRemove and in other.toAdd") + return nil, errors.New("diffFrom: outpoint both in d.toRemove and in other.toAdd") } } @@ -284,6 +301,56 @@ func (d *UTXODiff) diffFrom(other *UTXODiff) (*UTXODiff, error) { return &result, nil } +// WithDiffInPlace applies provided diff to this diff in-place, that would be the result if +// first d, and than diff were applied to the same base +func (d *UTXODiff) WithDiffInPlace(diff *UTXODiff) error { + for outpoint, entryToRemove := range diff.toRemove { + if d.toAdd.containsWithBlueScore(outpoint, entryToRemove.blockBlueScore) { + // If already exists in toAdd with the same blueScore - remove from toAdd + d.toAdd.remove(outpoint) + continue + } + if d.toRemove.contains(outpoint) { + // If already exists - this is an error + return ruleError(ErrWithDiff, fmt.Sprintf( + "WithDiffInPlace: outpoint %s both in d.toRemove and in diff.toRemove", outpoint)) + } + + // If not exists neither in toAdd nor in toRemove - add to toRemove + d.toRemove.add(outpoint, entryToRemove) + } + + for outpoint, entryToAdd := range diff.toAdd { + if d.toRemove.containsWithBlueScore(outpoint, entryToAdd.blockBlueScore) { + // If already exists in toRemove with the same blueScore - remove from toRemove + if d.toAdd.contains(outpoint) && !diff.toRemove.contains(outpoint) { + return ruleError(ErrWithDiff, fmt.Sprintf( + "WithDiffInPlace: outpoint %s both in d.toAdd and in diff.toAdd with no "+ + "corresponding entry in diff.toRemove", outpoint)) + } + d.toRemove.remove(outpoint) + continue + } + if existingEntry, ok := d.toAdd.get(outpoint); ok && + (existingEntry.blockBlueScore == entryToAdd.blockBlueScore || + !diff.toRemove.containsWithBlueScore(outpoint, existingEntry.blockBlueScore)) { + // If already exists - this is an error + return ruleError(ErrWithDiff, fmt.Sprintf( + "WithDiffInPlace: outpoint %s both in d.toAdd and in diff.toAdd", outpoint)) + } + + // If not exists neither in toAdd nor in toRemove, or exists in toRemove with different blueScore - add to toAdd + d.toAdd.add(outpoint, entryToAdd) + } + + // Apply diff.diffMultiset to d.diffMultiset + if d.useMultiset { + d.diffMultiset = d.diffMultiset.Union(diff.diffMultiset) + } + + return 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 // @@ -331,8 +398,7 @@ func (d *UTXODiff) WithDiff(diff *UTXODiff) (*UTXODiff, error) { // or diff.toRemove. // These are just "updates" to accepted blue score if diffEntry.blockBlueScore != utxoEntry.blockBlueScore && - (d.toRemove.containsWithBlueScore(outpoint, diffEntry.blockBlueScore) || - diff.toRemove.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore)) { + diff.toRemove.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore) { continue } return nil, ruleError(ErrWithDiff, fmt.Sprintf("WithDiff: outpoint %s both in d.toAdd and in other.toAdd", outpoint)) @@ -353,11 +419,10 @@ func (d *UTXODiff) WithDiff(diff *UTXODiff) (*UTXODiff, error) { // or diff.toAdd. // These are just "updates" to accepted blue score if diffEntry.blockBlueScore != utxoEntry.blockBlueScore && - (d.toAdd.containsWithBlueScore(outpoint, diffEntry.blockBlueScore) || - diff.toAdd.containsWithBlueScore(outpoint, utxoEntry.blockBlueScore)) { + d.toAdd.containsWithBlueScore(outpoint, diffEntry.blockBlueScore) { continue } - return nil, ruleError(ErrWithDiff, "WithDiff: transaction both in d.toRemove and in other.toRemove") + return nil, ruleError(ErrWithDiff, "WithDiff: outpoint both in d.toRemove and in other.toRemove") } } diff --git a/blockdag/utxoset_test.go b/blockdag/utxoset_test.go index e2af10488..dd9be4dc6 100644 --- a/blockdag/utxoset_test.go +++ b/blockdag/utxoset_test.go @@ -1,11 +1,12 @@ package blockdag import ( - "github.com/kaspanet/kaspad/util/subnetworkid" "math" "reflect" "testing" + "github.com/kaspanet/kaspad/util/subnetworkid" + "github.com/kaspanet/kaspad/ecc" "github.com/kaspanet/kaspad/util/daghash" "github.com/kaspanet/kaspad/wire" @@ -136,8 +137,11 @@ func TestUTXODiffRules(t *testing.T) { // 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 + // this.WithDiffInPlace(other) and compare it to expectedWithDiffResult // // Note: an expected nil result means that we expect the respective operation to fail + // See the following spreadsheet for a summary of all test-cases: + // https://docs.google.com/spreadsheets/d/1E8G3mp5y1-yifouwLLXRLueSRfXdDRwRKFieYE07buY/edit?usp=sharing tests := []struct { name string this *UTXODiff @@ -146,7 +150,7 @@ func TestUTXODiffRules(t *testing.T) { expectedWithDiffResult *UTXODiff }{ { - name: "one toAdd in this, one toAdd in other", + name: "first toAdd in this, first toAdd in other", this: &UTXODiff{ toAdd: utxoCollection{outpoint0: utxoEntry1}, toRemove: utxoCollection{}, @@ -162,7 +166,23 @@ func TestUTXODiffRules(t *testing.T) { expectedWithDiffResult: nil, }, { - name: "one toAdd in this, one toRemove in other", + name: "first in toAdd in this, second in toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd in this, second in toRemove in other", this: &UTXODiff{ toAdd: utxoCollection{outpoint0: utxoEntry1}, toRemove: utxoCollection{}, @@ -178,7 +198,36 @@ func TestUTXODiffRules(t *testing.T) { }, }, { - name: "one toAdd in this, empty other", + name: "first in toAdd in this and other, second in toRemove in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd in this and toRemove in other, second in toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{}, + }, + }, + { + name: "first in toAdd in this, empty other", this: &UTXODiff{ toAdd: utxoCollection{outpoint0: utxoEntry1}, toRemove: utxoCollection{}, @@ -197,7 +246,7 @@ func TestUTXODiffRules(t *testing.T) { }, }, { - name: "one toRemove in this, one toAdd in other", + name: "first in toRemove in this and in toAdd in other", this: &UTXODiff{ toAdd: utxoCollection{}, toRemove: utxoCollection{outpoint0: utxoEntry1}, @@ -213,7 +262,23 @@ func TestUTXODiffRules(t *testing.T) { }, }, { - name: "one toRemove in this, one toRemove in other", + name: "first in toRemove in this, second in toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + }, + { + name: "first in toRemove in this and other", this: &UTXODiff{ toAdd: utxoCollection{}, toRemove: utxoCollection{outpoint0: utxoEntry1}, @@ -229,7 +294,49 @@ func TestUTXODiffRules(t *testing.T) { expectedWithDiffResult: nil, }, { - name: "one toRemove in this, empty other", + name: "first in toRemove in this, second in toRemove in other", + this: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: nil, + }, + { + name: "first in toRemove in this and toAdd in other, second in toRemove in other", + this: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: nil, + }, + { + name: "first in toRemove in this and other, second in toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{}, + }, + expectedWithDiffResult: nil, + }, + { + name: "first in toRemove in this, empty other", this: &UTXODiff{ toAdd: utxoCollection{}, toRemove: utxoCollection{outpoint0: utxoEntry1}, @@ -248,7 +355,116 @@ func TestUTXODiffRules(t *testing.T) { }, }, { - name: "empty this, one toAdd in other", + name: "first in toAdd in this and other, second in toRemove in this", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd in this, second in toRemove in this and toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd in this and toRemove in other, second in toRemove in this", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + }, + { + name: "first in toAdd in this, second in toRemove in this and in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd and second in toRemove in both this and other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{}, + }, + expectedWithDiffResult: nil, + }, + { + name: "first in toAdd in this and toRemove in other, second in toRemove in this and toAdd in other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedDiffFromResult: nil, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{}, + }, + }, + { + name: "first in toAdd and second in toRemove in this, empty other", + this: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry2}, + toRemove: utxoCollection{outpoint0: utxoEntry1}, + }, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + }, + { + name: "empty this, first in toAdd in other", this: &UTXODiff{ toAdd: utxoCollection{}, toRemove: utxoCollection{}, @@ -267,7 +483,7 @@ func TestUTXODiffRules(t *testing.T) { }, }, { - name: "empty this, one toRemove in other", + name: "empty this, first in toRemove in other", this: &UTXODiff{ toAdd: utxoCollection{}, toRemove: utxoCollection{}, @@ -285,6 +501,25 @@ func TestUTXODiffRules(t *testing.T) { toRemove: utxoCollection{outpoint0: utxoEntry1}, }, }, + { + name: "empty this, first in toAdd and second in toRemove in other", + this: &UTXODiff{ + toAdd: utxoCollection{}, + toRemove: utxoCollection{}, + }, + other: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedDiffFromResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + expectedWithDiffResult: &UTXODiff{ + toAdd: utxoCollection{outpoint0: utxoEntry1}, + toRemove: utxoCollection{outpoint0: utxoEntry2}, + }, + }, { name: "empty this, empty other", this: &UTXODiff{ @@ -304,108 +539,6 @@ func TestUTXODiffRules(t *testing.T) { toRemove: utxoCollection{}, }, }, - { - name: "equal outpoints different blue scores: first in toAdd in this, second in toAdd in other", - this: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry2}, - toRemove: utxoCollection{}, - }, - expectedDiffFromResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry2}, - toRemove: utxoCollection{outpoint0: utxoEntry1}, - }, - expectedWithDiffResult: nil, - }, - { - name: "equal outpoints different blue scores: first in toRemove in this, second in toRemove in other", - this: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{outpoint0: utxoEntry1}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - expectedDiffFromResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - expectedWithDiffResult: nil, - }, - { - name: "equal outpoints different blue scores: first in toAdd and second in toRemove in this, empty other", - this: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{}, - }, - expectedDiffFromResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry2}, - toRemove: utxoCollection{outpoint0: utxoEntry1}, - }, - expectedWithDiffResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - }, - { - name: "equal outpoints different blue scores: empty this, first in toAdd and second in toRemove in other", - this: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - expectedDiffFromResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - expectedWithDiffResult: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - }, - { - name: "equal outpoints different blue scores: first in toAdd and second in toRemove in both this and other", - this: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - expectedDiffFromResult: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{}, - }, - expectedWithDiffResult: nil, - }, - { - name: "equal outpoints different blue scores: first in toAdd in this and toRemove in other, second in toRemove in this and toAdd in other", - this: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry1}, - toRemove: utxoCollection{outpoint0: utxoEntry2}, - }, - other: &UTXODiff{ - toAdd: utxoCollection{outpoint0: utxoEntry2}, - toRemove: utxoCollection{outpoint0: utxoEntry1}, - }, - expectedDiffFromResult: nil, - expectedWithDiffResult: &UTXODiff{ - toAdd: utxoCollection{}, - toRemove: utxoCollection{}, - }, - }, } for _, test := range tests { @@ -460,6 +593,24 @@ func TestUTXODiffRules(t *testing.T) { "Expected: \"%v\", got: \"%v\".", test.name, expectedWithDiffResult, withDiffResult) } + // Repeat WithDiff check this time using WithDiffInPlace + thisClone := this.clone() + err = thisClone.WithDiffInPlace(other) + + // Test whether WithDiffInPlace returned an error + isWithDiffInPlaceOk := err == nil + expectedIsWithDiffInPlaceOk := expectedWithDiffResult != nil + if isWithDiffInPlaceOk != expectedIsWithDiffInPlaceOk { + t.Errorf("unexpected WithDiffInPlace error in test \"%s\". "+ + "Expected: \"%t\", got: \"%t\".", test.name, expectedIsWithDiffInPlaceOk, isWithDiffInPlaceOk) + } + + // If not error, test the WithDiffInPlace result + if isWithDiffInPlaceOk && !thisClone.equal(expectedWithDiffResult) { + t.Errorf("unexpected WithDiffInPlace result in test \"%s\". "+ + "Expected: \"%v\", got: \"%v\".", test.name, expectedWithDiffResult, thisClone) + } + // Make sure that diffFrom after WithDiff results in the original other if isWithDiffOk { otherResult, err := this.diffFrom(withDiffResult)