From 8a29adb0cd46c4d1a1fe436876c81075831ee312 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 13 May 2022 18:34:12 -0500 Subject: [PATCH] standalone: Add transaction sanity check. 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. --- blockchain/standalone/README.md | 4 + blockchain/standalone/doc.go | 1 + blockchain/standalone/error.go | 22 +++++- blockchain/standalone/error_test.go | 7 +- blockchain/standalone/tx.go | 83 ++++++++++++++++++++- blockchain/standalone/tx_test.go | 111 +++++++++++++++++++++++++++- 6 files changed, 224 insertions(+), 4 deletions(-) diff --git a/blockchain/standalone/README.md b/blockchain/standalone/README.md index 2c941e992a..a15cbcd7c7 100644 --- a/blockchain/standalone/README.md +++ b/blockchain/standalone/README.md @@ -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 diff --git a/blockchain/standalone/doc.go b/blockchain/standalone/doc.go index 02c87c5a5a..c36477709e 100644 --- a/blockchain/standalone/doc.go +++ b/blockchain/standalone/doc.go @@ -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 diff --git a/blockchain/standalone/error.go b/blockchain/standalone/error.go index 4583c53b98..a3edcdb457 100644 --- a/blockchain/standalone/error.go +++ b/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. @@ -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. diff --git a/blockchain/standalone/error_test.go b/blockchain/standalone/error_test.go index 88bd3e549f..dc6b041535 100644 --- a/blockchain/standalone/error_test.go +++ b/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. @@ -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)) diff --git a/blockchain/standalone/tx.go b/blockchain/standalone/tx.go index a42505d74d..08439f38b3 100644 --- a/blockchain/standalone/tx.go +++ b/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" @@ -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 ( @@ -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 +} diff --git a/blockchain/standalone/tx_test.go b/blockchain/standalone/tx_test.go index 51447cb536..d8d18b5dcb 100644 --- a/blockchain/standalone/tx_test.go +++ b/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" @@ -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 + } + } +}