This updates the latest block version for the upcoming hard fork vote for DCP0007, DCP0008, and DCP0009 in order to reject old block versions once a super majority of the network has upgraded.
4235 lines
157 KiB
Go
4235 lines
157 KiB
Go
// Copyright (c) 2013-2016 The btcsuite developers
|
|
// Copyright (c) 2015-2021 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 (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/blockchain/standalone/v2"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/chaincfg/v3"
|
|
"github.com/decred/dcrd/database/v3"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/gcs/v3/blockcf2"
|
|
"github.com/decred/dcrd/txscript/v4"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
const (
|
|
// MaxSigOpsPerBlock is the maximum number of signature operations
|
|
// allowed for a block. This really should be based upon the max
|
|
// allowed block size for a network and any votes that might change it,
|
|
// however, since it was not updated to be based upon it before
|
|
// release, it will require a hard fork and associated vote agenda to
|
|
// change it. The original max block size for the protocol was 1MiB,
|
|
// so that is what this is based on.
|
|
MaxSigOpsPerBlock = 1000000 / 200
|
|
|
|
// MaxTimeOffsetSeconds is the maximum number of seconds a block time
|
|
// is allowed to be ahead of the current time. This is currently 2
|
|
// hours.
|
|
MaxTimeOffsetSeconds = 2 * 60 * 60
|
|
|
|
// MinCoinbaseScriptLen is the minimum length a coinbase script can be.
|
|
MinCoinbaseScriptLen = 2
|
|
|
|
// MaxCoinbaseScriptLen is the maximum length a coinbase script can be.
|
|
MaxCoinbaseScriptLen = 100
|
|
|
|
// maxUniqueCoinbaseNullDataSize is the maximum number of bytes allowed
|
|
// in the pushed data output of the coinbase output that is used to
|
|
// ensure the coinbase has a unique hash.
|
|
maxUniqueCoinbaseNullDataSize = 256
|
|
|
|
// medianTimeBlocks is the number of previous blocks which should be
|
|
// used to calculate the median time used to validate block timestamps.
|
|
medianTimeBlocks = 11
|
|
|
|
// earlyVoteBitsValue is the only value of VoteBits allowed in a block
|
|
// header before stake validation height.
|
|
earlyVoteBitsValue = 0x0001
|
|
|
|
// maxRevocationsPerBlock is the maximum number of revocations that are
|
|
// allowed per block.
|
|
maxRevocationsPerBlock = 255
|
|
|
|
// MaxTAddsPerBlock is the maximum number of treasury add txs that are
|
|
// allowed per block.
|
|
MaxTAddsPerBlock = 20
|
|
|
|
// A ticket commitment output is an OP_RETURN script with a 30-byte data
|
|
// push that consists of a 20-byte hash for the payment hash, 8 bytes
|
|
// for the amount to commit to (with the upper bit flag set to indicate
|
|
// the hash is for a pay-to-script-hash address, otherwise the hash is a
|
|
// pay-to-pubkey-hash), and 2 bytes for the fee limits. Thus, 1 byte
|
|
// for the OP_RETURN + 1 byte for the data push + 20 bytes for the
|
|
// payment hash means the encoded amount is at offset 22. Then, 8 bytes
|
|
// for the amount means the encoded fee limits are at offset 30.
|
|
commitHashStartIdx = 2
|
|
commitHashEndIdx = commitHashStartIdx + 20
|
|
commitAmountStartIdx = commitHashEndIdx
|
|
commitAmountEndIdx = commitAmountStartIdx + 8
|
|
commitFeeLimitStartIdx = commitAmountEndIdx
|
|
commitFeeLimitEndIdx = commitFeeLimitStartIdx + 2
|
|
|
|
// commitP2SHFlag specifies the bitmask to apply to an amount decoded from
|
|
// a ticket commitment in order to determine if it is a pay-to-script-hash
|
|
// commitment. The value is derived from the fact it is encoded as the most
|
|
// significant bit in the amount.
|
|
commitP2SHFlag = uint64(1 << 63)
|
|
|
|
// submissionOutputIdx is the index of the stake submission output of a
|
|
// ticket transaction.
|
|
submissionOutputIdx = 0
|
|
|
|
// checkForDuplicateHashes checks for duplicate hashes when validating
|
|
// blocks. Because of the rule inserting the height into the second (nonce)
|
|
// txOut, there should never be a duplicate transaction hash that overwrites
|
|
// another. However, because there is a 2^128 chance of a collision, the
|
|
// paranoid user may wish to turn this feature on.
|
|
checkForDuplicateHashes = false
|
|
)
|
|
|
|
// mustParseHash converts the passed big-endian hex string into a
|
|
// chainhash.Hash and will panic if there is an error. It only differs from the
|
|
// one available in chainhash in that it will panic so errors in the source code
|
|
// be detected. It will only (and must only) be called with hard-coded, and
|
|
// therefore known good, hashes.
|
|
func mustParseHash(s string) *chainhash.Hash {
|
|
hash, err := chainhash.NewHashFromStr(s)
|
|
if err != nil {
|
|
panic("invalid hash in source file: " + s)
|
|
}
|
|
return hash
|
|
}
|
|
|
|
var (
|
|
// zeroHash is the zero value for a chainhash.Hash and is defined as a
|
|
// package level variable to avoid the need to create a new instance
|
|
// every time a check is needed.
|
|
zeroHash = &chainhash.Hash{}
|
|
|
|
// earlyFinalState is the only value of the final state allowed in a
|
|
// block header before stake validation height.
|
|
earlyFinalState = [6]byte{0x00}
|
|
|
|
// The following blocks violate DCP0005 as they were submitted with an old
|
|
// version after the majority of the network had upgraded. See
|
|
// isDCP0005Violation for more details. They are defined as package level
|
|
// variables to avoid the need to create new instances every time a check is
|
|
// needed.
|
|
block413762Hash = mustParseHash("00000000000000002086ed61f62a546f3bfa8f3e567b003f097efa982008c47b")
|
|
block414036Hash = mustParseHash("0000000000000000194fa59310b9e988ecb23de0c716d6e8f1b2aa31d9592387")
|
|
block424011Hash = mustParseHash("0000000000000000317fc6c7a8a6578be7dfa9c96eb81d620050a3732b02d572")
|
|
block428809Hash = mustParseHash("00000000000000003147798ccffcecaa420fb1c7934d8f4e33809a871ee34aaa")
|
|
block430191Hash = mustParseHash("00000000000000002127ad6d4cb30cc16f6344589b417e42650388bb0690a88e")
|
|
)
|
|
|
|
// voteBitsApproveParent returns whether or not the passed vote bits indicate
|
|
// the regular transaction tree of the parent block should be considered valid.
|
|
func voteBitsApproveParent(voteBits uint16) bool {
|
|
return dcrutil.IsFlagSet16(voteBits, dcrutil.BlockValid)
|
|
}
|
|
|
|
// headerApprovesParent returns whether or not the vote bits in the passed
|
|
// header indicate the regular transaction tree of the parent block should be
|
|
// considered valid.
|
|
func headerApprovesParent(header *wire.BlockHeader) bool {
|
|
return voteBitsApproveParent(header.VoteBits)
|
|
}
|
|
|
|
// isNullOutpoint determines whether or not a previous transaction output point
|
|
// is set.
|
|
func isNullOutpoint(outpoint *wire.OutPoint) bool {
|
|
if outpoint.Index == math.MaxUint32 &&
|
|
outpoint.Hash.IsEqual(zeroHash) &&
|
|
outpoint.Tree == wire.TxTreeRegular {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isNullFraudProof determines whether or not a previous transaction fraud
|
|
// proof is set.
|
|
func isNullFraudProof(txIn *wire.TxIn) bool {
|
|
switch {
|
|
case txIn.BlockHeight != wire.NullBlockHeight:
|
|
return false
|
|
case txIn.BlockIndex != wire.NullBlockIndex:
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsExpiredTx returns where or not the passed transaction is expired according
|
|
// to the given block height.
|
|
//
|
|
// This function only differs from IsExpired in that it works with a raw wire
|
|
// transaction as opposed to a higher level util transaction.
|
|
func IsExpiredTx(tx *wire.MsgTx, blockHeight int64) bool {
|
|
expiry := tx.Expiry
|
|
return expiry != wire.NoExpiryValue && blockHeight >= int64(expiry)
|
|
}
|
|
|
|
// IsExpired returns where or not the passed transaction is expired according to
|
|
// the given block height.
|
|
//
|
|
// This function only differs from IsExpiredTx in that it works with a higher
|
|
// level util transaction as opposed to a raw wire transaction.
|
|
func IsExpired(tx *dcrutil.Tx, blockHeight int64) bool {
|
|
return IsExpiredTx(tx.MsgTx(), blockHeight)
|
|
}
|
|
|
|
// SequenceLockActive determines if all of the inputs to a given transaction
|
|
// have achieved a relative age that surpasses the requirements specified by
|
|
// their respective sequence locks as calculated by CalcSequenceLock. A single
|
|
// sequence lock is sufficient because the calculated lock selects the minimum
|
|
// required time and block height from all of the non-disabled inputs after
|
|
// which the transaction can be included.
|
|
func SequenceLockActive(lock *SequenceLock, blockHeight int64, medianTime time.Time) bool {
|
|
// The transaction is not yet mature if it has not yet reached the
|
|
// required minimum time and block height according to its sequence
|
|
// locks.
|
|
if blockHeight <= lock.MinHeight || medianTime.Unix() <= lock.MinTime {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsFinalizedTransaction determines whether or not a transaction is finalized.
|
|
func IsFinalizedTransaction(tx *dcrutil.Tx, blockHeight int64, blockTime time.Time) bool {
|
|
// Lock time of zero means the transaction is finalized.
|
|
msgTx := tx.MsgTx()
|
|
lockTime := msgTx.LockTime
|
|
if lockTime == 0 {
|
|
return true
|
|
}
|
|
|
|
// The lock time field of a transaction is either a block height at
|
|
// which the transaction is finalized or a timestamp depending on if the
|
|
// value is before the txscript.LockTimeThreshold. When it is under the
|
|
// threshold it is a block height.
|
|
var blockTimeOrHeight int64
|
|
if lockTime < txscript.LockTimeThreshold {
|
|
blockTimeOrHeight = blockHeight
|
|
} else {
|
|
blockTimeOrHeight = blockTime.Unix()
|
|
}
|
|
if int64(lockTime) < blockTimeOrHeight {
|
|
return true
|
|
}
|
|
|
|
// At this point, the transaction's lock time hasn't occurred yet, but
|
|
// the transaction might still be finalized if the sequence number
|
|
// for all transaction inputs is maxed out.
|
|
for _, txIn := range msgTx.TxIn {
|
|
if txIn.Sequence != math.MaxUint32 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkTransactionSanity performs some preliminary checks on a transaction to
|
|
// ensure it is sane. These checks are context free.
|
|
func checkTransactionSanity(tx *wire.MsgTx, params *chaincfg.Params) error {
|
|
// A transaction must have at least one input.
|
|
if len(tx.TxIn) == 0 {
|
|
return ruleError(ErrNoTxInputs, "transaction has no inputs")
|
|
}
|
|
|
|
// A transaction must have at least one output.
|
|
if len(tx.TxOut) == 0 {
|
|
return ruleError(ErrNoTxOutputs, "transaction has no outputs")
|
|
}
|
|
|
|
// A transaction must not exceed the maximum allowed size when serialized.
|
|
serializedTxSize := tx.SerializeSize()
|
|
if serializedTxSize > params.MaxTxSize {
|
|
str := fmt.Sprintf("serialized transaction is too big - got %d, max %d",
|
|
serializedTxSize, params.MaxTxSize)
|
|
return ruleError(ErrTxTooBig, str)
|
|
}
|
|
|
|
// Ensure the transaction amounts are in range. Each transaction output
|
|
// must not be negative or more than the max allowed per transaction. Also,
|
|
// the total of all outputs must abide by the same restrictions. All
|
|
// amounts in a transaction are in a unit value known as an atom. One
|
|
// Decred is a quantity of atoms as defined by the AtomsPerCoin constant.
|
|
var totalAtom int64
|
|
for _, txOut := range tx.TxOut {
|
|
atom := txOut.Value
|
|
if atom < 0 {
|
|
str := fmt.Sprintf("transaction output has negative value of %v",
|
|
atom)
|
|
return ruleError(ErrBadTxOutValue, str)
|
|
}
|
|
if atom > dcrutil.MaxAmount {
|
|
str := fmt.Sprintf("transaction output value of %v is higher than "+
|
|
"max allowed value of %v", atom, dcrutil.MaxAmount)
|
|
return ruleError(ErrBadTxOutValue, str)
|
|
}
|
|
|
|
// Two's complement int64 overflow guarantees that any overflow is
|
|
// detected and reported. This is impossible for Decred, but perhaps
|
|
// possible if an alt increases the total money supply.
|
|
totalAtom += atom
|
|
if totalAtom < 0 {
|
|
str := fmt.Sprintf("total value of all transaction outputs "+
|
|
"exceeds max allowed value of %v", dcrutil.MaxAmount)
|
|
return ruleError(ErrBadTxOutValue, str)
|
|
}
|
|
if totalAtom > dcrutil.MaxAmount {
|
|
str := fmt.Sprintf("total value of all transaction outputs is %v "+
|
|
"which is higher than max allowed value of %v", totalAtom,
|
|
dcrutil.MaxAmount)
|
|
return ruleError(ErrBadTxOutValue, str)
|
|
}
|
|
}
|
|
|
|
// Check for duplicate transaction inputs.
|
|
existingTxOut := make(map[wire.OutPoint]struct{})
|
|
for _, txIn := range tx.TxIn {
|
|
if _, exists := existingTxOut[txIn.PreviousOutPoint]; exists {
|
|
str := "transaction contains duplicate inputs"
|
|
return ruleError(ErrDuplicateTxInputs, str)
|
|
}
|
|
existingTxOut[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AgendaFlags is a bitmask defining additional agendas to consider as active
|
|
// when checking transactions. This can be useful for callers who are able to
|
|
// accurately determine what agendas are active or otherwise desire any
|
|
// additional validation checks associated with a given agenda being active.
|
|
type AgendaFlags uint32
|
|
|
|
const (
|
|
// AFTreasuryEnabled may be set to indicate that the treasury agenda should
|
|
// be considered as active when checking a transaction so that any
|
|
// additional checks which depend on the agenda being active are applied.
|
|
AFTreasuryEnabled AgendaFlags = 1 << iota
|
|
|
|
// AFExplicitVerUpgrades may be set to indicate that the explicit version
|
|
// upgrades agenda should be considered as active when checking a
|
|
// transaction so that any additional checks which depend on the agenda
|
|
// being active are applied.
|
|
AFExplicitVerUpgrades
|
|
|
|
// AFAutoRevocationsEnabled may be set to indicate that the automatic ticket
|
|
// revocations agenda should be considered as active when checking a
|
|
// transaction so that any additional checks which depend on the agenda being
|
|
// active are applied.
|
|
AFAutoRevocationsEnabled
|
|
|
|
// AFNone is a convenience value to specifically indicate no flags.
|
|
AFNone AgendaFlags = 0
|
|
)
|
|
|
|
// IsTreasuryEnabled returns whether the flags indicate that the treasury agenda
|
|
// is enabled.
|
|
func (flags AgendaFlags) IsTreasuryEnabled() bool {
|
|
return flags&AFTreasuryEnabled == AFTreasuryEnabled
|
|
}
|
|
|
|
// IsExplicitVerUpgradesEnabled returns whether the flags indicate that the
|
|
// explicit version upgrades agenda is enabled.
|
|
func (flags AgendaFlags) IsExplicitVerUpgradesEnabled() bool {
|
|
return flags&AFExplicitVerUpgrades == AFExplicitVerUpgrades
|
|
}
|
|
|
|
// IsAutoRevocationsEnabled returns whether the flags indicate that the
|
|
// automatic ticket revocations agenda is enabled.
|
|
func (flags AgendaFlags) IsAutoRevocationsEnabled() bool {
|
|
return flags&AFAutoRevocationsEnabled == AFAutoRevocationsEnabled
|
|
}
|
|
|
|
// determineCheckTxFlags returns the flags to use when checking transactions
|
|
// based on the agendas that are active as of the block AFTER the given node.
|
|
func (b *BlockChain) determineCheckTxFlags(prevNode *blockNode) (AgendaFlags, error) {
|
|
// Determine if the treasury agenda is active as of the block being checked.
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(prevNode)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Determine if the explicit version upgrades agenda is active as of the
|
|
// block being checked.
|
|
explicitUpgradesActive, err := b.isExplicitVerUpgradesAgendaActive(prevNode)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active as of the
|
|
// block being checked.
|
|
isAutoRevocationsEnabled, err := b.isAutoRevocationsAgendaActive(prevNode)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Create and return agenda flags for checking transactions based on which
|
|
// ones are active as of the block being checked.
|
|
checkTxFlags := AFNone
|
|
if isTreasuryEnabled {
|
|
checkTxFlags |= AFTreasuryEnabled
|
|
}
|
|
if explicitUpgradesActive {
|
|
checkTxFlags |= AFExplicitVerUpgrades
|
|
}
|
|
if isAutoRevocationsEnabled {
|
|
checkTxFlags |= AFAutoRevocationsEnabled
|
|
}
|
|
return checkTxFlags, nil
|
|
}
|
|
|
|
// checkTransactionContext performs several validation checks on the transaction
|
|
// which depend on having the full block data for all of its ancestors
|
|
// available, most likely because the checks depend on whether or not an agenda
|
|
// is active, because that involves tallying the result of votes that are part
|
|
// of the block data.
|
|
//
|
|
// The flags may be used to specify which agendas should be considered as active
|
|
// in order to change how the validation rules are applied accordingly.
|
|
func checkTransactionContext(tx *wire.MsgTx, params *chaincfg.Params, flags AgendaFlags) error {
|
|
// Determine active agendas based on flags.
|
|
isTreasuryEnabled := flags.IsTreasuryEnabled()
|
|
explicitUpgradesActive := flags.IsExplicitVerUpgradesEnabled()
|
|
isAutoRevocationsEnabled := flags.IsAutoRevocationsEnabled()
|
|
|
|
// Reject transaction versions greater than the highest currently supported
|
|
// version. Any future consensus changes that result in hard-forking
|
|
// changes to transactions (aka those that are not backwards compatible) are
|
|
// expected to only be applied to a new transaction version and to update
|
|
// this code accordingly so that the newer transaction version can be used
|
|
// as a guaranteed proxy for an agenda having passed and become active.
|
|
//
|
|
// Note that prior to the explicit version upgrades agenda, transaction
|
|
// versions are allowed to go up to a max uint16, so fall back to that value
|
|
// accordingly.
|
|
maxAllowedTxVer := ^uint16(0)
|
|
switch {
|
|
case explicitUpgradesActive:
|
|
maxAllowedTxVer = 3
|
|
}
|
|
if tx.Version > maxAllowedTxVer {
|
|
str := fmt.Sprintf("transaction version %d is greater than the max "+
|
|
"allowed version %d)", tx.Version, maxAllowedTxVer)
|
|
return ruleError(ErrTxVersionTooHigh, str)
|
|
}
|
|
|
|
// Determine type.
|
|
var isCoinBase, isVote, isTicket, isRevocation bool
|
|
var isTreasuryBase, isTreasuryAdd, isTreasurySpend bool
|
|
switch stake.DetermineTxType(tx, isTreasuryEnabled, isAutoRevocationsEnabled) {
|
|
case stake.TxTypeSSGen:
|
|
isVote = true
|
|
case stake.TxTypeSStx:
|
|
isTicket = true
|
|
case stake.TxTypeSSRtx:
|
|
isRevocation = true
|
|
case stake.TxTypeTreasuryBase:
|
|
isTreasuryBase = true
|
|
case stake.TxTypeTAdd:
|
|
isTreasuryAdd = true
|
|
case stake.TxTypeTSpend:
|
|
isTreasurySpend = true
|
|
default:
|
|
// Determine if we are dealing with a coinbase.
|
|
isCoinBase = standalone.IsCoinBaseTx(tx, isTreasuryEnabled)
|
|
}
|
|
|
|
// Take action on type.
|
|
switch {
|
|
case isVote:
|
|
// Check script length of stake base signature.
|
|
slen := len(tx.TxIn[0].SignatureScript)
|
|
if slen < MinCoinbaseScriptLen || slen > MaxCoinbaseScriptLen {
|
|
str := fmt.Sprintf("stakebase transaction script length of %d is "+
|
|
"out of range (min: %d, max: %d)", slen, MinCoinbaseScriptLen,
|
|
MaxCoinbaseScriptLen)
|
|
return ruleError(ErrBadStakebaseScriptLen, str)
|
|
}
|
|
|
|
// The script must be set to the one specified by the network.
|
|
if !bytes.Equal(tx.TxIn[0].SignatureScript, params.StakeBaseSigScript) {
|
|
str := fmt.Sprintf("stakebase transaction signature script was "+
|
|
"set to disallowed value (got %x, want %x)",
|
|
tx.TxIn[0].SignatureScript, params.StakeBaseSigScript)
|
|
return ruleError(ErrBadStakebaseScrVal, str)
|
|
}
|
|
|
|
// The ticket reference hash in an SSGen tx must not be null.
|
|
ticketHash := &tx.TxIn[1].PreviousOutPoint
|
|
if isNullOutpoint(ticketHash) {
|
|
str := "vote ticket input refers to previous output that is null"
|
|
return ruleError(ErrBadTxInput, str)
|
|
}
|
|
|
|
case isRevocation:
|
|
// Revocation transactions MUST be version 2 if the automatic ticket
|
|
// revocations agenda is active.
|
|
if isAutoRevocationsEnabled &&
|
|
tx.Version != stake.TxVersionAutoRevocations {
|
|
|
|
str := fmt.Sprintf("revocation transaction version is %d instead of %d",
|
|
tx.Version, stake.TxVersionAutoRevocations)
|
|
return ruleError(ErrInvalidRevocationTxVersion, str)
|
|
}
|
|
|
|
case isCoinBase:
|
|
// The referenced outpoint must be null.
|
|
if !isNullOutpoint(&tx.TxIn[0].PreviousOutPoint) {
|
|
str := "coinbase transaction does not have a null outpoint"
|
|
return ruleError(ErrBadCoinbaseOutpoint, str)
|
|
}
|
|
|
|
// The fraud proof must also be null.
|
|
if !isNullFraudProof(tx.TxIn[0]) {
|
|
str := "coinbase transaction fraud proof is non-null"
|
|
return ruleError(ErrBadCoinbaseFraudProof, str)
|
|
}
|
|
|
|
slen := len(tx.TxIn[0].SignatureScript)
|
|
if slen < MinCoinbaseScriptLen || slen > MaxCoinbaseScriptLen {
|
|
str := fmt.Sprintf("coinbase transaction script length of %d is "+
|
|
"out of range (min: %d, max: %d)", slen, MinCoinbaseScriptLen,
|
|
MaxCoinbaseScriptLen)
|
|
return ruleError(ErrBadCoinbaseScriptLen, str)
|
|
}
|
|
|
|
case isTreasuryBase:
|
|
// The referenced outpoint must be null.
|
|
if !isNullOutpoint(&tx.TxIn[0].PreviousOutPoint) {
|
|
str := "treasurybase transaction does not have a null outpoint"
|
|
return ruleError(ErrBadTreasurybaseOutpoint, str)
|
|
}
|
|
|
|
// The fraud proof must also be null.
|
|
if !isNullFraudProof(tx.TxIn[0]) {
|
|
str := "treasurybase transaction fraud proof is non-null"
|
|
return ruleError(ErrBadTreasurybaseFraudProof, str)
|
|
}
|
|
|
|
// SignatureScript length must be zero.
|
|
slen := len(tx.TxIn[0].SignatureScript)
|
|
if slen != 0 {
|
|
str := fmt.Sprintf("treasurybase transaction script length is not "+
|
|
"zero: %v", slen)
|
|
return ruleError(ErrBadTreasurybaseScriptLen, str)
|
|
}
|
|
|
|
case isTreasurySpend:
|
|
// The referenced outpoint must be null.
|
|
if !isNullOutpoint(&tx.TxIn[0].PreviousOutPoint) {
|
|
str := "treasury spend transaction does not have a null outpoint"
|
|
return ruleError(ErrBadTSpendOutpoint, str)
|
|
}
|
|
|
|
// The fraud proof must also be null.
|
|
if !isNullFraudProof(tx.TxIn[0]) {
|
|
str := "treasury spend transaction fraud proof is non-null"
|
|
return ruleError(ErrBadTSpendFraudProof, str)
|
|
}
|
|
|
|
// Check script length of stake base signature.
|
|
slen := len(tx.TxIn[0].SignatureScript)
|
|
if slen != stake.TSpendScriptLen {
|
|
str := fmt.Sprintf("treasury spend transaction script length of "+
|
|
"%d is invalid (required: %d)", slen, stake.TSpendScriptLen)
|
|
return ruleError(ErrBadTSpendScriptLen, str)
|
|
}
|
|
|
|
case isTreasuryAdd:
|
|
if len(tx.TxOut) == 2 && tx.TxOut[1].Value == 0 {
|
|
str := "treasury add transaction change cannot be 0"
|
|
return ruleError(ErrInvalidTAddChange, str)
|
|
}
|
|
|
|
// Note the fallthrough. Treasury add transactions require the default
|
|
// test. Do not move this case!
|
|
fallthrough
|
|
|
|
default:
|
|
// Previous transaction outputs referenced by the inputs to this
|
|
// transaction must not be null.
|
|
for txInIdx, txIn := range tx.TxIn {
|
|
prevOut := &txIn.PreviousOutPoint
|
|
if isNullOutpoint(prevOut) {
|
|
str := fmt.Sprintf("transaction input %d refers to previous "+
|
|
"output that is null", txInIdx)
|
|
return ruleError(ErrBadTxInput, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enforce additional rules on regular (non-stake) transactions.
|
|
isStakeTx := isVote || isTicket || isRevocation || isTreasuryAdd ||
|
|
isTreasurySpend || isTreasuryBase
|
|
if !isStakeTx {
|
|
// Note that prior to the explicit version upgrades agenda, transaction
|
|
// script versions are allowed to go up to a max uint16, so fall back to
|
|
// that value accordingly.
|
|
maxAllowedScriptVer := ^uint16(0)
|
|
switch {
|
|
case explicitUpgradesActive:
|
|
maxAllowedScriptVer = 0
|
|
}
|
|
|
|
for txOutIdx, txOut := range tx.TxOut {
|
|
// Reject transaction script versions greater than the highest
|
|
// currently supported version. Any future consensus changes that
|
|
// result in introduction of a new script version are expected to
|
|
// update this code accordingly so that the newer transaction script
|
|
// version can be used as a guaranteed proxy for an agenda having
|
|
// passed and become active.
|
|
//
|
|
// It is also worth noting that this check only applies to regular
|
|
// transactions because stake transactions are individually and
|
|
// separately enforced to be a specific script version.
|
|
if txOut.Version > maxAllowedScriptVer {
|
|
str := fmt.Sprintf("script version %d is greater than the max "+
|
|
"allowed version %d)", txOut.Version, maxAllowedScriptVer)
|
|
return ruleError(ErrScriptVersionTooHigh, str)
|
|
}
|
|
|
|
// Ensure that non-stake transactions have no outputs with opcodes
|
|
// that are not allowed outside of the stake transactions.
|
|
hasOp, err := txscript.ContainsStakeOpCodes(txOut.PkScript,
|
|
isTreasuryEnabled)
|
|
if err != nil {
|
|
return ruleError(ErrScriptMalformed, err.Error())
|
|
}
|
|
if hasOp {
|
|
str := fmt.Sprintf("non-stake transaction output %d contains "+
|
|
"stake opcode", txOutIdx)
|
|
return ruleError(ErrRegTxCreateStakeOut, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckTransactionSanity performs some preliminary checks on a transaction to
|
|
// ensure it is sane. These checks are context free.
|
|
func CheckTransactionSanity(tx *wire.MsgTx, params *chaincfg.Params) error {
|
|
return checkTransactionSanity(tx, params)
|
|
}
|
|
|
|
// CheckTransaction performs several validation checks on a transaction that
|
|
// include both preliminary sanity checks that are context free as well as those
|
|
// which depend on whether or not an agenda is active.
|
|
//
|
|
// The flags may be used to specify which agendas should be considered as active
|
|
// in order to change how the validation rules are applied accordingly.
|
|
func CheckTransaction(tx *wire.MsgTx, params *chaincfg.Params, flags AgendaFlags) error {
|
|
err := checkTransactionSanity(tx, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return checkTransactionContext(tx, params, flags)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
return checkProofOfStake(block, posLimit)
|
|
}
|
|
|
|
// standaloneToChainRuleError attempts to convert the passed error from a
|
|
// standalone.RuleError to a blockchain.RuleError with the equivalent error
|
|
// kind. The error is simply passed through without modification if it is
|
|
// not a standalone.RuleError, not one of the specifically recognized
|
|
// error kinds, or nil.
|
|
func standaloneToChainRuleError(err error) error {
|
|
// Convert standalone package rule errors to blockchain rule errors.
|
|
switch {
|
|
case errors.Is(err, standalone.ErrUnexpectedDifficulty):
|
|
return ruleError(ErrUnexpectedDifficulty, err.Error())
|
|
case errors.Is(err, standalone.ErrHighHash):
|
|
return ruleError(ErrHighHash, err.Error())
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// checkProofOfWork ensures the block header bits which indicate the target
|
|
// difficulty is in min/max range and that the block hash is less than the
|
|
// target difficulty as claimed.
|
|
//
|
|
// The flags modify the behavior of this function as follows:
|
|
// - BFNoPoWCheck: The check to ensure the block hash is less than the target
|
|
// difficulty is not performed.
|
|
func checkProofOfWork(header *wire.BlockHeader, powLimit *big.Int, flags BehaviorFlags) error {
|
|
// Only ensure the target difficulty bits are in the valid range when the
|
|
// the flag to avoid proof of work checks is set.
|
|
if flags&BFNoPoWCheck == BFNoPoWCheck {
|
|
err := standalone.CheckProofOfWorkRange(header.Bits, powLimit)
|
|
return standaloneToChainRuleError(err)
|
|
}
|
|
|
|
// Perform all proof of work checks when the flag is not set:
|
|
//
|
|
// - The target difficulty must be larger than zero.
|
|
// - The target difficulty must be less than the maximum allowed.
|
|
// - The block hash must be less than the claimed target.
|
|
blockHash := header.BlockHash()
|
|
err := standalone.CheckProofOfWork(&blockHash, header.Bits, powLimit)
|
|
return standaloneToChainRuleError(err)
|
|
}
|
|
|
|
// checkBlockHeaderSanity performs some preliminary checks on a block header to
|
|
// ensure it is sane before continuing with processing. These checks are
|
|
// context free.
|
|
//
|
|
// The flags do not modify the behavior of this function directly, however they
|
|
// are needed to pass along to checkProofOfWork.
|
|
func checkBlockHeaderSanity(header *wire.BlockHeader, timeSource MedianTimeSource, flags BehaviorFlags, chainParams *chaincfg.Params) error {
|
|
// The stake validation height should always be at least stake enabled
|
|
// height, so assert it because the code below relies on that assumption.
|
|
stakeValidationHeight := uint32(chainParams.StakeValidationHeight)
|
|
stakeEnabledHeight := uint32(chainParams.StakeEnabledHeight)
|
|
if stakeEnabledHeight > stakeValidationHeight {
|
|
return AssertError(fmt.Sprintf("checkBlockHeaderSanity called "+
|
|
"with stake enabled height %d after stake validation "+
|
|
"height %d", stakeEnabledHeight, stakeValidationHeight))
|
|
}
|
|
|
|
// Ensure the proof of work bits in the block header is in min/max
|
|
// range and the block hash is less than the target value described by
|
|
// the bits.
|
|
err := checkProofOfWork(header, chainParams.PowLimit, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A block timestamp must not have a greater precision than one second.
|
|
// This check is necessary because Go time.Time values support
|
|
// nanosecond precision whereas the consensus rules only apply to
|
|
// seconds and it's much nicer to deal with standard Go time values
|
|
// instead of converting to seconds everywhere.
|
|
if !header.Timestamp.Equal(time.Unix(header.Timestamp.Unix(), 0)) {
|
|
str := fmt.Sprintf("block timestamp of %v has a higher "+
|
|
"precision than one second", header.Timestamp)
|
|
return ruleError(ErrInvalidTime, str)
|
|
}
|
|
|
|
// Ensure the block time is not too far in the future.
|
|
maxTimestamp := timeSource.AdjustedTime().Add(time.Second *
|
|
MaxTimeOffsetSeconds)
|
|
if header.Timestamp.After(maxTimestamp) {
|
|
str := fmt.Sprintf("block timestamp of %v is too far in the "+
|
|
"future", header.Timestamp)
|
|
return ruleError(ErrTimeTooNew, str)
|
|
}
|
|
|
|
// A block must not contain any votes or revocations, its vote bits
|
|
// must be 0x0001, and its final state must be all zeroes before
|
|
// stake validation begins.
|
|
if header.Height < stakeValidationHeight {
|
|
if header.Voters > 0 {
|
|
errStr := fmt.Sprintf("block at height %d commits to "+
|
|
"%d votes before stake validation height %d",
|
|
header.Height, header.Voters,
|
|
stakeValidationHeight)
|
|
return ruleError(ErrInvalidEarlyStakeTx, errStr)
|
|
}
|
|
|
|
if header.Revocations > 0 {
|
|
errStr := fmt.Sprintf("block at height %d commits to "+
|
|
"%d revocations before stake validation height %d",
|
|
header.Height, header.Revocations,
|
|
stakeValidationHeight)
|
|
return ruleError(ErrInvalidEarlyStakeTx, errStr)
|
|
}
|
|
|
|
if header.VoteBits != earlyVoteBitsValue {
|
|
errStr := fmt.Sprintf("block at height %d commits to "+
|
|
"invalid vote bits before stake validation "+
|
|
"height %d (expected %x, got %x)",
|
|
header.Height, stakeValidationHeight,
|
|
earlyVoteBitsValue, header.VoteBits)
|
|
return ruleError(ErrInvalidEarlyVoteBits, errStr)
|
|
}
|
|
|
|
if header.FinalState != earlyFinalState {
|
|
errStr := fmt.Sprintf("block at height %d commits to "+
|
|
"invalid final state before stake validation "+
|
|
"height %d (expected %x, got %x)",
|
|
header.Height, stakeValidationHeight,
|
|
earlyFinalState, header.FinalState)
|
|
return ruleError(ErrInvalidEarlyFinalState, errStr)
|
|
}
|
|
}
|
|
|
|
// A block must not contain fewer votes than the minimum required to
|
|
// reach majority once stake validation height has been reached.
|
|
if header.Height >= stakeValidationHeight {
|
|
majority := (chainParams.TicketsPerBlock / 2) + 1
|
|
if header.Voters < majority {
|
|
errStr := fmt.Sprintf("block does not commit to enough "+
|
|
"votes (min: %d, got %d)", majority,
|
|
header.Voters)
|
|
return ruleError(ErrNotEnoughVotes, errStr)
|
|
}
|
|
}
|
|
|
|
// The block header must not claim to contain more votes than the
|
|
// maximum allowed.
|
|
if header.Voters > chainParams.TicketsPerBlock {
|
|
errStr := fmt.Sprintf("block commits to too many votes (max: "+
|
|
"%d, got %d)", chainParams.TicketsPerBlock, header.Voters)
|
|
return ruleError(ErrTooManyVotes, errStr)
|
|
}
|
|
|
|
// The block must not contain more ticket purchases than the maximum
|
|
// allowed.
|
|
if header.FreshStake > chainParams.MaxFreshStakePerBlock {
|
|
errStr := fmt.Sprintf("block commits to too many ticket "+
|
|
"purchases (max: %d, got %d)",
|
|
chainParams.MaxFreshStakePerBlock, header.FreshStake)
|
|
return ruleError(ErrTooManySStxs, errStr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkBlockSanity performs some preliminary checks on a block to ensure it is
|
|
// sane before continuing with block processing. These checks are context
|
|
// free.
|
|
//
|
|
// The flags do not modify the behavior of this function directly, however they
|
|
// are needed to pass along to checkBlockHeaderSanity.
|
|
func checkBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, flags BehaviorFlags, chainParams *chaincfg.Params) error {
|
|
msgBlock := block.MsgBlock()
|
|
header := &msgBlock.Header
|
|
err := checkBlockHeaderSanity(header, timeSource, flags, chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// All ticket purchases must meet the difficulty specified by the block
|
|
// header.
|
|
err = checkProofOfStake(block, chainParams.MinimumStakeDiff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A block must have at least one regular transaction.
|
|
numTx := len(msgBlock.Transactions)
|
|
if numTx == 0 {
|
|
return ruleError(ErrNoTransactions, "block does not contain "+
|
|
"any transactions")
|
|
}
|
|
|
|
// A block must not exceed the maximum allowed block payload when
|
|
// serialized.
|
|
//
|
|
// This is a quick and context-free sanity check of the maximum block
|
|
// size according to the wire protocol. Even though the wire protocol
|
|
// already prevents blocks bigger than this limit, there are other
|
|
// methods of receiving a block that might not have been checked
|
|
// already. A separate block size is enforced later that takes into
|
|
// account the network-specific block size and the results of block
|
|
// size votes. Typically that block size is more restrictive than this
|
|
// one.
|
|
serializedSize := msgBlock.SerializeSize()
|
|
if serializedSize > wire.MaxBlockPayload {
|
|
str := fmt.Sprintf("serialized block is too big - got %d, "+
|
|
"max %d", serializedSize, wire.MaxBlockPayload)
|
|
return ruleError(ErrBlockTooBig, str)
|
|
}
|
|
if header.Size != uint32(serializedSize) {
|
|
str := fmt.Sprintf("serialized block is not size indicated in "+
|
|
"header - got %d, expected %d", header.Size,
|
|
serializedSize)
|
|
return ruleError(ErrWrongBlockSize, str)
|
|
}
|
|
|
|
// Do some preliminary checks on each regular transaction to ensure they
|
|
// are sane.
|
|
transactions := block.Transactions()
|
|
for _, tx := range transactions {
|
|
msgTx := tx.MsgTx()
|
|
err := checkTransactionSanity(msgTx, chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Do some preliminary checks on stake transactions that do not require
|
|
// additional context to determine their type to ensure they are sane while
|
|
// tallying each type before continuing.
|
|
var totalTickets int64
|
|
for _, stx := range msgBlock.STransactions {
|
|
err := checkTransactionSanity(stx, chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stake.IsSStx(stx) {
|
|
totalTickets++
|
|
}
|
|
}
|
|
|
|
// A block header must commit to the actual number of tickets purchases that
|
|
// are in the block.
|
|
if int64(header.FreshStake) != totalTickets {
|
|
str := fmt.Sprintf("block header commitment to %d ticket purchases "+
|
|
"does not match %d contained in the block", header.FreshStake,
|
|
totalTickets)
|
|
return ruleError(ErrFreshStakeMismatch, str)
|
|
}
|
|
|
|
// Check for duplicate transactions.
|
|
existingTxHashes := make(map[chainhash.Hash]struct{})
|
|
stakeTransactions := block.STransactions()
|
|
allTransactions := append(transactions, stakeTransactions...)
|
|
for _, tx := range allTransactions {
|
|
hash := tx.Hash()
|
|
if _, exists := existingTxHashes[*hash]; exists {
|
|
str := fmt.Sprintf("block contains duplicate transaction %v", hash)
|
|
return ruleError(ErrDuplicateTx, str)
|
|
}
|
|
existingTxHashes[*hash] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckBlockSanity performs some preliminary checks on a block to ensure it is
|
|
// sane before continuing with block processing. These checks are context
|
|
// free.
|
|
func CheckBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, chainParams *chaincfg.Params) error {
|
|
return checkBlockSanity(block, timeSource, BFNone, chainParams)
|
|
}
|
|
|
|
// isDCP0005Violation returns whether or not the block is known to violate
|
|
// DCP0005. Due to a bug that has since been fixed, some blocks were accepted
|
|
// that violated DCP0005 by still being submitted on an old block version
|
|
// (version 6 on mainnet) when the majority of the network had already upgraded
|
|
// to the new version specified by DCP0005 (version 7 on mainnet).
|
|
//
|
|
// The blocks that violated DCP0005 due to specifying the old version were
|
|
// otherwise valid and are whitelisted by this function so that they will be
|
|
// accepted when fully syncing the chain without checkpoints.
|
|
func isDCP0005Violation(network wire.CurrencyNet, header *wire.BlockHeader, blockHash *chainhash.Hash) bool {
|
|
switch network {
|
|
case wire.MainNet:
|
|
// 430191 is the height of the last block on mainnet that violated
|
|
// DCP0005, so return false immediately if the height is greater than
|
|
// that.
|
|
if header.Height > 430191 {
|
|
return false
|
|
}
|
|
|
|
// Return whether or not the block is any of the following 5 mainnet
|
|
// blocks that are known to violate DCP0005.
|
|
return header.Height == 413762 && *blockHash == *block413762Hash ||
|
|
header.Height == 414036 && *blockHash == *block414036Hash ||
|
|
header.Height == 424011 && *blockHash == *block424011Hash ||
|
|
header.Height == 428809 && *blockHash == *block428809Hash ||
|
|
header.Height == 430191 && *blockHash == *block430191Hash
|
|
|
|
case wire.TestNet3:
|
|
// 323282 is the height of the last block on testnet that violated
|
|
// DCP0005. Return true if the height is less than or equal to 323282
|
|
// so that the DCP0005 version check is not enforced until after that
|
|
// point.
|
|
return header.Height <= 323282
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// checkBlockHeaderPositional performs several validation checks on the block
|
|
// header which depend on its position within the block chain and having the
|
|
// headers of all ancestors available. These checks do not, and must not, rely
|
|
// on having the full block data of all ancestors available.
|
|
//
|
|
// The flags modify the behavior of this function as follows:
|
|
// - BFFastAdd: All checks except those involving comparing the header against
|
|
// the checkpoints and expected height are not performed.
|
|
//
|
|
// This function MUST be called with the chain state lock held (for reads).
|
|
func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error {
|
|
// The genesis block is valid by definition.
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
|
|
fastAdd := flags&BFFastAdd == BFFastAdd
|
|
if !fastAdd {
|
|
// Ensure the difficulty specified in the block header matches
|
|
// the calculated difficulty based on the previous block and
|
|
// difficulty retarget rules.
|
|
expDiff := b.calcNextRequiredDifficulty(prevNode, header.Timestamp)
|
|
blockDifficulty := header.Bits
|
|
if blockDifficulty != expDiff {
|
|
str := fmt.Sprintf("block difficulty of %d is not the "+
|
|
"expected value of %d", blockDifficulty, expDiff)
|
|
return ruleError(ErrUnexpectedDifficulty, str)
|
|
}
|
|
|
|
// Ensure the timestamp for the block header is after the
|
|
// median time of the last several blocks (medianTimeBlocks).
|
|
medianTime := prevNode.CalcPastMedianTime()
|
|
if !header.Timestamp.After(medianTime) {
|
|
str := "block timestamp of %v is not after expected %v"
|
|
str = fmt.Sprintf(str, header.Timestamp, medianTime)
|
|
return ruleError(ErrTimeTooOld, str)
|
|
}
|
|
}
|
|
|
|
// The height of this block is one more than the referenced previous
|
|
// block.
|
|
blockHeight := prevNode.height + 1
|
|
|
|
// Ensure the header commits to the correct height based on the height it
|
|
// actually connects in the blockchain.
|
|
if int64(header.Height) != blockHeight {
|
|
errStr := fmt.Sprintf("block header commitment to height %d "+
|
|
"does not match chain height %d", header.Height,
|
|
blockHeight)
|
|
return ruleError(ErrBadBlockHeight, errStr)
|
|
}
|
|
|
|
// Prevent blocks that fork the main chain before the most recently known
|
|
// checkpoint. This prevents storage of new, otherwise valid, blocks which
|
|
// build off of old blocks that are likely at a much easier difficulty and
|
|
// therefore could be used to waste cache and disk space.
|
|
checkpoint := b.checkpointNode
|
|
blockHash := header.BlockHash()
|
|
if checkpoint != nil && blockHeight < checkpoint.height &&
|
|
(!prevNode.IsAncestorOf(checkpoint) ||
|
|
b.index.LookupNode(&blockHash) == nil) {
|
|
|
|
str := fmt.Sprintf("block at height %d forks the main chain before "+
|
|
"the previous checkpoint at height %d", blockHeight,
|
|
checkpoint.height)
|
|
return ruleError(ErrForkTooOld, str)
|
|
}
|
|
|
|
// Ensure chain matches up to predetermined checkpoints.
|
|
if !b.verifyCheckpoint(blockHeight, &blockHash) {
|
|
str := fmt.Sprintf("block at height %d does not match "+
|
|
"checkpoint hash", blockHeight)
|
|
return ruleError(ErrBadCheckpoint, str)
|
|
}
|
|
|
|
if !fastAdd {
|
|
// Reject old version blocks once a majority of the network has
|
|
// upgraded.
|
|
//
|
|
// Note that the latest block version for all networks other than the
|
|
// main network is one higher.
|
|
latestBlockVersion := int32(9)
|
|
dcp0005Version := int32(7)
|
|
if b.chainParams.Net != wire.MainNet {
|
|
latestBlockVersion++
|
|
dcp0005Version++
|
|
}
|
|
for version := latestBlockVersion; version > 1; version-- {
|
|
// Skip the version check for blocks that are known to violate DCP0005.
|
|
// See isDCP0005Violation for more details.
|
|
if version == dcp0005Version &&
|
|
isDCP0005Violation(b.chainParams.Net, header, &blockHash) {
|
|
|
|
continue
|
|
}
|
|
|
|
if header.Version < version && b.isMajorityVersion(version,
|
|
prevNode, b.chainParams.BlockRejectNumRequired) {
|
|
|
|
str := "new blocks with version %d are no longer valid"
|
|
str = fmt.Sprintf(str, header.Version)
|
|
return ruleError(ErrBlockVersionTooOld, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkBlockDataPositional performs several validation checks on the block data
|
|
// (not including the header) which depend on its position within the block
|
|
// chain and having the headers of all ancestors available. These checks do
|
|
// not, and must not, rely on having the full block data of all ancestors
|
|
// available.
|
|
//
|
|
// The flags modify the behavior of this function as follows:
|
|
// - BFFastAdd: The transactions are not checked to see if they are expired.
|
|
//
|
|
// This function MUST be called with the chain state lock held (for reads).
|
|
func (b *BlockChain) checkBlockDataPositional(block *dcrutil.Block, prevNode *blockNode, flags BehaviorFlags) error {
|
|
// The genesis block is valid by definition.
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
|
|
fastAdd := flags&BFFastAdd == BFFastAdd
|
|
if !fastAdd {
|
|
// The height of this block is one more than the referenced previous
|
|
// block.
|
|
blockHeight := prevNode.height + 1
|
|
|
|
// Ensure all transactions in the block are not expired.
|
|
for _, tx := range block.Transactions() {
|
|
if IsExpired(tx, blockHeight) {
|
|
str := fmt.Sprintf("block contains expired regular "+
|
|
"transaction %v (expiration height %d)", tx.Hash(),
|
|
tx.MsgTx().Expiry)
|
|
return ruleError(ErrExpiredTx, str)
|
|
}
|
|
}
|
|
for _, stx := range block.STransactions() {
|
|
if IsExpired(stx, blockHeight) {
|
|
str := fmt.Sprintf("block contains expired stake "+
|
|
"transaction %v (expiration height %d)", stx.Hash(),
|
|
stx.MsgTx().Expiry)
|
|
return ruleError(ErrExpiredTx, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkBlockPositional performs several validation checks on the block (both
|
|
// its header and data) which depend on its position within the block chain and
|
|
// having the headers of all ancestors available. These checks do not, and must
|
|
// not, rely on having the full block data of all ancestors available.
|
|
//
|
|
// The flags do not modify the behavior of this function directly, however they
|
|
// are needed to pass along to checkBlockHeaderPositional and
|
|
// checkBlockDataPositional.
|
|
func (b *BlockChain) checkBlockPositional(block *dcrutil.Block, prevNode *blockNode, flags BehaviorFlags) error {
|
|
// The genesis block is valid by definition.
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
|
|
// Perform all block header related validation checks that depend on its
|
|
// position within the block chain and having the headers of all
|
|
// ancestors available, but do not rely on having the full block data of
|
|
// all ancestors available.
|
|
header := &block.MsgBlock().Header
|
|
err := b.checkBlockHeaderPositional(header, prevNode, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Perform all block data related validation checks that depend on its
|
|
// position within the block chain and having the headers of all ancestors
|
|
// available, but do not rely on having the full block data of all ancestors
|
|
// available.
|
|
return b.checkBlockDataPositional(block, prevNode, flags)
|
|
}
|
|
|
|
// checkBlockHeaderContext performs several validation checks on the block
|
|
// header which depend on having the full block data for all of its ancestors
|
|
// available. This includes checks which depend on tallying the results of
|
|
// votes, because votes are part of the block data.
|
|
//
|
|
// It should be noted that rule changes that have become buried deep enough
|
|
// typically will eventually be transitioned to using well-known activation
|
|
// points for efficiency purposes at which point the associated checks no longer
|
|
// require having direct access to the historical votes, and therefore may be
|
|
// transitioned to checkBlockHeaderPositional at that time. Conversely, any
|
|
// checks in that function which become conditional based on the results of a
|
|
// vote will necessarily need to be transitioned to this function.
|
|
//
|
|
// The flags modify the behavior of this function as follows:
|
|
// - BFFastAdd: No check are performed.
|
|
//
|
|
// This function MUST be called with the chain state lock held (for writes).
|
|
func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error {
|
|
// The genesis block is valid by definition.
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
|
|
fastAdd := flags&BFFastAdd == BFFastAdd
|
|
if !fastAdd {
|
|
// Ensure the stake difficulty specified in the block header
|
|
// matches the calculated difficulty based on the previous block
|
|
// and difficulty retarget rules.
|
|
expSDiff, err := b.calcNextRequiredStakeDifficulty(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if header.SBits != expSDiff {
|
|
errStr := fmt.Sprintf("block stake difficulty of %d "+
|
|
"is not the expected value of %d", header.SBits,
|
|
expSDiff)
|
|
return ruleError(ErrUnexpectedDifficulty, errStr)
|
|
}
|
|
|
|
// Enforce the stake version in the header once a majority of
|
|
// the network has upgraded to version 3 blocks.
|
|
if header.Version >= 3 && b.isMajorityVersion(3, prevNode,
|
|
b.chainParams.BlockEnforceNumRequired) {
|
|
|
|
expectedStakeVer := b.calcStakeVersion(prevNode)
|
|
if header.StakeVersion != expectedStakeVer {
|
|
str := fmt.Sprintf("block stake version of %d "+
|
|
"is not the expected version of %d",
|
|
header.StakeVersion, expectedStakeVer)
|
|
return ruleError(ErrBadStakeVersion, str)
|
|
}
|
|
}
|
|
|
|
// Ensure the header commits to the correct pool size based on
|
|
// its position within the chain.
|
|
parentStakeNode, err := b.fetchStakeNode(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
calcPoolSize := uint32(parentStakeNode.PoolSize())
|
|
if header.PoolSize != calcPoolSize {
|
|
errStr := fmt.Sprintf("block header commitment to "+
|
|
"pool size %d does not match expected size %d",
|
|
header.PoolSize, calcPoolSize)
|
|
return ruleError(ErrPoolSize, errStr)
|
|
}
|
|
|
|
// Ensure the header commits to the correct final state of the
|
|
// ticket lottery.
|
|
calcFinalState := parentStakeNode.FinalState()
|
|
if header.FinalState != calcFinalState {
|
|
errStr := fmt.Sprintf("block header commitment to "+
|
|
"final state of the ticket lottery %x does not "+
|
|
"match expected value %x", header.FinalState,
|
|
calcFinalState)
|
|
return ruleError(ErrInvalidFinalState, errStr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkCoinbaseUniqueHeight checks to ensure that for all blocks height > 1 the
|
|
// coinbase contains the height encoding to make coinbase hash collisions
|
|
// impossible.
|
|
func checkCoinbaseUniqueHeight(blockHeight int64, block *dcrutil.Block, treasuryEnabled bool) error {
|
|
// Block 0 and 1 are special and don't need the coinbase height checks.
|
|
if blockHeight < 2 {
|
|
return nil
|
|
}
|
|
|
|
// Choose the correct output that encodes the height depending on whether
|
|
// or not the treasury agenda is active.
|
|
//
|
|
// Prior to activation of the agenda, output 0 is the project subsidy and
|
|
// output 1 encodes the height. Once the agenda is active, the project
|
|
// subsidy is moved to the treasurybase in the stake tree and thus output 0
|
|
// then encodes the height.
|
|
nullDataOutIdx := 1
|
|
if treasuryEnabled {
|
|
nullDataOutIdx = 0
|
|
}
|
|
|
|
// There must be at least enough outputs to contain the one that encodes the
|
|
// height.
|
|
coinbaseTx := block.MsgBlock().Transactions[0]
|
|
if len(coinbaseTx.TxOut) < nullDataOutIdx+1 {
|
|
str := fmt.Sprintf("block %s is missing required coinbase outputs ("+
|
|
"num outputs: %d, min required: %d)", block.Hash(),
|
|
len(coinbaseTx.TxOut), nullDataOutIdx+1)
|
|
return ruleError(ErrFirstTxNotCoinbase, str)
|
|
}
|
|
|
|
// Only version 0 scripts are currently valid.
|
|
const scriptVersion = 0
|
|
nullDataOut := coinbaseTx.TxOut[nullDataOutIdx]
|
|
if nullDataOut.Version != scriptVersion {
|
|
str := fmt.Sprintf("block %s coinbase output %d script version %d is "+
|
|
"not the required version %d", block.Hash(), nullDataOutIdx,
|
|
nullDataOut.Version, scriptVersion)
|
|
return ruleError(ErrFirstTxNotCoinbase, str)
|
|
}
|
|
|
|
// The nulldata in the coinbase must be a single OP_RETURN followed by a
|
|
// data push up to maxUniqueCoinbaseNullDataSize bytes and the first 4 bytes
|
|
// of that data must be the encoded height of the block so that every
|
|
// coinbase created has a unique transaction hash.
|
|
//
|
|
// NOTE: This is intentionally not using script type determination functions
|
|
// from stdscript because those are specifically for standardness checks
|
|
// which can change over time and this function enforces consensus rules.
|
|
//
|
|
// Also of note is that technically normal nulldata scripts support encoding
|
|
// numbers via small opcodes, however, for legacy reasons, the consensus
|
|
// rules require the block height to be encoded as a 4-byte little-endian
|
|
// uint32 pushed via a normal data push, as opposed to using the normal
|
|
// number handling semantics of scripts, so this is specialized to
|
|
// accommodate that.
|
|
var nullData []byte
|
|
pkScript := nullDataOut.PkScript
|
|
if len(pkScript) > 1 && pkScript[0] == txscript.OP_RETURN {
|
|
tokenizer := txscript.MakeScriptTokenizer(scriptVersion, pkScript[1:])
|
|
if tokenizer.Next() && tokenizer.Done() && tokenizer.Opcode() <=
|
|
txscript.OP_PUSHDATA4 {
|
|
|
|
nullData = tokenizer.Data()
|
|
}
|
|
}
|
|
if len(nullData) > maxUniqueCoinbaseNullDataSize {
|
|
str := fmt.Sprintf("block %s coinbase output %d pushes %d bytes which "+
|
|
"is more than allowed value of %d", block.Hash(), nullDataOutIdx,
|
|
len(nullData), maxUniqueCoinbaseNullDataSize)
|
|
return ruleError(ErrFirstTxNotCoinbase, str)
|
|
}
|
|
if len(nullData) < 4 {
|
|
str := fmt.Sprintf("block %s coinbase output %d pushes %d bytes which "+
|
|
"is too short to encode height", block.Hash(), nullDataOutIdx,
|
|
len(nullData))
|
|
return ruleError(ErrFirstTxNotCoinbase, str)
|
|
}
|
|
|
|
// Check the height and ensure it is correct.
|
|
cbHeight := binary.LittleEndian.Uint32(nullData[0:4])
|
|
if cbHeight != uint32(blockHeight) {
|
|
header := &block.MsgBlock().Header
|
|
str := fmt.Sprintf("block %s coinbase output %d encodes height %d "+
|
|
"instead of expected height %d (prev block: %s, header height %d)",
|
|
block.Hash(), nullDataOutIdx, cbHeight, uint32(blockHeight),
|
|
header.PrevBlock, header.Height)
|
|
return ruleError(ErrCoinbaseHeight, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkTreasurybaseUniqueHeight ensures that for all blocks height > 1 the
|
|
// treasurybase contains the height encoding to make treasurybase hash
|
|
// collisions impossible.
|
|
func checkTreasurybaseUniqueHeight(blockHeight int64, block *dcrutil.Block) error {
|
|
// Block 0 and 1 are special and don't need the treasurybase height checks.
|
|
if blockHeight < 2 {
|
|
return nil
|
|
}
|
|
|
|
if len(block.MsgBlock().STransactions) == 0 {
|
|
return AssertError(fmt.Sprintf("checkTreasurybaseUniqueHeight must be "+
|
|
"called with a block that has already been verified to have at "+
|
|
"least one stake transaction (block %s)", block.Hash()))
|
|
}
|
|
|
|
// Treasurybase output 0 is the subsidy and output 1 encodes the height.
|
|
const nullDataOutIdx = 1
|
|
trsybaseTx := block.MsgBlock().STransactions[0]
|
|
if len(trsybaseTx.TxOut) < nullDataOutIdx+1 {
|
|
str := fmt.Sprintf("block %s is missing required OP_RETURN output ("+
|
|
"num outputs: %d, min required: %d) ", block.Hash(),
|
|
len(trsybaseTx.TxOut), nullDataOutIdx+1)
|
|
return ruleError(ErrFirstTxNotTreasurybase, str)
|
|
}
|
|
|
|
// Only version 0 scripts are currently valid.
|
|
const scriptVersion = 0
|
|
nullDataOut := trsybaseTx.TxOut[nullDataOutIdx]
|
|
if nullDataOut.Version != scriptVersion {
|
|
str := fmt.Sprintf("block %s treasurybase output %d script version %d "+
|
|
"is not the required version %d", block.Hash(), nullDataOutIdx,
|
|
nullDataOut.Version, scriptVersion)
|
|
return ruleError(ErrFirstTxNotTreasurybase, str)
|
|
}
|
|
|
|
// The nulldata in the treasurybase must be a single OP_RETURN followed by a
|
|
// data push of 12 bytes which encodes the height of the block followed by
|
|
// random data.
|
|
//
|
|
// NOTE: This is intentionally not using script type determination functions
|
|
// from stdscript because those are specifically for standardness checks
|
|
// which can change over time and this function enforces consensus rules.
|
|
//
|
|
// Also of note is that technically normal nulldata scripts support encoding
|
|
// numbers via small opcodes, however, for legacy reasons, the consensus
|
|
// rules require the block height to be encoded as a 4-byte little-endian
|
|
// uint32 pushed via a normal data push, as opposed to using the normal
|
|
// number handling semantics of scripts, so this is specialized to
|
|
// accommodate that.
|
|
var nullData []byte
|
|
pkScript := nullDataOut.PkScript
|
|
if len(pkScript) == 14 && pkScript[0] == txscript.OP_RETURN &&
|
|
pkScript[1] == txscript.OP_DATA_12 {
|
|
|
|
nullData = pkScript[2:6] // Encoded height.
|
|
}
|
|
if len(nullData) != 4 {
|
|
str := fmt.Sprintf("block %s treasurybase output %d is invalid",
|
|
block.Hash(), nullDataOutIdx)
|
|
return ruleError(ErrTreasurybaseTxNotOpReturn, str)
|
|
}
|
|
|
|
// Check the height and ensure it is correct.
|
|
encodedHeight := binary.LittleEndian.Uint32(nullData[0:4])
|
|
if encodedHeight != uint32(blockHeight) {
|
|
header := &block.MsgBlock().Header
|
|
str := fmt.Sprintf("block %s treasurybase output %d encodes height %d "+
|
|
"instead of expected height %d (prev block: %s, header height %d)",
|
|
block.Hash(), nullDataOutIdx, encodedHeight, uint32(blockHeight),
|
|
header.PrevBlock, header.Height)
|
|
return ruleError(ErrTreasurybaseHeight, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkTicketRedeemers performs validation of all votes and revocations in the
|
|
// block. The validation rules include:
|
|
// - Votes MUST spend tickets that are actually allowed to vote per the
|
|
// lottery
|
|
// - Revocations MUST spend tickets that were missed or have expired
|
|
// - If the automatic ticket revocations agenda is active, then the block MUST
|
|
// contain revocation transactions for all tickets that will become missed
|
|
// or expired as of that block
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func checkTicketRedeemers(voteTicketHashes, revocationTicketHashes, winners,
|
|
expiringNextBlock []chainhash.Hash,
|
|
existsMissedTicket func(ticket chainhash.Hash) bool, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
// Determine the winning ticket hashes and create a map that tracks whether
|
|
// a vote exists in the block for each winning hash.
|
|
winningHashes := make(map[chainhash.Hash]bool, len(winners))
|
|
for _, ticketHash := range winners {
|
|
winningHashes[ticketHash] = false
|
|
}
|
|
|
|
// Iterate through all votes in the block, validate that they are spending
|
|
// tickets that are eligible to vote, and mark the corresponding winning hash
|
|
// as voted in the winning hashes map.
|
|
for _, voteTicketHash := range voteTicketHashes {
|
|
// Ensure the ticket being spent is actually eligible to vote in this block.
|
|
if _, ok := winningHashes[voteTicketHash]; !ok {
|
|
errStr := fmt.Sprintf("block contains vote for "+
|
|
"ineligible ticket %s (eligible tickets: %s)",
|
|
voteTicketHash, winners)
|
|
return ruleError(ErrTicketUnavailable, errStr)
|
|
}
|
|
|
|
// Mark that a vote exists for the ticket hash.
|
|
winningHashes[voteTicketHash] = true
|
|
}
|
|
|
|
// Create a map of the ticket hashes that will become missed as of the block.
|
|
missedTicketHashes := make(map[chainhash.Hash]struct{}, len(winningHashes))
|
|
for ticketHash, hasVote := range winningHashes {
|
|
// If a winning ticket does not have a vote in this block, it will be
|
|
// missed.
|
|
if !hasVote {
|
|
missedTicketHashes[ticketHash] = struct{}{}
|
|
}
|
|
}
|
|
numMissed := len(missedTicketHashes)
|
|
|
|
// Create a map of the ticket hashes that will become expired as of the block.
|
|
numExpiring := len(expiringNextBlock)
|
|
expiringTicketHashes := make(map[chainhash.Hash]struct{}, numExpiring)
|
|
for _, ticketHash := range expiringNextBlock {
|
|
expiringTicketHashes[ticketHash] = struct{}{}
|
|
}
|
|
|
|
// Create a map of the ticket hashes that are being revoked in the block.
|
|
revokedTicketHashes := make(map[chainhash.Hash]struct{},
|
|
numMissed+numExpiring)
|
|
|
|
// Iterate through all revocations in the block, validate that they are
|
|
// spending tickets that are eligible to be revoked, and add the revocation
|
|
// to the revocations map.
|
|
for _, revocationTicketHash := range revocationTicketHashes {
|
|
// Determine if the ticket being revoked will become missed or expired as of
|
|
// this block.
|
|
_, missedInBlock := missedTicketHashes[revocationTicketHash]
|
|
_, expiringInBlock := expiringTicketHashes[revocationTicketHash]
|
|
missedOrExpiredInBlock := missedInBlock || expiringInBlock
|
|
|
|
// Ensure the ticket being spent is actually eligible to be revoked in this
|
|
// block. If the automatic ticket revocations agenda is active, then
|
|
// revocations are allowed if the ticket will become missed or expired as of
|
|
// this block.
|
|
eligible := (isAutoRevocationsEnabled && missedOrExpiredInBlock) ||
|
|
existsMissedTicket(revocationTicketHash)
|
|
if !eligible {
|
|
errStr := fmt.Sprintf("block contains revocation of ineligible ticket %s",
|
|
revocationTicketHash)
|
|
return ruleError(ErrInvalidSSRtx, errStr)
|
|
}
|
|
|
|
// Add the ticket hash to the revoked hashes map.
|
|
revokedTicketHashes[revocationTicketHash] = struct{}{}
|
|
}
|
|
|
|
// If the automatic ticket revocations agenda is active, then the block MUST
|
|
// contain revocation transactions for all tickets that will become missed or
|
|
// expired as of this block.
|
|
if isAutoRevocationsEnabled {
|
|
// Check that the block contains revocations for the tickets that will
|
|
// become missed as of this block.
|
|
for ticketHash := range missedTicketHashes {
|
|
if _, ok := revokedTicketHashes[ticketHash]; !ok {
|
|
errStr := fmt.Sprintf("block does not contain a revocation for ticket "+
|
|
"that is becoming missed as of this block: %s", ticketHash)
|
|
return ruleError(ErrNoMissedTicketRevocation, errStr)
|
|
}
|
|
}
|
|
|
|
// Check that the block contains revocations for the tickets that will
|
|
// become expired as of this block.
|
|
for ticketHash := range expiringTicketHashes {
|
|
if _, ok := revokedTicketHashes[ticketHash]; !ok {
|
|
errStr := fmt.Sprintf("block does not contain a revocation for ticket "+
|
|
"that is becoming expired as of this block: %s", ticketHash)
|
|
return ruleError(ErrNoExpiredTicketRevocation, errStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkMerkleRoots validates the merkle root(s) in the block header match the
|
|
// calculated value(s).
|
|
//
|
|
// Prior to the activation of the header commitments agenda, the regular
|
|
// transaction tree must match the merkle root field and the stake transaction
|
|
// tree must match the stake root field.
|
|
//
|
|
// Conversely, when the header commitments agenda is active, the merkle root
|
|
// field of the header is required to be the root of a merkle tree that has the
|
|
// individual merkle roots of the two transaction trees as leaves.
|
|
//
|
|
// This function MUST be called with the chain state lock held (for writes).
|
|
func (b *BlockChain) checkMerkleRoots(block *wire.MsgBlock, prevNode *blockNode) error {
|
|
header := &block.Header
|
|
|
|
hdrCommitmentsActive, err := b.isHeaderCommitmentsAgendaActive(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hdrCommitmentsActive {
|
|
// Build the two merkle trees and use their calculated merkle roots as
|
|
// leaves to another merkle tree and ensure the final calculated merkle
|
|
// root matches the entry in the block header.
|
|
wantMerkleRoot := standalone.CalcCombinedTxTreeMerkleRoot(
|
|
block.Transactions, block.STransactions)
|
|
if header.MerkleRoot != wantMerkleRoot {
|
|
str := fmt.Sprintf("block merkle root is invalid - block header "+
|
|
"indicates %v, but calculated value is %v", header.MerkleRoot,
|
|
wantMerkleRoot)
|
|
return ruleError(ErrBadMerkleRoot, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Fall back to the old behavior.
|
|
|
|
// Build merkle tree and ensure the calculated merkle root matches the
|
|
// entry in the block header.
|
|
wantMerkleRoot := standalone.CalcTxTreeMerkleRoot(block.Transactions)
|
|
if header.MerkleRoot != wantMerkleRoot {
|
|
str := fmt.Sprintf("block merkle root is invalid - block header "+
|
|
"indicates %v, but calculated value is %v", header.MerkleRoot,
|
|
wantMerkleRoot)
|
|
return ruleError(ErrBadMerkleRoot, str)
|
|
}
|
|
|
|
// Build the stake tx tree merkle root too and check it.
|
|
wantStakeRoot := standalone.CalcTxTreeMerkleRoot(block.STransactions)
|
|
if header.StakeRoot != wantStakeRoot {
|
|
str := fmt.Sprintf("block stake merkle root is invalid - block header "+
|
|
"indicates %v, but calculated value is %v", header.StakeRoot,
|
|
wantStakeRoot)
|
|
return ruleError(ErrBadMerkleRoot, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkBlockContext performs several validation checks on the block which
|
|
// depend on having the full block data for all of its ancestors available.
|
|
// This includes checks which depend on tallying the results of votes, because
|
|
// votes are part of the block data.
|
|
//
|
|
// It should be noted that rule changes that have become buried deep enough
|
|
// typically will eventually be transitioned to using well-known activation
|
|
// points for efficiency purposes at which point the associated checks no longer
|
|
// require having direct access to the historical votes, and therefore may be
|
|
// transitioned to checkBlockPositional at that time. Conversely, any checks in
|
|
// that function which become conditional based on the results of a vote will
|
|
// necessarily need to be transitioned to this function.
|
|
//
|
|
// The flags modify the behavior of this function as follows:
|
|
// - BFFastAdd:
|
|
// - The max block size is not checked
|
|
// - The calculated merkle root(s) of the transaction trees are not checked
|
|
// against the associated entries in the header
|
|
// - Transactions are not checked to see if they are finalized
|
|
// - The included votes, revocations, and treasury spend transactions are
|
|
// not verified to be allowed
|
|
//
|
|
// The flags are also passed to checkBlockHeaderContext. See its documentation
|
|
// for how the flags modify its behavior.
|
|
func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode, flags BehaviorFlags) error {
|
|
// The genesis block is valid by definition.
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
|
|
// No need to check the block again when it has already been checked.
|
|
if b.recentContextChecks.Contains(*block.Hash()) {
|
|
return nil
|
|
}
|
|
|
|
// Perform all block header related validation checks which depend on
|
|
// having the full block data for all of its ancestors available.
|
|
msgBlock := block.MsgBlock()
|
|
header := &msgBlock.Header
|
|
err := b.checkBlockHeaderContext(header, prevNode, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create agenda flags for checking transactions based on which ones are
|
|
// active as of the block being checked.
|
|
checkTxFlags, err := b.determineCheckTxFlags(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
isTreasuryEnabled := checkTxFlags.IsTreasuryEnabled()
|
|
isAutoRevocationsEnabled := checkTxFlags.IsAutoRevocationsEnabled()
|
|
|
|
// The first transaction in a block's regular tree must be a coinbase.
|
|
if !standalone.IsCoinBaseTx(msgBlock.Transactions[0], isTreasuryEnabled) {
|
|
str := "first transaction in block is not a coinbase"
|
|
return ruleError(ErrFirstTxNotCoinbase, str)
|
|
}
|
|
|
|
// The height of this block is one more than the referenced previous block.
|
|
blockHeight := prevNode.height + 1
|
|
|
|
// Ensure the coinbase contains the block height encoded in the expected
|
|
// output depending on its version.
|
|
err = checkCoinbaseUniqueHeight(blockHeight, block, isTreasuryEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isTreasuryEnabled {
|
|
// The first transaction in a block's stake tree must be a treasurybase.
|
|
if len(msgBlock.STransactions) == 0 ||
|
|
!standalone.IsTreasuryBase(msgBlock.STransactions[0]) {
|
|
|
|
str := "first transaction in stake tree is not a treasurybase"
|
|
return ruleError(ErrFirstTxNotTreasurybase, str)
|
|
}
|
|
|
|
// Ensure the treasurybase contains the block height encoded in the
|
|
// expected output.
|
|
err := checkTreasurybaseUniqueHeight(blockHeight, block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for txIdx, tx := range msgBlock.Transactions {
|
|
// Perform additional contextual validation checks on each regular
|
|
// transaction.
|
|
err := checkTransactionContext(tx, b.chainParams, checkTxFlags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A block must not have more than one coinbase.
|
|
if txIdx > 0 && standalone.IsCoinBaseTx(tx, isTreasuryEnabled) {
|
|
str := fmt.Sprintf("block contains second coinbase at index %d",
|
|
txIdx)
|
|
return ruleError(ErrMultipleCoinbases, str)
|
|
}
|
|
|
|
// A block must not have stake transactions in the regular transaction
|
|
// tree.
|
|
txType := stake.DetermineTxType(tx, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
if txType != stake.TxTypeRegular {
|
|
str := fmt.Sprintf("block contains a stake transaction in the "+
|
|
"regular transaction tree at index %d", txIdx)
|
|
return ruleError(ErrStakeTxInRegularTree, str)
|
|
}
|
|
}
|
|
|
|
for txIdx, stx := range msgBlock.STransactions {
|
|
// Perform additional contextual validation checks on each stake
|
|
// transaction.
|
|
err := checkTransactionContext(stx, b.chainParams, checkTxFlags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A block must not have more than one treasurybase when the treasury
|
|
// agenda is active.
|
|
if txIdx > 0 && isTreasuryEnabled && standalone.IsTreasuryBase(stx) {
|
|
str := fmt.Sprintf("block contains second treasurybase at index %d",
|
|
txIdx)
|
|
return ruleError(ErrMultipleTreasurybases, str)
|
|
}
|
|
}
|
|
|
|
// Do some preliminary checks on stake transactions and tally how many of
|
|
// each type there are as well as the number of yes votes approving the
|
|
// previous block.
|
|
//
|
|
// Also, in the case the treasury agenda is active and there are one or more
|
|
// treasury spend transactions, keep track of them for use below.
|
|
stakeValidationHeight := uint32(b.chainParams.StakeValidationHeight)
|
|
var totalTickets, totalVotes, totalRevocations int64
|
|
var totalTreasuryAdd, totalTreasurySpend, totalTreasurybase int64
|
|
var totalYesVotes int64
|
|
var treasurySpendTxns []*wire.MsgTx
|
|
for txIdx, stx := range msgBlock.STransactions {
|
|
// A block must not have regular transactions in the stake transaction
|
|
// tree.
|
|
txType := stake.DetermineTxType(stx, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
if txType == stake.TxTypeRegular {
|
|
str := fmt.Sprintf("block contains regular transaction in stake "+
|
|
"transaction tree at index %d", txIdx)
|
|
return ruleError(ErrRegTxInStakeTree, str)
|
|
}
|
|
|
|
switch txType {
|
|
case stake.TxTypeSStx:
|
|
totalTickets++
|
|
|
|
case stake.TxTypeSSGen:
|
|
totalVotes++
|
|
|
|
// All votes in a block must commit to the parent of the block once
|
|
// stake validation height has been reached.
|
|
if header.Height >= stakeValidationHeight {
|
|
votedHash, votedHeight := stake.SSGenBlockVotedOn(stx)
|
|
if (votedHash != header.PrevBlock) || (votedHeight !=
|
|
header.Height-1) {
|
|
|
|
str := fmt.Sprintf("vote %s at index %d is for parent "+
|
|
"block %s (height %d) versus expected parent block %s "+
|
|
"(height %d)", stx.TxHash(), txIdx, votedHash,
|
|
votedHeight, header.PrevBlock, header.Height-1)
|
|
return ruleError(ErrVotesOnWrongBlock, str)
|
|
}
|
|
|
|
// Tally how many votes approve the previous block for use when
|
|
// validating the header commitment.
|
|
if voteBitsApproveParent(stake.SSGenVoteBits(stx)) {
|
|
totalYesVotes++
|
|
}
|
|
}
|
|
|
|
case stake.TxTypeSSRtx:
|
|
totalRevocations++
|
|
}
|
|
|
|
// Count treasury-related stake transactions when the agenda is active.
|
|
if isTreasuryEnabled {
|
|
switch txType {
|
|
case stake.TxTypeTAdd:
|
|
totalTreasuryAdd++
|
|
|
|
case stake.TxTypeTSpend:
|
|
totalTreasurySpend++
|
|
treasurySpendTxns = append(treasurySpendTxns, stx)
|
|
|
|
case stake.TxTypeTreasuryBase:
|
|
totalTreasurybase++
|
|
}
|
|
}
|
|
}
|
|
|
|
// A block must not contain more than the maximum allowed number of treasury
|
|
// add transactions.
|
|
if totalTreasuryAdd > MaxTAddsPerBlock {
|
|
str := fmt.Sprintf("block contains %d treasury adds which exceeds the "+
|
|
"maximum allowed amount of %d", totalTreasuryAdd, MaxTAddsPerBlock)
|
|
return ruleError(ErrTooManyTAdds, str)
|
|
}
|
|
|
|
// A block must only contain stake transactions of the allowed types.
|
|
//
|
|
// NOTE: This is not possible to hit at the time this comment was written
|
|
// because all transactions which are not specifically one of the recognized
|
|
// stake transaction forms are considered regular transactions and those are
|
|
// rejected above. However, if a new stake transaction type is added, that
|
|
// implicit condition would no longer hold and therefore an explicit check
|
|
// is performed here.
|
|
numStakeTx := int64(len(msgBlock.STransactions))
|
|
expectedNumStakeTx := totalTickets + totalVotes + totalRevocations +
|
|
totalTreasuryAdd + totalTreasurySpend + totalTreasurybase
|
|
if numStakeTx != expectedNumStakeTx {
|
|
str := fmt.Sprintf("block contains an unexpected number of stake "+
|
|
"transactions (contains %d, expected %d)", numStakeTx,
|
|
expectedNumStakeTx)
|
|
return ruleError(ErrNonstandardStakeTx, str)
|
|
}
|
|
|
|
// A block header must commit to the actual number of votes that are in the
|
|
// block.
|
|
if int64(header.Voters) != totalVotes {
|
|
str := fmt.Sprintf("block header commitment to %d votes does not "+
|
|
"match %d contained in the block", header.Voters, totalVotes)
|
|
return ruleError(ErrVotesMismatch, str)
|
|
}
|
|
|
|
// A block header must commit to the same previous block acceptance
|
|
// semantics expressed by the votes once stake validation height has been
|
|
// reached.
|
|
if header.Height >= stakeValidationHeight {
|
|
totalNoVotes := totalVotes - totalYesVotes
|
|
headerApproves := headerApprovesParent(header)
|
|
votesApprove := totalYesVotes > totalNoVotes
|
|
if headerApproves != votesApprove {
|
|
str := fmt.Sprintf("block header commitment to previous block "+
|
|
"approval does not match votes (header claims: %v, votes: %v)",
|
|
headerApproves, votesApprove)
|
|
return ruleError(ErrIncongruentVotebit, str)
|
|
}
|
|
}
|
|
|
|
// A block must not contain anything other than ticket purchases and
|
|
// treasury transactions (when agenda is enabled) prior to stake validation
|
|
// height.
|
|
if header.Height < stakeValidationHeight {
|
|
numExpected := totalTickets + totalTreasuryAdd + totalTreasurybase
|
|
if int64(len(msgBlock.STransactions)) != numExpected {
|
|
str := fmt.Sprintf("block contains disallowed stake transactions "+
|
|
"before stake validation height %d (total: %d, expected %d)",
|
|
stakeValidationHeight, len(msgBlock.STransactions), numExpected)
|
|
return ruleError(ErrInvalidEarlyStakeTx, str)
|
|
}
|
|
}
|
|
|
|
// A block must not contain more than the maximum allowed number of
|
|
// revocations.
|
|
if totalRevocations > maxRevocationsPerBlock {
|
|
str := fmt.Sprintf("block contains %d revocations which exceeds the "+
|
|
"maximum allowed amount of %d", totalRevocations,
|
|
maxRevocationsPerBlock)
|
|
return ruleError(ErrTooManyRevocations, str)
|
|
}
|
|
|
|
// A block header must commit to the actual number of revocations that
|
|
// are in the block.
|
|
if int64(header.Revocations) != totalRevocations {
|
|
str := fmt.Sprintf("block header commitment to %d revocations does "+
|
|
"not match %d contained in the block", header.Revocations,
|
|
totalRevocations)
|
|
return ruleError(ErrRevocationsMismatch, str)
|
|
}
|
|
|
|
// The number of signature operations must be less than the maximum allowed
|
|
// per block.
|
|
totalSigOps := 0
|
|
regularTxns := block.Transactions()
|
|
stakeTxns := block.STransactions()
|
|
allTxns := make([]*dcrutil.Tx, 0, len(regularTxns)+len(stakeTxns))
|
|
allTxns = append(allTxns, regularTxns...)
|
|
allTxns = append(allTxns, stakeTxns...)
|
|
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, isTreasuryEnabled)
|
|
totalSigOps += CountSigOps(tx, isCoinBase, isSSGen, isTreasuryEnabled)
|
|
if totalSigOps < lastSigOps || totalSigOps > MaxSigOpsPerBlock {
|
|
str := fmt.Sprintf("block contains too many signature operations "+
|
|
"- got %v, max %v", totalSigOps, MaxSigOpsPerBlock)
|
|
return ruleError(ErrTooManySigOps, str)
|
|
}
|
|
}
|
|
|
|
fastAdd := flags&BFFastAdd == BFFastAdd
|
|
if !fastAdd {
|
|
// A block must not exceed the maximum allowed size as defined by the
|
|
// network parameters and the current status of any hard fork votes to
|
|
// change it when serialized.
|
|
maxBlockSize, err := b.maxBlockSize(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serializedSize := int64(block.MsgBlock().Header.Size)
|
|
if serializedSize > maxBlockSize {
|
|
str := fmt.Sprintf("serialized block is too big - got %d, max %d",
|
|
serializedSize, maxBlockSize)
|
|
return ruleError(ErrBlockTooBig, str)
|
|
}
|
|
|
|
// The calculated merkle root(s) of the transaction trees must match
|
|
// the associated entries in the header.
|
|
err = b.checkMerkleRoots(block.MsgBlock(), prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Switch to using the past median time of the block prior to the block
|
|
// being checked for all checks related to lock times once the stake
|
|
// vote for the agenda is active.
|
|
blockTime := header.Timestamp
|
|
lnFeaturesActive, err := b.isLNFeaturesAgendaActive(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lnFeaturesActive {
|
|
blockTime = prevNode.CalcPastMedianTime()
|
|
}
|
|
|
|
// Ensure all transactions in the block are finalized.
|
|
for _, tx := range block.Transactions() {
|
|
if !IsFinalizedTransaction(tx, blockHeight, blockTime) {
|
|
str := fmt.Sprintf("block contains unfinalized regular "+
|
|
"transaction %v", tx.Hash())
|
|
return ruleError(ErrUnfinalizedTx, str)
|
|
}
|
|
}
|
|
for _, stx := range block.STransactions() {
|
|
if !IsFinalizedTransaction(stx, blockHeight, blockTime) {
|
|
str := fmt.Sprintf("block contains unfinalized stake "+
|
|
"transaction %v", stx.Hash())
|
|
return ruleError(ErrUnfinalizedTx, str)
|
|
|
|
}
|
|
}
|
|
|
|
// Ensure that all votes are only for winning tickets and all
|
|
// revocations are actually eligible to be revoked once stake validation
|
|
// height has been reached.
|
|
if header.Height >= stakeValidationHeight {
|
|
parentStakeNode, err := b.fetchStakeNode(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create slices of the ticket hashes that votes and revocations are
|
|
// spending in the block.
|
|
ticketsPerBlock := b.chainParams.TicketsPerBlock
|
|
voteTicketHashes := make([]chainhash.Hash, 0, ticketsPerBlock)
|
|
revocationTicketHashes := make([]chainhash.Hash, 0, ticketsPerBlock)
|
|
for _, stx := range block.MsgBlock().STransactions {
|
|
// Append vote ticket hashes.
|
|
if stake.IsSSGen(stx, isTreasuryEnabled) {
|
|
ticketHash := stx.TxIn[1].PreviousOutPoint.Hash
|
|
voteTicketHashes = append(voteTicketHashes, ticketHash)
|
|
|
|
continue
|
|
}
|
|
|
|
// Append revocation ticket hashes.
|
|
if stake.IsSSRtx(stx, isAutoRevocationsEnabled) {
|
|
ticketHash := stx.TxIn[0].PreviousOutPoint.Hash
|
|
revocationTicketHashes = append(revocationTicketHashes, ticketHash)
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Validate that all votes and revocations in the block are redeeming
|
|
// tickets according to consensus rules.
|
|
err = checkTicketRedeemers(voteTicketHashes, revocationTicketHashes,
|
|
parentStakeNode.Winners(), parentStakeNode.ExpiringNextBlock(),
|
|
parentStakeNode.ExistsMissedTicket, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Treasury spend transactions must only be present in blocks that fall
|
|
// on a treasury voting interval (TVI) after a full voting window is
|
|
// possible. Treasury spend transactions are not allowed before a full
|
|
// voting window is possible since it is not possible for them to have
|
|
// the required number of votes at that point.
|
|
//
|
|
// Also, blocks 0 and 1 are special, so they must be exempted.
|
|
if isTreasuryEnabled && blockHeight > 1 {
|
|
tvi := b.chainParams.TreasuryVoteInterval
|
|
isTVI := standalone.IsTreasuryVoteInterval(uint64(blockHeight), tvi)
|
|
if !isTVI && len(treasurySpendTxns) != 0 {
|
|
tx := treasurySpendTxns[0]
|
|
curHeight := uint64(blockHeight)
|
|
nextTVI := curHeight + (tvi - (curHeight % tvi))
|
|
str := fmt.Sprintf("block contains treasury spend %s while "+
|
|
"not on a treasury vote interval (block height: %d, next "+
|
|
"TVI: %d)", tx.TxHash(), blockHeight, nextTVI)
|
|
return ruleError(ErrNotTVI, str)
|
|
}
|
|
|
|
if isTVI {
|
|
minRequiredExpiry := 2 + uint64(stakeValidationHeight) +
|
|
tvi*b.chainParams.TreasuryVoteIntervalMultiplier
|
|
for _, tx := range treasurySpendTxns {
|
|
if uint64(tx.Expiry) < minRequiredExpiry {
|
|
str := fmt.Sprintf("block contains treasury spend "+
|
|
"transaction %s before a full voting window is "+
|
|
"possible (height: %d, expiry: %d, min required "+
|
|
"expiry: %d)", tx.TxHash(), blockHeight, tx.Expiry,
|
|
minRequiredExpiry)
|
|
return ruleError(ErrInvalidTVoteWindow, str)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDupTxs ensures blocks do not contain duplicate transactions which
|
|
// 'overwrite' older transactions that are not fully spent. This prevents an
|
|
// attack where a coinbase and all of its dependent transactions could be
|
|
// duplicated to effectively revert the overwritten transactions to a single
|
|
// confirmation thereby making them vulnerable to a double spend.
|
|
//
|
|
// For more details, see https://en.bitcoin.it/wiki/BIP_0030 and
|
|
// http://r6.ca/blog/20120206T005236Z.html.
|
|
//
|
|
// Decred: Check the stake transactions to make sure they don't have this txid
|
|
// too.
|
|
func (b *BlockChain) checkDupTxs(txSet []*dcrutil.Tx, view *UtxoViewpoint, tree int8) error {
|
|
if !checkForDuplicateHashes {
|
|
return nil
|
|
}
|
|
|
|
// Fetch utxos for all of the transaction outputs in this block.
|
|
// Typically, there will not be any utxos for any of the outputs.
|
|
filteredSet := make(ViewFilteredSet)
|
|
for _, tx := range txSet {
|
|
outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
|
|
for txOutIdx := range tx.MsgTx().TxOut {
|
|
outpoint.Index = uint32(txOutIdx)
|
|
filteredSet.add(view, &outpoint)
|
|
}
|
|
}
|
|
err := view.fetchUtxosMain(filteredSet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Duplicate transaction outputs are only allowed if the previous
|
|
// transaction output is spent.
|
|
for _, tx := range txSet {
|
|
outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
|
|
for txOutIdx := range tx.MsgTx().TxOut {
|
|
outpoint.Index = uint32(txOutIdx)
|
|
entry := view.LookupEntry(outpoint)
|
|
if entry != nil && !entry.IsSpent() {
|
|
str := fmt.Sprintf("tried to overwrite transaction output %v "+
|
|
"at block height %d that is not spent",
|
|
outpoint, entry.BlockHeight())
|
|
return ruleError(ErrOverwriteTx, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractStakePubKeyHash extracts a pubkey hash from the passed public key
|
|
// script if it is a standard pay-to-pubkey-hash script tagged with the provided
|
|
// stake opcode. It will return nil otherwise.
|
|
func extractStakePubKeyHash(script []byte, stakeOpcode byte) []byte {
|
|
if len(script) == 26 &&
|
|
script[0] == stakeOpcode &&
|
|
script[1] == txscript.OP_DUP &&
|
|
script[2] == txscript.OP_HASH160 &&
|
|
script[3] == txscript.OP_DATA_20 &&
|
|
script[24] == txscript.OP_EQUALVERIFY &&
|
|
script[25] == txscript.OP_CHECKSIG {
|
|
|
|
return script[4:24]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isStakePubKeyHash returns whether or not the passed public key script is a
|
|
// standard pay-to-pubkey-hash script tagged with the provided stake opcode.
|
|
func isStakePubKeyHash(script []byte, stakeOpcode byte) bool {
|
|
return extractStakePubKeyHash(script, stakeOpcode) != nil
|
|
}
|
|
|
|
// extractStakeScriptHash extracts a script hash from the passed public key
|
|
// script if it is a standard pay-to-script-hash script tagged with the provided
|
|
// stake opcode. It will return nil otherwise.
|
|
func extractStakeScriptHash(script []byte, stakeOpcode byte) []byte {
|
|
if len(script) == 24 &&
|
|
script[0] == stakeOpcode &&
|
|
script[1] == txscript.OP_HASH160 &&
|
|
script[2] == txscript.OP_DATA_20 &&
|
|
script[23] == txscript.OP_EQUAL {
|
|
|
|
return script[3:23]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isStakeScriptHash returns whether or not the passed public key script is a
|
|
// standard pay-to-script-hash script tagged with the provided stake opcode.
|
|
func isStakeScriptHash(script []byte, stakeOpcode byte) bool {
|
|
return extractStakeScriptHash(script, stakeOpcode) != nil
|
|
}
|
|
|
|
// isAllowedTicketInputScriptForm returns whether or not the passed public key
|
|
// script is one of the allowed forms for a ticket input.
|
|
func isAllowedTicketInputScriptForm(script []byte) bool {
|
|
return isPubKeyHash(script) || isScriptHash(script) ||
|
|
isStakePubKeyHash(script, txscript.OP_SSGEN) ||
|
|
isStakeScriptHash(script, txscript.OP_SSGEN) ||
|
|
isStakePubKeyHash(script, txscript.OP_SSRTX) ||
|
|
isStakeScriptHash(script, txscript.OP_SSRTX) ||
|
|
isStakePubKeyHash(script, txscript.OP_SSTXCHANGE) ||
|
|
isStakeScriptHash(script, txscript.OP_SSTXCHANGE)
|
|
}
|
|
|
|
// extractTicketCommitAmount extracts and decodes the amount from a ticket
|
|
// output commitment script.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided script is
|
|
// a commitment output script or the function may panic.
|
|
func extractTicketCommitAmount(script []byte) int64 {
|
|
// Extract the encoded amount from the commitment output associated with
|
|
// the input. The MSB of the encoded amount specifies if the output is
|
|
// P2SH, so it must be cleared to get the decoded amount.
|
|
amtBytes := script[commitAmountStartIdx:commitAmountEndIdx]
|
|
amtEncoded := binary.LittleEndian.Uint64(amtBytes)
|
|
return int64(amtEncoded & ^commitP2SHFlag)
|
|
}
|
|
|
|
// checkTicketPurchaseInputs performs a series of checks on the inputs to a
|
|
// ticket purchase transaction. An example of some of the checks include
|
|
// verifying all inputs exist, ensuring the input type requirements are met,
|
|
// and validating the output commitments coincide with the inputs.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided transaction
|
|
// is a ticket purchase.
|
|
func checkTicketPurchaseInputs(msgTx *wire.MsgTx, view *UtxoViewpoint) error {
|
|
// Assert there are two outputs for each input to the ticket as well as the
|
|
// additional voting rights output.
|
|
if (len(msgTx.TxIn)*2 + 1) != len(msgTx.TxOut) {
|
|
panicf("attempt to check ticket purchase inputs on tx %s which does "+
|
|
"not appear to be a ticket purchase (%d inputs, %d outputs)",
|
|
msgTx.TxHash(), len(msgTx.TxIn), len(msgTx.TxOut))
|
|
}
|
|
|
|
for txInIdx := 0; txInIdx < len(msgTx.TxIn); txInIdx++ {
|
|
txIn := msgTx.TxIn[txInIdx]
|
|
entry := view.LookupEntry(txIn.PreviousOutPoint)
|
|
if entry == nil || entry.IsSpent() {
|
|
str := fmt.Sprintf("output %v referenced from transaction %s:%d "+
|
|
"either does not exist or has already been spent",
|
|
txIn.PreviousOutPoint, msgTx.TxHash(), txInIdx)
|
|
return ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// Ensure the output being spent is one of the allowed script forms. In
|
|
// particular, the allowed forms are pay-to-pubkey-hash and
|
|
// pay-to-script-hash either in the standard form (without a stake
|
|
// opcode) or their stake-tagged variant (with a stake opcode).
|
|
pkScriptVer := entry.ScriptVersion()
|
|
if pkScriptVer != 0 {
|
|
str := fmt.Sprintf("output %v script version %d referenced by "+
|
|
"ticket %s:%d is not supported", txIn.PreviousOutPoint,
|
|
pkScriptVer, msgTx.TxHash(), txInIdx)
|
|
return ruleError(ErrTicketInputScript, str)
|
|
}
|
|
pkScript := entry.PkScript()
|
|
if !isAllowedTicketInputScriptForm(pkScript) {
|
|
str := fmt.Sprintf("output %v referenced from ticket %s:%d "+
|
|
"is not pay-to-pubkey-hash or pay-to-script-hash (script: %x)",
|
|
txIn.PreviousOutPoint, msgTx.TxHash(), txInIdx, pkScript)
|
|
return ruleError(ErrTicketInputScript, str)
|
|
}
|
|
|
|
// Extract the amount from the commitment output associated with the
|
|
// input and ensure it matches the expected amount calculated from the
|
|
// actual input amount and change.
|
|
commitmentOutIdx := txInIdx*2 + 1
|
|
commitmentScript := msgTx.TxOut[commitmentOutIdx].PkScript
|
|
commitmentAmount := extractTicketCommitAmount(commitmentScript)
|
|
inputAmount := entry.Amount()
|
|
change := msgTx.TxOut[commitmentOutIdx+1].Value
|
|
adjustedAmount := commitmentAmount + change
|
|
if adjustedAmount != inputAmount {
|
|
str := fmt.Sprintf("ticket output %d pays a different amount than "+
|
|
"the associated input %d (input: %v, commitment: %v, change: "+
|
|
"%v)", commitmentOutIdx, txInIdx, inputAmount, commitmentAmount,
|
|
change)
|
|
return ruleError(ErrTicketCommitment, str)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isStakeSubmission returns whether or not the passed public key script is a
|
|
// standard pay-to-script-hash or pay-to-script-hash script tagged with the
|
|
// stake submission opcode.
|
|
func isStakeSubmission(script []byte) bool {
|
|
return isStakePubKeyHash(script, txscript.OP_SSTX) ||
|
|
isStakeScriptHash(script, txscript.OP_SSTX)
|
|
}
|
|
|
|
// extractTicketCommitHash extracts the commitment hash from a ticket output
|
|
// commitment script.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided script is
|
|
// a commitment output script or the function may panic.
|
|
func extractTicketCommitHash(script []byte) []byte {
|
|
return script[commitHashStartIdx:commitHashEndIdx]
|
|
}
|
|
|
|
// isTicketCommitP2SH returns whether or not the passed ticket output commitment
|
|
// script commits to a hash which represents a pay-to-script-hash output. When
|
|
// false, it commits to a hash which represents a pay-to-pubkey-hash output.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided script is
|
|
// a commitment output script or the function may panic.
|
|
func isTicketCommitP2SH(script []byte) bool {
|
|
// The MSB of the encoded amount specifies if the output is P2SH. Since
|
|
// it is encoded with little endian, the MSB is in final byte in the encoded
|
|
// amount.
|
|
//
|
|
// This is a faster equivalent of:
|
|
//
|
|
// amtBytes := script[commitAmountStartIdx:commitAmountEndIdx]
|
|
// amtEncoded := binary.LittleEndian.Uint64(amtBytes)
|
|
// return (amtEncoded & commitP2SHFlag) != 0
|
|
//
|
|
return script[commitAmountEndIdx-1]&0x80 != 0
|
|
}
|
|
|
|
// calcTicketReturnAmounts calculates the required amounts to return from a
|
|
// ticket for the given original contribution amounts, the price the ticket was
|
|
// purchased for, and the vote subsidy (if any) to include.
|
|
//
|
|
// Since multiple inputs can be used to purchase a ticket, each one contributes
|
|
// a portion of the overall ticket purchase, including the transaction fee.
|
|
// Thus, when claiming the ticket, either due to it being selected to vote, or
|
|
// being revoked, each output must receive the same proportion of the total
|
|
// amount returned.
|
|
//
|
|
// After the original contribution amounts are evenly distributed to each
|
|
// output, there may be a remainder left over of 1 to numOutputs - 1 atoms. For
|
|
// votes, this remainder ends up as part of the transaction fee. If the
|
|
// automatic ticket revocations agenda is NOT active, then this is the same for
|
|
// revocations and the remainder ends up as part of the transaction fee. If the
|
|
// automatic ticket revocations agenda IS active, then for revocations each atom
|
|
// in the remainder is added to an output index that is selected in a uniformly
|
|
// random manner, where Hash256PRNG seeded with the previous header bytes is
|
|
// used for the deterministic pseudorandom selection.
|
|
//
|
|
// The vote subsidy must be 0 for revocations since, unlike votes, they do not
|
|
// produce any additional subsidy.
|
|
func calcTicketReturnAmounts(ticketOuts []*stake.MinimalOutput,
|
|
ticketPurchaseAmount, voteSubsidy int64, prevHeaderBytes []byte, isVote,
|
|
isAutoRevocationsEnabled bool) []int64 {
|
|
|
|
// Make an initial pass over the ticket commitments to calculate the overall
|
|
// contribution sum. This is necessary because the output amounts are
|
|
// required to be scaled to maintain the same proportions as the original
|
|
// contributions to the ticket, and the overall contribution sum is needed
|
|
// to calculate those proportions.
|
|
//
|
|
// The calculations also require more than 64-bits, so convert it to a big
|
|
// integer here to avoid multiple conversions later.
|
|
var contributionSum int64
|
|
for i := 1; i < len(ticketOuts); i += 2 {
|
|
contributionSum += extractTicketCommitAmount(ticketOuts[i].PkScript)
|
|
}
|
|
contributionSumBig := big.NewInt(contributionSum)
|
|
|
|
// Create a slice for the return amounts.
|
|
numReturnAmounts := uint32((len(ticketOuts) - 1) / 2)
|
|
returnAmounts := make([]int64, numReturnAmounts)
|
|
|
|
// Calculate the return amounts and track the total return amount.
|
|
totalOutputAmt := ticketPurchaseAmount + voteSubsidy
|
|
totalOutputAmtBig := big.NewInt(totalOutputAmt)
|
|
totalReturnAmount := int64(0)
|
|
for i := 0; i < len(returnAmounts); i++ {
|
|
// This is effectively equivalent to:
|
|
//
|
|
// total output amount
|
|
// return amount = ------------------- * contribution amount
|
|
// total contributions
|
|
//
|
|
// However, in order to avoid floating point math, it uses 64.32 fixed point
|
|
// integer division to perform:
|
|
//
|
|
// -- --
|
|
// | total output amount * contribution amount * 2^32 |
|
|
// | ------------------------------------------------ |
|
|
// return amount = | total contributions |
|
|
// -- --
|
|
// ----------------------------------------------------
|
|
// 2^32
|
|
//
|
|
ticketOut := ticketOuts[i*2+1]
|
|
returnAmtBig := big.NewInt(extractTicketCommitAmount(ticketOut.PkScript))
|
|
returnAmtBig.Mul(returnAmtBig, totalOutputAmtBig)
|
|
returnAmtBig.Lsh(returnAmtBig, 32)
|
|
returnAmtBig.Div(returnAmtBig, contributionSumBig)
|
|
returnAmtBig.Rsh(returnAmtBig, 32)
|
|
returnAmounts[i] = returnAmtBig.Int64()
|
|
|
|
// Add to the total return amount.
|
|
totalReturnAmount += returnAmounts[i]
|
|
}
|
|
|
|
// At this point, the original contribution amounts have been evenly
|
|
// distributed to each output.
|
|
//
|
|
// For votes, return now and any remainder left over will become part of the
|
|
// transaction fee.
|
|
if isVote {
|
|
return returnAmounts
|
|
}
|
|
|
|
// For revocations, if the automatic ticket revocations agenda is active, and
|
|
// there is a remainder after distributing the returns, then select a
|
|
// uniformly pseudorandom output index to receive each remaining atom.
|
|
if isAutoRevocationsEnabled && totalReturnAmount < totalOutputAmt {
|
|
remainder := totalOutputAmt - totalReturnAmount
|
|
prng := stake.NewHash256PRNG(prevHeaderBytes)
|
|
for i := int64(0); i < remainder; i++ {
|
|
returnIndex := prng.UniformRandom(numReturnAmounts)
|
|
returnAmounts[returnIndex] += 1
|
|
}
|
|
}
|
|
|
|
return returnAmounts
|
|
}
|
|
|
|
// extractTicketCommitFeeLimits extracts the encoded fee limits from a ticket
|
|
// output commitment script.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided script is
|
|
// a commitment output script or the function may panic.
|
|
func extractTicketCommitFeeLimits(script []byte) uint16 {
|
|
encodedLimits := script[commitFeeLimitStartIdx:commitFeeLimitEndIdx]
|
|
return binary.LittleEndian.Uint16(encodedLimits)
|
|
}
|
|
|
|
// checkTicketSubmissionInput ensures the provided unspent transaction output is
|
|
// a supported ticket submission output in terms of the script version and type
|
|
// as well as ensuring the transaction that houses the output is a ticket.
|
|
//
|
|
// Note that the returned error is not a RuleError, so the it is up to the
|
|
// caller convert it accordingly.
|
|
func checkTicketSubmissionInput(ticketUtxo *UtxoEntry) error {
|
|
submissionScriptVer := ticketUtxo.ScriptVersion()
|
|
if submissionScriptVer != 0 {
|
|
return fmt.Errorf("script version %d is not supported",
|
|
submissionScriptVer)
|
|
}
|
|
submissionScript := ticketUtxo.PkScript()
|
|
if !isStakeSubmission(submissionScript) {
|
|
return fmt.Errorf("not a supported stake submission script (script: "+
|
|
"%x)", submissionScript)
|
|
}
|
|
|
|
// Ensure the referenced output is from a ticket. This also proves the form
|
|
// of the transaction and its outputs are as expected so subsequent code is
|
|
// able to avoid a lot of additional overhead rechecking things.
|
|
if ticketUtxo.TransactionType() != stake.TxTypeSStx {
|
|
return fmt.Errorf("not a submission script")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkTicketRedeemerCommitments ensures the outputs of the provided
|
|
// transaction, which MUST be either a vote or revocation, adhere to the
|
|
// commitments in the provided ticket outputs. The vote subsidy MUST be zero
|
|
// for revocations since they do not produce any additional subsidy.
|
|
//
|
|
// NOTE: This is only intended to be a helper to refactor out common code from
|
|
// checkVoteInputs and checkRevocationInputs.
|
|
func checkTicketRedeemerCommitments(ticketHash *chainhash.Hash,
|
|
ticketOuts []*stake.MinimalOutput, msgTx *wire.MsgTx, isVote bool,
|
|
voteSubsidy int64, prevHeader *wire.BlockHeader, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
// The outputs that satisfy the commitments of the ticket start at offset
|
|
// 2 for votes while they start at 0 for revocations. Also, the payments
|
|
// must be tagged with the appropriate stake opcode depending on whether it
|
|
// is a vote or a revocation. Finally, the fee limits in the original
|
|
// ticket commitment differ for votes and revocations, so choose the correct
|
|
// bit flag and mask accordingly.
|
|
startIdx := 2
|
|
reqStakeOpcode := byte(txscript.OP_SSGEN)
|
|
hasFeeLimitFlag := uint16(stake.SStxVoteFractionFlag)
|
|
feeLimitMask := uint16(stake.SStxVoteReturnFractionMask)
|
|
if !isVote {
|
|
startIdx = 0
|
|
reqStakeOpcode = txscript.OP_SSRTX
|
|
hasFeeLimitFlag = stake.SStxRevFractionFlag
|
|
feeLimitMask = stake.SStxRevReturnFractionMask
|
|
}
|
|
ticketPaidAmt := ticketOuts[submissionOutputIdx].Value
|
|
|
|
// Get the previous header bytes.
|
|
prevHeaderBytes, err := prevHeader.Bytes()
|
|
if err != nil {
|
|
str := fmt.Sprintf("failed to serialize header for block %v",
|
|
prevHeader.BlockHash())
|
|
return ruleError(ErrSerializeHeader, str)
|
|
}
|
|
|
|
// Calculate the expected output amounts.
|
|
expectedOutAmts := calcTicketReturnAmounts(ticketOuts, ticketPaidAmt,
|
|
voteSubsidy, prevHeaderBytes, isVote, isAutoRevocationsEnabled)
|
|
|
|
// If treasury is enabled and we have a vote then we need to subtract
|
|
// one from the length of TxOut slice.
|
|
var extra int
|
|
if isTreasuryEnabled {
|
|
tvt, _ := stake.CheckSSGenVotes(msgTx, isTreasuryEnabled)
|
|
if tvt != nil {
|
|
extra = 1
|
|
}
|
|
}
|
|
|
|
for txOutIdx := startIdx; txOutIdx < len(msgTx.TxOut)-extra; txOutIdx++ {
|
|
// Ensure the output is paying to the address and type specified by the
|
|
// original commitment in the ticket and is a version 0 script.
|
|
//
|
|
// Note that this specifically limits the types of allowed outputs to
|
|
// P2PKH and P2SH, since the original commitment has the same
|
|
// limitation.
|
|
txOut := msgTx.TxOut[txOutIdx]
|
|
if txOut.Version != 0 {
|
|
str := fmt.Sprintf("output %s:%d script version %d is not "+
|
|
"supported", msgTx.TxHash(), txOutIdx, txOut.Version)
|
|
return ruleError(ErrBadPayeeScriptVersion, str)
|
|
}
|
|
|
|
commitmentOutIdx := (txOutIdx-startIdx)*2 + 1
|
|
commitmentScript := ticketOuts[commitmentOutIdx].PkScript
|
|
var paymentHash []byte
|
|
if isTicketCommitP2SH(commitmentScript) {
|
|
paymentHash = extractStakeScriptHash(txOut.PkScript, reqStakeOpcode)
|
|
if paymentHash == nil {
|
|
str := fmt.Sprintf("output %s:%d payment script type is not "+
|
|
"pay-to-script-hash as required by ticket output "+
|
|
"commitment %s:%d", msgTx.TxHash(), txOutIdx, ticketHash,
|
|
commitmentOutIdx)
|
|
return ruleError(ErrBadPayeeScriptType, str)
|
|
}
|
|
} else {
|
|
paymentHash = extractStakePubKeyHash(txOut.PkScript, reqStakeOpcode)
|
|
if paymentHash == nil {
|
|
str := fmt.Sprintf("output %s:%d payment script type is not "+
|
|
"pay-to-pubkey-hash as required by ticket output "+
|
|
"commitment %s:%d", msgTx.TxHash(), txOutIdx, ticketHash,
|
|
commitmentOutIdx)
|
|
return ruleError(ErrBadPayeeScriptType, str)
|
|
}
|
|
}
|
|
commitmentHash := extractTicketCommitHash(commitmentScript)
|
|
if !bytes.Equal(paymentHash, commitmentHash) {
|
|
str := fmt.Sprintf("output %s:%d does not pay to the hash "+
|
|
"specified by ticket output commitment %s:%d (ticket commits "+
|
|
"to %x, output pays %x)", msgTx.TxHash(), txOutIdx, ticketHash,
|
|
commitmentOutIdx, commitmentHash, paymentHash)
|
|
return ruleError(ErrMismatchedPayeeHash, str)
|
|
}
|
|
|
|
// Determine the fee limit that is imposed. If the transaction is a
|
|
// revocation, the version is greater than or equal to 2, and the automatic
|
|
// ticket revocation agenda is active, then the fee MUST be zero.
|
|
// Otherwise, the encoded fee limit from the ticket output commitment script
|
|
// should be used.
|
|
var feeLimitsEncoded uint16
|
|
var hasFeeLimit bool
|
|
if isVote || !isAutoRevocationsEnabled ||
|
|
msgTx.Version < stake.TxVersionAutoRevocations {
|
|
|
|
feeLimitsEncoded = extractTicketCommitFeeLimits(commitmentScript)
|
|
hasFeeLimit = feeLimitsEncoded&hasFeeLimitFlag != 0
|
|
}
|
|
|
|
// Ensure the amount paid adheres to the commitment while taking into
|
|
// account any fee limits that might be imposed. The output amount must
|
|
// exactly match the calculated amount when not encumbered with a
|
|
// fee limit. On the other hand, when it is encumbered, it must be
|
|
// between the minimum amount imposed by the fee limit and the
|
|
// calculated amount.
|
|
expectedOutAmt := expectedOutAmts[txOutIdx-startIdx]
|
|
if !hasFeeLimit {
|
|
// The output amount must exactly match the calculated amount when
|
|
// not encumbered with a fee limit.
|
|
if txOut.Value != expectedOutAmt {
|
|
str := fmt.Sprintf("output %s:%d does not pay the expected "+
|
|
"amount per ticket output commitment %s:%d (expected %d, "+
|
|
"output pays %d)", msgTx.TxHash(), txOutIdx, ticketHash,
|
|
commitmentOutIdx, expectedOutAmt, txOut.Value)
|
|
return ruleError(ErrBadPayeeValue, str)
|
|
}
|
|
} else {
|
|
// Calculate the minimum allowed amount based on the fee limit.
|
|
//
|
|
// Notice that, since the fee limit is in terms of a log2 value, and
|
|
// amounts are int64, anything greater than or equal to 63 is
|
|
// interpreted to mean allow spending the entire amount as a fee.
|
|
// This allows fast left shifts to be used to calculate the fee
|
|
// limit while preventing degenerate cases such as negative numbers
|
|
// for 63.
|
|
var amtLimitLow int64
|
|
feeLimitLog2 := feeLimitsEncoded & feeLimitMask
|
|
if feeLimitLog2 < 63 {
|
|
feeLimit := int64(1 << uint64(feeLimitLog2))
|
|
if feeLimit < expectedOutAmt {
|
|
amtLimitLow = expectedOutAmt - feeLimit
|
|
}
|
|
}
|
|
|
|
// The output must not be less than the minimum amount.
|
|
if txOut.Value < amtLimitLow {
|
|
str := fmt.Sprintf("output %s:%d pays less than the expected "+
|
|
"expected amount per ticket output commitment %s:%d "+
|
|
"(lowest allowed %d, output pays %d)", msgTx.TxHash(),
|
|
txOutIdx, ticketHash, commitmentOutIdx, amtLimitLow,
|
|
txOut.Value)
|
|
return ruleError(ErrBadPayeeValue, str)
|
|
}
|
|
|
|
// The output must not be more than the expected amount.
|
|
if txOut.Value > expectedOutAmt {
|
|
str := fmt.Sprintf("output %s:%d pays more than the expected "+
|
|
"amount per ticket output commitment %s:%d (expected %d, "+
|
|
"output pays %d)", msgTx.TxHash(), txOutIdx, ticketHash,
|
|
commitmentOutIdx, expectedOutAmt, txOut.Value)
|
|
return ruleError(ErrBadPayeeValue, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkVoteInputs performs a series of checks on the inputs to a vote
|
|
// transaction. An example of some of the checks include verifying the
|
|
// referenced ticket exists, the stakebase input commits to correct subsidy,
|
|
// the output amounts adhere to the commitments of the referenced ticket, and
|
|
// the ticket maturity requirements are met.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided transaction
|
|
// is a vote.
|
|
func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx,
|
|
txHeight int64, view *UtxoViewpoint, params *chaincfg.Params,
|
|
prevHeader *wire.BlockHeader, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
ticketMaturity := int64(params.TicketMaturity)
|
|
voteHash := tx.Hash()
|
|
msgTx := tx.MsgTx()
|
|
|
|
// Calculate the theoretical stake vote subsidy by extracting the vote
|
|
// height.
|
|
//
|
|
// WARNING: This really should be calculating the subsidy based on the
|
|
// height of the block the vote is contained in as opposed to the block it
|
|
// is voting on. Unfortunately, this is now part of consensus, so changing
|
|
// it requires a hard fork vote.
|
|
_, heightVotingOn := stake.SSGenBlockVotedOn(msgTx)
|
|
voteSubsidy := subsidyCache.CalcStakeVoteSubsidy(int64(heightVotingOn))
|
|
|
|
// The input amount specified by the stakebase must commit to the subsidy
|
|
// generated by the vote.
|
|
stakebase := msgTx.TxIn[0]
|
|
if stakebase.ValueIn != voteSubsidy {
|
|
str := fmt.Sprintf("vote subsidy input value of %v is not %v",
|
|
stakebase.ValueIn, voteSubsidy)
|
|
return ruleError(ErrBadStakebaseAmountIn, str)
|
|
}
|
|
|
|
// The second input to a vote must be the first output of the ticket the
|
|
// vote is associated with.
|
|
const ticketInIdx = 1
|
|
ticketIn := msgTx.TxIn[ticketInIdx]
|
|
if ticketIn.PreviousOutPoint.Index != submissionOutputIdx {
|
|
str := fmt.Sprintf("vote %s:%d references output %d instead of the "+
|
|
"first output", voteHash, ticketInIdx,
|
|
ticketIn.PreviousOutPoint.Index)
|
|
return ruleError(ErrInvalidVoteInput, str)
|
|
}
|
|
|
|
// Ensure the referenced ticket is available.
|
|
ticketHash := &ticketIn.PreviousOutPoint.Hash
|
|
ticketUtxo := view.LookupEntry(ticketIn.PreviousOutPoint)
|
|
if ticketUtxo == nil || ticketUtxo.IsSpent() {
|
|
str := fmt.Sprintf("ticket output %v referenced by vote %s:%d either "+
|
|
"does not exist or has already been spent",
|
|
ticketIn.PreviousOutPoint, voteHash, ticketInIdx)
|
|
return ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// Ensure the referenced transaction output is a supported ticket submission
|
|
// output in terms of the script version and type as well as ensuring the
|
|
// transaction that houses the output is a ticket. This also proves the
|
|
// form of the transaction and its outputs are as expected so subsequent
|
|
// code is able to avoid a lot of additional overhead rechecking things.
|
|
if err := checkTicketSubmissionInput(ticketUtxo); err != nil {
|
|
str := fmt.Sprintf("output %v referenced by vote %s:%d consensus "+
|
|
"violation: %s", ticketIn.PreviousOutPoint, voteHash, ticketInIdx,
|
|
err.Error())
|
|
return ruleError(ErrInvalidVoteInput, str)
|
|
}
|
|
|
|
// Ensure the vote is not spending a ticket which has not yet reached the
|
|
// required ticket maturity.
|
|
//
|
|
// NOTE: A ticket stake submission (OP_SSTX tagged output) can only be spent
|
|
// in the block AFTER the entire ticket maturity has passed, hence the +1.
|
|
originHeight := ticketUtxo.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev < ticketMaturity+1 {
|
|
str := fmt.Sprintf("tried to spend ticket output from transaction "+
|
|
"%v from height %d at height %d before required ticket maturity "+
|
|
"of %d+1 blocks", ticketHash, originHeight, txHeight,
|
|
ticketMaturity)
|
|
return ruleError(ErrImmatureTicketSpend, str)
|
|
}
|
|
|
|
ticketOuts := ticketUtxo.TicketMinimalOutputs()
|
|
if len(ticketOuts) == 0 {
|
|
panicf("missing extra stake data for ticket %v -- probable database "+
|
|
"corruption", ticketHash)
|
|
}
|
|
|
|
// Ensure the number of payment outputs matches the number of commitments
|
|
// made by the associated ticket.
|
|
//
|
|
// The vote transaction outputs must consist of an OP_RETURN output that
|
|
// indicates the block being voted on, an OP_RETURN with the user-provided
|
|
// vote bits, and an output that corresponds to every commitment output in
|
|
// the ticket associated with the vote. The associated ticket outputs
|
|
// consist of a stake submission output, and two outputs for each payment
|
|
// commitment such that the first one of the pair is a commitment amount and
|
|
// the second one is the amount of change sent back to the contributor to
|
|
// the ticket based on their original funding amount.
|
|
var extra int
|
|
if isTreasuryEnabled {
|
|
// If we have votes we need to subtract them from the next
|
|
// check.
|
|
tvt, _ := stake.CheckSSGenVotes(msgTx, isTreasuryEnabled)
|
|
if tvt != nil {
|
|
extra = 1
|
|
}
|
|
}
|
|
|
|
numVotePayments := len(msgTx.TxOut) - 2 - extra
|
|
if numVotePayments*2 != len(ticketOuts)-1 {
|
|
str := fmt.Sprintf("vote %s makes %d payments when input ticket %s "+
|
|
"has %d commitments", voteHash, numVotePayments, ticketHash,
|
|
len(ticketOuts)-1)
|
|
return ruleError(ErrBadNumPayees, str)
|
|
}
|
|
|
|
// Ensure the outputs adhere to the ticket commitments.
|
|
return checkTicketRedeemerCommitments(ticketHash, ticketOuts, msgTx,
|
|
true, voteSubsidy, prevHeader, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
}
|
|
|
|
// checkRevocationInputs performs a series of checks on the inputs to a
|
|
// revocation transaction. An example of some of the checks include verifying
|
|
// the referenced ticket exists, the output amounts adhere to the commitments of
|
|
// the ticket, and the ticket maturity requirements are met.
|
|
//
|
|
// NOTE: The caller MUST have already determined that the provided transaction
|
|
// is a revocation.
|
|
func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint,
|
|
params *chaincfg.Params, prevHeader *wire.BlockHeader, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
ticketMaturity := int64(params.TicketMaturity)
|
|
revokeHash := tx.Hash()
|
|
msgTx := tx.MsgTx()
|
|
|
|
// The first input to a revocation must be the first output of the ticket
|
|
// the revocation is associated with.
|
|
const ticketInIdx = 0
|
|
ticketIn := msgTx.TxIn[ticketInIdx]
|
|
if ticketIn.PreviousOutPoint.Index != submissionOutputIdx {
|
|
str := fmt.Sprintf("revocation %s:%d references output %d instead of "+
|
|
"the first output", revokeHash, ticketInIdx,
|
|
ticketIn.PreviousOutPoint.Index)
|
|
return ruleError(ErrInvalidRevokeInput, str)
|
|
}
|
|
|
|
// Ensure the referenced ticket is available.
|
|
ticketHash := &ticketIn.PreviousOutPoint.Hash
|
|
ticketUtxo := view.LookupEntry(ticketIn.PreviousOutPoint)
|
|
if ticketUtxo == nil || ticketUtxo.IsSpent() {
|
|
str := fmt.Sprintf("ticket output %v referenced from revocation %s:%d "+
|
|
"either does not exist or has already been spent",
|
|
ticketIn.PreviousOutPoint, revokeHash, ticketInIdx)
|
|
return ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// Ensure the referenced transaction output is a supported ticket submission
|
|
// output in terms of the script version and type as well as ensuring the
|
|
// transaction that houses the output is a ticket. This also proves the
|
|
// form of the transaction and its outputs are as expected so subsequent
|
|
// code is able to avoid a lot of additional overhead rechecking things.
|
|
if err := checkTicketSubmissionInput(ticketUtxo); err != nil {
|
|
str := fmt.Sprintf("output %v referenced by revocation %s:%d consensus "+
|
|
"violation: %s", ticketIn.PreviousOutPoint, revokeHash, ticketInIdx,
|
|
err.Error())
|
|
return ruleError(ErrInvalidRevokeInput, str)
|
|
}
|
|
|
|
// Ensure the revocation is not spending a ticket which has not yet reached
|
|
// the required ticket maturity.
|
|
//
|
|
// NOTE: A ticket stake submission (OP_SSTX tagged output) can only be spent
|
|
// in the block AFTER the entire ticket maturity has passed, and, in order
|
|
// to be revoked, the ticket must have been missed which can't possibly
|
|
// happen for another block after that, hence the +2. However, if the
|
|
// automatic ticket revocations agenda is active, +1 is used since revocations
|
|
// are allowed (and in fact, are required) in the block in which the ticket is
|
|
// missed or expired.
|
|
originHeight := ticketUtxo.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
revocationAdditionalMaturity := int64(2)
|
|
if isAutoRevocationsEnabled {
|
|
revocationAdditionalMaturity = 1
|
|
}
|
|
|
|
if blocksSincePrev < ticketMaturity+revocationAdditionalMaturity {
|
|
str := fmt.Sprintf("tried to spend ticket output from transaction "+
|
|
"%v from height %d at height %d before required ticket maturity "+
|
|
"of %d+%d blocks", ticketHash, originHeight, txHeight,
|
|
ticketMaturity, revocationAdditionalMaturity)
|
|
return ruleError(ErrImmatureTicketSpend, str)
|
|
}
|
|
|
|
ticketOuts := ticketUtxo.TicketMinimalOutputs()
|
|
if len(ticketOuts) == 0 {
|
|
panicf("missing extra stake data for ticket %v -- probable database "+
|
|
"corruption", ticketHash)
|
|
}
|
|
|
|
// Ensure the number of payment outputs matches the number of commitments
|
|
// made by the associated ticket.
|
|
//
|
|
// The revocation transaction outputs must consist of an output that
|
|
// corresponds to every commitment output in the ticket associated with the
|
|
// revocation. The associated ticket outputs consist of a stake submission
|
|
// output, and two outputs for each payment commitment such that the first
|
|
// one of the pair is a commitment amount and the second one is the amount
|
|
// of change sent back to the contributor to the ticket based on their
|
|
// original funding amount.
|
|
numRevocationPayments := len(msgTx.TxOut)
|
|
if numRevocationPayments*2 != len(ticketOuts)-1 {
|
|
str := fmt.Sprintf("revocation %s makes %d payments when input ticket "+
|
|
"%s has %d commitments", revokeHash, numRevocationPayments,
|
|
ticketHash, len(ticketOuts)-1)
|
|
return ruleError(ErrBadNumPayees, str)
|
|
}
|
|
|
|
// Ensure the outputs adhere to the ticket commitments. Zero is passed for
|
|
// the vote subsidy since revocations do not produce any subsidy.
|
|
return checkTicketRedeemerCommitments(ticketHash, ticketOuts, msgTx,
|
|
false, 0, prevHeader, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
}
|
|
|
|
// 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
|
|
// requirements are met, detecting double spends, validating all values and
|
|
// fees are in the legal range and the total output amount doesn't exceed the
|
|
// input amount, and verifying the signatures to prove the spender was the
|
|
// owner of the Decred and therefore allowed to spend them. As it checks the
|
|
// inputs, it also calculates the total fees for the transaction and returns
|
|
// that value.
|
|
//
|
|
// NOTE: The transaction MUST have already been sanity checked with the
|
|
// 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) (int64, error) {
|
|
|
|
// Coinbase transactions have no inputs.
|
|
msgTx := tx.MsgTx()
|
|
if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) {
|
|
return 0, nil
|
|
}
|
|
|
|
// Treasurybase transactions have no inputs.
|
|
if isTreasuryEnabled && standalone.IsTreasuryBase(msgTx) {
|
|
return 0, nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Decred stake transaction testing.
|
|
// -------------------------------------------------------------------
|
|
|
|
// Perform additional checks on ticket purchase transactions such as
|
|
// ensuring the input type requirements are met and the output commitments
|
|
// coincide with the inputs.
|
|
isTicket := stake.IsSStx(msgTx)
|
|
if isTicket {
|
|
if err := checkTicketPurchaseInputs(msgTx, view); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
// Perform additional checks on vote transactions such as verifying that the
|
|
// referenced ticket exists, the stakebase input commits to correct subsidy,
|
|
// the output amounts adhere to the commitments of the referenced ticket,
|
|
// and the ticket maturity requirements are met.
|
|
//
|
|
// Also keep track of whether or not it is a vote since some inputs need
|
|
// to be skipped later.
|
|
isVote := stake.IsSSGen(msgTx, isTreasuryEnabled)
|
|
if isVote {
|
|
err := checkVoteInputs(subsidyCache, tx, txHeight, view,
|
|
chainParams, prevHeader, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
// Perform additional checks on revocation transactions such as verifying
|
|
// the referenced ticket exists, the output amounts adhere to the
|
|
// commitments of the ticket, and the ticket maturity requirements are met.
|
|
//
|
|
// Also keep track of whether or not it is a revocation since some inputs
|
|
// need to be skipped later.
|
|
isRevocation := stake.IsSSRtx(msgTx, isAutoRevocationsEnabled)
|
|
if isRevocation {
|
|
err := checkRevocationInputs(tx, txHeight, view, chainParams, prevHeader,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
// Perform additional checks on TSpend transactions.
|
|
var (
|
|
isTSpend bool
|
|
signature, pubKey []byte
|
|
err error
|
|
)
|
|
reqStakeOutMaturity := int64(chainParams.SStxChangeMaturity)
|
|
if isTreasuryEnabled {
|
|
signature, pubKey, err = stake.CheckTSpend(msgTx)
|
|
isTSpend = err == nil
|
|
reqStakeOutMaturity = int64(chainParams.CoinbaseMaturity)
|
|
}
|
|
|
|
// If we have a TSpend verify the signature.
|
|
if isTSpend {
|
|
// Check if this is a sanctioned PI key.
|
|
if !chainParams.PiKeyExists(pubKey) {
|
|
str := fmt.Sprintf("Unknown Pi Key: %x", pubKey)
|
|
return 0, ruleError(ErrUnknownPiKey, str)
|
|
}
|
|
|
|
// Verify that the signature is valid and corresponds to the
|
|
// provided public key.
|
|
err = verifyTSpendSignature(msgTx, signature, pubKey)
|
|
if err != nil {
|
|
str := fmt.Sprintf("Could not verify TSpend "+
|
|
"signature: %v", err)
|
|
return 0, ruleError(ErrInvalidPiSignature, str)
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Decred general transaction testing (and a few stake exceptions).
|
|
// -------------------------------------------------------------------
|
|
|
|
txHash := tx.Hash()
|
|
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.CalcStakeVoteSubsidy(
|
|
int64(heightVotingOn))
|
|
totalAtomIn += stakeVoteSubsidy
|
|
continue
|
|
}
|
|
|
|
// idx can only be 0 in this case but check it anyway.
|
|
if isTSpend && idx == 0 {
|
|
totalAtomIn += txIn.ValueIn
|
|
continue
|
|
}
|
|
|
|
txInOutpoint := txIn.PreviousOutPoint
|
|
txInHash := &txInOutpoint.Hash
|
|
originTxIndex := txInOutpoint.Index
|
|
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,
|
|
txHash, idx)
|
|
return 0, ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// Check fraud proof witness data.
|
|
|
|
// Using zero value outputs as inputs is banned.
|
|
if utxoEntry.Amount() == 0 {
|
|
str := fmt.Sprintf("tried to spend zero value output "+
|
|
"from input %v, idx %v", txInHash,
|
|
originTxIndex)
|
|
return 0, ruleError(ErrZeroValueOutputSpend, str)
|
|
}
|
|
|
|
if checkFraudProof {
|
|
if txIn.ValueIn !=
|
|
utxoEntry.Amount() {
|
|
str := fmt.Sprintf("bad fraud check value in "+
|
|
"(expected %v, given %v) for txIn %v",
|
|
utxoEntry.Amount(),
|
|
txIn.ValueIn, idx)
|
|
return 0, ruleError(ErrFraudAmountIn, str)
|
|
}
|
|
|
|
if int64(txIn.BlockHeight) != utxoEntry.BlockHeight() {
|
|
str := fmt.Sprintf("bad fraud check block "+
|
|
"height (expected %v, given %v) for "+
|
|
"txIn %v", utxoEntry.BlockHeight(),
|
|
txIn.BlockHeight, idx)
|
|
return 0, ruleError(ErrFraudBlockHeight, str)
|
|
}
|
|
|
|
if txIn.BlockIndex != utxoEntry.BlockIndex() {
|
|
str := fmt.Sprintf("bad fraud check block "+
|
|
"index (expected %v, given %v) for "+
|
|
"txIn %v", utxoEntry.BlockIndex(),
|
|
txIn.BlockIndex, idx)
|
|
return 0, ruleError(ErrFraudBlockIndex, str)
|
|
}
|
|
}
|
|
|
|
// Ensure the transaction is not spending coins which have not
|
|
// yet reached the required coinbase maturity.
|
|
coinbaseMaturity := int64(chainParams.CoinbaseMaturity)
|
|
if utxoEntry.IsCoinBase() {
|
|
originHeight := utxoEntry.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev < coinbaseMaturity {
|
|
str := fmt.Sprintf("tx %v tried to spend "+
|
|
"coinbase transaction %v from height "+
|
|
"%v at height %v before required "+
|
|
"maturity of %v blocks", txHash,
|
|
txInHash, originHeight, txHeight,
|
|
coinbaseMaturity)
|
|
return 0, ruleError(ErrImmatureSpend, str)
|
|
}
|
|
}
|
|
|
|
// Ensure that the transaction is not spending coins from a
|
|
// transaction that included an expiry but which has not yet
|
|
// reached coinbase maturity many blocks.
|
|
if utxoEntry.HasExpiry() {
|
|
originHeight := utxoEntry.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev < coinbaseMaturity {
|
|
str := fmt.Sprintf("tx %v tried to spend "+
|
|
"transaction %v including an expiry "+
|
|
"from height %v at height %v before "+
|
|
"required maturity of %v blocks",
|
|
txHash, txInHash, originHeight,
|
|
txHeight, coinbaseMaturity)
|
|
return 0, ruleError(ErrExpiryTxSpentEarly, str)
|
|
}
|
|
}
|
|
|
|
// OP_TGEN tagged outputs can only be spent after coinbase maturity
|
|
// many blocks.
|
|
if isTreasuryEnabled && stake.IsTreasuryGenScript(
|
|
utxoEntry.ScriptVersion(), utxoEntry.PkScript()) {
|
|
|
|
originHeight := utxoEntry.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev < coinbaseMaturity {
|
|
str := fmt.Sprintf("tried to spend OP_TGEN output from tx %v "+
|
|
"from height %v at height %v before required maturity of "+
|
|
"%v blocks", txInHash, originHeight, txHeight,
|
|
coinbaseMaturity)
|
|
return 0, ruleError(ErrImmatureSpend, str)
|
|
}
|
|
}
|
|
|
|
// The only transaction types that are allowed to spend from OP_SSTX
|
|
// tagged outputs are votes and revocations. So, check all the inputs
|
|
// from non votes and revocations and make sure that they spend no
|
|
// OP_SSTX tagged outputs.
|
|
if !(isVote || isRevocation) {
|
|
if stake.IsTicketPurchaseScript(utxoEntry.ScriptVersion(),
|
|
utxoEntry.PkScript()) {
|
|
errSSGen := stake.CheckSSGen(msgTx,
|
|
isTreasuryEnabled)
|
|
errSSRtx := stake.CheckSSRtx(msgTx, isAutoRevocationsEnabled)
|
|
errStr := fmt.Sprintf("Tx %v attempted to "+
|
|
"spend an OP_SSTX tagged output, "+
|
|
"however it was not an SSGen or SSRtx"+
|
|
" tx; SSGen err: %v, SSRtx err: %v",
|
|
txHash, errSSGen.Error(),
|
|
errSSRtx.Error())
|
|
return 0, ruleError(ErrTxSStxOutSpend, errStr)
|
|
}
|
|
}
|
|
|
|
// OP_SSGEN and OP_SSRTX tagged outputs can only be spent after
|
|
// coinbase maturity many blocks.
|
|
if stake.IsRevocationScript(utxoEntry.ScriptVersion(),
|
|
utxoEntry.PkScript()) ||
|
|
stake.IsVoteScript(utxoEntry.ScriptVersion(),
|
|
utxoEntry.PkScript()) {
|
|
originHeight := utxoEntry.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev < reqStakeOutMaturity {
|
|
str := fmt.Sprintf("tried to spend OP_SSGEN or"+
|
|
" OP_SSRTX output from tx %v from "+
|
|
"height %v at height %v before "+
|
|
"required maturity of %v blocks",
|
|
txInHash, originHeight, txHeight,
|
|
coinbaseMaturity)
|
|
return 0, ruleError(ErrImmatureSpend, str)
|
|
}
|
|
}
|
|
|
|
// Ticket change outputs may only be spent after ticket change
|
|
// maturity many blocks.
|
|
if stake.IsStakeChangeScript(utxoEntry.ScriptVersion(),
|
|
utxoEntry.PkScript()) {
|
|
originHeight := utxoEntry.BlockHeight()
|
|
blocksSincePrev := txHeight - originHeight
|
|
if blocksSincePrev <
|
|
int64(chainParams.SStxChangeMaturity) {
|
|
str := fmt.Sprintf("tried to spend ticket change"+
|
|
" output from tx %v from height %v at "+
|
|
"height %v before required maturity "+
|
|
"of %v blocks", txInHash, originHeight,
|
|
txHeight, chainParams.SStxChangeMaturity)
|
|
return 0, ruleError(ErrImmatureSpend, str)
|
|
}
|
|
}
|
|
|
|
// Ensure the transaction amounts are in range. Each of the
|
|
// output values of the input transactions must not be negative
|
|
// or more than the max allowed per transaction. All amounts
|
|
// in a transaction are in a unit value known as an atom. One
|
|
// Decred is a quantity of atoms as defined by the AtomPerCoin
|
|
// constant.
|
|
originTxAtom := utxoEntry.Amount()
|
|
if originTxAtom < 0 {
|
|
str := fmt.Sprintf("transaction output has negative "+
|
|
"value of %v", originTxAtom)
|
|
return 0, ruleError(ErrBadTxOutValue, str)
|
|
}
|
|
if originTxAtom > dcrutil.MaxAmount {
|
|
str := fmt.Sprintf("transaction output value of %v is "+
|
|
"higher than max allowed value of %v",
|
|
originTxAtom, dcrutil.MaxAmount)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Calculate the total output amount for this transaction. It is safe
|
|
// to ignore overflow and out of range errors here because those error
|
|
// conditions would have already been caught by checkTransactionSanity.
|
|
var totalAtomOut int64
|
|
for _, txOut := range tx.MsgTx().TxOut {
|
|
totalAtomOut += txOut.Value
|
|
}
|
|
|
|
// Ensure the transaction does not spend more than its inputs.
|
|
if totalAtomIn < totalAtomOut {
|
|
str := fmt.Sprintf("total value of all transaction inputs for "+
|
|
"transaction %v is %v which is less than the amount "+
|
|
"spent of %v", txHash, totalAtomIn, totalAtomOut)
|
|
return 0, ruleError(ErrSpendTooHigh, str)
|
|
}
|
|
|
|
txFeeInAtom := totalAtomIn - totalAtomOut
|
|
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 {
|
|
msgTx := tx.MsgTx()
|
|
|
|
totalSigOps := 0
|
|
|
|
if isTreasuryEnabled && stake.IsTreasuryBase(msgTx) {
|
|
return totalSigOps
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
numSigOps := txscript.GetSigOpCount(txIn.SignatureScript,
|
|
isTreasuryEnabled)
|
|
totalSigOps += numSigOps
|
|
}
|
|
}
|
|
|
|
// Accumulate the number of signature operations in all transaction
|
|
// outputs.
|
|
for _, txOut := range msgTx.TxOut {
|
|
numSigOps := txscript.GetSigOpCount(txOut.PkScript,
|
|
isTreasuryEnabled)
|
|
totalSigOps += numSigOps
|
|
}
|
|
|
|
return totalSigOps
|
|
}
|
|
|
|
// 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) {
|
|
// 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 {
|
|
return 0, nil
|
|
}
|
|
|
|
// Treasury spend and treasurybase transactions have no P2SH inputs, but
|
|
// they are only recognized as the associated transactions once the
|
|
// treasury agenda is active.
|
|
msgTx := tx.MsgTx()
|
|
if isTreasuryEnabled {
|
|
if stake.IsTSpend(msgTx) || stake.IsTreasuryBase(msgTx) {
|
|
return 0, nil
|
|
}
|
|
}
|
|
|
|
// Accumulate the number of signature operations in all transaction
|
|
// inputs.
|
|
totalSigOps := 0
|
|
for txInIndex, txIn := range msgTx.TxIn {
|
|
// 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)
|
|
return 0, ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// We're only interested in pay-to-script-hash types, so skip
|
|
// this input if it's not one.
|
|
pkScript := utxoEntry.PkScript()
|
|
if !txscript.IsPayToScriptHash(pkScript) {
|
|
continue
|
|
}
|
|
|
|
// Count the precise number of signature operations in the
|
|
// referenced public key script.
|
|
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)
|
|
return 0, ruleError(ErrTooManySigOps, str)
|
|
}
|
|
}
|
|
|
|
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.
|
|
// TxTree true == Regular, false == Stake
|
|
func checkNumSigOps(tx *dcrutil.Tx, view *UtxoViewpoint, index int, txTree bool, cumulativeSigOps int, isTreasuryEnabled bool) (int, error) {
|
|
msgTx := tx.MsgTx()
|
|
isSSGen := stake.IsSSGen(msgTx, isTreasuryEnabled)
|
|
numsigOps := CountSigOps(tx, (index == 0) && txTree, 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, (index == 0) && txTree,
|
|
isSSGen, view, isTreasuryEnabled)
|
|
if err != nil {
|
|
log.Tracef("CountP2SHSigOps failed; error returned %v", err)
|
|
return 0, err
|
|
}
|
|
|
|
startCumSigOps := cumulativeSigOps
|
|
cumulativeSigOps += numsigOps
|
|
cumulativeSigOps += numP2SHSigOps
|
|
|
|
// 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)
|
|
return 0, ruleError(ErrTooManySigOps, str)
|
|
}
|
|
|
|
return cumulativeSigOps, nil
|
|
}
|
|
|
|
// checkStakeBaseAmounts calculates the total amount given as subsidy from
|
|
// single stakebase transactions (votes) within a block. This function skips a
|
|
// ton of checks already performed by CheckTransactionInputs.
|
|
func checkStakeBaseAmounts(subsidyCache *standalone.SubsidyCache, height int64, txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool) error {
|
|
for _, tx := range txs {
|
|
msgTx := tx.MsgTx()
|
|
if stake.IsSSGen(msgTx, isTreasuryEnabled) {
|
|
// Ensure the input is available.
|
|
txInOutpoint := msgTx.TxIn[1].PreviousOutPoint
|
|
txInHash := &txInOutpoint.Hash
|
|
utxoEntry, exists := view.entries[txInOutpoint]
|
|
if !exists || utxoEntry == nil {
|
|
str := fmt.Sprintf("couldn't find input tx %v "+
|
|
"for stakebase amounts check", txInHash)
|
|
return ruleError(ErrTicketUnavailable, str)
|
|
}
|
|
|
|
originTxAtom := utxoEntry.Amount()
|
|
|
|
totalOutputs := int64(0)
|
|
// Sum up the outputs.
|
|
for _, out := range msgTx.TxOut {
|
|
totalOutputs += out.Value
|
|
}
|
|
|
|
difference := totalOutputs - originTxAtom
|
|
|
|
// Subsidy aligns with the height we're voting on, not
|
|
// with the height of the current block.
|
|
calcSubsidy := subsidyCache.CalcStakeVoteSubsidy(height - 1)
|
|
|
|
if difference > calcSubsidy {
|
|
str := fmt.Sprintf("ssgen tx %v spent more "+
|
|
"than allowed (spent %v, allowed %v)",
|
|
tx.Hash(), difference, calcSubsidy)
|
|
return ruleError(ErrSSGenSubsidy, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getStakeBaseAmounts calculates the total amount given as subsidy from the
|
|
// collective stakebase transactions (votes) within a block. This function
|
|
// skips a ton of checks already performed by CheckTransactionInputs.
|
|
func getStakeBaseAmounts(txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool) (int64, error) {
|
|
totalInputs := int64(0)
|
|
totalOutputs := int64(0)
|
|
for _, tx := range txs {
|
|
msgTx := tx.MsgTx()
|
|
if stake.IsSSGen(msgTx, isTreasuryEnabled) {
|
|
// Ensure the input is available.
|
|
txInOutpoint := msgTx.TxIn[1].PreviousOutPoint
|
|
txInHash := &txInOutpoint.Hash
|
|
utxoEntry, exists := view.entries[txInOutpoint]
|
|
if !exists || utxoEntry == nil {
|
|
str := fmt.Sprintf("couldn't find input tx %v "+
|
|
"for stakebase amounts get", txInHash)
|
|
return 0, ruleError(ErrTicketUnavailable, str)
|
|
}
|
|
|
|
originTxAtom := utxoEntry.Amount()
|
|
|
|
totalInputs += originTxAtom
|
|
|
|
// Sum up the outputs.
|
|
for _, out := range msgTx.TxOut {
|
|
totalOutputs += out.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalOutputs - totalInputs, nil
|
|
}
|
|
|
|
// getStakeTreeFees determines the amount of fees for in the stake tx tree of
|
|
// some node given a transaction store.
|
|
func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabled bool) (dcrutil.Amount, error) {
|
|
totalInputs := int64(0)
|
|
totalOutputs := int64(0)
|
|
for _, tx := range txs {
|
|
msgTx := tx.MsgTx()
|
|
isSSGen := stake.IsSSGen(msgTx, isTreasuryEnabled)
|
|
var isTSpend, isTreasuryBase bool
|
|
if isTreasuryEnabled {
|
|
isTSpend = stake.IsTSpend(msgTx)
|
|
isTreasuryBase = stake.IsTreasuryBase(msgTx)
|
|
}
|
|
|
|
for i, in := range msgTx.TxIn {
|
|
// Ignore stakebases.
|
|
if isSSGen && i == 0 {
|
|
continue
|
|
}
|
|
|
|
// Ignore TSpend.
|
|
if isTSpend && i == 0 {
|
|
continue
|
|
}
|
|
|
|
// Ignore treasury base.
|
|
if isTreasuryBase && i == 0 {
|
|
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.CalcStakeVoteSubsidy(height - 1)
|
|
}
|
|
|
|
if isTSpend {
|
|
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 TxStore.
|
|
// After ensuring the transaction is valid, the transaction is connected to the
|
|
// UTXO viewpoint. TxTree true == Regular, false == Stake
|
|
func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node *blockNode, txs []*dcrutil.Tx, view *UtxoViewpoint, stxos *[]spentTxOut, txTree bool) error {
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(node.parent)
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
|
|
// Perform several checks on the inputs for each transaction. Also
|
|
// accumulate the total fees. This could technically be combined with
|
|
// the loop above instead of running another loop over the
|
|
// transactions, but by separating it we can avoid running the more
|
|
// expensive (though still relatively cheap as compared to running the
|
|
// scripts) checks against all the inputs when the signature operations
|
|
// are out of bounds.
|
|
totalFees := int64(inputFees) // Stake tx tree carry forward
|
|
prevHeader := node.parent.Header()
|
|
var cumulativeSigOps int
|
|
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, txTree,
|
|
cumulativeSigOps, isTreasuryEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// This step modifies the txStore and marks the tx outs used
|
|
// spent, so be aware of this.
|
|
txFee, err := CheckTransactionInputs(b.subsidyCache, tx,
|
|
node.height, view, true, /* check fraud proofs */
|
|
b.chainParams, &prevHeader, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
log.Tracef("CheckTransactionInputs failed; error "+
|
|
"returned: %v", err)
|
|
return 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")
|
|
}
|
|
|
|
// Connect the transaction to the UTXO viewpoint, so that in
|
|
// flight transactions may correctly validate.
|
|
err = view.connectTransaction(tx, node.height, uint32(idx),
|
|
stxos, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// The total output values of the coinbase transaction must not exceed
|
|
// the expected subsidy value plus total transaction fees gained from
|
|
// mining the block. It is safe to ignore overflow and out of range
|
|
// errors here because those error conditions would have already been
|
|
// caught by checkTransactionSanity.
|
|
if txTree { //TxTreeRegular
|
|
// Apply penalty to fees if we're at stake validation height.
|
|
if node.height >= b.chainParams.StakeValidationHeight {
|
|
totalFees *= int64(node.voters)
|
|
totalFees /= int64(b.chainParams.TicketsPerBlock)
|
|
}
|
|
|
|
var totalAtomOutRegular int64
|
|
|
|
for _, txOut := range txs[0].MsgTx().TxOut {
|
|
totalAtomOutRegular += txOut.Value
|
|
}
|
|
|
|
var expAtomOut int64
|
|
if node.height == 1 {
|
|
expAtomOut = b.subsidyCache.CalcBlockSubsidy(node.height)
|
|
} else {
|
|
subsidyWork := b.subsidyCache.CalcWorkSubsidy(node.height,
|
|
node.voters)
|
|
subsidyTax := b.subsidyCache.CalcTreasurySubsidy(node.height,
|
|
node.voters, isTreasuryEnabled)
|
|
if isTreasuryEnabled {
|
|
// When TreasuryBase is enabled the subsidyTax
|
|
// lives in STransactions.
|
|
expAtomOut = subsidyWork + totalFees
|
|
} else {
|
|
expAtomOut = subsidyWork + subsidyTax + totalFees
|
|
}
|
|
}
|
|
|
|
// AmountIn for the input should be equal to the subsidy.
|
|
coinbaseIn := txs[0].MsgTx().TxIn[0]
|
|
subsidyWithoutFees := expAtomOut - totalFees
|
|
if (coinbaseIn.ValueIn != subsidyWithoutFees) &&
|
|
(node.height > 0) {
|
|
errStr := fmt.Sprintf("bad coinbase subsidy in input;"+
|
|
" got %v, expected %v", coinbaseIn.ValueIn,
|
|
subsidyWithoutFees)
|
|
return ruleError(ErrBadCoinbaseAmountIn, errStr)
|
|
}
|
|
|
|
if totalAtomOutRegular > expAtomOut {
|
|
str := fmt.Sprintf("coinbase transaction for block %v"+
|
|
" pays %v which is more than expected value "+
|
|
"of %v", node.hash, totalAtomOutRegular,
|
|
expAtomOut)
|
|
return ruleError(ErrBadCoinbaseValue, str)
|
|
}
|
|
} else { // TxTreeStake
|
|
// When treasury is enabled check treasurybase value
|
|
if node.height > 1 && isTreasuryEnabled {
|
|
if len(txs) == 0 {
|
|
// This should be impossible to hit but we
|
|
// mimic the other len tests in this block.
|
|
str := fmt.Sprintf("empty tx tree stake, "+
|
|
"expected treasurybase at height %v",
|
|
node.height)
|
|
return ruleError(ErrNoStakeTx, str)
|
|
}
|
|
subsidyTax := b.subsidyCache.CalcTreasurySubsidy(node.height,
|
|
node.voters, isTreasuryEnabled)
|
|
treasurybaseIn := txs[0].MsgTx().TxIn[0]
|
|
if treasurybaseIn.ValueIn != subsidyTax {
|
|
errStr := fmt.Sprintf("bad treasurybase "+
|
|
"subsidy in input; got %v, expected %v",
|
|
treasurybaseIn.ValueIn,
|
|
subsidyTax)
|
|
return ruleError(ErrBadTreasurybaseAmountIn, errStr)
|
|
}
|
|
|
|
}
|
|
|
|
if len(txs) == 0 &&
|
|
node.height < b.chainParams.StakeValidationHeight {
|
|
return 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)
|
|
}
|
|
|
|
err := checkStakeBaseAmounts(b.subsidyCache, node.height,
|
|
txs, view, isTreasuryEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
totalAtomOutStake, err := getStakeBaseAmounts(txs, view,
|
|
isTreasuryEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var expAtomOut int64
|
|
if node.height >= b.chainParams.StakeValidationHeight {
|
|
// Subsidy aligns with the height we're voting on, not
|
|
// with the height of the current block.
|
|
expAtomOut = b.subsidyCache.CalcStakeVoteSubsidy(node.height-1) *
|
|
int64(node.voters)
|
|
} else {
|
|
expAtomOut = totalFees
|
|
}
|
|
|
|
if totalAtomOutStake > expAtomOut {
|
|
str := fmt.Sprintf("stakebase transactions for block "+
|
|
"pays %v which is more than expected value "+
|
|
"of %v", totalAtomOutStake, expAtomOut)
|
|
return ruleError(ErrBadStakebaseValue, str)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// consensusScriptVerifyFlags returns the script flags that must be used when
|
|
// executing transaction scripts to enforce the consensus rules. This includes
|
|
// any flags required as the result of any agendas that have passed and become
|
|
// active.
|
|
func (b *BlockChain) consensusScriptVerifyFlags(node *blockNode) (txscript.ScriptFlags, error) {
|
|
scriptFlags := txscript.ScriptVerifyCleanStack |
|
|
txscript.ScriptVerifyCheckLockTimeVerify
|
|
|
|
// Enable enforcement of OP_CSV and OP_SHA256 if the stake vote
|
|
// for the agenda is active.
|
|
lnFeaturesActive, err := b.isLNFeaturesAgendaActive(node.parent)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if lnFeaturesActive {
|
|
scriptFlags |= txscript.ScriptVerifyCheckSequenceVerify
|
|
scriptFlags |= txscript.ScriptVerifySHA256
|
|
}
|
|
|
|
// Enable OP_TADD/OP_TSPEND/OP_TGEN if treasury opcodes are active.
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(node.parent)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if isTreasuryEnabled {
|
|
scriptFlags |= txscript.ScriptVerifyTreasury
|
|
}
|
|
|
|
return scriptFlags, err
|
|
}
|
|
|
|
// tspendChecks verifies that a TSpend is allowed to be mined in the provided
|
|
// 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.
|
|
func (b *BlockChain) tspendChecks(prevNode *blockNode, block *dcrutil.Block) error {
|
|
blockHeight := prevNode.height + 1
|
|
isTVI := standalone.IsTreasuryVoteInterval(uint64(blockHeight),
|
|
b.chainParams.TreasuryVoteInterval)
|
|
|
|
// NOTE: Treasury spend transactions are rejected from blocks that are not
|
|
// on a treasury vote interval during the contextual block checks. Thus,
|
|
// there are no additional checks in that case.
|
|
if !isTVI {
|
|
return nil
|
|
}
|
|
|
|
var totalTSpendAmount int64
|
|
for _, stx := range block.STransactions() {
|
|
if !stake.IsTSpend(stx.MsgTx()) {
|
|
continue
|
|
}
|
|
|
|
// Assert that the treasury spend is inside the correct window.
|
|
exp := stx.MsgTx().Expiry
|
|
if !standalone.InsideTSpendWindow(blockHeight,
|
|
exp, b.chainParams.TreasuryVoteInterval,
|
|
b.chainParams.TreasuryVoteIntervalMultiplier) {
|
|
|
|
str := fmt.Sprintf("block at height %d contains treasury spend "+
|
|
"transaction %v with expiry %v that is outside of the valid "+
|
|
"window", blockHeight, stx.Hash(), exp)
|
|
return ruleError(ErrInvalidTSpendWindow, str)
|
|
}
|
|
|
|
// A valid TSPEND always stores the entire amount that the
|
|
// treasury is spending in the first TxIn.
|
|
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)
|
|
}
|
|
|
|
// Verify this TSpend hash has not been included in a
|
|
// prior block.
|
|
err := b.checkTSpendExists(prevNode, *stx.Hash())
|
|
if err != nil {
|
|
str := fmt.Sprintf("block contains a TSpend "+
|
|
"transaction (%v) that has been mined "+
|
|
"in another block: %v", stx.Hash(), err)
|
|
return ruleError(ErrTSpendExists, str)
|
|
}
|
|
|
|
// Verify that this TSpend has enough votes to be
|
|
// included on the blockchain.
|
|
err = b.checkTSpendHasVotes(prevNode, stx)
|
|
if err != nil {
|
|
str := fmt.Sprintf("block contains a TSpend "+
|
|
"transaction (%v) that does not have "+
|
|
"enough votes: %v", stx.Hash(), err)
|
|
return ruleError(ErrNotEnoughTSpendVotes, str)
|
|
}
|
|
}
|
|
|
|
// Check the aggregate of all TSpend transactions is within bounds of
|
|
// the treasury account. This function is only called when we are on a
|
|
// TVI and if we have accumulated expenditures.
|
|
if isTVI && totalTSpendAmount > 0 {
|
|
// Verify TSpend expenditure is within range.
|
|
err := b.checkTSpendsExpenditure(prevNode, totalTSpendAmount)
|
|
if err != nil {
|
|
str := fmt.Sprintf("block contains a TSpend that "+
|
|
"has an invalid expenditure: %v", err)
|
|
return ruleError(ErrInvalidExpenditure, str)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkConnectBlock performs several checks to confirm connecting the passed
|
|
// block to the chain represented by the passed view does not violate any
|
|
// rules. In addition, the passed view is updated to spend all of the
|
|
// referenced outputs and add all of the new utxos created by block. Thus, the
|
|
// view will represent the state of the chain as if the block were actually
|
|
// connected and consequently the best hash for the view is also updated to
|
|
// passed block.
|
|
//
|
|
// When the header commitments parameter is not nil, it will be updated with the
|
|
// commitment data created by the function in order to perform validation so
|
|
// the caller is able to reuse it without having to recreate it. The caller may
|
|
// specify nil if the data is not desired.
|
|
//
|
|
// An example of some of the checks performed are ensuring connecting the block
|
|
// would not cause any duplicate transaction hashes for old transactions that
|
|
// aren't already fully spent, double spends, exceeding the maximum allowed
|
|
// signature operations per block, invalid values in relation to the expected
|
|
// block subsidy, or fail transaction script validation.
|
|
//
|
|
// The CheckConnectBlockTemplate function makes use of this function to perform
|
|
// the bulk of its work.
|
|
//
|
|
// This function MUST be called with the chain state lock held (for writes).
|
|
func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.Block, view *UtxoViewpoint, stxos *[]spentTxOut, hdrCommitments *headerCommitmentData) error {
|
|
// Ensure the view is for the node being checked.
|
|
parentHash := &block.MsgBlock().Header.PrevBlock
|
|
if !view.BestHash().IsEqual(parentHash) {
|
|
return AssertError(fmt.Sprintf("inconsistent view when "+
|
|
"checking block connection: best hash is %v instead "+
|
|
"of expected %v", view.BestHash(), parentHash))
|
|
}
|
|
|
|
// Check that either the coinbase or the treasurybase pays the treasury the
|
|
// correct amount depending on the result of the treasury agenda vote that
|
|
// modifies the semantics of the payout.
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(node.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isTreasuryEnabled {
|
|
if len(block.STransactions()) == 0 {
|
|
return AssertError("invalid STransactions length")
|
|
}
|
|
err = checkTreasuryBase(b.subsidyCache,
|
|
block.STransactions()[0], node.height, node.voters,
|
|
b.chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err = coinbasePaysTreasuryAddress(b.subsidyCache,
|
|
block.Transactions()[0], node.height, node.voters,
|
|
b.chainParams, isTreasuryEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 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 before the latest known good
|
|
// checkpoint since the validity is verified via the checkpoints (all
|
|
// transactions are included in the merkle root hash and any changes
|
|
// will therefore be detected by the next checkpoint). This is a huge
|
|
// optimization because running the scripts is the most time consuming
|
|
// portion of block handling.
|
|
checkpoint := b.LatestCheckpoint()
|
|
runScripts := !b.noVerify
|
|
if checkpoint != nil && node.height <= checkpoint.Height {
|
|
runScripts = false
|
|
}
|
|
var scriptFlags txscript.ScriptFlags
|
|
if runScripts {
|
|
var err error
|
|
scriptFlags, err = b.consensusScriptVerifyFlags(node)
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
|
|
// Disconnect all of the transactions in the regular transaction tree of
|
|
// the parent if the block being checked votes against it.
|
|
if node.height > 1 && !voteBitsApproveParent(node.voteBits) {
|
|
err = view.disconnectDisapprovedBlock(b.db, parent,
|
|
isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Ensure the stake transaction tree does not contain any transactions
|
|
// that 'overwrite' older transactions which are not fully spent.
|
|
err = b.checkDupTxs(block.STransactions(), view, wire.TxTreeStake)
|
|
if err != nil {
|
|
log.Tracef("checkDupTxs failed for cur TxTreeStake: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Load all of the utxos referenced by the inputs for all transactions
|
|
// in the block don't already exist in the utxo view from the database.
|
|
//
|
|
// These utxo entries are needed for verification of things such as
|
|
// transaction inputs, counting pay-to-script-hashes, and scripts.
|
|
err = view.fetchInputUtxos(block, isTreasuryEnabled, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.checkTransactionsAndConnect(0, node, block.STransactions(),
|
|
view, stxos, false)
|
|
if err != nil {
|
|
log.Tracef("checkTransactionsAndConnect failed for "+
|
|
"TxTreeStake: %v", err)
|
|
return err
|
|
}
|
|
|
|
stakeTreeFees, err := getStakeTreeFees(b.subsidyCache, node.height,
|
|
block.STransactions(), view, isTreasuryEnabled)
|
|
if err != nil {
|
|
log.Tracef("getStakeTreeFees failed for TxTreeStake: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Enforce all relative lock times via sequence numbers for the stake
|
|
// transaction tree once the stake vote for the agenda is active.
|
|
var prevMedianTime time.Time
|
|
lnFeaturesActive, err := b.isLNFeaturesAgendaActive(node.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lnFeaturesActive {
|
|
// Use the past median time of the *previous* block in order
|
|
// to determine if the transactions in the current block are
|
|
// final.
|
|
prevMedianTime = node.parent.CalcPastMedianTime()
|
|
|
|
for _, stx := range block.STransactions() {
|
|
sequenceLock, err := b.calcSequenceLock(node, stx,
|
|
view, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !SequenceLockActive(sequenceLock, node.height,
|
|
prevMedianTime) {
|
|
|
|
str := fmt.Sprintf("block contains " +
|
|
"stake transaction whose input " +
|
|
"sequence locks are not met")
|
|
return ruleError(ErrUnfinalizedTx, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
if runScripts {
|
|
err = checkBlockScripts(block, view, false, scriptFlags,
|
|
b.sigCache, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
log.Tracef("checkBlockScripts failed; error returned "+
|
|
"on txtreestake of cur block: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Ensure the regular transaction tree does not contain any transactions
|
|
// that 'overwrite' older transactions which are not fully spent.
|
|
err = b.checkDupTxs(block.Transactions(), view, wire.TxTreeRegular)
|
|
if err != nil {
|
|
log.Tracef("checkDupTxs failed for cur TxTreeRegular: %v", err)
|
|
return err
|
|
}
|
|
|
|
err = b.checkTransactionsAndConnect(stakeTreeFees, node,
|
|
block.Transactions(), view, stxos, true)
|
|
if err != nil {
|
|
log.Tracef("checkTransactionsAndConnect failed for cur "+
|
|
"TxTreeRegular: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Enforce all relative lock times via sequence numbers for the regular
|
|
// transaction tree once the stake vote for the agenda is active.
|
|
if lnFeaturesActive {
|
|
// Skip the coinbase since it does not have any inputs and thus
|
|
// lock times do not apply.
|
|
for _, tx := range block.Transactions()[1:] {
|
|
sequenceLock, err := b.calcSequenceLock(node, tx, view, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !SequenceLockActive(sequenceLock, node.height,
|
|
prevMedianTime) {
|
|
|
|
str := fmt.Sprintf("block contains " +
|
|
"transaction whose input sequence " +
|
|
"locks are not met")
|
|
return ruleError(ErrUnfinalizedTx, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a version 2 GCS filter for the block and set the filter into the
|
|
// caller provided header commitment data when requested.
|
|
//
|
|
// This approach is used because the filter creation requires the state of
|
|
// the utxo set after all transactions in the block have been added to it
|
|
// since some of the referenced scripts might be outputs of transactions
|
|
// earlier in the block which will typically not already be available in the
|
|
// view for the caller until after this function returns. That means the
|
|
// caller would have to perform duplicate work that this function already
|
|
// performs to be able to create the filter. Since the filter is is needed
|
|
// at this point to verify the header commitments, a good option is to
|
|
// simply create it here and allow the caller to request the filter be
|
|
// returned to it.
|
|
filter, err := blockcf2.Regular(block.MsgBlock(), view)
|
|
if err != nil {
|
|
return ruleError(ErrMissingTxOut, err.Error())
|
|
}
|
|
if hdrCommitments != nil {
|
|
hdrCommitments.filter = filter
|
|
}
|
|
|
|
// The calculated commitment root must match the associated entry in the
|
|
// header once the vote for the header commitments agenda is active.
|
|
//
|
|
// The header commitments agenda combines the existing stake tree merkle
|
|
// root header field with the regular merkle root field and repurposes the
|
|
// stake root field to house the commitment root instead.
|
|
hdrCommitmentsActive, err := b.isHeaderCommitmentsAgendaActive(node.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hdrCommitmentsActive {
|
|
wantCommitmentRoot := CalcCommitmentRootV1(filter.Hash())
|
|
header := &block.MsgBlock().Header
|
|
if header.StakeRoot != wantCommitmentRoot {
|
|
str := fmt.Sprintf("block commitment root is invalid - block "+
|
|
"header indicates %v, but calculated value is %v",
|
|
header.StakeRoot, wantCommitmentRoot)
|
|
return ruleError(ErrBadCommitmentRoot, str)
|
|
}
|
|
}
|
|
|
|
if runScripts {
|
|
err = checkBlockScripts(block, view, true, scriptFlags,
|
|
b.sigCache, isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
log.Tracef("checkBlockScripts failed; error returned "+
|
|
"on txtreeregular of cur block: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// First block has special rules concerning the ledger.
|
|
if node.height == 1 {
|
|
err := blockOneCoinbasePaysTokens(block.Transactions()[0],
|
|
b.chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update the best hash for view to include this block since all of its
|
|
// transactions have been connected.
|
|
view.SetBestHash(&node.hash)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckConnectBlockTemplate fully validates that connecting the passed block to
|
|
// either the tip of the main chain or its parent does not violate any consensus
|
|
// rules, aside from the proof of work requirement. The block must connect to
|
|
// the current tip of the main chain or its parent.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) CheckConnectBlockTemplate(block *dcrutil.Block) error {
|
|
b.chainLock.Lock()
|
|
defer b.chainLock.Unlock()
|
|
|
|
// Skip the proof of work check as this is just a block template.
|
|
flags := BFNoPoWCheck
|
|
|
|
// The block template must build off the current tip of the main chain
|
|
// or its parent.
|
|
tip := b.bestChain.Tip()
|
|
var prevNode *blockNode
|
|
parentHash := block.MsgBlock().Header.PrevBlock
|
|
if parentHash == tip.hash {
|
|
prevNode = tip
|
|
} else if tip.parent != nil && parentHash == tip.parent.hash {
|
|
prevNode = tip.parent
|
|
}
|
|
if prevNode == nil {
|
|
var str string
|
|
if tip.parent != nil {
|
|
str = fmt.Sprintf("previous block must be the current chain tip "+
|
|
"%s or its parent %s, but got %s", tip.hash, tip.parent.hash,
|
|
parentHash)
|
|
} else {
|
|
str = fmt.Sprintf("previous block must be the current chain tip "+
|
|
"%s, but got %s", tip.hash, parentHash)
|
|
}
|
|
return ruleError(ErrInvalidTemplateParent, str)
|
|
}
|
|
|
|
// Perform context-free sanity checks on the block and its transactions.
|
|
err := checkBlockSanity(block, b.timeSource, flags, b.chainParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The block must pass all of the validation rules which depend on having
|
|
// the headers of all ancestors available, but do not rely on having the
|
|
// full block data of all ancestors available.
|
|
err = b.checkBlockPositional(block, prevNode, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The block must pass all of the validation rules which depend on having
|
|
// the full block data for all of its ancestors available.
|
|
err = b.checkBlockContext(block, prevNode, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newNode := newBlockNode(&block.MsgBlock().Header, prevNode)
|
|
newNode.populateTicketInfo(stake.FindSpentTicketsInBlock(block.MsgBlock()))
|
|
|
|
// Use the chain state as is when extending the main (best) chain.
|
|
if prevNode.hash == tip.hash {
|
|
// Grab the parent block since it is required throughout the block
|
|
// connection process.
|
|
parent, err := b.fetchMainChainBlockByNode(prevNode)
|
|
if err != nil {
|
|
return ruleError(ErrMissingParent, err.Error())
|
|
}
|
|
|
|
view := NewUtxoViewpoint(b.utxoCache)
|
|
view.SetBestHash(&tip.hash)
|
|
|
|
return b.checkConnectBlock(newNode, block, parent, view, nil, nil)
|
|
}
|
|
|
|
// At this point, the block template must be building on the parent of the
|
|
// current tip due to the previous checks, so undo the transactions and
|
|
// spend information for the tip block to reach the point of view of the
|
|
// block template.
|
|
view := NewUtxoViewpoint(b.utxoCache)
|
|
view.SetBestHash(&tip.hash)
|
|
tipBlock, err := b.fetchMainChainBlockByNode(tip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parent, err := b.fetchMainChainBlockByNode(tip.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load all of the spent txos for the tip block from the spend journal.
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var stxos []spentTxOut
|
|
err = b.db.View(func(dbTx database.Tx) error {
|
|
stxos, err = dbFetchSpendJournalEntry(dbTx, tipBlock, isTreasuryEnabled)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine if the automatic ticket revocations agenda is active as of the
|
|
// block being checked.
|
|
isAutoRevocationsEnabled, err := b.isAutoRevocationsAgendaActive(prevNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the view to unspend all of the spent txos and remove the utxos
|
|
// created by the tip block. Also, if the block votes against its parent,
|
|
// reconnect all of the regular transactions.
|
|
err = view.disconnectBlock(tipBlock, parent, stxos, isTreasuryEnabled,
|
|
isAutoRevocationsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The view is now from the point of view of the parent of the current tip
|
|
// block. Ensure the block template can be connected without violating any
|
|
// rules.
|
|
return b.checkConnectBlock(newNode, block, parent, view, nil, nil)
|
|
}
|
|
|
|
// checkTicketExhaustion ensures that extending the provided block with a block
|
|
// that contains the specified number of ticket purchases will not result in
|
|
// a chain that is unrecoverable due to inevitable ticket exhaustion. This
|
|
// scenario happens when the number of live tickets drops below the number of
|
|
// tickets that is needed to reach the next block at which any outstanding
|
|
// immature ticket purchases that would provide the necessary live tickets
|
|
// mature.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) checkTicketExhaustion(prevNode *blockNode, ticketPurchases uint8) error {
|
|
// Nothing to do if the chain is not far enough along where ticket
|
|
// exhaustion could be an issue.
|
|
//
|
|
// Note that the +1 added to the ticket maturity is because the lottery for
|
|
// each block selects tickets from the state of the pool as of the previous
|
|
// block and the first votes are required to be included at stake validation
|
|
// height, which means the tickets need to be live as of one block prior.
|
|
//
|
|
// Another way to look at it is that matured tickets do not become eligible
|
|
// for selection until the block AFTER the maturity period.
|
|
nextHeight := prevNode.height + 1
|
|
stakeValidationHeight := b.chainParams.StakeValidationHeight
|
|
ticketMaturity := int64(b.chainParams.TicketMaturity)
|
|
if nextHeight+ticketMaturity+1 < stakeValidationHeight {
|
|
return nil
|
|
}
|
|
|
|
// Calculate the final pool size of live tickets after the ticket maturity
|
|
// period relative to the next block height.
|
|
//
|
|
// Note that the pool size in the block header indicates the pool size
|
|
// BEFORE applying the block, so this adjusts the final pool size
|
|
// accordingly by adding any tickets that matured in said previous block as
|
|
// well as subtracting tickets consumed by votes if at least at stake
|
|
// validation height. This is why both the call to sum purchased tickets
|
|
// and the number of voting blocks in the maturity period are one more than
|
|
// might otherwise be expected.
|
|
//
|
|
// In addition, adjust the pool size for all tickets that will mature during
|
|
// the ticket maturity interval, which includes any tickets purchased in the
|
|
// block that is extending the block being checked, as well as all votes
|
|
// that will consume tickets.
|
|
finalPoolSize := int64(prevNode.poolSize)
|
|
finalPoolSize += b.sumPurchasedTickets(prevNode, ticketMaturity+1)
|
|
finalPoolSize += int64(ticketPurchases)
|
|
votingBlocksInMaturityPeriod := ticketMaturity + 2
|
|
if prevNode.height < stakeValidationHeight {
|
|
nonVotingBlocks := stakeValidationHeight - prevNode.height
|
|
votingBlocksInMaturityPeriod -= nonVotingBlocks
|
|
}
|
|
votesPerBlock := int64(b.chainParams.TicketsPerBlock)
|
|
finalPoolSize -= votingBlocksInMaturityPeriod * votesPerBlock
|
|
|
|
// Ensure that the final pool of live tickets after the ticket maturity
|
|
// period will have enough tickets to prevent becoming unrecoverable.
|
|
if finalPoolSize < votesPerBlock {
|
|
purchasesNeeded := votesPerBlock - finalPoolSize
|
|
str := fmt.Sprintf("extending block %s (height %d) with a block that "+
|
|
"contains fewer than %d ticket purchase(s) would result in an "+
|
|
"unrecoverable chain due to ticket exhaustion", prevNode.hash,
|
|
prevNode.height, purchasesNeeded)
|
|
return ruleError(ErrTicketExhaustion, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckTicketExhaustion ensures that extending the block associated with the
|
|
// provided hash with a block that contains the specified number of ticket
|
|
// purchases will not result in a chain that is unrecoverable due to inevitable
|
|
// ticket exhaustion. This scenario happens when the number of live tickets
|
|
// drops below the number of tickets that is needed to reach the next block at
|
|
// which any outstanding immature ticket purchases that would provide the
|
|
// necessary live tickets mature.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) CheckTicketExhaustion(hash *chainhash.Hash, ticketPurchases uint8) error {
|
|
node := b.index.LookupNode(hash)
|
|
if node == nil {
|
|
return unknownBlockError(hash)
|
|
}
|
|
|
|
return b.checkTicketExhaustion(node, ticketPurchases)
|
|
}
|