[DEV-104] Disable chained txs (#57)

* [DEV-104] Disable chained transactions

* [DEV-104] add TestApplyUTXOChanges

* [DEV-104] remove isCompatible

* [DEV-104] add TestDiffFromTx

* [DEV-104] reorder variables in TestApplyUTXOChanges

* [DEV-104] rename node -> containgNode in diffFromTx
This commit is contained in:
Ori Newman 2018-09-03 16:57:23 +03:00 committed by Svarog
parent 37cd482db3
commit 86feb42cc4
5 changed files with 284 additions and 36 deletions

View File

@ -696,18 +696,28 @@ func (pns provisionalNodeSet) newProvisionalNode(node *blockNode, withRelatives
// verifyAndBuildUTXO verifies all transactions in the given block (in provisionalNode format) and builds its UTXO
func (p *provisionalNode) verifyAndBuildUTXO(virtual *VirtualBlock, db database.DB) (utxoSet, error) {
utxo, err := p.pastUTXO(virtual, db)
pastUTXO, err := p.pastUTXO(virtual, db)
if err != nil {
return nil, err
}
diff := newUTXODiff()
for _, tx := range p.transactions {
ok := utxo.addTx(tx.MsgTx(), p.original.height)
if !ok {
return nil, fmt.Errorf("transaction %v is not compatible with UTXO", tx)
txDiff, err := pastUTXO.diffFromTx(tx.MsgTx(), p.original)
if err != nil {
return nil, err
}
diff, err = diff.withDiff(txDiff)
if err != nil {
return nil, err
}
}
utxo, err := pastUTXO.withDiff(diff)
if err != nil {
return nil, err
}
return utxo, nil
}

View File

@ -5,14 +5,15 @@
package blockdag
import (
"bou.ke/monkey"
"errors"
"github.com/daglabs/btcd/database"
"reflect"
"strings"
"testing"
"time"
"bou.ke/monkey"
"github.com/daglabs/btcd/database"
"math/rand"
"github.com/daglabs/btcd/dagconfig"
@ -650,20 +651,6 @@ func TestIntervalBlockHashes(t *testing.T) {
}
}
// TestPastUTXOErrors tests all error-cases in restoreUTXO.
// The non-error-cases are tested in the more general tests.
func TestVerifyAndBuildUTXOErrors(t *testing.T) {
targetErrorMessage := "not compatible with UTXO"
testErrorThroughPatching(
t,
targetErrorMessage,
(*diffUTXOSet).addTx,
func(fus *diffUTXOSet, tx *wire.MsgTx, blockHeight int32) bool {
return false
},
)
}
// TestPastUTXOErrors tests all error-cases in restoreUTXO.
// The non-error-cases are tested in the more general tests.
func TestPastUTXOErrors(t *testing.T) {

View File

@ -3,9 +3,10 @@ package blockdag
import (
"errors"
"fmt"
"github.com/daglabs/btcd/wire"
"sort"
"strings"
"github.com/daglabs/btcd/wire"
)
// utxoCollection represents a set of UTXOs indexed by their outPoints
@ -36,6 +37,13 @@ func (uc utxoCollection) remove(outPoint wire.OutPoint) {
delete(uc, outPoint)
}
// get returns the UTXOEntry represented by provided outPoint,
// and a boolean value indicating if said UTXOEntry is in the set or not
func (uc utxoCollection) get(outPoint wire.OutPoint) (*UTXOEntry, bool) {
entry, ok := uc[outPoint]
return entry, ok
}
// contains returns a boolean value indicating whether a UTXO entry is in the set
func (uc utxoCollection) contains(outPoint wire.OutPoint) bool {
_, ok := uc[outPoint]
@ -263,8 +271,37 @@ type utxoSet interface {
fmt.Stringer
diffFrom(other utxoSet) (*utxoDiff, error)
withDiff(utxoDiff *utxoDiff) (utxoSet, error)
diffFromTx(tx *wire.MsgTx, node *blockNode) (*utxoDiff, error)
addTx(tx *wire.MsgTx, blockHeight int32) (ok bool)
clone() utxoSet
get(outPoint wire.OutPoint) (*UTXOEntry, bool)
}
// diffFromTx is a common implementation for diffFromTx, that works
// for both diff-based and full UTXO sets
// Returns a diff that is equivalent to provided transaction,
// or an error if provided transaction is not valid in the context of this UTXOSet
func diffFromTx(u utxoSet, tx *wire.MsgTx, containingNode *blockNode) (*utxoDiff, error) {
diff := newUTXODiff()
isCoinbase := IsCoinBaseTx(tx)
if !isCoinbase {
for _, txIn := range tx.TxIn {
if entry, ok := u.get(txIn.PreviousOutPoint); ok {
diff.toRemove.add(txIn.PreviousOutPoint, entry)
} else {
return nil, fmt.Errorf(
"Transaction %s is invalid because spends outpoint %s that is not in utxo set",
tx.TxHash(), txIn.PreviousOutPoint)
}
}
}
for i, txOut := range tx.TxOut {
hash := tx.TxHash()
entry := newUTXOEntry(txOut, isCoinbase, containingNode.height)
outPoint := *wire.NewOutPoint(&hash, uint32(i))
diff.toAdd.add(outPoint, entry)
}
return diff, nil
}
// fullUTXOSet represents a full list of transaction outputs and their values
@ -324,6 +361,12 @@ func (fus *fullUTXOSet) addTx(tx *wire.MsgTx, blockHeight int32) bool {
return true
}
// diffFromTx returns a diff that is equivalent to provided transaction,
// or an error if provided transaction is not valid in the context of this UTXOSet
func (fus *fullUTXOSet) diffFromTx(tx *wire.MsgTx, node *blockNode) (*utxoDiff, error) {
return diffFromTx(fus, tx, node)
}
func (fus *fullUTXOSet) containsInputs(tx *wire.MsgTx) bool {
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
@ -345,7 +388,7 @@ func (fus *fullUTXOSet) clone() utxoSet {
return &fullUTXOSet{utxoCollection: fus.utxoCollection.clone()}
}
func (fus *fullUTXOSet) getUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) {
func (fus *fullUTXOSet) get(outPoint wire.OutPoint) (*UTXOEntry, bool) {
utxoEntry, ok := fus.utxoCollection[outPoint]
return utxoEntry, ok
}
@ -391,11 +434,18 @@ func (dus *diffUTXOSet) withDiff(other *utxoDiff) (utxoSet, error) {
// 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, blockHeight int32) bool {
isCoinbase := IsCoinBaseTx(tx)
if !isCoinbase {
if !dus.containsInputs(tx) {
return false
}
isCoinBase := IsCoinBaseTx(tx)
if !isCoinBase && !dus.containsInputs(tx) {
return false
}
dus.appendTx(tx, blockHeight, isCoinBase)
return true
}
func (dus *diffUTXOSet) appendTx(tx *wire.MsgTx, blockHeight int32, isCoinBase bool) {
if !isCoinBase {
for _, txIn := range tx.TxIn {
outPoint := *wire.NewOutPoint(&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index)
@ -411,7 +461,7 @@ func (dus *diffUTXOSet) addTx(tx *wire.MsgTx, blockHeight int32) bool {
for i, txOut := range tx.TxOut {
hash := tx.TxHash()
outPoint := *wire.NewOutPoint(&hash, uint32(i))
entry := newUTXOEntry(txOut, isCoinbase, blockHeight)
entry := newUTXOEntry(txOut, isCoinBase, blockHeight)
if dus.utxoDiff.toRemove.contains(outPoint) {
dus.utxoDiff.toRemove.remove(outPoint)
@ -419,8 +469,6 @@ func (dus *diffUTXOSet) addTx(tx *wire.MsgTx, blockHeight int32) bool {
dus.utxoDiff.toAdd.add(outPoint, entry)
}
}
return true
}
func (dus *diffUTXOSet) containsInputs(tx *wire.MsgTx) bool {
@ -450,6 +498,12 @@ func (dus *diffUTXOSet) meldToBase() {
dus.utxoDiff = newUTXODiff()
}
// diffFromTx returns a diff that is equivalent to provided transaction,
// or an error if provided transaction is not valid in the context of this UTXOSet
func (dus *diffUTXOSet) diffFromTx(tx *wire.MsgTx, node *blockNode) (*utxoDiff, error) {
return diffFromTx(dus, tx, node)
}
func (dus *diffUTXOSet) String() string {
return fmt.Sprintf("{Base: %s, To Add: %s, To Remove: %s}", dus.base, dus.utxoDiff.toAdd, dus.utxoDiff.toRemove)
}
@ -466,3 +520,16 @@ func (dus *diffUTXOSet) collection() utxoCollection {
func (dus *diffUTXOSet) clone() utxoSet {
return newDiffUTXOSet(dus.base.clone().(*fullUTXOSet), dus.utxoDiff.clone())
}
// get returns the UTXOEntry associated with provided outPoint in this UTXOSet.
// Returns false in second output if this UTXOEntry was not found
func (dus *diffUTXOSet) get(outPoint wire.OutPoint) (*UTXOEntry, bool) {
if dus.utxoDiff.toRemove.contains(outPoint) {
return nil, false
}
if txOut, ok := dus.base.get(outPoint); ok {
return txOut, true
}
txOut, ok := dus.utxoDiff.toAdd.get(outPoint)
return txOut, ok
}

View File

@ -1,13 +1,19 @@
package blockdag
import (
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/wire"
"math"
"reflect"
"testing"
"math"
"github.com/daglabs/btcd/dagconfig"
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/wire"
)
var OpTrueScript = []byte{txscript.OpTrue}
// TestUTXOCollection makes sure that utxoCollection cloning and string representations work as expected.
func TestUTXOCollection(t *testing.T) {
hash0, _ := daghash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
@ -782,7 +788,7 @@ func TestDiffUTXOSet_addTx(t *testing.T) {
// Apply all transactions to diffSet, in order, with the initial block height startHeight
for i, transaction := range test.toAdd {
diffSet.addTx(transaction, test.startHeight + int32(i))
diffSet.addTx(transaction, test.startHeight+int32(i))
}
// Make sure that the result diffSet equals to the expectedSet
@ -792,3 +798,180 @@ func TestDiffUTXOSet_addTx(t *testing.T) {
}
}
}
// createCoinbaseTx returns a coinbase transaction with the requested number of
// outputs paying an appropriate subsidy based on the passed block height to the
// address associated with the harness. It automatically uses a standard
// signature script that starts with the block height
func createCoinbaseTx(blockHeight int32, numOutputs uint32) (*wire.MsgTx, error) {
// Create standard coinbase script.
extraNonce := int64(0)
coinbaseScript, err := txscript.NewScriptBuilder().
AddInt64(int64(blockHeight)).AddInt64(extraNonce).Script()
if err != nil {
return nil, err
}
tx := wire.NewMsgTx(wire.TxVersion)
tx.AddTxIn(&wire.TxIn{
// Coinbase transactions have no inputs, so previous outpoint is
// zero hash and max index.
PreviousOutPoint: *wire.NewOutPoint(&daghash.Hash{},
wire.MaxPrevOutIndex),
SignatureScript: coinbaseScript,
Sequence: wire.MaxTxInSequenceNum,
})
totalInput := CalcBlockSubsidy(blockHeight, &dagconfig.MainNetParams)
amountPerOutput := totalInput / int64(numOutputs)
remainder := totalInput - amountPerOutput*int64(numOutputs)
for i := uint32(0); i < numOutputs; i++ {
// Ensure the final output accounts for any remainder that might
// be left from splitting the input amount.
amount := amountPerOutput
if i == numOutputs-1 {
amount = amountPerOutput + remainder
}
tx.AddTxOut(&wire.TxOut{
PkScript: OpTrueScript,
Value: amount,
})
}
return tx, nil
}
func TestApplyUTXOChanges(t *testing.T) {
// Create a new database and dag instance to run tests against.
dag, teardownFunc, err := dagSetup("TestApplyUTXOChanges", &dagconfig.MainNetParams)
if err != nil {
t.Fatalf("Failed to setup dag instance: %v", err)
}
defer teardownFunc()
cbTx, err := createCoinbaseTx(1, 1)
if err != nil {
t.Errorf("createCoinbaseTx: %v", err)
}
chainedTx := wire.NewMsgTx(wire.TxVersion)
chainedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{Hash: cbTx.TxHash(), Index: 0},
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
})
chainedTx.AddTxOut(&wire.TxOut{
PkScript: OpTrueScript,
Value: int64(1),
})
//Fake block header
blockHeader := wire.NewBlockHeader(1, []daghash.Hash{dag.genesis.hash}, &daghash.Hash{}, 0, 0)
msgBlock1 := &wire.MsgBlock{
Header: *blockHeader,
Transactions: []*wire.MsgTx{cbTx, chainedTx},
}
block1 := util.NewBlock(msgBlock1)
var node1 blockNode
initBlockNode(&node1, blockHeader, setFromSlice(dag.genesis), dagconfig.MainNetParams.K)
//Checks that dag.applyUTXOChanges fails because we don't allow a transaction to spend another transaction from the same block
_, err = dag.applyUTXOChanges(&node1, block1)
if err == nil {
t.Errorf("applyUTXOChanges expected an error\n")
}
nonChainedTx := wire.NewMsgTx(wire.TxVersion)
nonChainedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{Hash: dag.dagParams.GenesisBlock.Transactions[0].TxHash(), Index: 0},
SignatureScript: nil, //Fake SigScript, because we don't check scripts validity in this test
Sequence: wire.MaxTxInSequenceNum,
})
nonChainedTx.AddTxOut(&wire.TxOut{
PkScript: OpTrueScript,
Value: int64(1),
})
msgBlock2 := &wire.MsgBlock{
Header: *blockHeader,
Transactions: []*wire.MsgTx{cbTx, nonChainedTx},
}
block2 := util.NewBlock(msgBlock2)
var node2 blockNode
initBlockNode(&node2, blockHeader, setFromSlice(dag.genesis), dagconfig.MainNetParams.K)
//Checks that dag.applyUTXOChanges doesn't fail because we all of its transaction are dependant on transactions from previous blocks
_, err = dag.applyUTXOChanges(&node2, block2)
if err != nil {
t.Errorf("applyUTXOChanges: %v", err)
}
}
func TestDiffFromTx(t *testing.T) {
fus := &fullUTXOSet{
utxoCollection: utxoCollection{},
}
cbTx, err := createCoinbaseTx(1, 1)
if err != nil {
t.Errorf("createCoinbaseTx: %v", err)
}
fus.addTx(cbTx, 1)
node := &blockNode{height: 2} //Fake node
cbOutpoint := wire.OutPoint{Hash: cbTx.TxHash(), Index: 0}
tx := wire.NewMsgTx(wire.TxVersion)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: cbOutpoint,
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
})
tx.AddTxOut(&wire.TxOut{
PkScript: OpTrueScript,
Value: int64(1),
})
diff, err := fus.diffFromTx(tx, node)
if err != nil {
t.Errorf("diffFromTx: %v", err)
}
if !reflect.DeepEqual(diff.toAdd, utxoCollection{
wire.OutPoint{Hash: tx.TxHash(), Index: 0}: newUTXOEntry(tx.TxOut[0], false, 2),
}) {
t.Errorf("diff.toAdd doesn't have the expected values")
}
if !reflect.DeepEqual(diff.toRemove, utxoCollection{
wire.OutPoint{Hash: cbTx.TxHash(), Index: 0}: newUTXOEntry(cbTx.TxOut[0], true, 1),
}) {
t.Errorf("diff.toRemove doesn't have the expected values")
}
//Test that we get an error if we don't have the outpoint inside the utxo set
invalidTx := wire.NewMsgTx(wire.TxVersion)
invalidTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{Hash: daghash.Hash{}, Index: 0},
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
})
invalidTx.AddTxOut(&wire.TxOut{
PkScript: OpTrueScript,
Value: int64(1),
})
_, err = fus.diffFromTx(invalidTx, node)
if err == nil {
t.Errorf("diffFromTx: expected an error but got <nil>")
}
//Test that we get an error if the outpoint is inside diffUTXOSet's toRemove
dus := newDiffUTXOSet(fus, &utxoDiff{
toAdd: utxoCollection{},
toRemove: utxoCollection{},
})
dus.addTx(tx, 2)
_, err = dus.diffFromTx(tx, node)
if err == nil {
t.Errorf("diffFromTx: expected an error but got <nil>")
}
}

View File

@ -6,6 +6,7 @@ package blockdag
import (
"sync"
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/wire"
)
@ -112,7 +113,7 @@ func (v *VirtualBlock) TipHashes() []daghash.Hash {
// SelectedTipHash returns the hash of the selected tip of the virtual block.
func (v *VirtualBlock) SelectedTipHash() daghash.Hash {
return v.SelectedTip().hash;
return v.SelectedTip().hash
}
// GetUTXOEntry returns the requested unspent transaction output. The returned
@ -121,5 +122,5 @@ func (v *VirtualBlock) SelectedTipHash() daghash.Hash {
// This function is safe for concurrent access. However, the returned entry (if
// any) is NOT.
func (v *VirtualBlock) GetUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) {
return v.utxoSet.getUTXOEntry(outPoint)
return v.utxoSet.get(outPoint)
}