diff --git a/blockchain/stake/treasury.go b/blockchain/stake/treasury.go index e78b06ee4..8bd029445 100644 --- a/blockchain/stake/treasury.go +++ b/blockchain/stake/treasury.go @@ -20,11 +20,12 @@ const ( TSpendScriptLen = 100 ) -// This file contains the functions that verify that treasury transactions -// strictly adhere to the specified format. +// ----------------------------------------------------------------------------- +// This file contains functions that verify that treasury transactions strictly +// adhere to the specified format. // // == User sends to treasury == -// TxIn: Normal TxIn signature scripts +// TxIn: Normal TxIn signature scripts // TxOut[0] OP_TADD // TxOut[1] optional OP_SSTXCHANGE // @@ -35,241 +36,284 @@ const ( // // == Spend from treasury == // TxIn[0] OP_TSPEND -// TxOut[0] OP_RETURN +// TxOut[0] OP_RETURN OP_DATA_32 [8]{LE encoded input value} [24]{random} // TxOut[1..N] OP_TGEN +// ----------------------------------------------------------------------------- -// checkTAdd verifies that the provided MsgTx is a valid TADD. -// Note: this function does not recognize treasurybase TADDs. -func checkTAdd(mtx *wire.MsgTx) error { - // Require version TxVersionTreasury. - if mtx.Version != wire.TxVersionTreasury { - return stakeRuleError(ErrTAddInvalidTxVersion, - fmt.Sprintf("invalid TADD script version: %v", - mtx.Version)) +// CheckTAdd verifies that the provided transaction satisfies the structural +// requirements to be a valid treasury add transaction. A treasury add +// transaction is one that sends existing funds to the decentralized treasury. +// +// A valid treasury add must have: +// - The transaction version set to [wire.TxVersionTreasury] +// - One or more normal inputs referencing the coins to spend +// - An output with a treasury add script (OP_TADD) +// - An optional second output that must be a stake change script +// (OP_SSTXCHANGE) when present +// - All script versions set to 0 +func CheckTAdd(tx *wire.MsgTx) error { + // The transaction version must be the required treasury version. + if tx.Version != wire.TxVersionTreasury { + str := fmt.Sprintf("treasury add transaction version is %d instead of %d", + tx.Version, wire.TxVersionTreasury) + return stakeRuleError(ErrTAddInvalidTxVersion, str) } - // A TADD consists of one OP_TADD in PkScript[0] followed by 0 or 1 - // stake change outputs. It also requires at least one input. - if !(len(mtx.TxOut) == 1 || len(mtx.TxOut) == 2) || len(mtx.TxIn) < 1 { - return stakeRuleError(ErrTAddInvalidCount, - fmt.Sprintf("invalid TADD script: TxIn %v TxOut %v", - len(mtx.TxIn), len(mtx.TxOut))) + // A treasury add must have at least one input and one or two outputs. + if len(tx.TxIn) < 1 { + const str = "treasury add transaction does not have any inputs" + return stakeRuleError(ErrTAddInvalidCount, str) + } + if len(tx.TxOut) != 1 && len(tx.TxOut) != 2 { + str := fmt.Sprintf("treasury add transaction has %d outputs instead "+ + "of 1 or 2", len(tx.TxOut)) + return stakeRuleError(ErrTAddInvalidCount, str) } // All output scripts must be version 0 and non-empty. const consensusScriptVer = 0 - for k := range mtx.TxOut { - if mtx.TxOut[k].Version != consensusScriptVer { - return stakeRuleError(ErrTAddInvalidVersion, - fmt.Sprintf("invalid script version found "+ - "in TADD TxOut: %v", k)) + for txOutIdx := range tx.TxOut { + txOut := tx.TxOut[txOutIdx] + if txOut.Version != consensusScriptVer { + str := fmt.Sprintf("treasury add transaction output %d script "+ + "version is %d instead of %d", txOutIdx, txOut.Version, + consensusScriptVer) + return stakeRuleError(ErrTAddInvalidVersion, str) } - - if len(mtx.TxOut[k].PkScript) == 0 { - return stakeRuleError(ErrTAddInvalidScriptLength, - fmt.Sprintf("zero script length found in "+ - "TADD: %v", k)) + if len(txOut.PkScript) == 0 { + str := fmt.Sprintf("treasury add transaction output %d script is "+ + "empty", txOutIdx) + return stakeRuleError(ErrTAddInvalidScriptLength, str) } } - // First output must be a TADD - if len(mtx.TxOut[0].PkScript) != 1 { - return stakeRuleError(ErrTAddInvalidLength, - fmt.Sprintf("TADD script length is not 1 byte, got %v", - len(mtx.TxOut[0].PkScript))) + // The first output must be a script that only consists of OP_TADD. + firstTxOut := tx.TxOut[0] + if len(firstTxOut.PkScript) != 1 { + str := fmt.Sprintf("treasury add transaction output 0 script length "+ + "is %d bytes instead of 1 byte", len(firstTxOut.PkScript)) + return stakeRuleError(ErrTAddInvalidLength, str) } - if mtx.TxOut[0].PkScript[0] != txscript.OP_TADD { - return stakeRuleError(ErrTAddInvalidOpcode, - fmt.Sprintf("first output must be a TADD, got 0x%x", - mtx.TxOut[0].PkScript[0])) + if firstTxOut.PkScript[0] != txscript.OP_TADD { + str := fmt.Sprintf("treasury add transaction output 0 script is 0x%x "+ + "instead of OP_TADD (0x%x)", firstTxOut.PkScript[0], + txscript.OP_TADD) + return stakeRuleError(ErrTAddInvalidOpcode, str) } - // Only 1 stake change output allowed. - if len(mtx.TxOut) == 2 { - // Script length has been already verified. - if !IsStakeChangeScript(mtx.TxOut[1].Version, mtx.TxOut[1].PkScript) { - return stakeRuleError(ErrTAddInvalidChange, - "second output must be an OP_SSTXCHANGE script") + // The second output must be a valid stake change output when present. + if len(tx.TxOut) == 2 { + changeTxOut := tx.TxOut[1] + if !IsStakeChangeScript(changeTxOut.Version, changeTxOut.PkScript) { + const str = "treasury add transaction output 1 is not a " + + "stake change script" + return stakeRuleError(ErrTAddInvalidChange, str) } } return nil } -// CheckTAdd exports checkTAdd for testing purposes. -func CheckTAdd(mtx *wire.MsgTx) error { - return checkTAdd(mtx) -} - -// IsTAdd returns true if the provided transaction is a proper TADD. +// IsTAdd returns whether or not the provided transaction satisfies the +// structural requirements to be a valid treasury add transaction. +// +// See the [CheckTAdd] documentation for more details. func IsTAdd(tx *wire.MsgTx) bool { - return checkTAdd(tx) == nil + return CheckTAdd(tx) == nil } -// CheckTSpend verifies if a MsgTx is a valid TSPEND. +// CheckTSpend verifies that the provided transaction satisfies the structural +// requirements to be a valid treasury spend transaction. It returns the +// signature and public key encoded in the first input when the error is nil. +// // This function DOES NOT check the signature or if the public key is a well -// known PI key. This is a convenience function to obtain the signature and -// public key without iterating over the same MsgTx over and over again. The -// return values are signature, public key and an error. -func CheckTSpend(mtx *wire.MsgTx) ([]byte, []byte, error) { - // Require version TxVersionTreasury. - if mtx.Version != wire.TxVersionTreasury { - return nil, nil, stakeRuleError(ErrTSpendInvalidTxVersion, - fmt.Sprintf("invalid TSpend script version: %v", - mtx.Version)) +// known PI key. It also DOES NOT check the input value encoded in the data +// push of the first output matches the input value. +// +// A valid treasury spend must have: +// - The transaction version set to [wire.TxVersionTreasury] +// - A single input with a treasury spend script ( OP_TSPEND) +// - The first output with a 32 byte nulldata script +// (<8-byte LE encoded input value + 24-byte random>) +// - One or more remaining outputs that must be treasury gen scripts (OP_TGEN +// followed by pay-to-pubkey-hash or pay-to-script-hash) +// - All script versions set to 0 +func CheckTSpend(tx *wire.MsgTx) ([]byte, []byte, error) { + // The transaction version must be the required treasury version. + if tx.Version != wire.TxVersionTreasury { + str := fmt.Sprintf("treasury spend transaction version is %d instead "+ + "of %d", tx.Version, wire.TxVersionTreasury) + return nil, nil, stakeRuleError(ErrTSpendInvalidTxVersion, str) } - // A valid TSPEND consists of a single TxIn that contains a signature, - // a public key and an OP_TSPEND opcode. - // - // There must be at least two outputs. The first must contain an - // OP_RETURN followed by a 32 byte data push of a random number. This - // is used to randomize the transaction hash. - // The second output must be a TGEN tagged P2SH or P2PKH script. - if len(mtx.TxIn) != 1 || len(mtx.TxOut) < 2 { - return nil, nil, stakeRuleError(ErrTSpendInvalidLength, - fmt.Sprintf("invalid TSPEND script lengths in: %v "+ - "out: %v", len(mtx.TxIn), len(mtx.TxOut))) + // A treasury spend must have exactly one input and at least two outputs. + if len(tx.TxIn) != 1 { + str := fmt.Sprintf("treasury spend transaction has %d inputs instead "+ + "of 1", len(tx.TxIn)) + return nil, nil, stakeRuleError(ErrTSpendInvalidLength, str) + } + if len(tx.TxOut) < 2 { + str := fmt.Sprintf("treasury spend transaction does not have enough "+ + "outputs (min: %d, have: %d)", 2, len(tx.TxOut)) + return nil, nil, stakeRuleError(ErrTSpendInvalidLength, str) } // All output scripts must be version 0 and non-empty. const consensusScriptVer = 0 - for k, txOut := range mtx.TxOut { + for txOutIdx, txOut := range tx.TxOut { if txOut.Version != consensusScriptVer { - return nil, nil, stakeRuleError(ErrTSpendInvalidVersion, - fmt.Sprintf("invalid script version found in "+ - "TxOut: %v", k)) + str := fmt.Sprintf("treasury spend transaction output %d script "+ + "version is %d instead of %d", txOutIdx, txOut.Version, + consensusScriptVer) + return nil, nil, stakeRuleError(ErrTSpendInvalidVersion, str) } if len(txOut.PkScript) == 0 { - return nil, nil, stakeRuleError(ErrTSpendInvalidScriptLength, - fmt.Sprintf("invalid TxOut script length %v: "+ - "%v", k, len(txOut.PkScript))) + str := fmt.Sprintf("treasury spend transaction output %d script "+ + "is empty", txOutIdx) + return nil, nil, stakeRuleError(ErrTSpendInvalidScriptLength, str) } } - txIn := mtx.TxIn[0].SignatureScript - if !(len(txIn) == TSpendScriptLen && - txIn[0] == txscript.OP_DATA_64 && - txIn[65] == txscript.OP_DATA_33 && - txIn[99] == txscript.OP_TSPEND) { - return nil, nil, stakeRuleError(ErrTSpendInvalidScript, - "TSPEND invalid tspend script") - } + // The single input must have the exact treasury spend script format: + // + // DATA_64 <64-byte schnorr signature> DATA_33 <33-byte pubkey> OP_TSPEND + txIn := tx.TxIn[0].SignatureScript + if len(txIn) != TSpendScriptLen || txIn[0] != txscript.OP_DATA_64 || + txIn[65] != txscript.OP_DATA_33 || txIn[99] != txscript.OP_TSPEND { - // Pull out signature, pubkey. + const str = "treasury spend transaction input 0 script is malformed" + return nil, nil, stakeRuleError(ErrTSpendInvalidScript, str) + } signature := txIn[1 : 1+schnorr.SignatureSize] pubKey := txIn[66 : 66+secp256k1.PubKeyBytesLenCompressed] + + // The public key must adhere to the strict compressed public key encoding. if !txscript.IsStrictCompressedPubKeyEncoding(pubKey) { - return nil, nil, stakeRuleError(ErrTSpendInvalidPubkey, - "TSPEND invalid public key") + str := fmt.Sprintf("treasury spend transaction input 0 public key %x "+ + "does not use strict compressed encoding", pubKey) + return nil, nil, stakeRuleError(ErrTSpendInvalidPubkey, str) } - // Make sure TxOut[0] contains an OP_RETURN followed by a 32 byte data - // push. - if !txscript.IsStrictNullData(mtx.TxOut[0].Version, - mtx.TxOut[0].PkScript, 32) { - return nil, nil, stakeRuleError(ErrTSpendInvalidTransaction, - "First TSPEND output should have been an OP_RETURN "+ - "followed by a 32 byte data push") + // The first output must be an OP_RETURN followed by a 32 byte data push. + firstTxOut := tx.TxOut[0] + if !txscript.IsStrictNullData(firstTxOut.Version, firstTxOut.PkScript, 32) { + const str = "treasury spend transaction output 0 script is not an " + + "OP_RETURN followed by a 32 byte data push" + return nil, nil, stakeRuleError(ErrTSpendInvalidTransaction, str) } - // Verify that the TxOut's contains a P2PKH or P2PKH scripts. - for k, txOut := range mtx.TxOut[1:] { - // All tx outs are tagged with OP_TGEN - if txOut.PkScript[0] != txscript.OP_TGEN { - return nil, nil, stakeRuleError(ErrTSpendInvalidTGen, - fmt.Sprintf("Output %v is not tagged with "+ - "OP_TGEN", k+1)) + // All outputs after the first one must have OP_TGEN tagged p2pkh or p2sh + // scripts. + for txOutIdx, txOut := range tx.TxOut[1:] { + script := txOut.PkScript + if script[0] != txscript.OP_TGEN { + str := fmt.Sprintf("treasury spend transaction output %d script "+ + "is not tagged with OP_TGEN", txOutIdx+1) + return nil, nil, stakeRuleError(ErrTSpendInvalidTGen, str) } - if !(isPubKeyHashScript(txOut.PkScript[1:]) || - isScriptHashScript(txOut.PkScript[1:])) { - - return nil, nil, stakeRuleError(ErrTSpendInvalidSpendScript, - fmt.Sprintf("Output %v is not P2SH or P2PKH", k+1)) + if !isPubKeyHashScript(script[1:]) && !isScriptHashScript(script[1:]) { + str := fmt.Sprintf("treasury spend transaction output %d script "+ + "is not pay-to-script-hash or pay-to-pubkey-hash", txOutIdx+1) + return nil, nil, stakeRuleError(ErrTSpendInvalidSpendScript, str) } } return signature, pubKey, nil } -// checkTSpend verifies if a MsgTx is a valid TSPEND. -func checkTSpend(mtx *wire.MsgTx) error { - _, _, err := CheckTSpend(mtx) - return err -} - -// IsTSpend returns true if the provided transaction is a proper TSPEND. +// IsTSpend returns whether or not the provided transaction satisfies the +// structural requirements to be a valid treasury spend transaction. +// +// See the [CheckTSpend] documentation for more details. func IsTSpend(tx *wire.MsgTx) bool { - return checkTSpend(tx) == nil + _, _, err := CheckTSpend(tx) + return err == nil } -// checkTreasuryBase verifies that the provided MsgTx is a treasury base. -func checkTreasuryBase(mtx *wire.MsgTx) error { - // Require version TxVersionTreasury. - if mtx.Version != wire.TxVersionTreasury { - return stakeRuleError(ErrTreasuryBaseInvalidTxVersion, - fmt.Sprintf("invalid treasurybase script version: %v", - mtx.Version)) +// CheckTreasuryBase verifies that the provided transaction satisfies the +// structural requirements to be a valid treasurybase transaction. +// +// A valid treasurybase must have: +// - The transaction version set to [wire.TxVersionTreasury] +// - A single treasurybase input (no signature script, null prevout) +// - An output with a treasury add script (OP_TADD) +// - An output with a 12 byte nulldata script +// (<4-byte LE encoded height + 8-byte random>) +// - All script versions set to 0 +func CheckTreasuryBase(tx *wire.MsgTx) error { + // The transaction version must be the required treasury version. + if tx.Version != wire.TxVersionTreasury { + str := fmt.Sprintf("treasurybase transaction version is %d instead of %d", + tx.Version, wire.TxVersionTreasury) + return stakeRuleError(ErrTreasuryBaseInvalidTxVersion, str) } - // A TADD consists of one OP_TADD in PkScript[0] followed by an - // OP_RETURN in PkScript[1]. - if len(mtx.TxIn) != 1 || len(mtx.TxOut) != 2 { - return stakeRuleError(ErrTreasuryBaseInvalidCount, - fmt.Sprintf("invalid treasurybase in/out script "+ - "count: %v/%v", len(mtx.TxIn), - len(mtx.TxOut))) + // A treasurybase must have exactly one input and two outputs. + if len(tx.TxIn) != 1 { + str := fmt.Sprintf("treasurybase transaction has %d inputs instead of 1", + len(tx.TxIn)) + return stakeRuleError(ErrTreasuryBaseInvalidCount, str) + } + if len(tx.TxOut) != 2 { + str := fmt.Sprintf("treasurybase transaction has %d output(s) instead "+ + "of 2", len(tx.TxOut)) + return stakeRuleError(ErrTreasuryBaseInvalidCount, str) } - // Ensure that there is no SignatureScript on the zeroth input. - if len(mtx.TxIn[0].SignatureScript) != 0 { - return stakeRuleError(ErrTreasuryBaseInvalidLength, - "treasurybase input 0 contains a script") + // The first input signature script must be empty and its previous output + // must be a null outpoint (max value index, a zero hash, regular tx tree). + if len(tx.TxIn[0].SignatureScript) != 0 { + str := fmt.Sprintf("treasurybase input 0 signature script is %d "+ + "byte(s) instead of 0", len(tx.TxIn[0].SignatureScript)) + return stakeRuleError(ErrTreasuryBaseInvalidLength, str) + } + if !isNullOutpoint(tx) { + prevOut := &tx.TxIn[0].PreviousOutPoint + str := fmt.Sprintf("treasurybase input 0 previous output %s:%d:%d is "+ + "not a null outpoint", prevOut.Hash, prevOut.Index, prevOut.Tree) + return stakeRuleError(ErrTreasuryBaseInvalid, str) } // All output scripts must be version 0. const consensusScriptVer = 0 - for k := range mtx.TxOut { - if mtx.TxOut[k].Version != consensusScriptVer { - return stakeRuleError(ErrTreasuryBaseInvalidVersion, - fmt.Sprintf("invalid script version found in "+ - "treasurybase: output %v", k)) + for txOutIdx, txOut := range tx.TxOut { + if txOut.Version != consensusScriptVer { + str := fmt.Sprintf("treasurybase transaction output %d script "+ + "version is %d instead of %d", txOutIdx, txOut.Version, + consensusScriptVer) + return stakeRuleError(ErrTreasuryBaseInvalidVersion, str) } } - // First output must be a TADD - if len(mtx.TxOut[0].PkScript) != 1 || - mtx.TxOut[0].PkScript[0] != txscript.OP_TADD { - return stakeRuleError(ErrTreasuryBaseInvalidOpcode0, - "first treasurybase output must be a TADD") + // The first output must be a script that only consists of OP_TADD. + firstTxOut := tx.TxOut[0] + if len(firstTxOut.PkScript) != 1 { + str := fmt.Sprintf("treasurybase transaction output 0 script length "+ + "is %d bytes instead of 1 byte", len(firstTxOut.PkScript)) + return stakeRuleError(ErrTreasuryBaseInvalidOpcode0, str) } - - // Required OP_RETURN, OP_DATA_12 <4 bytes le encoded height> - // <8 bytes random> = 14 bytes total. - if len(mtx.TxOut[1].PkScript) != 14 || - mtx.TxOut[1].PkScript[0] != txscript.OP_RETURN || - mtx.TxOut[1].PkScript[1] != txscript.OP_DATA_12 { - return stakeRuleError(ErrTreasuryBaseInvalidOpcode1, - "second treasurybase output must be an OP_RETURN "+ - "OP_DATA_12 data script") + if firstTxOut.PkScript[0] != txscript.OP_TADD { + str := fmt.Sprintf("treasurybase transaction output 0 script is 0x%x "+ + "instead of OP_TADD (0x%x)", firstTxOut.PkScript[0], + txscript.OP_TADD) + return stakeRuleError(ErrTreasuryBaseInvalidOpcode0, str) } - if !isNullOutpoint(mtx) { - return stakeRuleError(ErrTreasuryBaseInvalid, - "invalid treasurybase constants") + // The second output must be an OP_RETURN followed by a 12 byte data push. + opRetTxOut := tx.TxOut[1] + if !txscript.IsStrictNullData(opRetTxOut.Version, opRetTxOut.PkScript, 12) { + const str = "treasurybase transaction output 1 is not an OP_RETURN " + + "followed by a 12 byte data push" + return stakeRuleError(ErrTreasuryBaseInvalidOpcode1, str) } return nil } -// CheckTreasuryBase verifies that the provided MsgTx is a treasury base. This -// is exported for testing purposes. -func CheckTreasuryBase(mtx *wire.MsgTx) error { - return checkTreasuryBase(mtx) -} - -// IsTreasuryBase returns true if the provided transaction is a treasury base -// transaction. +// IsTreasuryBase returns whether or not the provided transaction satisfies the +// structural requirements to be a valid treasurybase transaction. +// +// See the [CheckTreasuryBase] documentation for more details. func IsTreasuryBase(tx *wire.MsgTx) bool { - return checkTreasuryBase(tx) == nil + return CheckTreasuryBase(tx) == nil } diff --git a/blockchain/stake/treasury_test.go b/blockchain/stake/treasury_test.go index 2b6827472..cb9dda465 100644 --- a/blockchain/stake/treasury_test.go +++ b/blockchain/stake/treasury_test.go @@ -1,21 +1,16 @@ -// Copyright (c) 2020-2024 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package stake import ( - "bytes" - "encoding/hex" + "encoding/binary" "errors" - "math" "math/rand" "testing" - "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" - "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/dcrd/wire" @@ -24,145 +19,73 @@ import ( // Private and public keys for tests. var ( // Serialized private key. - //privateKey = []byte{ - // 0x76, 0x87, 0x56, 0x13, 0x94, 0xcc, 0xc6, 0x11, - // 0x01, 0x51, 0xbd, 0x9f, 0x26, 0xd4, 0x22, 0x8e, - // 0xb2, 0xd5, 0x7b, 0xe1, 0x28, 0xc0, 0x36, 0x12, - // 0xe3, 0x9a, 0x84, 0x4a, 0x3e, 0xcd, 0x3c, 0xcf, - //} + // privateKey = hexToBytes("7687561394ccc6110151bd9f26d4228eb2d57be128c036" + + // "12e39a844a3ecd3ccf") // Serialized compressed public key. - publicKey = []byte{ - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, - } + publicKey = hexToBytes("02a4f64586e172c3d9a20cfa6c7ac8fb12f0115b3f69c3c3" + + "5aec933a4c47c7d92c") // Valid signature of chainhash.HashB([]byte("test message")) - validSignature = []byte{ - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - } - - // OP_DATA_64 OP_TSPEND - tspendValidKey = []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 valid public key - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, - 0xc2, // OP_TSPEND - } - - // OP_DATA_64 - tspendNoTSpend = []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 valid public key - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, // No OP_TSPEND - } + validSignature = hexToBytes("776984f68313b1ac629e624af0595bdc09d8ded02bc2" + + "b29fbdb39595e03ac8b0cf818ca536723e6390d3084e0e31c7942229153ce34d8739" + + "29b16088d9e1af43") +) - // nolint: dupword - // - // OP_DATA_64 OP_TSPEND OP_TSPEND - tspendTwoTSpend = []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 valid public key - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, // No OP_TSPEND - 0xc2, // OP_TSPEND - 0xc2, // Extra OP_TSPEND +// opReturnScript returns a provably-pruneable OP_RETURN script with the +// provided data. +func opReturnScript(data []byte) []byte { + builder := txscript.NewScriptBuilder() + script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script() + if err != nil { + panic(err) } - - // OP_DATA_64 OP_TSPEND OP_DATA_1 - tspendTrailingData = []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 valid public key - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, // No OP_TSPEND - 0xc2, // OP_TSPEND - 0x01, // OP_DATA_1, ByteIndex test in CheckTSpend + return script +} + +// treasurybaseOpReturnScript returns a script suitable for use as the second +// output of the treasurybase transaction of a new block. In particular, the +// serialized data used with the OP_RETURN starts with the block height and is +// followed by 8 bytes of cryptographically random data. +func treasurybaseOpReturnScript(blockHeight uint32) []byte { + data := make([]byte, 12) + binary.LittleEndian.PutUint32(data[0:4], blockHeight) + binary.LittleEndian.PutUint64(data[4:12], rand.Uint64()) + return opReturnScript(data) +} + +// treasurySpendOpReturnScript returns a script suitable for use as the first +// output of a treasury spend transaction. In particular, the serialized data +// used with the OP_RETURN starts with the total spend amount and is followed by +// 24 bytes of cryptographically random data. +func treasurySpendOpReturnScript(amount int64) []byte { + data := make([]byte, 32) + binary.LittleEndian.PutUint64(data[0:8], uint64(amount)) + rand.Read(data[8:]) + return opReturnScript(data) +} + +// treasurySpendSignature returns a treasury spend signature script with the +// provided signature and public key. +func treasurySpendSignature(sig, pubKey []byte) []byte { + builder := txscript.NewScriptBuilder() + builder.AddData(sig) + builder.AddData(pubKey) + builder.AddOp(txscript.OP_TSPEND) + script, err := builder.Script() + if err != nil { + panic(err) } -) + return script +} -// generateKeys generates all the keys that are hard coded in this file. -//func generateKeys() { -// key := secp256k1.PrivKeyFromBytes(privateKey) -// pubKey := key.PubKey() -// message := "test message" -// messageHash := chainhash.HashB([]byte(message)) -// signature, err := schnorr.Sign(key, messageHash) -// if err != nil { -// panic(err) -// } -// fmt.Printf("Sig 0x%x: %x\n", len(signature.Serialize()), -// signature.Serialize()) -// fmt.Printf("Public key 0x%x: %x\n", len(pubKey.SerializeCompressed()), -// pubKey.SerializeCompressed()) -// for k, v := range signature.Serialize() { -// if k%8 == 0 { -// fmt.Printf("\n") -// } -// fmt.Printf("0x%02x,", v) -// } -// fmt.Printf("\n") -//} -// -//func init() { -// generateKeys() -// panic("x") -//} +// fakeTreasurySpendSignature returns a signature script that is valid enough to +// pass all checks, but would fail if actually checked. This identification +// funcs in this package do not verify signatures, so valid signatures are not +// required for the tests. +func fakeTreasurySpendSignature() []byte { + return treasurySpendSignature(validSignature, publicKey) +} // newTxOut returns a new transaction output with the given parameters. func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut { @@ -173,1556 +96,643 @@ func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut { } } -// TestTreasuryIsFunctions goes through all valid treasury opcode combinations. +var ( + // opTrueScript is a simple public key script that contains the OP_TRUE + // opcode. + opTrueScript = []byte{txscript.OP_TRUE} + + // p2shOpTrueAddr is a pay-to-script-hash address that can be redeemed with + // [opTrueScript]. + p2shOpTrueAddr = func() *stdaddr.AddressScriptHashV0 { + params := chaincfg.RegNetParams() + addr, err := stdaddr.NewAddressScriptHashV0(opTrueScript, params) + if err != nil { + panic(err) + } + return addr + }() + + // baseTreasuryAddTx is a valid treasury add transaction that includes a + // change output. It is used as a base to be further manipulated in the + // tests. + baseTreasuryAddTx = func() *wire.MsgTx { + changeScriptVer, changeScript := p2shOpTrueAddr.StakeChangeScript() + + tx := wire.NewMsgTx() + tx.Version = wire.TxVersionTreasury + tx.AddTxIn(&wire.TxIn{}) // One input required + tx.AddTxOut(newTxOut(0, 0, []byte{txscript.OP_TADD})) + tx.AddTxOut(newTxOut(1, changeScriptVer, changeScript)) + return tx + }() + + // baseTreasuryBaseTx is a valid treasury base transaction that commits to a + // random height. It is used as a base to be further manipulated in the + // tests. + baseTreasuryBaseTx = func() *wire.MsgTx { + tx := wire.NewMsgTx() + tx.Version = wire.TxVersionTreasury + tx.AddTxIn(&wire.TxIn{ + // Treasurybase transactions have no inputs, so previous outpoint is + // zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint(zeroHash, wire.MaxPrevOutIndex, + wire.TxTreeRegular), + Sequence: wire.MaxTxInSequenceNum, + ValueIn: 0, + BlockHeight: wire.NullBlockHeight, + BlockIndex: wire.NullBlockIndex, + SignatureScript: nil, // Must be nil by consensus. + }) + tx.AddTxOut(newTxOut(0, 0, []byte{txscript.OP_TADD})) + tx.AddTxOut(newTxOut(0, 0, treasurybaseOpReturnScript(rand.Uint32()))) + return tx + }() + + // baseTreasurySpendTx is a valid treasury spend transaction that pays to a + // p2sh script. It is used as a base to be further manipulated in the + // tests. + baseTreasurySpendTx = func() *wire.MsgTx { + const payout = 1e8 + const fee = 5000 + payoutScriptVer, payoutScript := p2shOpTrueAddr.PayFromTreasuryScript() + + tx := wire.NewMsgTx() + tx.Version = wire.TxVersionTreasury + tx.AddTxIn(&wire.TxIn{ + // Treasury spend transactions have no inputs, so previous outpoint + // is zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint(zeroHash, wire.MaxPrevOutIndex, + wire.TxTreeRegular), + Sequence: wire.MaxTxInSequenceNum, + ValueIn: fee + payout, + BlockHeight: wire.NullBlockHeight, + BlockIndex: wire.NullBlockIndex, + SignatureScript: fakeTreasurySpendSignature(), + }) + tx.AddTxOut(newTxOut(0, 0, treasurySpendOpReturnScript(payout))) + tx.AddTxOut(newTxOut(0, payoutScriptVer, payoutScript)) + return tx + }() +) + +// TestTreasuryIsFunctions confirms the various treasury transaction type +// identification functions return the expected results. Each transaction is +// tested against all funcs to help ensure none of them are incorrectly detected +// as any other. func TestTreasuryIsFunctions(t *testing.T) { tests := []struct { - name string - createTx func() *wire.MsgTx - is func(*wire.MsgTx) bool - expected bool - check func(*wire.MsgTx) error + name string // test description + tx *wire.MsgTx // transaction to test + treasuryAdd bool // expected check is treasury add + treasuryBase bool // expected is treasury base + treasurySpend bool // expected is treasury spend }{{ - name: "tadd from user, no change", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - msgTx.AddTxIn(&wire.TxIn{}) // One input required - return msgTx - }, - is: IsTAdd, - expected: true, - check: checkTAdd, + name: "treasury add from user with change", + tx: baseTreasuryAddTx, + treasuryAdd: true, }, { - name: "check tadd from user, no change with istreasurybase", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - msgTx.AddTxIn(&wire.TxIn{}) // One input required - return msgTx - }, - is: IsTreasuryBase, - expected: false, - check: checkTreasuryBase, + name: "treasury add from user with no change", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxOut = tx.TxOut[:1] + return tx + }(), + treasuryAdd: true, }, { - // This is a valid stakebase but NOT a valid TADD. - name: "tadd from user, with OP_RETURN", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - // OP_RETURN - payload := make([]byte, chainhash.HashSize) - _, err = rand.Read(payload) - if err != nil { - panic(err) - } - builder = txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - script, err = builder.Script() - if err != nil { - panic(err) - } - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: []byte{txscript.OP_TRUE}, + // This passes stakebase checks but is NOT a valid TADD. + name: "treasury add from user with OP_RETURN", + tx: func() *wire.MsgTx { + params := chaincfg.RegNetParams() + + const voteSubsidy = 1e8 + const ticketPrice = 2e8 + tx := baseTreasuryBaseTx.Copy() + tx.TxIn[0].ValueIn = voteSubsidy + tx.TxIn[0].SignatureScript = params.StakeBaseSigScript + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *wire.NewOutPoint(zeroHash, 0, wire.TxTreeStake), + Sequence: wire.MaxTxInSequenceNum, + ValueIn: ticketPrice, + BlockHeight: wire.NullBlockHeight, + BlockIndex: wire.NullBlockIndex, + SignatureScript: opTrueScript, }) - return msgTx - }, - is: IsTAdd, - expected: false, - check: checkTAdd, - }, { - name: "tadd from user, with change", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) + if !IsStakeBase(tx) { + panic("transaction does not pass stakebase checks") } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - opTrueScript := []byte{txscript.OP_TRUE} - p2shOpTrueAddr, err := stdaddr.NewAddressScriptHashV0(opTrueScript, - chaincfg.MainNetParams()) - if err != nil { - panic(err) - } - changeScriptVer, changeScript := p2shOpTrueAddr.StakeChangeScript() - msgTx.AddTxOut(newTxOut(1, changeScriptVer, changeScript)) - msgTx.AddTxIn(&wire.TxIn{}) // One input required - return msgTx - }, - is: IsTAdd, - expected: true, - check: checkTAdd, + return tx + }(), }, { - name: "tadd from treasurybase", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - // OP_RETURN - payload := make([]byte, 12) - _, err = rand.Read(payload) - if err != nil { - panic(err) - } - builder = txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - script, err = builder.Script() - if err != nil { - panic(err) - } - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - // treasurybase - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: nil, - }) - - return msgTx - }, - is: IsTreasuryBase, - expected: true, - check: checkTreasuryBase, + name: "treasury add from treasurybase", + tx: baseTreasuryBaseTx, + treasuryBase: true, }, { - name: "check treasury base with tadd", - createTx: func() *wire.MsgTx { - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_TADD) - script, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - // OP_RETURN - payload := make([]byte, 12) - _, err = rand.Read(payload) - if err != nil { - panic(err) - } - builder = txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - script, err = builder.Script() - if err != nil { - panic(err) - } - msgTx.AddTxOut(wire.NewTxOut(0, script)) - - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: nil, - }) - - return msgTx - }, - is: IsTAdd, - expected: false, - check: checkTAdd, + name: "treasury spend p2sh", + tx: baseTreasurySpendTx, + treasurySpend: true, }, { - name: "tspend P2SH", - createTx: func() *wire.MsgTx { - // OP_RETURN <32 byte random> - payload := make([]byte, chainhash.HashSize) - _, err := rand.Read(payload) + name: "treasury spend p2pkh", + tx: func() *wire.MsgTx { + params := chaincfg.RegNetParams() + pkHash := stdaddr.Hash160(publicKey) + p2pkhAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0( + pkHash, params) if err != nil { panic(err) } - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - opretScript, err := builder.Script() + payoutScriptVer, payoutScript := p2pkhAddr.PayFromTreasuryScript() + + tx := baseTreasurySpendTx.Copy() + tx.TxOut[1].Version = payoutScriptVer + tx.TxOut[1].PkScript = payoutScript + return tx + }(), + treasurySpend: true, + }, { + name: "treasury spend invalid output 1 p2pk (not p2sh/p2pkh)", + tx: func() *wire.MsgTx { + // Start with a normal payment script for the p2pk and manually add + // the OP_TGEN prefix since there is no standard method to create + // the pay from treasury script on a p2pk address given it is + // invalid. + params := chaincfg.RegNetParams() + p2pkAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw( + publicKey, params) if err != nil { panic(err) } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, opretScript)) + payoutScriptVer, payScript := p2pkAddr.PaymentScript() + payoutScript := make([]byte, len(payScript)+1) + payoutScript[0] = txscript.OP_TGEN + copy(payoutScript[1:], payScript) + + tx := baseTreasurySpendTx.Copy() + tx.TxOut[1].Version = payoutScriptVer + tx.TxOut[1].PkScript = payoutScript + return tx + }(), + }} - // OP_TGEN - opTrueScript := []byte{txscript.OP_TRUE} - p2shOpTrueAddr, err := stdaddr.NewAddressScriptHashV0(opTrueScript, - chaincfg.MainNetParams()) - if err != nil { - panic(err) - } - genScriptVer, genScript := p2shOpTrueAddr.PayFromTreasuryScript() - msgTx.AddTxOut(newTxOut(0, genScriptVer, genScript)) + for _, test := range tests { + gotTreasuryAdd := IsTAdd(test.tx) + if gotTreasuryAdd != test.treasuryAdd { + t.Errorf("%s: unexpected treasury add result - got %v, want %v", + test.name, gotTreasuryAdd, test.treasuryAdd) + } - // tspend - builder = txscript.NewScriptBuilder() - builder.AddData(validSignature) - builder.AddData(publicKey) - builder.AddOp(txscript.OP_TSPEND) - tspendScript, err := builder.Script() - if err != nil { - panic(err) - } + gotTreasuryBase := IsTreasuryBase(test.tx) + if gotTreasuryBase != test.treasuryBase { + t.Errorf("%s: unexpected treasurybase result - got %v, want %v", + test.name, gotTreasuryBase, test.treasuryBase) + } - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: tspendScript, - }) + gotTreasurySpend := IsTSpend(test.tx) + if gotTreasurySpend != test.treasurySpend { + t.Errorf("%s: unexpected treasury spend result - got %v, want %v", + test.name, gotTreasurySpend, test.treasurySpend) + } + } +} - return msgTx - }, - is: IsTSpend, - expected: true, - check: checkTSpend, +// TestTreasurySpendErrors verifies that all check treasury spend errors can be +// hit and return the proper error. +func TestTreasurySpendErrors(t *testing.T) { + tests := []struct { + name string // test description + tx *wire.MsgTx // transaction to test + expected error // expected error + }{{ + name: "treasury spend invalid tx version", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.Version = 1 + return tx + }(), + expected: ErrTSpendInvalidTxVersion, }, { - name: "tspend invalid output 1 (not P2SH/P2PKH)", - createTx: func() *wire.MsgTx { - // OP_RETURN <32 byte random> - payload := make([]byte, chainhash.HashSize) - _, err := rand.Read(payload) - if err != nil { - panic(err) - } - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - opretScript, err := builder.Script() - if err != nil { - panic(err) - } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, opretScript)) - - // OP_TGEN - privKey := secp256k1.NewPrivateKey(new(secp256k1.ModNScalar).SetInt(1)) - pubKey := privKey.PubKey().SerializeCompressed() - p2pkAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw(pubKey, - chaincfg.MainNetParams()) - if err != nil { - panic(err) - } - p2pkScriptVer, p2pkScript := p2pkAddr.PaymentScript() - script := make([]byte, len(p2pkScript)+1) - script[0] = txscript.OP_TGEN - copy(script[1:], p2pkScript) - msgTx.AddTxOut(newTxOut(0, p2pkScriptVer, script)) - - // tspend - builder = txscript.NewScriptBuilder() - builder.AddData(validSignature) - builder.AddData(publicKey) - builder.AddOp(txscript.OP_TSPEND) - tspendScript, err := builder.Script() - if err != nil { - panic(err) + name: "treasury spend with invalid num inputs", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.TxIn = nil + return tx + }(), + expected: ErrTSpendInvalidLength, + }, { + name: "treasury spend with invalid num outputs", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.TxOut = nil + return tx + }(), + expected: ErrTSpendInvalidLength, + }, { + name: "treasury spend with an invalid script version", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.TxOut[1].Version = 1 + return tx + }(), + expected: ErrTSpendInvalidVersion, + }, { + name: "treasury spend with invalid output - no pubkey script", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.TxOut[1].PkScript = nil + return tx + }(), + expected: ErrTSpendInvalidScriptLength, + }, { + name: "treasury spend invalid input sig script - wrong script length", + tx: func() *wire.MsgTx { + sig := treasurySpendSignature(validSignature, nil) + tx := baseTreasurySpendTx.Copy() + tx.TxIn[0].SignatureScript = sig + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig script invalid - wrong sig len", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + if sig[0] != txscript.OP_DATA_64 { + panic("signature script format changed") } - - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: tspendScript, - }) - - return msgTx - }, - is: IsTSpend, - expected: false, - check: checkTSpend, + sig[0] = txscript.OP_DATA_65 // Wrong length. + return tx + }(), + expected: ErrTSpendInvalidScript, }, { - name: "tspend P2PKH", - createTx: func() *wire.MsgTx { - // OP_RETURN <32 byte random> - payload := make([]byte, chainhash.HashSize) - _, err := rand.Read(payload) - if err != nil { - panic(err) + name: "treasury spend input sig script invalid - wrong pubkey len", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + if sig[65] != txscript.OP_DATA_33 { + panic("signature script format changed") } - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_RETURN) - builder.AddData(payload) - opretScript, err := builder.Script() - if err != nil { - panic(err) + sig[65] = txscript.OP_DATA_34 // Wrong length. + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig invalid - wrong opcode for OP_TSPEND", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + if sig[len(sig)-1] != txscript.OP_TSPEND { + panic("signature script format changed") } - msgTx := wire.NewMsgTx() - msgTx.Version = wire.TxVersionTreasury - msgTx.AddTxOut(wire.NewTxOut(0, opretScript)) - - // OP_TGEN - privKey := secp256k1.NewPrivateKey(new(secp256k1.ModNScalar).SetInt(1)) - pubKey := privKey.PubKey() - pkHash := stdaddr.Hash160(pubKey.SerializeCompressed()) - p2pkhAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0( - pkHash, chaincfg.MainNetParams()) - if err != nil { - panic(err) + sig[len(sig)-1] = txscript.OP_RETURN // Wrong opcode. + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig invalid - no tspend opcode", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + tx.TxIn[0].SignatureScript = sig[:len(sig)-1] + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig invalid - two tspend opcodes", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + sig = append(sig, txscript.OP_TSPEND) + tx.TxIn[0].SignatureScript = sig + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig invalid - trailing data", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + sig := tx.TxIn[0].SignatureScript + sig = append(sig, 0x01) + tx.TxIn[0].SignatureScript = sig + return tx + }(), + expected: ErrTSpendInvalidScript, + }, { + name: "treasury spend input sig script invalid - bad pubkey type", + tx: func() *wire.MsgTx { + pubKey := make([]byte, len(publicKey)) + copy(pubKey, publicKey) + pubKey[0] |= 0x04 + sig := treasurySpendSignature(validSignature, pubKey) + + tx := baseTreasurySpendTx.Copy() + tx.TxIn[0].SignatureScript = sig + return tx + }(), + expected: ErrTSpendInvalidPubkey, + }, { + name: "treasury spend invalid - extra empty output", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + tx.AddTxOut(&wire.TxOut{}) + return tx + }(), + expected: ErrTSpendInvalidScriptLength, + }, { + name: "treasury spend invalid OP_RETURN output - short one byte", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + script := tx.TxOut[0].PkScript + script = script[:len(script)-1] + tx.TxOut[0].PkScript = script + return tx + }(), + expected: ErrTSpendInvalidTransaction, + }, { + name: "treasury spend payment output - wrong opcode for OP_TGEN", + tx: func() *wire.MsgTx { + tx := baseTreasurySpendTx.Copy() + if tx.TxOut[1].PkScript[0] != txscript.OP_TGEN { + panic("payment output format changed") } - genScriptVer, genScript := p2pkhAddr.PayFromTreasuryScript() - msgTx.AddTxOut(newTxOut(0, genScriptVer, genScript)) - - // tspend - builder = txscript.NewScriptBuilder() - builder.AddData(validSignature) - builder.AddData(publicKey) - builder.AddOp(txscript.OP_TSPEND) - tspendScript, err := builder.Script() + tx.TxOut[1].PkScript[0] = txscript.OP_RETURN + return tx + }(), + expected: ErrTSpendInvalidTGen, + }, { + name: "treasury spend payment output - unsupported p2pk", + tx: func() *wire.MsgTx { + // Start with a normal payment script for the p2pk and manually add + // the OP_TGEN prefix since there is no standard method to create + // the pay from treasury script on a p2pk address given it is + // invalid. + params := chaincfg.RegNetParams() + p2pkAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw( + publicKey, params) if err != nil { panic(err) } - - msgTx.AddTxIn(&wire.TxIn{ - // Stakebase transactions have no - // inputs, so previous outpoint is zero - // hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex, wire.TxTreeRegular), - Sequence: wire.MaxTxInSequenceNum, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - SignatureScript: tspendScript, - }) - - return msgTx - }, - is: IsTSpend, - expected: true, - check: checkTSpend, + payoutScriptVer, payScript := p2pkAddr.PaymentScript() + payoutScript := make([]byte, len(payScript)+1) + payoutScript[0] = txscript.OP_TGEN + copy(payoutScript[1:], payScript) + + tx := baseTreasurySpendTx.Copy() + tx.TxOut[1].Version = payoutScriptVer + tx.TxOut[1].PkScript = payoutScript + return tx + }(), + expected: ErrTSpendInvalidSpendScript, }} - for i, test := range tests { - if got := test.is(test.createTx()); got != test.expected { - // Obtain error - err := test.check(test.createTx()) - t.Fatalf("%v %v: failed got %v want %v error %v", - i, test.name, got, test.expected, err) - } - } -} - -var tspendTxInNoPubkey = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: []byte{ - 0xc2, // OP_TSPEND - }, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxInInvalidPubkey is a TxIn with an invalid key on the OP_TSPEND. -var tspendTxInInvalidPubkey = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: []byte{ - 0xc2, // OP_TSPEND - 0x23, // OP_DATA_35 - 0x03, // Valid pubkey version - 0x00, // invalid compressed key - }, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxInInvalidOpcode is a TxIn with an invalid opcode where OP_TSPEND was -// supposed to be. -var tspendTxInInvalidOpcode = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 valid public key - 0x02, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, - 0x6a, // OP_RETURN instead of OP_TSPEND - }, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxInInvalidPubkey2 is a TxIn with an invalid public key on the -// OP_TSPEND. -var tspendTxInInvalidPubkey2 = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: []byte{ - 0x40, // OP_DATA_64 valid signature - 0x77, 0x69, 0x84, 0xf6, 0x83, 0x13, 0xb1, 0xac, - 0x62, 0x9e, 0x62, 0x4a, 0xf0, 0x59, 0x5b, 0xdc, - 0x09, 0xd8, 0xde, 0xd0, 0x2b, 0xc2, 0xb2, 0x9f, - 0xbd, 0xb3, 0x95, 0x95, 0xe0, 0x3a, 0xc8, 0xb0, - 0xcf, 0x81, 0x8c, 0xa5, 0x36, 0x72, 0x3e, 0x63, - 0x90, 0xd3, 0x08, 0x4e, 0x0e, 0x31, 0xc7, 0x94, - 0x22, 0x29, 0x15, 0x3c, 0xe3, 0x4d, 0x87, 0x39, - 0x29, 0xb1, 0x60, 0x88, 0xd9, 0xe1, 0xaf, 0x43, - 0x21, // OP_DATA_33 INVALID public key - 0x00, 0xa4, 0xf6, 0x45, 0x86, 0xe1, 0x72, 0xc3, - 0xd9, 0xa2, 0x0c, 0xfa, 0x6c, 0x7a, 0xc8, 0xfb, - 0x12, 0xf0, 0x11, 0x5b, 0x3f, 0x69, 0xc3, 0xc3, - 0x5a, 0xec, 0x93, 0x3a, 0x4c, 0x47, 0xc7, 0xd9, - 0x2c, - 0xc2, // OP_TSPEND - }, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -var tspendTxOutValidReturn = wire.TxOut{ - Value: 500000000, - Version: 0, - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x20, // OP_DATA_32 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, -} - -var tspendTxOutInvalidReturn = wire.TxOut{ - Value: 500000000, - Version: 0, - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x20, // OP_DATA_32 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1 byte short - }, -} - -// tspendTxInValidPubkey is a TxIn with a public key on the OP_TSPEND. -var tspendTxInValidPubkey = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: tspendValidKey, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxInNoTSpend is a TxIn with a public key but not TSpend opcode. -var tspendTxInNoTSpend = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: tspendNoTSpend, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxInTwoTSpend is a TxIn with a public key but two TSpend opcodes. -var tspendTxInTwoTSpend = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: tspendTwoTSpend, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendTxTrailingData is a TxIn with a public key, one TSpend and an -// OP_DATA_1. -var tspendTxTrailingData = wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - Tree: wire.TxTreeRegular, - }, - SignatureScript: tspendTrailingData, - BlockHeight: wire.NullBlockHeight, - BlockIndex: wire.NullBlockIndex, - Sequence: 0xffffffff, -} - -// tspendInvalidInCount has an invalid TxIn count but a valid TxOut count. -var tspendInvalidInCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{}, - TxOut: []*wire.TxOut{ - {}, // 2 TxOuts is valid - {}, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidOutCount has a valid TxIn count but an invalid TxOut count. -var tspendInvalidOutCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInNoPubkey, - }, - TxOut: []*wire.TxOut{}, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidVersion has an invalid version in an out script. -var tspendInvalidVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInNoPubkey, - }, - TxOut: []*wire.TxOut{ - { - Version: 0, - PkScript: []byte{ - 0x6a, // OP_RETURN - }, - }, - { - Version: 1, // Fail - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidSignature has no publick key in the input script. -var tspendInvalidSignature = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInNoPubkey, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0x6a, // OP_RETURN - }, - }, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidSignature2 has an invalid public key in the input script. -var tspendInvalidSignature2 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInInvalidPubkey, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0x6a, // OP_RETURN - }, - }, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidOpcode has an invalid opcode in the first TxIn. -var tspendInvalidOpcode = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInInvalidOpcode, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0x6a, // OP_RETURN - }, - }, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidPubkey has an invalid public key on the TSPEND. -var tspendInvalidPubkey = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInInvalidPubkey2, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0x6a, // OP_RETURN - }, - }, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidScriptLength has an invalid TxOut that has a zero length. -var tspendInvalidScriptLength = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInValidPubkey, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - {}, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidTokenCount does not have enough tokens in input script. -var tspendInvalidTokenCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInNoTSpend, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidTokenCount2 has too many tokens on input script. -var tspendInvalidTokenCount2 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInTwoTSpend, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidTokenCount3 has trailing data after TSpend. -var tspendInvalidTokenCount3 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxTrailingData, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidTransaction has an invalid hash on the OP_RETURN. -var tspendInvalidTransaction = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInValidPubkey, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutInvalidReturn, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidTGen has an invalid TxOut that isn't tagged with an OP_TGEN. -var tspendInvalidTGen = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInValidPubkey, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - { - PkScript: []byte{ - 0x6a, // OP_RETURN instead of OP_TGEN - }}, - }, - LockTime: 0, - Expiry: 0, -} - -// tspendInvalidP2SH has an invalid TxOut that doesn't have a valid P2SH -// script. -var tspendInvalidP2SH = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - &tspendTxInValidPubkey, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - { - PkScript: []byte{ - 0xc3, // OP_TGEN - 0x00, // Invalid P2SH - }}, - }, - LockTime: 0, - Expiry: 0, -} - -var tspendInvalidTxVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 1, // Invalid version - TxIn: []*wire.TxIn{ - &tspendTxInValidPubkey, - }, - TxOut: []*wire.TxOut{ - &tspendTxOutValidReturn, - }, - LockTime: 0, - Expiry: 0, -} - -func TestTSpendGenerated(t *testing.T) { - rawScript := "03000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0200000000000000000000226a20562ce42e7531d1710ea1ee02628191190ef5152bbbcd23acca864433c4e4e7849cf1052a01000000000018c3a914f5a8302ee8695bf836258b8f2b57b38a0be14e478700000000520000000100f2052a0100000000000000ffffffff64408ea1c04f5e5dd59350847fad8b800887200ae7268da3b70488a605dd5f4ad28e6e240dbd483a8ba46324a047cf0d6c506e6ebb61d93cae6e868b86f31d9bda892103b459ccf3ce4935a676414fd9ec93ecf7c9dad081a52ed6993bf073c627499388c2" - s, err := hex.DecodeString(rawScript) - if err != nil { - t.Fatal(err) - } - var tx wire.MsgTx - err = tx.Deserialize(bytes.NewReader(s)) - if err != nil { - t.Fatalf("Deserialize: %v", err) - } - tx.Version = wire.TxVersionTreasury - - err = checkTSpend(&tx) - if err != nil { - t.Fatalf("checkTSpend: %v", err) - } -} - -func TestTSpendErrors(t *testing.T) { - tests := []struct { - name string - tx *wire.MsgTx - expected error - }{ - { - name: "tspendInvalidOutCount", - tx: tspendInvalidOutCount, - expected: ErrTSpendInvalidLength, - }, - { - name: "tspendInvalidInCount", - tx: tspendInvalidInCount, - expected: ErrTSpendInvalidLength, - }, - { - name: "tspendInvalidVersion", - tx: tspendInvalidVersion, - expected: ErrTSpendInvalidVersion, - }, - { - name: "tspendInvalidSignature", - tx: tspendInvalidSignature, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidSignature2", - tx: tspendInvalidSignature2, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidOpcode", - tx: tspendInvalidOpcode, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidPubkey", - tx: tspendInvalidPubkey, - expected: ErrTSpendInvalidPubkey, - }, - { - name: "tspendInvalidTokenCount", - tx: tspendInvalidTokenCount, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidTokenCount2", - tx: tspendInvalidTokenCount2, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidTokenCount3", - tx: tspendInvalidTokenCount3, - expected: ErrTSpendInvalidScript, - }, - { - name: "tspendInvalidScriptLength", - tx: tspendInvalidScriptLength, - expected: ErrTSpendInvalidScriptLength, - }, - { - name: "tspendInvalidTransaction", - tx: tspendInvalidTransaction, - expected: ErrTSpendInvalidTransaction, - }, - { - name: "tspendInvalidTGen", - tx: tspendInvalidTGen, - expected: ErrTSpendInvalidTGen, - }, - { - name: "tspendInvalidP2SH", - tx: tspendInvalidP2SH, - expected: ErrTSpendInvalidSpendScript, - }, - { - name: "tspendInvalidTxVersion", - tx: tspendInvalidTxVersion, - expected: ErrTSpendInvalidTxVersion, - }, - } - for i, tt := range tests { - test := dcrutil.NewTx(tt.tx) - test.SetTree(wire.TxTreeStake) - test.SetIndex(0) - err := checkTSpend(test.MsgTx()) - if !errors.Is(err, tt.expected) { - t.Errorf("%v: checkTSpend should have returned %v but "+ - "instead returned %v", tt.name, tt.expected, err) + for _, test := range tests { + _, _, err := CheckTSpend(test.tx) + if !errors.Is(err, test.expected) { + t.Errorf("%q: unexpected error -- got %v, want %v", test.name, err, + test.expected) } - if IsTSpend(test.MsgTx()) { - t.Errorf("IsTSpend claimed an invalid tspend is valid"+ - " %v %v", i, tt.name) + if IsTSpend(test.tx) { + t.Errorf("%q: IsTSpend claimed an invalid treasury spend is valid", + test.name) } } } -// taddInvalidOutCount has a valid TxIn count but an invalid TxOut count. -var taddInvalidOutCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{}, - TxOut: []*wire.TxOut{}, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidOutCount2 has a valid TxIn count but an invalid TxOut count. -var taddInvalidOutCount2 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Valid TxIn count - }, - TxOut: []*wire.TxOut{ - {}, - {}, - {}, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidOutCount3 has a valid TxIn count but an invalid TxIn count. -var taddInvalidOutCount3 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{}, - TxOut: []*wire.TxOut{ - {}, - {}, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidVersion has an invalid out script version. -var taddInvalidVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Empty TxIn - }, - TxOut: []*wire.TxOut{ - {Version: 1}, - {Version: 0}, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidScriptLength has a zero script length. -var taddInvalidScriptLength = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Empty TxIn - }, - TxOut: []*wire.TxOut{ - {Version: 0}, - {Version: 0}, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidLength has an invalid out script. -var taddInvalidLength = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Empty TxIn - }, - TxOut: []*wire.TxOut{ - {PkScript: []byte{ - 0xc2, // OP_TSPEND instead of OP_TADD - 0x00, // Fail length test - }}, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidLength has an invalid out script opcode. -var taddInvalidOpcode = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Empty TxIn - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc2, // OP_TSPEND instead of OP_TADD - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidChange has an invalid out chnage script. -var taddInvalidChange = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, // Empty TxIn - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x00, // Not OP_SSTXCHANGE - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// taddInvalidTxVersion has an invalid transaction version. -var taddInvalidTxVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 1, // Invalid - TxIn: []*wire.TxIn{}, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// TestTAddErrors verifies that all TADD errors can be hit and return the -// proper error. -func TestTAddErrors(t *testing.T) { +// TestTreasuryAddErrors verifies that all check treasury add errors can be hit +// and return the proper error. +func TestTreasuryAddErrors(t *testing.T) { tests := []struct { name string tx *wire.MsgTx expected error - }{ - { - name: "taddInvalidOutCount", - tx: taddInvalidOutCount, - expected: ErrTAddInvalidCount, - }, - { - name: "taddInvalidOutCount2", - tx: taddInvalidOutCount2, - expected: ErrTAddInvalidCount, - }, - { - name: "taddInvalidOutCount3", - tx: taddInvalidOutCount3, - expected: ErrTAddInvalidCount, - }, - { - name: "taddInvalidVersion", - tx: taddInvalidVersion, - expected: ErrTAddInvalidVersion, - }, - { - name: "taddInvalidScriptLength", - tx: taddInvalidScriptLength, - expected: ErrTAddInvalidScriptLength, - }, - { - name: "taddInvalidLength", - tx: taddInvalidLength, - expected: ErrTAddInvalidLength, - }, - { - name: "taddInvalidOpcode", - tx: taddInvalidOpcode, - expected: ErrTAddInvalidOpcode, - }, - { - name: "taddInvalidChange", - tx: taddInvalidChange, - expected: ErrTAddInvalidChange, - }, - { - name: "taddInvalidTxVersion", - tx: taddInvalidTxVersion, - expected: ErrTAddInvalidTxVersion, - }, - } - for i, tt := range tests { - test := dcrutil.NewTx(tt.tx) - test.SetTree(wire.TxTreeStake) - test.SetIndex(0) - err := checkTAdd(test.MsgTx()) - if !errors.Is(err, tt.expected) { - t.Errorf("%v: checkTAdd should have returned %v but "+ - "instead returned %v", tt.name, tt.expected, err) + }{{ + name: "treasury add invalid tx version", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.Version = 1 + return tx + }(), + expected: ErrTAddInvalidTxVersion, + }, { + name: "treasury add invalid num outputs - none", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxOut = nil + return tx + }(), + expected: ErrTAddInvalidCount, + }, { + name: "treasury add invalid num outputs - two change outputs", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.AddTxOut(tx.TxOut[1]) + return tx + }(), + expected: ErrTAddInvalidCount, + }, { + name: "treasury add invalid num inputs - none", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxIn = nil + return tx + }(), + expected: ErrTAddInvalidCount, + }, { + name: "treasury add with invalid output - bad script version", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxOut[0].Version = 1 + return tx + }(), + expected: ErrTAddInvalidVersion, + }, { + name: "treasury add with invalid output - missing script", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxOut[0].PkScript = nil + return tx + }(), + expected: ErrTAddInvalidScriptLength, + }, { + name: "treasury add with invalid output - extra trailing byte", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + tx.TxOut[0].PkScript = append(tx.TxOut[0].PkScript, txscript.OP_TRUE) + return tx + }(), + expected: ErrTAddInvalidLength, + }, { + name: "treasury add with invalid output - wrong opcode for OP_TADD", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + if tx.TxOut[0].PkScript[0] != txscript.OP_TADD { + panic("public key script format changed") + } + tx.TxOut[0].PkScript[0] = txscript.OP_TSPEND + return tx + }(), + expected: ErrTAddInvalidOpcode, + }, { + name: "treasury add with invalid output - wrong opcode for change", + tx: func() *wire.MsgTx { + tx := baseTreasuryAddTx.Copy() + if tx.TxOut[1].PkScript[0] != txscript.OP_SSTXCHANGE { + panic("public key script format changed") + } + tx.TxOut[1].PkScript = tx.TxOut[1].PkScript[1:] + return tx + }(), + expected: ErrTAddInvalidChange, + }} + + for _, test := range tests { + err := CheckTAdd(test.tx) + if !errors.Is(err, test.expected) { + t.Errorf("%q: unexpected error -- got %v, want %v", test.name, err, + test.expected) } - if IsTAdd(test.MsgTx()) { - t.Errorf("IsTAdd claimed an invalid tadd is valid"+ - " %v %v", i, tt.name) + if IsTAdd(test.tx) { + t.Errorf("%q: IsTAdd claimed an invalid tadd is valid", test.name) } } } -// treasurybaseInvalidInCount has an invalid TxIn count. -var treasurybaseInvalidInCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{}, - TxOut: []*wire.TxOut{ - {}, - {}, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOutCount has an invalid TxOut count. -var treasurybaseInvalidOutCount = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{}, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidVersion has an invalid out script version. -var treasurybaseInvalidVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - {Version: 0}, - {Version: 2}, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOpcode0 has an invalid out script opcode. -var treasurybaseInvalidOpcode0 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc2, // OP_TSPEND instead of OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x0c, // OP_DATA_12 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOpcode0Len has an invalid out script opcode length. -var treasurybaseInvalidOpcode0Len = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - { - PkScript: nil, // Invalid - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x0c, // OP_DATA_12 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOpcode1 has an invalid out script opcode. -var treasurybaseInvalidOpcode1 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0xc1, // OP_TADD instead of OP_RETURN - 0x0c, // OP_DATA_32 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOpcode1Len has an invalid out script opcode length. -var treasurybaseInvalidOpcode1Len = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: nil, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidOpcodeDataPush has an invalid out script data push in -// script 1 opcode 1. -var treasurybaseInvalidOpcodeDataPush = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - {}, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x05, // OP_DATA_5 instead of OP_DATA_4 - 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalid has invalid in script constants. -var treasurybaseInvalid = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Index: math.MaxUint32 - 1, - }, - }, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x0c, // OP_DATA_12 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalid2 has invalid in script constants. -var treasurybaseInvalid2 = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Index: math.MaxUint32, - Hash: chainhash.Hash{'m', 'o', 'o'}, - }, - }, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x0c, // OP_DATA_12 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidTxVersion has an invalid transaction version. -var treasurybaseInvalidTxVersion = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 1, // Invalid - TxIn: []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Index: math.MaxUint32, - Hash: chainhash.Hash{'m', 'o', 'o'}, - }, - }, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x0c, // OP_DATA_12 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// treasurybaseInvalidLength has an invalid transaction length. -var treasurybaseInvalidLength = &wire.MsgTx{ - SerType: wire.TxSerializeFull, - Version: 3, - TxIn: []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Index: math.MaxUint32, - Hash: chainhash.Hash{'m', 'o', 'o'}, - }, - SignatureScript: []byte{0x00}, - }, - }, - TxOut: []*wire.TxOut{ - { - PkScript: []byte{ - 0xc1, // OP_TADD - }, - }, - { - PkScript: []byte{ - 0x6a, // OP_RETURN - 0x04, // OP_DATA_4 - 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - LockTime: 0, - Expiry: 0, -} - -// TestTreasuryBaseErrors verifies that all treasurybase errors can be hit and -// return the proper error. +// TestTreasuryBaseErrors verifies that all check treasurybase errors can be hit +// and return the proper error. func TestTreasuryBaseErrors(t *testing.T) { tests := []struct { name string tx *wire.MsgTx expected error - }{ - { - name: "treasurybaseInvalidInCount", - tx: treasurybaseInvalidInCount, - expected: ErrTreasuryBaseInvalidCount, - }, - { - name: "treasurybaseInvalidOutCount", - tx: treasurybaseInvalidOutCount, - expected: ErrTreasuryBaseInvalidCount, - }, - { - name: "treasurybaseInvalidVersion", - tx: treasurybaseInvalidVersion, - expected: ErrTreasuryBaseInvalidVersion, - }, - { - name: "treasurybaseInvalidOpcode0", - tx: treasurybaseInvalidOpcode0, - expected: ErrTreasuryBaseInvalidOpcode0, - }, - { - name: "treasurybaseInvalidOpcode0Len", - tx: treasurybaseInvalidOpcode0Len, - expected: ErrTreasuryBaseInvalidOpcode0, - }, - { - name: "treasurybaseInvalidOpcode1", - tx: treasurybaseInvalidOpcode1, - expected: ErrTreasuryBaseInvalidOpcode1, - }, - { - name: "treasurybaseInvalidOpcode1Len", - tx: treasurybaseInvalidOpcode1Len, - expected: ErrTreasuryBaseInvalidOpcode1, - }, - { - name: "treasurybaseInvalidDataPush", - tx: treasurybaseInvalidOpcodeDataPush, - expected: ErrTreasuryBaseInvalidOpcode1, - }, - { - name: "treasurybaseInvalid", - tx: treasurybaseInvalid, - expected: ErrTreasuryBaseInvalid, - }, - { - name: "treasurybaseInvalid2", - tx: treasurybaseInvalid2, - expected: ErrTreasuryBaseInvalid, - }, - { - name: "treasurybaseInvalidTxVersion", - tx: treasurybaseInvalidTxVersion, - expected: ErrTreasuryBaseInvalidTxVersion, - }, - { - name: "treasurybaseInvalidLength", - tx: treasurybaseInvalidLength, - expected: ErrTreasuryBaseInvalidLength, - }, - } - for i, tt := range tests { - test := dcrutil.NewTx(tt.tx) - test.SetTree(wire.TxTreeStake) - test.SetIndex(0) - err := checkTreasuryBase(test.MsgTx()) - if !errors.Is(err, tt.expected) { - t.Errorf("%v: checkTreasuryBase should have returned "+ - "%v but instead returned %v", tt.name, tt.expected, err) + }{{ + name: "treasurybase invalid tx version", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.Version = 1 + return tx + }(), + expected: ErrTreasuryBaseInvalidTxVersion, + }, { + name: "treasurybase invalid num inputs - none", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxIn = nil + return tx + }(), + expected: ErrTreasuryBaseInvalidCount, + }, { + name: "treasurybase invalid num outputs - none", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxOut = nil + return tx + }(), + expected: ErrTreasuryBaseInvalidCount, + }, { + name: "treasurybase invalid num outputs - extra outupt", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxOut = append(tx.TxOut, newTxOut(1, 0, opTrueScript)) + return tx + }(), + expected: ErrTreasuryBaseInvalidCount, + }, { + name: "treasurybase invalid input 0 - non-empty signature script", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxIn[0].SignatureScript = []byte{txscript.OP_TRUE} + return tx + }(), + expected: ErrTreasuryBaseInvalidLength, + }, { + name: "treasurybase invalid output - bad script version", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxOut[1].Version = 2 + return tx + }(), + expected: ErrTreasuryBaseInvalidVersion, + }, { + name: "treasurybase invalid output 0 - wrong opcode for OP_TADD", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + if tx.TxOut[0].PkScript[0] != txscript.OP_TADD { + panic("public key script format changed") + } + tx.TxOut[0].PkScript[0] = txscript.OP_TSPEND + return tx + }(), + expected: ErrTreasuryBaseInvalidOpcode0, + }, { + name: "treasurybase invalid output 0 - extra trailing byte", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxOut[0].PkScript = append(tx.TxOut[0].PkScript, txscript.OP_TRUE) + return tx + }(), + expected: ErrTreasuryBaseInvalidOpcode0, + }, { + name: "treasurybase invalid output 1 - wrong opcode for OP_RETURN", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + if tx.TxOut[1].PkScript[0] != txscript.OP_RETURN { + panic("public key script format changed") + } + tx.TxOut[1].PkScript[0] = txscript.OP_TADD + return tx + }(), + expected: ErrTreasuryBaseInvalidOpcode1, + }, { + name: "treasurybase invalid output 1 - extra trailing byte", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxOut[1].PkScript = append(tx.TxOut[1].PkScript, txscript.OP_TRUE) + return tx + }(), + expected: ErrTreasuryBaseInvalidOpcode1, + }, { + name: "treasurybase invalid output 1 - wrong data push size", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + if tx.TxOut[1].PkScript[1] != txscript.OP_DATA_12 { + panic("public key script format changed") + } + tx.TxOut[1].PkScript[1] = txscript.OP_DATA_11 + return tx + }(), + expected: ErrTreasuryBaseInvalidOpcode1, + }, { + name: "treasurybase invalid input 0 - non-null hash", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxIn[0].PreviousOutPoint.Hash[0] = 0x01 + return tx + }(), + expected: ErrTreasuryBaseInvalid, + }, { + name: "treasurybase invalid input 0 - wrong prev index", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxIn[0].PreviousOutPoint.Index = 1 + return tx + }(), + expected: ErrTreasuryBaseInvalid, + }, { + name: "treasurybase invalid input 0 - wrong prev tree", + tx: func() *wire.MsgTx { + tx := baseTreasuryBaseTx.Copy() + tx.TxIn[0].PreviousOutPoint.Tree = wire.TxTreeStake + return tx + }(), + expected: ErrTreasuryBaseInvalid, + }} + for _, test := range tests { + err := CheckTreasuryBase(test.tx) + if !errors.Is(err, test.expected) { + t.Errorf("%q: unexpected error -- got %v, want %v", test.name, err, + test.expected) } - if IsTreasuryBase(test.MsgTx()) { - t.Errorf("IsTreasuryBase claimed an invalid treasury "+ - "base is valid %v %v", i, tt.name) + if IsTreasuryBase(test.tx) { + t.Errorf("%q: IsTreasuryBase claimed an invalid treasury base is "+ + "valid", test.name) } } } diff --git a/internal/blockchain/checkedmath.go b/internal/blockchain/checkedmath.go new file mode 100644 index 000000000..99faf9e78 --- /dev/null +++ b/internal/blockchain/checkedmath.go @@ -0,0 +1,24 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package blockchain + +// addUnsigned returns the sum of the two unsigned ints of the same size and +// whether or not the result is safe to use (aka no overflow occurred). +func addUnsigned[T ~uint16 | ~uint32 | ~uint64](a, b T) (T, bool) { + sum := a + b + return sum, sum >= a +} + +// addSigned returns the sum of the two signed ints of the same size and whether +// or not the result is safe to use (aka no overflow or underflow occurred). +func addSigned[T ~int16 | ~int32 | ~int64](a, b T) (T, bool) { + // Overflow only occurs when adding a positive value when the sum is <= to + // left summand. Likewise, underflow only occurs when adding a non-positive + // value when the sum is > the left summand. The following is the logical + // negation of the result of testing both conditions at once so the returned + // flag indicates their absence. + sum := a + b + return sum, (sum > a) == (b > 0) +} diff --git a/internal/blockchain/checkedmath_test.go b/internal/blockchain/checkedmath_test.go new file mode 100644 index 000000000..7ce7f81c6 --- /dev/null +++ b/internal/blockchain/checkedmath_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package blockchain + +import ( + "testing" +) + +// testAddUnsigned ensures [addUnsigned] produces the expected results for the +// given supported unsigned type. It uses generics to avoid repeating the tests +// for each type. +func testAddUnsigned[T uint16 | uint32 | uint64](t *testing.T, typ string) { + t.Helper() + + var maxVal = ^T(0) + tests := []struct { + name string // test description + a, b T // unsigned vals to test + sum T // expected sum + ok bool // expected result + }{ + // No overflow cases. + {"zero", 0, 0, 0, true}, + {"max + zero", maxVal, 0, maxVal, true}, + {"zero + max", 0, maxVal, maxVal, true}, + {"small positive", 10, 20, 30, true}, + {"max edge", maxVal - 1, 1, maxVal, true}, + {"max edge rev", 1, maxVal - 1, maxVal, true}, + {"halfmax + halfmax", maxVal / 2, maxVal / 2, maxVal - 1, true}, + {"mid + halfmax", maxVal/2 + 1, maxVal / 2, maxVal, true}, + {"halfmax + mid", maxVal / 2, maxVal/2 + 1, maxVal, true}, + + // Overflow cases. + {"max + 1 overflow exact", maxVal, 1, 0, false}, + {"1 + max overflow exact", 1, maxVal, 0, false}, + {"small overflow", maxVal - 5, 6, 0, false}, + {"small overflow rev", 6, maxVal - 5, 0, false}, + {"mid overflow", maxVal/2 + 1, maxVal/2 + 1, 0, false}, + {"mid + max overflow", maxVal/2 + 1, maxVal, maxVal / 2, false}, + {"max + mid overflow", maxVal, maxVal/2 + 1, maxVal / 2, false}, + } + + for _, test := range tests { + sum, ok := addUnsigned(test.a, test.b) + if sum != test.sum || ok != test.ok { + t.Errorf("%q (%s): unexpected result - got (%v, %v), want (%v, %v)", + test.name, typ, sum, ok, test.sum, test.ok) + } + } +} + +// TestAddUnsigned ensures [addUnsigned] produces the expected results for all +// three supported unsigned int types (uint16, uint32, uint64). +func TestAddUnsigned(t *testing.T) { + testAddUnsigned[uint16](t, "uint16") + testAddUnsigned[uint32](t, "uint32") + testAddUnsigned[uint64](t, "uint64") +} + +// testAddSigned ensures [addSigned] produces the expected results for the given +// supported signed type. It uses generics to avoid repeating the tests for +// each type. +func testAddSigned[T int16 | int32 | int64](t *testing.T, typ string, maxVal T) { + t.Helper() + + minVal := ^maxVal + tests := []struct { + name string // test description + a, b T // signed vals to test + sum T // expected sum + ok bool // expected result + }{ + // No overflow or underflow cases. + {"zero", 0, 0, 0, true}, + {"min + zero", minVal, 0, minVal, true}, + {"zero + min", 0, minVal, minVal, true}, + {"max + zero", maxVal, 0, maxVal, true}, + {"zero + max", 0, maxVal, maxVal, true}, + {"small positive", 10, 20, 30, true}, + {"small negative", -10, -20, -30, true}, + {"mixed small", 100, -50, 50, true}, + {"mixed small rev", -100, 50, -50, true}, + {"min edge", minVal + 1, -1, minVal, true}, + {"max edge", maxVal - 1, 1, maxVal, true}, + {"min + max", minVal, maxVal, -1, true}, + {"max + min", maxVal, minVal, -1, true}, + {"halfmin + halfmin", minVal / 2, minVal / 2, minVal, true}, + {"mid + min", maxVal/2 + 1, minVal, minVal / 2, true}, + {"min + mid", minVal, maxVal/2 + 1, minVal / 2, true}, + + // Overflow cases. + {"max + 1 overflow exact", maxVal, 1, minVal, false}, + {"1 + max overflow exact", 1, maxVal, minVal, false}, + {"small overflow", maxVal - 5, 6, minVal, false}, + {"small overflow rev", 6, maxVal - 5, minVal, false}, + {"pos to neg mid overflow", maxVal/2 + 1, maxVal/2 + 1, minVal, false}, + {"mid + max overflow", maxVal/2 + 1, maxVal, minVal/2 - 1, false}, + {"max + mid overflow", maxVal, maxVal/2 + 1, minVal/2 - 1, false}, + {"max + max overflow", maxVal, maxVal, -2, false}, + + // Underflow cases. + {"min + (-1) underflow exact", minVal, -1, maxVal, false}, + {"-1 + min underflow exact", -1, minVal, maxVal, false}, + {"small underflow", minVal + 5, -6, maxVal, false}, + {"small underflow rev", -6, minVal + 5, maxVal, false}, + {"neg to pos mid underflow", minVal/2 - 1, minVal / 2, maxVal, false}, + {"neg to pos mid underflow rev", minVal / 2, minVal/2 - 1, maxVal, false}, + {"halfmin + min underflow", minVal / 2, minVal, maxVal/2 + 1, false}, + {"min + halfmin underflow", minVal, minVal / 2, maxVal/2 + 1, false}, + {"min + min underflow", minVal, minVal, 0, false}, + } + + for _, test := range tests { + sum, ok := addSigned(test.a, test.b) + if sum != test.sum || ok != test.ok { + t.Errorf("%q (%s): unexpected result - got (%v, %v), want (%v, %v)", + test.name, typ, sum, ok, test.sum, test.ok) + } + } +} + +// TestAddSigned ensures [addSigned] produces the expected results for all three +// supported signed int types (int16, int32, int64). +func TestAddSigned(t *testing.T) { + testAddSigned[int16](t, "int16", 1<<15-1) + testAddSigned[int32](t, "int32", 1<<31-1) + testAddSigned[int64](t, "int64", 1<<63-1) +} diff --git a/internal/blockchain/error.go b/internal/blockchain/error.go index 5382d360b..f4c864cb1 100644 --- a/internal/blockchain/error.go +++ b/internal/blockchain/error.go @@ -477,14 +477,30 @@ const ( // block that is not at a TVI interval. ErrNotTVI = ErrorKind("ErrNotTVI") - // ErrInvalidTSpendWindow indicates that this treasury spend - // transaction is outside of the allowed window. + // ErrInvalidTreasurySpendExpiry indicates that a treasury spend transaction + // has an invalid expiry. + ErrInvalidTreasurySpendExpiry = ErrorKind("ErrInvalidTreasurySpendExpiry") + + // ErrInvalidTSpendWindow indicates that a treasury spend transaction is + // outside of the allowed window. ErrInvalidTSpendWindow = ErrorKind("ErrInvalidTSpendWindow") // ErrNotEnoughTSpendVotes indicates that a treasury spend transaction // does not have enough votes to be included in block. ErrNotEnoughTSpendVotes = ErrorKind("ErrNotEnoughTSpendVotes") + // ErrTooManyTreasurySpendVotes indicates that the number of treasury spend + // votes in a treasury voting window exceeeded maximum allowable number of + // votes. + // + // In practice, this implies there was an unexpected overflow when tallying + // votes since there is not directly an explicit upper bound on the allowed + // votes. Rather, the upper bound is implicit due to the size of the voting + // window and the maximum number of allowed stake votes per block. + // + // This error is not possible to hit at the time this comment was written. + ErrTooManyTreasurySpendVotes = ErrorKind("ErrTooManyTreasurySpendVotes") + // ErrInvalidTSpendValueIn indicates that a treasury spend transaction // ValueIn does not match the encoded copy in the first TxOut. ErrInvalidTSpendValueIn = ErrorKind("ErrInvalidTSpendValueIn") diff --git a/internal/blockchain/error_test.go b/internal/blockchain/error_test.go index 2262e06ed..4dcd630a8 100644 --- a/internal/blockchain/error_test.go +++ b/internal/blockchain/error_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2014 The btcsuite developers -// Copyright (c) 2015-2023 The Decred developers +// Copyright (c) 2015-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -121,8 +121,10 @@ func TestErrorKindStringer(t *testing.T) { {ErrInvalidPiSignature, "ErrInvalidPiSignature"}, {ErrInvalidTVoteWindow, "ErrInvalidTVoteWindow"}, {ErrNotTVI, "ErrNotTVI"}, + {ErrInvalidTreasurySpendExpiry, "ErrInvalidTreasurySpendExpiry"}, {ErrInvalidTSpendWindow, "ErrInvalidTSpendWindow"}, {ErrNotEnoughTSpendVotes, "ErrNotEnoughTSpendVotes"}, + {ErrTooManyTreasurySpendVotes, "ErrTooManyTreasurySpendVotes"}, {ErrInvalidTSpendValueIn, "ErrInvalidTSpendValueIn"}, {ErrTSpendExists, "ErrTSpendExists"}, {ErrInvalidExpenditure, "ErrInvalidExpenditure"}, diff --git a/internal/blockchain/treasury.go b/internal/blockchain/treasury.go index 091b595ea..3c3e7e15d 100644 --- a/internal/blockchain/treasury.go +++ b/internal/blockchain/treasury.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2025 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -976,104 +976,105 @@ func (b *BlockChain) CheckTSpendExists(prevHash, tspend chainhash.Hash) error { return b.checkTSpendExists(prevNode, tspend) } -// getVotes returns yes and no votes for the provided hash. -func getVotes(votes []stake.TreasuryVoteTuple, hash *chainhash.Hash) (yes int, no int) { - if votes == nil { - return - } - - for _, v := range votes { - if !hash.IsEqual(&v.Hash) { - continue - } - - switch v.Vote { - case stake.TreasuryVoteYes: - yes++ - case stake.TreasuryVoteNo: - no++ - default: - // Can't happen. - trsyLog.Criticalf("getVotes: invalid vote 0x%v", v.Vote) - } - } - - return -} - // tspendVotes is a structure that contains a treasury vote tally for a given // window. type tspendVotes struct { start uint32 // Start block end uint32 // End block - yes int // Yes vote tally - no int // No vote tally + yes uint32 // Yes vote tally + no uint32 // No vote tally } -// tSpendCountVotes returns the vote tally for a given tspend up to the -// specified block. Note that this function errors if the block is outside the -// voting window for the given tspend. +// tSpendCountVotes returns the total yes and no vote counts for a given +// treasury spend up to the specified block. +// +// An error is returned if the block is outside the voting window for the given +// treasury spend. +// +// This function is safe for concurrent access. func (b *BlockChain) tSpendCountVotes(prevNode *blockNode, tspend *dcrutil.Tx) (*tspendVotes, error) { - var ( - t tspendVotes - err error - ) + // Shorter version of various parameter for convenience. + tvi := b.chainParams.TreasuryVoteInterval + tvim := b.chainParams.TreasuryVoteIntervalMultiplier expiry := tspend.MsgTx().Expiry - t.start, t.end, err = standalone.CalcTSpendWindow(expiry, - b.chainParams.TreasuryVoteInterval, - b.chainParams.TreasuryVoteIntervalMultiplier) + start, end, err := standalone.CalcTSpendWindow(expiry, tvi, tvim) if err != nil { - return nil, err + return nil, standaloneToChainRuleError(err) } + trsySpendHash := tspend.Hash() nextHeight := prevNode.height + 1 trsyLog.Tracef("Counting votes for treasury spend %s (height %d, voting "+ - "window [%d, %d], expiry %d)", tspend.Hash(), nextHeight, t.start, - t.end, expiry) - - // Ensure tspend is within the window. - if !standalone.InsideTSpendWindow(nextHeight, - expiry, b.chainParams.TreasuryVoteInterval, - b.chainParams.TreasuryVoteIntervalMultiplier) { - err = fmt.Errorf("tspend outside of window: nextHeight %v "+ - "start %v expiry %v", nextHeight, t.start, expiry) - return nil, err - } - - node := prevNode - for { - if node.height < int64(t.start) { - break - } - - // Find SSGen and peel out votes. - var xblock *dcrutil.Block - xblock, err = b.fetchBlockByNode(node) + "window [%d, %d], expiry %d)", trsySpendHash, nextHeight, start, end, + expiry) + + // Ensure the treasury spend is within its valid voting window. + if !standalone.InsideTSpendWindow(nextHeight, expiry, tvi, tvim) { + str := fmt.Sprintf("treasury spend %v at height %d with expiry %d is "+ + "outside of the valid window [%d, %d]", trsySpendHash, nextHeight, + expiry, start, end) + return nil, ruleError(ErrInvalidTSpendWindow, str) + } + + // Tally the total number of yes and no votes in the voting window. + var totalYes, totalNo uint32 + for n := prevNode; n != nil && n.height >= int64(start); n = n.parent { + // Find stake votes and extract treasury spend votes. + var ok bool + var block *dcrutil.Block + block, err = b.fetchBlockByNode(n) if err != nil { // Should not happen. return nil, err } - for _, v := range xblock.STransactions() { - votes, err := stake.CheckSSGenVotes(v.MsgTx()) + for _, stx := range block.MsgBlock().STransactions { + // Nothing to do for transactions that are not stake votes or do not + // contain any treasury spend votes. + votes, err := stake.CheckSSGenVotes(stx) if err != nil { - // Not an SSGEN + // Not a stake vote. + continue + } + if len(votes) == 0 { continue } - // Find our vote bits. - yes, no := getVotes(votes, tspend.Hash()) - t.yes += yes - t.no += no - } + // Find and tally votes for the treasury spend hash. + for _, v := range votes { + if *trsySpendHash != v.Hash { + continue + } - node = node.parent - if node == nil { - break + switch v.Vote { + case stake.TreasuryVoteYes: + totalYes, ok = addUnsigned(totalYes, 1) + if !ok { + str := fmt.Sprintf("yes vote for treasury spend %v at "+ + "height %d causes yes count to overflow", + trsySpendHash, n.height) + return nil, ruleError(ErrTooManyTreasurySpendVotes, str) + } + + case stake.TreasuryVoteNo: + totalNo, ok = addUnsigned(totalNo, 1) + if !ok { + str := fmt.Sprintf("no vote for treasury spend %v at "+ + "height %d causes no count to overflow", + trsySpendHash, n.height) + return nil, ruleError(ErrTooManyTreasurySpendVotes, str) + } + } + } } } - return &t, nil + return &tspendVotes{ + start: start, + end: end, + yes: totalYes, + no: totalNo, + }, nil } // TSpendCountVotes tallies the votes given for the specified tspend during its @@ -1086,20 +1087,18 @@ func (b *BlockChain) tSpendCountVotes(prevNode *blockNode, tspend *dcrutil.Tx) ( // the next block is outside the voting interval. // // This function is safe for concurrent access. -func (b *BlockChain) TSpendCountVotes(blockHash *chainhash.Hash, tspend *dcrutil.Tx) (yesVotes, noVotes int64, err error) { - b.index.RLock() - defer b.index.RUnlock() - - prevNode := b.index.lookupNode(blockHash) +func (b *BlockChain) TSpendCountVotes(blockHash *chainhash.Hash, tspend *dcrutil.Tx) (yesVotes, noVotes uint32, err error) { + prevNode := b.index.LookupNode(blockHash) if prevNode == nil { return 0, 0, unknownBlockError(blockHash) } + tv, err := b.tSpendCountVotes(prevNode, tspend) if err != nil { return 0, 0, err } - return int64(tv.yes), int64(tv.no), nil + return tv.yes, tv.no, nil } // checkTSpendHasVotes verifies that the provided TSpend has enough votes to be diff --git a/internal/blockchain/treasury_test.go b/internal/blockchain/treasury_test.go index e296bca3c..db309a5ee 100644 --- a/internal/blockchain/treasury_test.go +++ b/internal/blockchain/treasury_test.go @@ -505,13 +505,13 @@ func TestTSpendVoteCount(t *testing.T) { t.Fatalf("invalid end block got %v wanted %v", tv.end, end) } - expectedYesVotes := 0 // We voted a bunch of times outside the window + const expectedYesVotes = 0 // We voted a bunch of times outside the window expectedNoVotes := tvi * mul * uint64(params.TicketsPerBlock) - if expectedYesVotes != tv.yes { - t.Fatalf("invalid yes votes got %v wanted %v", expectedYesVotes, tv.yes) + if tv.yes != expectedYesVotes { + t.Fatalf("invalid yes votes got %v wanted %v", tv.yes, expectedYesVotes) } - if expectedNoVotes != uint64(tv.no) { - t.Fatalf("invalid no votes got %v wanted %v", expectedNoVotes, tv.no) + if uint64(tv.no) != expectedNoVotes { + t.Fatalf("invalid no votes got %v wanted %v", tv.no, expectedNoVotes) } // --------------------------------------------------------------------- @@ -614,9 +614,8 @@ func TestTSpendVoteCount(t *testing.T) { if err != nil { t.Fatal(err) } - if int(quorum-1) != tv.yes { - t.Fatalf("unexpected yesVote count got %v wanted %v", - tv.yes, quorum-1) + if uint64(tv.yes) != quorum-1 { + t.Fatalf("unexpected yesVote count got %v wanted %v", tv.yes, quorum-1) } // Hit exact yes vote quorum @@ -647,9 +646,8 @@ func TestTSpendVoteCount(t *testing.T) { if err != nil { t.Fatal(err) } - if int(quorum) != tv.yes { - t.Fatalf("unexpected yesVote count got %v wanted %v", - tv.yes, quorum) + if uint64(tv.yes) != quorum { + t.Fatalf("unexpected yesVote count got %v wanted %v", tv.yes, quorum) } // Verify TSpend can be added exactly on quorum. @@ -2344,7 +2342,7 @@ func TestTreasuryBaseCorners(t *testing.T) { // corruptTreasurybaseValueIn is a munge function which modifies the // provided block by mutating the treasurybase in value. corruptTreasurybaseValueIn := func(b *wire.MsgBlock) { - b.STransactions[0].TxIn[0].ValueIn-- + b.STransactions[0].TxIn[0].ValueIn++ } // corruptTreasurybaseValueOut is a munge function which modifies the @@ -2432,6 +2430,28 @@ func TestTreasuryBaseCorners(t *testing.T) { g.NextBlock("invalidout0", nil, outs[1:], corruptTreasurybaseValueOut) g.RejectTipBlock(ErrTreasurybaseOutValue) + // ------------------------------------------------------------------------- + // Treasury base spends more than input amount. + // ------------------------------------------------------------------------- + + g.SetTip(startTip) + g.NextBlock("invalidout1", nil, outs[1:], func(b *wire.MsgBlock) { + b.STransactions[0].TxOut[1].Value++ + }) + g.RejectTipBlock(ErrSpendTooHigh) + + // ------------------------------------------------------------------------- + // Treasury base spends the correct amount overall, but does not give the + // correct amount to the treasury. + // ------------------------------------------------------------------------- + + g.SetTip(startTip) + g.NextBlock("invalidout2", nil, outs[1:], func(b *wire.MsgBlock) { + b.STransactions[0].TxOut[0].Value-- + b.STransactions[0].TxOut[1].Value++ + }) + g.RejectTipBlock(ErrTreasurybaseOutValue) + // Note we can't hit the following errors in consensus: // * ErrFirstTxNotTreasurybase (missing OP_RETURN) // * ErrFirstTxNotTreasurybase (version) @@ -2853,13 +2873,13 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { numNodes int64 set func(*BlockChain, *blockNode) blockVersion int32 - expectedYesVotes int - expectedNoVotes int + expectedYesVotes uint32 + expectedNoVotes uint32 failure bool }{{ name: "All yes", numNodes: int64(end), - expectedYesVotes: int(tpb) * int(tvi*mul), + expectedYesVotes: uint32(tpb) * uint32(tvi*mul), expectedNoVotes: 0, failure: false, set: func(bc *BlockChain, node *blockNode) { @@ -2887,7 +2907,7 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { name: "All no", numNodes: int64(end), expectedYesVotes: 0, - expectedNoVotes: int(tpb) * int(tvi*mul), + expectedNoVotes: uint32(tpb) * uint32(tvi*mul), failure: true, set: func(bc *BlockChain, node *blockNode) { // Create block. @@ -2979,7 +2999,7 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "All yes quorum", numNodes: int64(end), - expectedYesVotes: int(quorum), + expectedYesVotes: uint32(quorum), expectedNoVotes: 0, failure: false, set: func(bc *BlockChain, node *blockNode) { @@ -3010,7 +3030,7 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "All yes quorum - 1", numNodes: int64(end), - expectedYesVotes: int(quorum - 1), + expectedYesVotes: uint32(quorum - 1), expectedNoVotes: 0, failure: true, set: func(bc *BlockChain, node *blockNode) { @@ -3041,7 +3061,7 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "All yes quorum + 1", numNodes: int64(end), - expectedYesVotes: int(quorum + 1), + expectedYesVotes: uint32(quorum + 1), expectedNoVotes: 0, failure: false, set: func(bc *BlockChain, node *blockNode) { @@ -3072,8 +3092,8 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "Exactly yes required", numNodes: int64(end), - expectedYesVotes: int(requiredVotes), - expectedNoVotes: int(maxVotes) - int(requiredVotes), + expectedYesVotes: uint32(requiredVotes), + expectedNoVotes: maxVotes - uint32(requiredVotes), failure: false, set: func(bc *BlockChain, node *blockNode) { // Create block. @@ -3105,8 +3125,8 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "Yes required - 1", numNodes: int64(end), - expectedYesVotes: int(requiredVotes - 1), - expectedNoVotes: int(maxVotes) - int(requiredVotes-1), + expectedYesVotes: uint32(requiredVotes - 1), + expectedNoVotes: maxVotes - uint32(requiredVotes-1), failure: true, set: func(bc *BlockChain, node *blockNode) { // Create block. @@ -3138,8 +3158,8 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "Yes required + 1", numNodes: int64(end), - expectedYesVotes: int(requiredVotes + 1), - expectedNoVotes: int(maxVotes) - int(requiredVotes+1), + expectedYesVotes: uint32(requiredVotes + 1), + expectedNoVotes: maxVotes - uint32(requiredVotes+1), failure: false, set: func(bc *BlockChain, node *blockNode) { // Create block. @@ -3171,7 +3191,7 @@ func TestTSpendVoteCountSynthetic(t *testing.T) { }, { name: "Exactly yes required with abstain", numNodes: int64(end), - expectedYesVotes: int(requiredVotes), + expectedYesVotes: uint32(requiredVotes), expectedNoVotes: 0, failure: false, set: func(bc *BlockChain, node *blockNode) { diff --git a/internal/blockchain/validate.go b/internal/blockchain/validate.go index 216377cb9..f4f89a30e 100644 --- a/internal/blockchain/validate.go +++ b/internal/blockchain/validate.go @@ -629,32 +629,30 @@ func CheckTransaction(tx *wire.MsgTx, params *chaincfg.Params, flags AgendaFlags // checkProofOfStake ensures that all ticket purchases in the block pay at least // the amount required by the block header stake bits which indicate the target // stake difficulty (aka ticket price) as claimed. -func checkProofOfStake(block *dcrutil.Block, posLimit int64) error { - msgBlock := block.MsgBlock() - for _, staketx := range block.STransactions() { - msgTx := staketx.MsgTx() - if stake.IsSStx(msgTx) { - commitValue := msgTx.TxOut[0].Value - - // Check for underflow block sbits. - if commitValue < msgBlock.Header.SBits { - errStr := fmt.Sprintf("Stake tx %v has a "+ - "commitment value less than the "+ - "minimum stake difficulty specified in"+ - " the block (%v)", staketx.Hash(), - msgBlock.Header.SBits) - return ruleError(ErrNotEnoughStake, errStr) - } +func checkProofOfStake(block *dcrutil.Block, minStakeDiff int64) error { + header := &block.MsgBlock().Header + for _, stx := range block.STransactions() { + msgTx := stx.MsgTx() + if !stake.IsSStx(msgTx) { + continue + } - // Check if it's above the PoS limit. - if commitValue < posLimit { - errStr := fmt.Sprintf("Stake tx %v has a "+ - "commitment value less than the "+ - "minimum stake difficulty for the "+ - "network (%v)", staketx.Hash(), - posLimit) - return ruleError(ErrStakeBelowMinimum, errStr) - } + // The ticket price must not be below the stake difficulty claimed by + // the block header. + ticketPaidAmt := msgTx.TxOut[submissionOutputIdx].Value + if ticketPaidAmt < header.SBits { + str := fmt.Sprintf("ticket %v pays %v below the stake difficulty "+ + "%v committed to by the block header", stx.Hash(), + ticketPaidAmt, header.SBits) + return ruleError(ErrNotEnoughStake, str) + } + + // The ticket price must not be below the network minimum. + if ticketPaidAmt < minStakeDiff { + str := fmt.Sprintf("ticket %v pays %v below the network minimum "+ + "stake difficulty of %v", stx.Hash(), ticketPaidAmt, + minStakeDiff) + return ruleError(ErrStakeBelowMinimum, str) } } @@ -692,6 +690,8 @@ func standaloneToChainRuleError(err error) error { return ruleError(ErrFraudAmountIn, err.Error()) case errors.Is(err, standalone.ErrDuplicateTxInputs): return ruleError(ErrDuplicateTxInputs, err.Error()) + case errors.Is(err, standalone.ErrInvalidTSpendExpiry): + return ruleError(ErrInvalidTreasurySpendExpiry, err.Error()) } return err @@ -2163,9 +2163,9 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode return ruleError(ErrRevocationsMismatch, str) } - // The number of signature operations must be less than the maximum allowed - // per block. - totalSigOps := 0 + // The number of signature operations must not overflow the accumulator and + // be less than the maximum allowed per block. + var totalSigOps uint32 regularTxns := block.Transactions() stakeTxns := block.STransactions() allTxns := make([]*dcrutil.Tx, 0, len(regularTxns)+len(stakeTxns)) @@ -2174,12 +2174,21 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode for _, tx := range allTxns { msgTx := tx.MsgTx() - // We could potentially overflow the accumulator so check for overflow. - lastSigOps := totalSigOps isCoinBase := standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) isSSGen := stake.IsSSGen(msgTx) - totalSigOps += CountSigOps(tx, isCoinBase, isSSGen, isTreasuryEnabled) - if totalSigOps < lastSigOps || totalSigOps > MaxSigOpsPerBlock { + numSigOps, err := countSigOps(tx, isCoinBase, isSSGen, isTreasuryEnabled) + if err != nil { + return err + } + + var ok bool + totalSigOps, ok = addUnsigned(totalSigOps, numSigOps) + if !ok { + str := fmt.Sprintf("tx %v causes block signature operation count "+ + "to overflow", tx.Hash()) + return ruleError(ErrTooManySigOps, str) + } + if totalSigOps > MaxSigOpsPerBlock { str := fmt.Sprintf("block contains too many signature operations "+ "- got %v, max %v", totalSigOps, MaxSigOpsPerBlock) return ruleError(ErrTooManySigOps, str) @@ -3083,6 +3092,68 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, false, 0, prevHeader, isTreasuryEnabled, isAutoRevocationsEnabled) } +// extractTreasurySpendCommitAmount extracts and decodes the amount from a +// treasury spend output commitment script. +// +// NOTE: The caller MUST have already determined that the provided script is the +// script from the first output of a treasury spend and that the script is well +// formed as required or the function may panic. +func extractTreasurySpendCommitAmount(script []byte) int64 { + // A treasury spend commitment output is an OP_RETURN script with a 32-byte + // data push that consists of an 8-byte little-endian amount to commit to + // and a 24-byte random value. Thus, 1 byte for the OP_RETURN + 1 byte for + // the data push means the encoded amount is at offset 2. + const startIdx = 2 + const endIdx = startIdx + 8 + encodedValue := script[startIdx:endIdx] + return int64(binary.LittleEndian.Uint64(encodedValue)) +} + +// checkTreasurySpendInputs performs a series of checks on the inputs to a +// treasury spend transaction. An example of some of the checks include +// verifying the input values are sane and the spend amount commitment in the +// first output matches the input amount. +// +// NOTE: The caller MUST have already determined that the provided transaction +// is a treasury spend or the function may panic. +func checkTreasurySpendInputs(msgTx *wire.MsgTx) error { + // Assert there is at least one input and one output. + if len(msgTx.TxIn) < 1 || len(msgTx.TxOut) < 1 { + panicf("attempt to check treasury spend inputs on tx %s which does "+ + "not appear to be a treasury spend (%d inputs, %d outputs)", + msgTx.TxHash(), len(msgTx.TxIn), len(msgTx.TxOut)) + } + + // Ensure all input amounts are sane. This is only a fast check for + // obviously invalid values. The expenditure policy is enforced separately. + for _, txIn := range msgTx.TxIn { + valueIn := txIn.ValueIn + if valueIn < 0 { + str := fmt.Sprintf("treasury spend has negative value of %v", + valueIn) + return ruleError(ErrBadTxInput, str) + } + if valueIn > dcrutil.MaxAmount { + str := fmt.Sprintf("treasury spend value of %v is higher than "+ + "max allowed value of %v", valueIn, dcrutil.MaxAmount) + return ruleError(ErrBadTxInput, str) + } + } + + // A valid treasury spend must specify the entire amount that the treasury + // is spending in the first input and commit to that amount in the script of + // the first output. + valueIn := msgTx.TxIn[0].ValueIn + commitmentAmt := extractTreasurySpendCommitAmount(msgTx.TxOut[0].PkScript) + if valueIn != commitmentAmt { + str := fmt.Sprintf("treasury spend input value %v does not match "+ + "spend amount commitment %v", valueIn, commitmentAmt) + return ruleError(ErrInvalidTSpendValueIn, str) + } + + return nil +} + // CheckTransactionInputs performs a series of checks on the inputs to a // transaction to ensure they are valid. An example of some of the checks // include verifying all inputs exist, ensuring the coinbase seasoning @@ -3094,21 +3165,20 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, // that value. // // NOTE: The transaction MUST have already been sanity checked with the -// standalone.CheckTransactionSanity function prior to calling this function. +// [standalone.CheckTransactionSanity] function prior to calling this function. func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, checkFraudProof bool, chainParams *chaincfg.Params, prevHeader *wire.BlockHeader, isTreasuryEnabled, isAutoRevocationsEnabled bool, subsidySplitVariant standalone.SubsidySplitVariant) (int64, error) { - // Coinbase transactions have no inputs. + // NOTE: This check MUST come before the coinbase check because a + // treasurybase will be identified as a coinbase as well. msgTx := tx.MsgTx() - if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) { - return 0, nil - } + isTreasuryBase := isTreasuryEnabled && standalone.IsTreasuryBase(msgTx) - // Treasurybase transactions have no inputs. - if isTreasuryEnabled && standalone.IsTreasuryBase(msgTx) { + // Coinbase transactions have no inputs. + if !isTreasuryBase && standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) { return 0, nil } @@ -3169,11 +3239,11 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, // they have a valid signature from a sanctioned key. // // Also keep track of whether or not it is a treasury spend for later. - var isTSpend bool + var isTreasurySpend bool if isTreasuryEnabled { signature, pubKey, err := stake.CheckTSpend(msgTx) - isTSpend = err == nil - if isTSpend { + isTreasurySpend = err == nil + if isTreasurySpend { // The public key used to sign the treasury spend must be one of the // sanctioned pi keys. if !chainParams.PiKeyExists(pubKey) { @@ -3192,26 +3262,74 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, } } + // Perform additional checks on treasury spends such as verifying the input + // values are sane and the spend amount commitment in the first output + // matches the input amount. + if isTreasurySpend { + err := checkTreasurySpendInputs(tx.MsgTx()) + if err != nil { + return 0, err + } + } + // ------------------------------------------------------------------- // Decred general transaction testing (and a few stake exceptions). // ------------------------------------------------------------------- - txHash := tx.Hash() + // sumTotalAtomIn is a convenience func that adds the provided amount to the + // total sum of all inputs while ensuring that the accumulator does not + // overflow. It also ensures the total does not exceed the max allowed per + // transaction. + // + // In practice, at the time this comment is being written, it is not + // possible to overflow the accumulator due to the combination of limits + // placed on the amounts and number of inputs possible per transaction, but + // be safe and check anyway in case that is no longer true at some point in + // the future. var totalAtomIn int64 - for idx, txIn := range msgTx.TxIn { - // Inputs won't exist for stakebase tx, so ignore them. - if isVote && idx == 0 { - // However, do add the reward amount. - _, heightVotingOn := stake.SSGenBlockVotedOn(msgTx) - stakeVoteSubsidy := subsidyCache.CalcStakeVoteSubsidyV3( - int64(heightVotingOn), subsidySplitVariant) - totalAtomIn += stakeVoteSubsidy - continue + sumTotalAtomIn := func(amount int64) error { + var ok bool + totalAtomIn, ok = addSigned(totalAtomIn, amount) + if !ok { + const str = "total value of all transaction inputs overflows " + + "accumulator" + return ruleError(ErrBadTxOutValue, str) + } + if totalAtomIn > dcrutil.MaxAmount { + str := fmt.Sprintf("total value of all transaction inputs is %v "+ + "which is higher than max allowed value of %v", totalAtomIn, + dcrutil.MaxAmount) + return ruleError(ErrBadTxOutValue, str) } - // idx can only be 0 in this case but check it anyway. - if isTSpend && idx == 0 { - totalAtomIn += txIn.ValueIn + return nil + } + + txHash := tx.Hash() + for idx, txIn := range msgTx.TxIn { + // The stakebase of votes, treasurybases, and treasuryspends do not have + // normal inputs, so handle them separately. + // + // Their input value commitments all contribute to the total input sum + // and are safe to use here because they have been proven to commit to + // correct values that are in a valid range previously. + // + // The input values correspond to the following: + // - Stakebases: the stake vote subsidy + // - Treasurybases: the treasury subsidy + // - Treasury spends: the amount to deduct from the treasury + // + // Treasurybases and treasury spends only have a single input, so their + // index can only be 0. That means their input sums could technically + // just be set directly. However, be paranoid and double check in case + // that ever changes in the future. + if idx == 0 && (isVote || isTreasuryBase || isTreasurySpend) { + // The total of all input amounts must not be more than the max + // allowed per transaction. Also, ensure the accumulator does not + // overflow. + if err := sumTotalAtomIn(txIn.ValueIn); err != nil { + return 0, err + } continue } @@ -3372,16 +3490,10 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, return 0, ruleError(ErrBadTxOutValue, str) } - // The total of all outputs must not be more than the max allowed per - // transaction. Also, we could potentially overflow the accumulator so - // check for overflow. - lastAtomIn := totalAtomIn - totalAtomIn += originTxAtom - if totalAtomIn < lastAtomIn || totalAtomIn > dcrutil.MaxAmount { - str := fmt.Sprintf("total value of all transaction inputs is %v "+ - "which is higher than max allowed value of %v", totalAtomIn, - dcrutil.MaxAmount) - return 0, ruleError(ErrBadTxOutValue, str) + // The total of all input amounts must not be more than the max allowed + // per transaction. Also, ensure the accumulator does not overflow. + if err := sumTotalAtomIn(originTxAtom); err != nil { + return 0, err } } @@ -3406,58 +3518,64 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, return txFeeInAtom, nil } -// CountSigOps returns the number of signature operations for all transaction -// input and output scripts in the provided transaction. This uses the -// quicker, but imprecise, signature operation counting mechanism from -// txscript. -func CountSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isSSGen bool, isTreasuryEnabled bool) int { +// countSigOps returns the number of signature operations for all input and +// output scripts in the provided transaction. This uses the quicker, but +// imprecise, signature operation counting mechanism from the script engine. +func countSigOps(tx *dcrutil.Tx, isCoinBase bool, isVote bool, isTreasuryEnabled bool) (uint32, error) { + // Treasurybases do not have any signature operations. msgTx := tx.MsgTx() - - totalSigOps := 0 - if isTreasuryEnabled && stake.IsTreasuryBase(msgTx) { - return totalSigOps + return 0, nil } - if !isCoinBaseTx { - // Accumulate the number of signature operations in all - // transaction inputs. - for i, txIn := range msgTx.TxIn { - // Skip stakebase inputs. - if isSSGen && i == 0 { - continue - } + // Determine which transaction inputs to consider. Coinbase transactions + // and the first input (aka stakebase) of a vote do not have normal input + // scripts to consider. + var txInStartIdx int + txIns := msgTx.TxIn + if isCoinBase { + txIns = nil + } else if isVote && len(txIns) > 0 { + txInStartIdx = 1 + txIns = txIns[txInStartIdx:] + } - numSigOps := txscript.GetSigOpCount(txIn.SignatureScript, - isTreasuryEnabled) - totalSigOps += numSigOps + // Accumulate the number of signature operations in all relevant transaction + // inputs. + var totalSigOps uint32 + var ok bool + for txInIdx, txIn := range txIns { + sigScript := txIn.SignatureScript + numSigOps := txscript.GetSigOpCount(sigScript, isTreasuryEnabled) + totalSigOps, ok = addUnsigned(totalSigOps, uint32(numSigOps)) + if !ok { + str := fmt.Sprintf("input %v:%d signature script causes signature "+ + "operation count to overflow", tx.Hash(), txInIdx+txInStartIdx) + return 0, ruleError(ErrTooManySigOps, str) } } - // Accumulate the number of signature operations in all transaction - // outputs. - for _, txOut := range msgTx.TxOut { - numSigOps := txscript.GetSigOpCount(txOut.PkScript, - isTreasuryEnabled) - totalSigOps += numSigOps + // Accumulate the number of signature operations in all transaction outputs. + for txOutIdx, txOut := range msgTx.TxOut { + numSigOps := txscript.GetSigOpCount(txOut.PkScript, isTreasuryEnabled) + totalSigOps, ok = addUnsigned(totalSigOps, uint32(numSigOps)) + if !ok { + str := fmt.Sprintf("output %v:%d public key script causes "+ + "signature operation count to overflow", tx.Hash(), txOutIdx) + return 0, ruleError(ErrTooManySigOps, str) + } } - return totalSigOps + return totalSigOps, nil } -// CountP2SHSigOps returns the number of signature operations for all input +// countP2SHSigOps returns the number of signature operations for all input // transactions which are of the pay-to-script-hash type. This uses the // precise, signature operation counting mechanism from the script engine which // requires access to the input transaction scripts. -func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, view *UtxoViewpoint, isTreasuryEnabled bool) (int, error) { +func countP2SHSigOps(tx *dcrutil.Tx, isCoinBase bool, isVote bool, view *UtxoViewpoint, isTreasuryEnabled bool) (uint32, error) { // Coinbase transactions have no interesting inputs. - if isCoinBaseTx { - return 0, nil - } - - // Stakebase (SSGen) transactions have no P2SH inputs. Same with SSRtx, - // but they will still pass the checks below. - if isStakeBaseTx { + if isCoinBase { return 0, nil } @@ -3471,42 +3589,44 @@ func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, view } } - // Accumulate the number of signature operations in all transaction - // inputs. - totalSigOps := 0 - for txInIndex, txIn := range msgTx.TxIn { + // The first input (aka stakebase) of votes have no P2SH inputs. + var txInStartIdx int + txIns := msgTx.TxIn + if isVote && len(txIns) > 0 { + txInStartIdx = 1 + txIns = txIns[txInStartIdx:] + } + + // Accumulate the number of signature operations from all pay-to-script-hash + // transaction inputs. + var totalSigOps uint32 + for txInIndex, txIn := range txIns { // Ensure the referenced input transaction is available. txInOutpoint := txIn.PreviousOutPoint utxoEntry := view.LookupEntry(txInOutpoint) if utxoEntry == nil || utxoEntry.IsSpent() { - str := fmt.Sprintf("output %v referenced from "+ - "transaction %s:%d either does not exist or "+ - "has already been spent", txInOutpoint, - tx.Hash(), txInIndex) + str := fmt.Sprintf("output %v referenced from transaction %s:%d "+ + "either does not exist or has already been spent", txInOutpoint, + tx.Hash(), txInIndex+txInStartIdx) return 0, ruleError(ErrMissingTxOut, str) } - // We're only interested in pay-to-script-hash types, so skip - // this input if it's not one. + // Skip inputs that aren't pay-to-script-hash. pkScript := utxoEntry.PkScript() if !txscript.IsPayToScriptHash(pkScript) { continue } - // Count the precise number of signature operations in the - // referenced public key script. + // Count the precise number of signature operations in the referenced + // public key script. + var ok bool sigScript := txIn.SignatureScript numSigOps := txscript.GetPreciseSigOpCount(sigScript, pkScript, isTreasuryEnabled) - - // We could potentially overflow the accumulator so check for - // overflow. - lastSigOps := totalSigOps - totalSigOps += numSigOps - if totalSigOps < lastSigOps { - str := fmt.Sprintf("the public key script from output "+ - "%v contains too many signature operations - "+ - "overflow", txInOutpoint) + totalSigOps, ok = addUnsigned(totalSigOps, uint32(numSigOps)) + if !ok { + str := fmt.Sprintf("output %v public key script causes signature "+ + "operations count to overflow", txInOutpoint) return 0, ruleError(ErrTooManySigOps, str) } } @@ -3514,42 +3634,32 @@ func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, view return totalSigOps, nil } -// checkNumSigOps Checks the number of P2SH signature operations to make -// sure they don't overflow the limits. It takes a cumulative number of sig -// ops as an argument and increments will each call. -func checkNumSigOps(tx *dcrutil.Tx, view *UtxoViewpoint, index int, stakeTree bool, cumulativeSigOps int, isTreasuryEnabled bool) (int, error) { - msgTx := tx.MsgTx() - isSSGen := stake.IsSSGen(msgTx) - isCoinbaseTx := (index == 0) && !stakeTree - numsigOps := CountSigOps(tx, isCoinbaseTx, isSSGen, isTreasuryEnabled) - - // Since the first (and only the first) transaction has already been - // verified to be a coinbase transaction, use (i == 0) && TxTree as an - // optimization for the flag to countP2SHSigOps for whether or not the - // transaction is a coinbase transaction rather than having to do a - // full coinbase check again. - numP2SHSigOps, err := CountP2SHSigOps(tx, isCoinbaseTx, isSSGen, view, - isTreasuryEnabled) +// CountTotalSigOps returns the total number of signature operations for the +// given transaction. This includes all input and output scripts as well as +// signature operations in any redeemed pay-to-script-hash inputs. +func CountTotalSigOps(tx *dcrutil.Tx, isCoinBase, isVote bool, view *UtxoViewpoint, isTreasuryEnabled bool) (uint32, error) { + // Count the number of regular signature operations. + numSigOps, err := countSigOps(tx, isCoinBase, isVote, isTreasuryEnabled) if err != nil { - log.Tracef("CountP2SHSigOps failed; error returned %v", err) return 0, err } - startCumSigOps := cumulativeSigOps - cumulativeSigOps += numsigOps - cumulativeSigOps += numP2SHSigOps + // Count the number of precise pay-to-script-hash signature operations. + numP2SHSigOps, err := countP2SHSigOps(tx, isCoinBase, isVote, view, + isTreasuryEnabled) + if err != nil { + return 0, err + } - // Check for overflow or going over the limits. We have to do - // this on every loop iteration to avoid overflow. - if cumulativeSigOps < startCumSigOps || - cumulativeSigOps > MaxSigOpsPerBlock { - str := fmt.Sprintf("block contains too many signature "+ - "operations - got %v, max %v", cumulativeSigOps, - MaxSigOpsPerBlock) + // Ensure the combined total does not overflow. + totalSigOps, ok := addUnsigned(numSigOps, numP2SHSigOps) + if !ok { + str := fmt.Sprintf("tx %v total signature operations overflow", + tx.Hash()) return 0, ruleError(ErrTooManySigOps, str) } - return cumulativeSigOps, nil + return totalSigOps, nil } // checkStakeBaseAmounts calculates the total amount given as subsidy from @@ -3632,96 +3742,28 @@ func getStakeBaseAmounts(txs []*dcrutil.Tx, view *UtxoViewpoint) (int64, error) return totalOutputs - totalInputs, nil } -// getStakeTreeFees determines the amount of fees for in the stake tx tree of -// some node given a utxo view. -func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, - txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool, - subsidySplitVariant standalone.SubsidySplitVariant) (dcrutil.Amount, error) { - - totalInputs := int64(0) - totalOutputs := int64(0) - for _, tx := range txs { - msgTx := tx.MsgTx() - isSSGen := stake.IsSSGen(msgTx) - isTreasuryBase := isTreasuryEnabled && stake.IsTreasuryBase(msgTx) - isTreasurySpend := isTreasuryEnabled && stake.IsTSpend(msgTx) - - for i, in := range msgTx.TxIn { - // Ignore stakebases. - if isSSGen && i == 0 { - continue - } - - // Ignore treasury spends and treasurybases since they have no - // inputs. - if isTreasuryBase || isTreasurySpend { - continue - } - - txInOutpoint := in.PreviousOutPoint - txInHash := &txInOutpoint.Hash - utxoEntry, exists := view.entries[txInOutpoint] - if !exists || utxoEntry == nil { - str := fmt.Sprintf("couldn't find input tx "+ - "%v for stake tree fee calculation", - txInHash) - return 0, ruleError(ErrTicketUnavailable, str) - } - - originTxAtom := utxoEntry.Amount() - - totalInputs += originTxAtom - } - - for _, out := range msgTx.TxOut { - totalOutputs += out.Value - } - - // For votes, subtract the subsidy to determine actual fees. - if isSSGen { - // Subsidy aligns with the height we're voting on, not with the - // height of the current block. - totalOutputs -= subsidyCache.CalcStakeVoteSubsidyV3(height-1, - subsidySplitVariant) - } - - if isTreasurySpend { - totalOutputs -= msgTx.TxIn[0].ValueIn - } - - if isTreasuryBase { - totalOutputs -= msgTx.TxIn[0].ValueIn - } - } - - if totalInputs < totalOutputs { - str := fmt.Sprintf("negative cumulative fees found in stake " + - "tx tree") - return 0, ruleError(ErrStakeFees, str) - } - - return dcrutil.Amount(totalInputs - totalOutputs), nil -} - // checkTransactionsAndConnect is the local function used to check the // transaction inputs for a transaction list given a predetermined utxo view. // After ensuring the transaction is valid, the transaction is connected to the // utxo view. +// +// It returns the total fees paid by the transactions or 0 when the error is not +// nil. func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node *blockNode, txs []*dcrutil.Tx, view *UtxoViewpoint, stxos *[]spentTxOut, stakeTree bool, - subsidySplitVariant standalone.SubsidySplitVariant) error { + subsidySplitVariant standalone.SubsidySplitVariant) (dcrutil.Amount, error) { isTreasuryEnabled, err := b.isTreasuryAgendaActive(node.parent) if err != nil { - return err + return 0, err } // Determine if the automatic ticket revocations agenda is active as of the // block being checked. isAutoRevocationsEnabled, err := b.isAutoRevocationsAgendaActive(node.parent) if err != nil { - return err + return 0, err } // Perform several checks on the inputs for each transaction. Also @@ -3737,15 +3779,33 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, } totalFees := int64(inputFees) // Stake tx tree carry forward prevHeader := node.parent.Header() - var cumulativeSigOps int + var cumulativeSigOps uint32 for idx, tx := range txs { - // Ensure that the number of signature operations is not beyond - // the consensus limit. - var err error - cumulativeSigOps, err = checkNumSigOps(tx, view, idx, stakeTree, - cumulativeSigOps, isTreasuryEnabled) + // Since the first (and only the first) transaction has already been + // verified to be a coinbase transaction, use (idx == 0) && !stakeTree + // as an optimization rather than having to do a full coinbase check + // again. + isCoinBase := (idx == 0) && !stakeTree + isVote := stakeTree && stake.IsSSGen(tx.MsgTx()) + + // The number of signature operations must not overflow the accumulator + // and be less than the maximum allowed per block. + var ok bool + numSigOps, err := CountTotalSigOps(tx, isCoinBase, isVote, view, + isTreasuryEnabled) if err != nil { - return err + return 0, err + } + cumulativeSigOps, ok = addUnsigned(cumulativeSigOps, numSigOps) + if !ok { + str := fmt.Sprintf("tx %v causes block signature operation count "+ + "to overflow", tx.Hash()) + return 0, ruleError(ErrTooManySigOps, str) + } + if cumulativeSigOps > MaxSigOpsPerBlock { + str := fmt.Sprintf("block contains too many signature operations "+ + "- got %v, max %v", cumulativeSigOps, MaxSigOpsPerBlock) + return 0, ruleError(ErrTooManySigOps, str) } // Perform a series of checks on the inputs to the transaction to ensure @@ -3763,16 +3823,14 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, isTreasuryEnabled, isAutoRevocationsEnabled, subsidySplitVariant) if err != nil { log.Tracef("CheckTransactionInputs failed; error returned: %v", err) - return err + return 0, err } - // Sum the total fees and ensure we don't overflow the - // accumulator. - lastTotalFees := totalFees - totalFees += txFee - if totalFees < lastTotalFees { - return ruleError(ErrBadFees, "total fees for block "+ - "overflows accumulator") + // Sum the total fees and ensure they do overflow the accumulator. + totalFees, ok = addSigned(totalFees, txFee) + if !ok { + const str = "total fees for block overflows accumulator" + return 0, ruleError(ErrBadFees, str) } // Update the view to mark all utxos spent by the transaction as spent @@ -3789,13 +3847,13 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, err := view.connectRegularTransaction(tx, node.height, uint32(idx), inFlightRegularTx, stxos, isTreasuryEnabled) if err != nil { - return err + return 0, err } } else { err := view.connectStakeTransaction(tx, node.height, uint32(idx), stxos, isTreasuryEnabled) if err != nil { - return err + return 0, err } } } @@ -3843,7 +3901,7 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, errStr := fmt.Sprintf("bad coinbase subsidy in input;"+ " got %v, expected %v", coinbaseIn.ValueIn, subsidyWithoutFees) - return ruleError(ErrBadCoinbaseAmountIn, errStr) + return 0, ruleError(ErrBadCoinbaseAmountIn, errStr) } if totalAtomOutRegular > expAtomOut { @@ -3851,7 +3909,7 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, " pays %v which is more than expected value "+ "of %v", node.hash, totalAtomOutRegular, expAtomOut) - return ruleError(ErrBadCoinbaseValue, str) + return 0, ruleError(ErrBadCoinbaseValue, str) } } else { // TxTreeStake // When treasury is enabled check treasurybase value @@ -3862,7 +3920,7 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, str := fmt.Sprintf("empty tx tree stake, "+ "expected treasurybase at height %v", node.height) - return ruleError(ErrNoStakeTx, str) + return 0, ruleError(ErrNoStakeTx, str) } subsidyTax := b.subsidyCache.CalcTreasurySubsidy(node.height, node.voters, isTreasuryEnabled) @@ -3872,30 +3930,30 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, "subsidy in input; got %v, expected %v", treasurybaseIn.ValueIn, subsidyTax) - return ruleError(ErrBadTreasurybaseAmountIn, errStr) + return 0, ruleError(ErrBadTreasurybaseAmountIn, errStr) } } if len(txs) == 0 && node.height < b.chainParams.StakeValidationHeight { - return nil + return dcrutil.Amount(totalFees), nil } if len(txs) == 0 && node.height >= b.chainParams.StakeValidationHeight { str := fmt.Sprintf("empty tx tree stake in block " + "after stake validation height") - return ruleError(ErrNoStakeTx, str) + return 0, ruleError(ErrNoStakeTx, str) } err := checkStakeBaseAmounts(b.subsidyCache, node.height, txs, view, subsidySplitVariant) if err != nil { - return err + return 0, err } totalAtomOutStake, err := getStakeBaseAmounts(txs, view) if err != nil { - return err + return 0, err } var expAtomOut int64 @@ -3911,11 +3969,11 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, str := fmt.Sprintf("stakebase transactions for block pays %v "+ "which is more than expected value of %v", totalAtomOutStake, expAtomOut) - return ruleError(ErrBadStakebaseValue, str) + return 0, ruleError(ErrBadStakebaseValue, str) } } - return nil + return dcrutil.Amount(totalFees), nil } // consensusScriptVerifyFlags returns the script flags that must be used when @@ -3953,6 +4011,9 @@ func (b *BlockChain) consensusScriptVerifyFlags(node *blockNode) (txscript.Scrip // block. It verifies that it is on a TVI, is within the correct window, has // not been mined before and that it doesn't overspend the treasury. This // function assumes that the treasury agenda is enabled. +// +// The caller MUST have already have already called [checkTreasurySpendInputs] +// on all treasury spends in the block and prior to calling this method. func (b *BlockChain) tspendChecks(prevNode *blockNode, block *dcrutil.Block) error { blockHeight := prevNode.height + 1 isTVI := standalone.IsTreasuryVoteInterval(uint64(blockHeight), @@ -3983,23 +4044,21 @@ func (b *BlockChain) tspendChecks(prevNode *blockNode, block *dcrutil.Block) err return ruleError(ErrInvalidTSpendWindow, str) } - // A valid TSPEND always stores the entire amount that the - // treasury is spending in the first TxIn. + // A valid treasury spend always stores the entire amount that the + // treasury is spending in the first input. It is safe to use since it + // has already been verified to match the commitment value. + // + // The extra overflow checks could technically be avoided here because + // the treasury spends are a subset of all transactions in the tree + // which means the overall sum for all transactions would overflow and + // cause the block to be rejected anyway, but be paranoid to protect + // against refactors violating that assumption. + var ok bool valueIn := stx.MsgTx().TxIn[0].ValueIn - totalTSpendAmount += valueIn - - // Verify that the ValueIn amount is identical to the LE - // encoded ValueIn in the OP_RETURN. Since the TSpend has been - // validated to be correct we simply index the bytes directly - // without additional checks. - leValueIn := stx.MsgTx().TxOut[0].PkScript[2 : 2+8] - valueInOpRet := int64(binary.LittleEndian.Uint64(leValueIn)) - if valueIn != valueInOpRet { - str := fmt.Sprintf("block contains TSpend transaction "+ - "(%v) that did not encode ValueIn correctly "+ - "got %v wanted %v", stx.Hash(), valueInOpRet, - valueIn) - return ruleError(ErrInvalidTSpendValueIn, str) + totalTSpendAmount, ok = addSigned(totalTSpendAmount, valueIn) + if !ok { + const str = "total value of all treasury spends overflows accumulator" + return ruleError(ErrBadTxOutValue, str) } // Verify this TSpend hash has not been included in a @@ -4092,15 +4151,6 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B } } - // Verify treasury spends. This is done relatively late because the - // database needs to be coherent. - if isTreasuryEnabled { - err = b.tspendChecks(node.parent, block) - if err != nil { - return err - } - } - // Don't run scripts if this node is both an ancestor of the assumed valid // block and an ancestor of the best header since the validity is verified // via the assumed valid node (all transactions are included in the merkle @@ -4170,18 +4220,21 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B } const stakeTreeTrue = true - err = b.checkTransactionsAndConnect(0, node, block.STransactions(), - view, stxos, stakeTreeTrue, subsidySplitVariant) + stakeTreeFees, err := b.checkTransactionsAndConnect(0, node, + block.STransactions(), view, stxos, stakeTreeTrue, subsidySplitVariant) if err != nil { log.Tracef("checkTransactionsAndConnect failed for stake tree: %v", err) return err } - stakeTreeFees, err := getStakeTreeFees(b.subsidyCache, node.height, - block.STransactions(), view, isTreasuryEnabled, subsidySplitVariant) - if err != nil { - log.Tracef("getStakeTreeFees failed for stake tree: %v", err) - return err + // Verify treasury spends. This is done relatively late because the + // database needs to be coherent and it depends on input checks already + // being done. + if isTreasuryEnabled { + err = b.tspendChecks(node.parent, block) + if err != nil { + return err + } } // Enforce all relative lock times via sequence numbers for the stake @@ -4233,7 +4286,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B } const stakeTreeFalse = false - err = b.checkTransactionsAndConnect(stakeTreeFees, node, + _, err = b.checkTransactionsAndConnect(stakeTreeFees, node, block.Transactions(), view, stxos, stakeTreeFalse, subsidySplitVariant) if err != nil { log.Tracef("checkTransactionsAndConnect failed for regular tree: %v", diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go index a69a1293f..6985a76b3 100644 --- a/internal/mempool/mempool.go +++ b/internal/mempool/mempool.go @@ -1130,7 +1130,7 @@ func (mp *TxPool) FetchTransaction(txHash *chainhash.Hash) (*dcrutil.Tx, error) // newTxDesc returns a new TxDesc instance that captures mempool state // relevant to the provided transaction at the current time. func (mp *TxPool) newTxDesc(tx *dcrutil.Tx, txType stake.TxType, height int64, - fee int64, totalSigOps int, txSize int64) *TxDesc { + fee int64, totalSigOps uint32, txSize int64) *TxDesc { return &TxDesc{ TxDesc: mining.TxDesc{ @@ -1582,13 +1582,13 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, allowHighFees, // you should add code here to check that the transaction does a // reasonable number of ECDSA signature verifications. - // Don't allow transactions with an excessive number of signature - // operations which would result in making it impossible to mine. Since - // the coinbase address itself can contain signature operations, the - // maximum allowed signature operations per transaction is less than - // the maximum allowed signature operations per block. - numP2SHSigOps, err := blockchain.CountP2SHSigOps(tx, false, - (txType == stake.TxTypeSSGen), utxoView, isTreasuryEnabled) + // Don't allow transactions with an excessive number of signature operations + // which would result in making it impossible to mine. Since the coinbase + // address itself can contain signature operations, the maximum allowed + // signature operations per transaction is less than the maximum allowed + // signature operations per block. + totalSigOps, err := blockchain.CountTotalSigOps(tx, false, isVote, utxoView, + isTreasuryEnabled) if err != nil { var cerr blockchain.RuleError if errors.As(err, &cerr) { @@ -1596,10 +1596,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, allowHighFees, } return nil, err } - - numSigOps := blockchain.CountSigOps(tx, false, isVote, isTreasuryEnabled) - totalSigOps := numP2SHSigOps + numSigOps - if totalSigOps > mp.cfg.Policy.MaxSigOpsPerTx { + if totalSigOps > uint32(mp.cfg.Policy.MaxSigOpsPerTx) { str := fmt.Sprintf("transaction %v has too many sigops: %d > %d", txHash, totalSigOps, mp.cfg.Policy.MaxSigOpsPerTx) return nil, txRuleError(ErrNonStandard, str) diff --git a/internal/mining/mining.go b/internal/mining/mining.go index 26ef90341..302a837dc 100644 --- a/internal/mining/mining.go +++ b/internal/mining/mining.go @@ -110,10 +110,12 @@ type Config struct { // tspend has enough votes to be included in a block AFTER the specified block. CheckTSpendHasVotes func(prevHash chainhash.Hash, tspend *dcrutil.Tx) error - // CountSigOps defines the function to use to count the number of signature - // operations for all transaction input and output scripts in the provided - // transaction. - CountSigOps func(tx *dcrutil.Tx, isCoinBaseTx bool, isSSGen bool, isTreasuryEnabled bool) int + // CountTotalSigOps defines the function to use to count the total number of + // signature operations for the given transaction. This includes all input + // and output scripts as well as signature operations in any redeemed + // pay-to-script-hash inputs. + CountTotalSigOps func(tx *dcrutil.Tx, isCoinBaseTx, isVoteTx bool, + view *blockchain.UtxoViewpoint, isTreasuryEnabled bool) (uint32, error) // FetchUtxoEntry defines the function to use to load and return the requested // unspent transaction output from the point of view of the main chain tip. @@ -224,7 +226,7 @@ type TxDesc struct { Fee int64 // TotalSigOps is the total signature operations for this transaction. - TotalSigOps int + TotalSigOps uint32 // TxSize is the size of the transaction. TxSize int64 @@ -240,7 +242,7 @@ type TxAncestorStats struct { SizeBytes int64 // TotalSigOps is the total number of signature operations of all ancestors. - TotalSigOps int + TotalSigOps uint32 // NumAncestors is the total number of ancestors for a given transaction. NumAncestors int @@ -421,7 +423,7 @@ type BlockTemplate struct { // SigOpCounts contains the number of signature operations each // transaction in the generated template performs. - SigOpCounts []int64 + SigOpCounts []uint64 // Height is the height at which the block template connects to the main // chain. @@ -879,7 +881,7 @@ func (g *BlkTmplGenerator) handleTooFewVoters(nextHeight int64, bt := &BlockTemplate{ Block: &block, Fees: []int64{0}, - SigOpCounts: []int64{0}, + SigOpCounts: []uint64{0}, Height: int64(tipHeader.Height), ValidPayAddress: miningAddress != nil, } @@ -992,12 +994,18 @@ func (g *BlkTmplGenerator) createRevocationFromTicket(ticketHash *chainhash.Hash } revocationTx := dcrutil.NewTx(revocationMsgTx) revocationTx.SetTree(wire.TxTreeStake) + + totalSigOps, err := g.cfg.CountTotalSigOps(revocationTx, false, false, + blockUtxos, isTreasuryEnabled) + if err != nil { + return nil, err + } + txDesc := &TxDesc{ - Tx: revocationTx, - Type: stake.TxTypeSSRtx, - TotalSigOps: g.cfg.CountSigOps(revocationTx, false, false, - isTreasuryEnabled), - TxSize: int64(revocationMsgTx.SerializeSize()), + Tx: revocationTx, + Type: stake.TxTypeSSRtx, + TotalSigOps: totalSigOps, + TxSize: int64(revocationMsgTx.SerializeSize()), } return txDesc, nil @@ -1316,8 +1324,8 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress stdaddr.Address) (*Bloc // the coinbase fee which will be updated later. txFees := make([]int64, 0, len(sourceTxns)) txFeesMap := make(map[chainhash.Hash]int64) - txSigOpCounts := make([]int64, 0, len(sourceTxns)) - txSigOpCountsMap := make(map[chainhash.Hash]int64) + txSigOpCounts := make([]uint64, 0, len(sourceTxns)) + txSigOpCountsMap := make(map[chainhash.Hash]uint64) txFees = append(txFees, -1) // Updated once known log.Debugf("Considering %d transactions for inclusion to new block", @@ -1443,7 +1451,7 @@ mempoolLoop: // trees if they fail one of the stake checks below the priorityQueue // pop loop. This is buggy, but not catastrophic behaviour. A future // release should fix it. TODO - blockSigOps := int64(0) + blockSigOps := uint64(0) totalFees := int64(0) numSStx := 0 @@ -1693,8 +1701,8 @@ nextPriorityQueueItem: // Enforce maximum signature operations per block. Also check // for overflow. - numSigOps := int64(prioItem.txDesc.TotalSigOps) - numSigOpsBundle := numSigOps + int64(ancestorStats.TotalSigOps) + numSigOps := uint64(prioItem.txDesc.TotalSigOps) + numSigOpsBundle := numSigOps + uint64(ancestorStats.TotalSigOps) if blockSigOps+numSigOpsBundle < blockSigOps || blockSigOps+numSigOpsBundle > blockchain.MaxSigOpsPerBlock { log.Tracef("Skipping tx %s because it would "+ @@ -1778,7 +1786,7 @@ nextPriorityQueueItem: // template. blockTxns = append(blockTxns, bundledTx) blockSize += uint32(bundledTx.MsgTx().SerializeSize()) - bundledTxSigOps := int64(bundledTxDesc.TotalSigOps) + bundledTxSigOps := uint64(bundledTxDesc.TotalSigOps) blockSigOps += bundledTxSigOps // Accumulate the SStxs in the block, because only a certain number @@ -2058,16 +2066,19 @@ nextPriorityQueueItem: g.cfg.ChainParams, isTreasuryEnabled, subsidySplitVariant) coinbaseTx.SetTree(wire.TxTreeRegular) - numCoinbaseSigOps := int64(g.cfg.CountSigOps(coinbaseTx, true, - false, isTreasuryEnabled)) + numCoinbaseSigOps, err := g.cfg.CountTotalSigOps(coinbaseTx, true, false, + blockUtxos, isTreasuryEnabled) + if err != nil { + log.Debug(err) + return nil, err + } blockSize += uint32(coinbaseTx.MsgTx().SerializeSize()) - blockSigOps += numCoinbaseSigOps + blockSigOps += uint64(numCoinbaseSigOps) txFeesMap[*coinbaseTx.Hash()] = 0 - txSigOpCountsMap[*coinbaseTx.Hash()] = numCoinbaseSigOps + txSigOpCountsMap[*coinbaseTx.Hash()] = uint64(numCoinbaseSigOps) if treasuryBase != nil { txFeesMap[*treasuryBase.Hash()] = 0 - n := int64(g.cfg.CountSigOps(treasuryBase, true, false, isTreasuryEnabled)) - txSigOpCountsMap[*treasuryBase.Hash()] = n + txSigOpCountsMap[*treasuryBase.Hash()] = 0 } // Build tx lists for regular tx. @@ -2129,7 +2140,7 @@ nextPriorityQueueItem: totalFees /= int64(g.cfg.ChainParams.TicketsPerBlock) } - txSigOpCounts = append(txSigOpCounts, numCoinbaseSigOps) + txSigOpCounts = append(txSigOpCounts, uint64(numCoinbaseSigOps)) // Now that the actual transactions have been selected, update the // block size for the real transaction count and coinbase value with diff --git a/internal/mining/mining_harness_test.go b/internal/mining/mining_harness_test.go index 7ce294a8a..4eaa938fa 100644 --- a/internal/mining/mining_harness_test.go +++ b/internal/mining/mining_harness_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -392,23 +392,15 @@ func (p *fakeTxSource) IsRegTxTreeKnownDisapproved(hash *chainhash.Hash) bool { // CountTotalSigOps returns the total number of signature operations for the // given transaction. -func (p *fakeTxSource) CountTotalSigOps(tx *dcrutil.Tx, txType stake.TxType) (int, error) { +func (p *fakeTxSource) CountTotalSigOps(tx *dcrutil.Tx, txType stake.TxType) (uint32, error) { isVote := txType == stake.TxTypeSSGen - isStakeBase := txType == stake.TxTypeSSGen utxoView, err := p.fetchInputUtxos(tx, p.chain.isTreasuryAgendaActive) if err != nil { return 0, err } - sigOps := blockchain.CountSigOps(tx, false, isVote, + return blockchain.CountTotalSigOps(tx, false, isVote, utxoView, p.chain.isTreasuryAgendaActive) - p2shSigOps, err := blockchain.CountP2SHSigOps(tx, false, isStakeBase, - utxoView, p.chain.isTreasuryAgendaActive) - if err != nil { - return 0, err - } - - return sigOps + p2shSigOps, nil } // fetchRedeemers returns all transactions that reference an outpoint for the @@ -508,7 +500,7 @@ func (p *fakeTxSource) removeOrphanDoubleSpends(tx *dcrutil.Tx) { } // addTransaction adds the passed transaction to the fake tx source. -func (p *fakeTxSource) addTransaction(tx *dcrutil.Tx, txType stake.TxType, height int64, fee int64, totalSigOps int) { +func (p *fakeTxSource) addTransaction(tx *dcrutil.Tx, txType stake.TxType, height int64, fee int64, totalSigOps uint32) { // Add the transaction to the pool and mark the referenced outpoints // as spent by the pool. txDesc := TxDesc{ @@ -1299,7 +1291,7 @@ func (m *miningHarness) CreateVote(ticket *dcrutil.Tx, mungers ...func(*wire.Msg // CountTotalSigOps returns the total number of signature operations for the // given transaction. -func (m *miningHarness) CountTotalSigOps(tx *dcrutil.Tx) (int, error) { +func (m *miningHarness) CountTotalSigOps(tx *dcrutil.Tx) (uint32, error) { txType := stake.DetermineTxType(tx.MsgTx()) return m.txSource.CountTotalSigOps(tx, txType) } @@ -1459,7 +1451,7 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable isAutoRevocationsEnabled, subsidySplitVariant) }, CheckTSpendHasVotes: chain.CheckTSpendHasVotes, - CountSigOps: blockchain.CountSigOps, + CountTotalSigOps: blockchain.CountTotalSigOps, FetchUtxoEntry: chain.FetchUtxoEntry, FetchUtxoView: chain.FetchUtxoView, FetchUtxoViewParentTemplate: chain.FetchUtxoViewParentTemplate, diff --git a/internal/mining/mining_view_test.go b/internal/mining/mining_view_test.go index 47651fc19..a9371ffd7 100644 --- a/internal/mining/mining_view_test.go +++ b/internal/mining/mining_view_test.go @@ -94,7 +94,7 @@ func TestMiningView(t *testing.T) { subject *dcrutil.Tx expectedAncestorFees int64 expectedSizeBytes int64 - expectedSigOps int + expectedSigOps uint32 ancestors []*dcrutil.Tx descendants map[chainhash.Hash]*dcrutil.Tx orderedAncestors [][]*dcrutil.Tx diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index 4a078f181..97b88fde9 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -440,7 +440,7 @@ type Chain interface { // TSpendCountVotes returns the votes for the specified tspend up to // the specified block. - TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (int64, int64, error) + TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (uint32, uint32, error) // InvalidateBlock manually invalidates the provided block as if the block // had violated a consensus rule and marks all of its descendants as having diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index da4e0c1dd..f4907bdd8 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -3437,7 +3437,7 @@ func handleGetTreasurySpendVotes(_ context.Context, s *Server, cmd any) (any, er // We only count votes for tspends that are inside their voting // window. Otherwise we just return the appropriate vote start // and end heights for it. - var yes, no int64 + var yes, no uint32 insideWindow := standalone.InsideTSpendWindow(blockHeight, expiry, tvi, mul) minedBlock, isMined := endBlocks[*txHash] if insideWindow || isMined { @@ -3470,8 +3470,8 @@ func handleGetTreasurySpendVotes(_ context.Context, s *Server, cmd any) (any, er Expiry: int64(expiry), VoteStart: int64(start), VoteEnd: int64(end), - YesVotes: yes, - NoVotes: no, + YesVotes: int64(yes), + NoVotes: int64(no), } } diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index 562282735..ac6d8fd3b 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -127,8 +127,8 @@ func (u *testRPCUtxoEntry) TicketMinimalOutputs() []*stake.MinimalOutput { // tspendVotes is used to mock the results of a chain TSpendCountVotes call. type tspendVotes struct { - yes int64 - no int64 + yes uint32 + no uint32 err error } @@ -434,7 +434,7 @@ func (c *testRPCChain) FetchTSpend(chainhash.Hash) ([]chainhash.Hash, error) { // TSpendCountVotes counts the number of votes a given tspend has received up // to the given block. -func (c *testRPCChain) TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (int64, int64, error) { +func (c *testRPCChain) TSpendCountVotes(*chainhash.Hash, *dcrutil.Tx) (uint32, uint32, error) { return c.tspendVotes.yes, c.tspendVotes.no, c.tspendVotes.err } diff --git a/server.go b/server.go index 3f823bc93..5439f135c 100644 --- a/server.go +++ b/server.go @@ -4308,7 +4308,7 @@ func newServer(ctx context.Context, profiler *profileServer, isAutoRevocationsEnabled, subsidySplitVariant) }, CheckTSpendHasVotes: s.chain.CheckTSpendHasVotes, - CountSigOps: blockchain.CountSigOps, + CountTotalSigOps: blockchain.CountTotalSigOps, FetchUtxoEntry: s.chain.FetchUtxoEntry, FetchUtxoView: s.chain.FetchUtxoView, FetchUtxoViewParentTemplate: s.chain.FetchUtxoViewParentTemplate,