dcrd/blockchain/validate.go
Ryan Staudt d1b1fdba52 blockchain: Reject old block vers for HFV.
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.
2021-11-22 11:50:41 -06:00

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)
}