Skip to content

Commit

Permalink
standalone: Add transaction sanity check.
Browse files Browse the repository at this point in the history
This adds a new exported function to the blockchain/standalone module
named CheckTransactionSanity which can be used to perform basic
transaction sanity checks.

It also updates the documentation and includes comprehensive tests.

The primary motivation for this change is that there are several
consumers that currently make use of this functionality and it lives in
the blockchain package which is slated to made internal thereby would
otherwise become inaccessible to external consumers.

Another nice benefit of making this logic available via the
blockchain/standalone module is that it intentionally requires way less
dependencies which is ideal for consumers.
  • Loading branch information
davecgh committed May 13, 2022
1 parent bbccddf commit 8a29adb
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 4 deletions.
4 changes: 4 additions & 0 deletions blockchain/standalone/README.md
Expand Up @@ -34,6 +34,10 @@ The provided functions fall into the following categories:
- Stake vote subsidy for a given height
- Treasury subsidy for a given height and number of votes
- Coinbase transaction identification
- Merkle tree inclusion proofs
- Generate an inclusion proof for a given tree and leaf index
- Verify a leaf is a member of the tree at a given index via the proof
- Transaction sanity checking

## Installation and Updating

Expand Down
1 change: 1 addition & 0 deletions blockchain/standalone/doc.go
Expand Up @@ -35,6 +35,7 @@ The provided functions fall into the following categories:
- Merkle tree inclusion proofs
- Generate an inclusion proof for a given tree and leaf index
- Verify a leaf is a member of the tree at a given index via the proof
- Transaction sanity checking
Errors
Expand Down
22 changes: 21 additions & 1 deletion blockchain/standalone/error.go
@@ -1,4 +1,4 @@
// Copyright (c) 2019-2020 The Decred developers
// Copyright (c) 2019-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -23,6 +23,26 @@ const (
// ErrInvalidTSpendExpiry indicates that an invalid expiry was
// provided when calculating the treasury spend voting window.
ErrInvalidTSpendExpiry = ErrorKind("ErrInvalidTSpendExpiry")

// ErrNoTxInputs indicates a transaction does not have any inputs. A valid
// transaction must have at least one input.
ErrNoTxInputs = ErrorKind("ErrNoTxInputs")

// ErrNoTxOutputs indicates a transaction does not have any outputs. A
// valid transaction must have at least one output.
ErrNoTxOutputs = ErrorKind("ErrNoTxOutputs")

// ErrTxTooBig indicates a transaction exceeds the maximum allowed size when
// serialized.
ErrTxTooBig = ErrorKind("ErrTxTooBig")

// ErrBadTxOutValue indicates an output value for a transaction is
// invalid in some way such as being out of range.
ErrBadTxOutValue = ErrorKind("ErrBadTxOutValue")

// ErrDuplicateTxInputs indicates a transaction references the same
// input more than once.
ErrDuplicateTxInputs = ErrorKind("ErrDuplicateTxInputs")
)

// Error satisfies the error interface and prints human-readable errors.
Expand Down
7 changes: 6 additions & 1 deletion blockchain/standalone/error_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2019-2021 The Decred developers
// Copyright (c) 2019-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -19,6 +19,11 @@ func TestErrorKindStringer(t *testing.T) {
{ErrUnexpectedDifficulty, "ErrUnexpectedDifficulty"},
{ErrHighHash, "ErrHighHash"},
{ErrInvalidTSpendExpiry, "ErrInvalidTSpendExpiry"},
{ErrNoTxInputs, "ErrNoTxInputs"},
{ErrNoTxOutputs, "ErrNoTxOutputsv"},
{ErrTxTooBig, "ErrTxTooBig"},
{ErrBadTxOutValue, "ErrBadTxOutValue"},
{ErrDuplicateTxInputs, "ErrDuplicateTxInputs"},
}

t.Logf("Running %d tests", len(tests))
Expand Down
83 changes: 82 additions & 1 deletion blockchain/standalone/tx.go
@@ -1,11 +1,12 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2020 The Decred developers
// Copyright (c) 2015-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package standalone

import (
"fmt"
"math"

"github.com/decred/dcrd/chaincfg/chainhash"
Expand All @@ -21,6 +22,16 @@ const (
opTAdd = 0xc1
opTSpend = 0xc2
opTGen = 0xc3

// These constants are defined here to avoid a dependency on dcrutil. They
// are used in consensus code which can't be changed without a vote anyway,
// so not referring to them directly via dcrutil is safe.
//
// atomsPerCoin is the number of atoms in one coin.
//
// maxAtoms is the maximum transaction amount allowed in atoms.
atomsPerCoin = 1e8
maxAtoms = 21e6 * atomsPerCoin
)

var (
Expand Down Expand Up @@ -130,3 +141,73 @@ func IsTreasuryBase(tx *wire.MsgTx) bool {

return isNullOutpoint(tx)
}

// CheckTransactionSanity performs some preliminary checks on a transaction to
// ensure it is sane. These checks are context free.
func CheckTransactionSanity(tx *wire.MsgTx, maxTxSize uint64) error {
// A transaction must have at least one input.
if len(tx.TxIn) == 0 {
return ruleError(ErrNoTxInputs, "transaction has no inputs")
}

// A transaction must have at least one output.
if len(tx.TxOut) == 0 {
return ruleError(ErrNoTxOutputs, "transaction has no outputs")
}

// A transaction must not exceed the maximum allowed size when serialized.
serializedTxSize := uint64(tx.SerializeSize())
if serializedTxSize > maxTxSize {
str := fmt.Sprintf("serialized transaction is too big - got %d, max %d",
serializedTxSize, maxTxSize)
return ruleError(ErrTxTooBig, str)
}

// Ensure the transaction amounts are in range. Each transaction output
// must not be negative or more than the max allowed per transaction. Also,
// the total of all outputs must abide by the same restrictions. All
// amounts in a transaction are in a unit value known as an atom. One
// Decred is a quantity of atoms as defined by the AtomsPerCoin constant.
var totalAtoms int64
for _, txOut := range tx.TxOut {
atoms := txOut.Value
if atoms < 0 {
str := fmt.Sprintf("transaction output has negative value of %v",
atoms)
return ruleError(ErrBadTxOutValue, str)
}
if atoms > maxAtoms {
str := fmt.Sprintf("transaction output value of %v is higher than "+
"max allowed value of %v", atoms, maxAtoms)
return ruleError(ErrBadTxOutValue, str)
}

// Two's complement int64 overflow guarantees that any overflow is
// detected and reported. This is impossible for Decred, but perhaps
// possible if an alt increases the total money supply.
totalAtoms += atoms
if totalAtoms < 0 {
str := fmt.Sprintf("total value of all transaction outputs "+
"exceeds max allowed value of %v", maxAtoms)
return ruleError(ErrBadTxOutValue, str)
}
if totalAtoms > maxAtoms {
str := fmt.Sprintf("total value of all transaction outputs is %v "+
"which is higher than max allowed value of %v", totalAtoms,
maxAtoms)
return ruleError(ErrBadTxOutValue, str)
}
}

// Check for duplicate transaction inputs.
existingTxOut := make(map[wire.OutPoint]struct{})
for _, txIn := range tx.TxIn {
if _, exists := existingTxOut[txIn.PreviousOutPoint]; exists {
str := "transaction contains duplicate inputs"
return ruleError(ErrDuplicateTxInputs, str)
}
existingTxOut[txIn.PreviousOutPoint] = struct{}{}
}

return nil
}
111 changes: 110 additions & 1 deletion blockchain/standalone/tx_test.go
@@ -1,11 +1,13 @@
// Copyright (c) 2019-2020 The Decred developers
// Copyright (c) 2019-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package standalone

import (
"bytes"
"encoding/hex"
"errors"
"testing"

"github.com/decred/dcrd/wire"
Expand Down Expand Up @@ -280,3 +282,110 @@ func TestIsTreasurybaseTx(t *testing.T) {
}
}
}

// TestCheckTransactionSanity ensures transaction sanity checking works as
// intended.
func TestCheckTransactionSanity(t *testing.T) {
// Create a base transaction that is further manipulated in the tests below
// to test error conditions.
//
// This is mainnet block 373, tx[5] (two inputs, two outputs).
txHex := "010000000201261057a5ecaf6edede86c5446c62f067f30d654117668325090" +
"9ac3e45bec00100000000ffffffff03c65ad19cb990cc916e38dc94f0255f344c5e9" +
"b7af3b69bfa19931f6027e44c0100000000ffffffff02c1c57600000000000000197" +
"6a914e07c0b2a499312f5d95e3bd4f126e618087a15a588ac402c420600000000000" +
"01976a91411f2b3135e457259009bdd14cfcb942eec58bd7a88ac000000000000000" +
"0023851f6050000000073010000040000006a473044022009ff5aed5d2e5eeec8931" +
"9d0a700b7abdf842e248641804c82dee17df446c24202207c252cc36199ea8a6cc71" +
"d2252a3f7e61f9cce272dff82c5818e3bf08167e3a6012102773925f9ee53837aa0e" +
"fba2212f71ee8ab20aeb603fa7324a8c2555efe5c482709ec0e01000000002501000" +
"0050000006a47304402201165136a2b792cc6d7e75f576ed64e1919cbf954afb989f" +
"8590844a628e58def02206ba7e60f5ae9810794297359cc883e7ff97ecd21bc7177f" +
"cc668a84f64a4b9120121026a4151513b4e6650e3d213451037cd6b78ed829d12ed1" +
"d43d5d34ce0834831e9"
txBytes, err := hex.DecodeString(txHex)
if err != nil {
t.Fatalf("unexpected err parsing base tx hex: %v", err)
}
var baseTx wire.MsgTx
if err := baseTx.FromBytes(txBytes); err != nil {
t.Fatalf("nexpected err parsing base tx: %v", err)
}

const maxTxSize = 393216
tests := []struct {
name string // test description
tx *wire.MsgTx // transaction to test
err error // expected error
}{{
name: "ok",
tx: &baseTx,
err: nil,
}, {
name: "transaction has no inputs",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxIn = nil
return tx
}(),
err: ErrNoTxInputs,
}, {
name: "transaction has no outputs",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxOut = nil
return tx
}(),
err: ErrNoTxOutputs,
}, {
name: "transaction too big",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxOut[0].PkScript = bytes.Repeat([]byte{0x00}, maxTxSize)
return tx
}(),
err: ErrTxTooBig,
}, {
name: "transaction with negative output amount",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxOut[0].Value = -1
return tx
}(),
err: ErrBadTxOutValue,
}, {
name: "transaction with single output amount > max per tx",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxOut[0].Value = maxAtoms + 1
return tx
}(),
err: ErrBadTxOutValue,
}, {
name: "transaction with outputs sum > max per tx",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxOut[0].Value = maxAtoms
tx.TxOut[1].Value = 1
return tx
}(),
err: ErrBadTxOutValue,
}, {
name: "transaction spending duplicate input",
tx: func() *wire.MsgTx {
tx := baseTx.Copy()
tx.TxIn[1].PreviousOutPoint = tx.TxIn[0].PreviousOutPoint
return tx
}(),
err: ErrDuplicateTxInputs,
}}

for _, test := range tests {
err := CheckTransactionSanity(test.tx, maxTxSize)
if !errors.Is(err, test.err) {
t.Errorf("%q: unexpected err -- got %v, want %v", test.name, err,
test.err)
continue
}
}
}

0 comments on commit 8a29adb

Please sign in to comment.