This introduces automatic ticket revocations as defined in DCP0009,
including:
- Allowing tickets to be revoked by anyone instead of requiring a valid
signature from the voting address
- Requiring blocks to contain ticket revocation transactions for all
tickets that become missed or expired as of that block
An overview of the changes is as follows:
- blockchain/scriptval.go
- ValidateTransactionScripts, checkBlockScripts
- Skip script validation for version 2 revocations if the automatic
ticket revocations agenda is active
- blockchain/stake/staketx.go, blockchain/stake/staketx_test.go
- Introduce TxVersionAutoRevocations const for version 2 revocations
- CalculateRewards, CalculateRevocationRewards
- Add CalculateRevocationRewards to handle the required updates for
revocations while leaving CalculateRewards as-is for votes
- 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
- Add additional tests to cover the new code paths
- CheckSSRtx
- Add checks to ensure that the input does not have a signature
script and does not have a fee if the automatic ticket revocations
agenda is active
- Add additional tests to cover the new code paths
- blockchain/validate.go, blockchain/validate_test.go
- checkTransactionContext
- Add rule that revocation transactions MUST be version 2 if the
automatic ticket revocations agenda is active.
- checkTicketRedeemers
- Add rule that 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
- Add rule that if the automatic ticket revocations agenda is
active, then revocations can spend tickets that will become missed
or expired as of that block (previously they could not be spent
until the block following the block where they became missed or
expired)
- Update to accept the referenced ticket hashes rather than the
overall block to simplify adding comprehensive unit tests for the
function
- calcTicketReturnAmounts
- Update to calculate the amounts for all outputs rather than a
single output
- 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
- checkTicketRedeemerCommitments
- Add rule that if the transaction is a version 2 revocation and the
automatic ticket revocation agenda is active, then the fee MUST be
zero
- Use the updated calcTicketReturnAmounts function so that returns
include the full amount, which allows for ensuring a truly zero
fee
- checkRevocationInputs
- Add rule that if the automatic ticket revocations agenda is
active, then revocations can spend tickets after ticket maturity
+ 1 blocks (in the block that they become missed or expired)
rather than after ticket maturity + 2 blocks (in the block AFTER
they become missed or expired)
- internal/mempool/mempool.go, internal/mempool/mempool_test.go
- Pass best block header to CheckTransactionInputs
- Pass isAutoRevocationsEnabled to ValidateTransactionScripts
- internal/mining/mining.go, internal/mining/mining_harness_test.go,
internal/mining/mining_test.go, internal/mining/mining_view_test.go
- Add a helper function, createRevocationFromTicket, to create a
revocation transaction from a ticket hash
- If the automatic ticket revocations agenda is active, then create
version 2 revocation transactions for all tickets that will become
missed or expired as of the block being created
- Create and insert these revocations into the priority queue AFTER
votes are done being added to ensure that revocations are added for
votes that fail validation checks or are otherwise not included into
the block
- Pass best block header to CheckTransactionInputs
- Pass isAutoRevocationsEnabled to ValidateTransactionScripts
- internal/rpcserver/interface.go, internal/rpcserver/rpcserver.go,
internal/rpcserver/rpcserverhandlers_test.go
- handleCreateRawSSRtx
- If the automatic ticket revocations agenda is active:
- Validate that the fee is zero
- Set the transaction version to 2
- Pass previous header bytes and isAutoRevocationsEnabled flag to
CreateRevocationFromTicket in order to create version 2 revocation
transactions properly with the updated rules
- Add additional tests to cover the new code paths
- server.go
- Pass previous block header to CheckTransactionInputs
- Pass isAutoRevocationsEnabled to ValidateTransactionScripts
277 lines
8.0 KiB
Go
277 lines
8.0 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 (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"runtime"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/txscript/v4"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
// txValidateItem holds a transaction along with which input to validate.
|
|
type txValidateItem struct {
|
|
txInIndex int
|
|
txIn *wire.TxIn
|
|
tx *dcrutil.Tx
|
|
}
|
|
|
|
// txValidator provides a type which asynchronously validates transaction
|
|
// inputs. It provides several channels for communication and a processing
|
|
// function that is intended to be in run multiple goroutines.
|
|
type txValidator struct {
|
|
validateChan chan *txValidateItem
|
|
resultChan chan error
|
|
utxoView *UtxoViewpoint
|
|
flags txscript.ScriptFlags
|
|
sigCache *txscript.SigCache
|
|
}
|
|
|
|
// sendResult sends the result of a script pair validation on the internal
|
|
// result channel while respecting the context. The allows orderly
|
|
// shutdown when the validation process is aborted early due to a validation
|
|
// error in one of the other goroutines.
|
|
func (v *txValidator) sendResult(ctx context.Context, result error) {
|
|
select {
|
|
case v.resultChan <- result:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
// validateHandler consumes items to validate from the internal validate channel
|
|
// and returns the result of the validation on the internal result channel. It
|
|
// must be run as a goroutine.
|
|
func (v *txValidator) validateHandler(ctx context.Context) {
|
|
out:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break out
|
|
|
|
case txVI := <-v.validateChan:
|
|
// Ensure the referenced input utxo is available.
|
|
txIn := txVI.txIn
|
|
utxo := v.utxoView.LookupEntry(txIn.PreviousOutPoint)
|
|
if utxo == nil {
|
|
str := fmt.Sprintf("unable to find unspent "+
|
|
"output %v referenced from "+
|
|
"transaction %s:%d",
|
|
txIn.PreviousOutPoint, txVI.tx.Hash(),
|
|
txVI.txInIndex)
|
|
err := ruleError(ErrMissingTxOut, str)
|
|
v.sendResult(ctx, err)
|
|
break out
|
|
}
|
|
|
|
// Create a new script engine for the script pair.
|
|
sigScript := txIn.SignatureScript
|
|
version := utxo.ScriptVersion()
|
|
pkScript := utxo.PkScript()
|
|
|
|
vm, err := txscript.NewEngine(pkScript, txVI.tx.MsgTx(),
|
|
txVI.txInIndex, v.flags, version, v.sigCache)
|
|
if err != nil {
|
|
str := fmt.Sprintf("failed to parse input "+
|
|
"%s:%d which references output %v - "+
|
|
"%v (input script bytes %x, prev output "+
|
|
"script bytes %x)", txVI.tx.Hash(),
|
|
txVI.txInIndex, txIn.PreviousOutPoint,
|
|
err, sigScript, pkScript)
|
|
err := ruleError(ErrScriptMalformed, str)
|
|
v.sendResult(ctx, err)
|
|
break out
|
|
}
|
|
|
|
// Execute the script pair.
|
|
if err := vm.Execute(); err != nil {
|
|
str := fmt.Sprintf("failed to validate input "+
|
|
"%s:%d which references output %v - "+
|
|
"%v (input script bytes %x, prev output "+
|
|
"script bytes %x)", txVI.tx.Hash(),
|
|
txVI.txInIndex, txIn.PreviousOutPoint,
|
|
err, sigScript, pkScript)
|
|
err := ruleError(ErrScriptValidation, str)
|
|
v.sendResult(ctx, err)
|
|
break out
|
|
}
|
|
|
|
// Validation succeeded.
|
|
v.sendResult(ctx, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate validates the scripts for all of the passed transaction inputs using
|
|
// multiple goroutines.
|
|
func (v *txValidator) Validate(items []*txValidateItem) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Limit the number of goroutines to do script validation based on the
|
|
// number of processor cores. This help ensure the system stays
|
|
// reasonably responsive under heavy load.
|
|
maxGoRoutines := runtime.NumCPU() * 3
|
|
if maxGoRoutines <= 0 {
|
|
maxGoRoutines = 1
|
|
}
|
|
if maxGoRoutines > len(items) {
|
|
maxGoRoutines = len(items)
|
|
}
|
|
|
|
// Start up validation handlers that are used to asynchronously
|
|
// validate each transaction input.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
for i := 0; i < maxGoRoutines; i++ {
|
|
go v.validateHandler(ctx)
|
|
}
|
|
|
|
// Validate each of the inputs. The context is canceled when any
|
|
// errors occur so all processing goroutines exit regardless of which
|
|
// input had the validation error.
|
|
numInputs := len(items)
|
|
currentItem := 0
|
|
processedItems := 0
|
|
for processedItems < numInputs {
|
|
// Only send items while there are still items that need to
|
|
// be processed. The select statement will never select a nil
|
|
// channel.
|
|
var validateChan chan *txValidateItem
|
|
var item *txValidateItem
|
|
if currentItem < numInputs {
|
|
validateChan = v.validateChan
|
|
item = items[currentItem]
|
|
}
|
|
|
|
select {
|
|
case validateChan <- item:
|
|
currentItem++
|
|
|
|
case err := <-v.resultChan:
|
|
processedItems++
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
cancel()
|
|
return nil
|
|
}
|
|
|
|
// newTxValidator returns a new instance of txValidator to be used for
|
|
// validating transaction scripts asynchronously.
|
|
func newTxValidator(utxoView *UtxoViewpoint, flags txscript.ScriptFlags, sigCache *txscript.SigCache) *txValidator {
|
|
return &txValidator{
|
|
validateChan: make(chan *txValidateItem),
|
|
resultChan: make(chan error),
|
|
utxoView: utxoView,
|
|
sigCache: sigCache,
|
|
flags: flags,
|
|
}
|
|
}
|
|
|
|
// ValidateTransactionScripts validates the scripts for the passed transaction
|
|
// using multiple goroutines.
|
|
func ValidateTransactionScripts(tx *dcrutil.Tx, utxoView *UtxoViewpoint,
|
|
flags txscript.ScriptFlags, sigCache *txscript.SigCache,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
// Skip revocations if the automatic ticket revocations agenda is active and
|
|
// the transaction version is greater than or equal to 2. This is allowed
|
|
// since consensus rules enforce that revocations MUST pay to the addresses
|
|
// specified by the original commitments in the ticket.
|
|
msgTx := tx.MsgTx()
|
|
if isAutoRevocationsEnabled &&
|
|
msgTx.Version >= stake.TxVersionAutoRevocations &&
|
|
stake.IsSSRtx(msgTx, isAutoRevocationsEnabled) {
|
|
|
|
return nil
|
|
}
|
|
|
|
// Collect all of the transaction inputs and required information for
|
|
// validation.
|
|
txIns := msgTx.TxIn
|
|
txValItems := make([]*txValidateItem, 0, len(txIns))
|
|
for txInIdx, txIn := range txIns {
|
|
// Skip coinbases.
|
|
if txIn.PreviousOutPoint.Index == math.MaxUint32 {
|
|
continue
|
|
}
|
|
|
|
txVI := &txValidateItem{
|
|
txInIndex: txInIdx,
|
|
txIn: txIn,
|
|
tx: tx,
|
|
}
|
|
txValItems = append(txValItems, txVI)
|
|
}
|
|
|
|
// Validate all of the inputs.
|
|
return newTxValidator(utxoView, flags, sigCache).Validate(txValItems)
|
|
}
|
|
|
|
// checkBlockScripts executes and validates the scripts for all transactions in
|
|
// the passed block using multiple goroutines.
|
|
// txTree = true is TxTreeRegular, txTree = false is TxTreeStake.
|
|
func checkBlockScripts(block *dcrutil.Block, utxoView *UtxoViewpoint, txTree bool,
|
|
scriptFlags txscript.ScriptFlags, sigCache *txscript.SigCache,
|
|
isAutoRevocationsEnabled bool) error {
|
|
|
|
// Collect all of the transaction inputs and required information for
|
|
// validation for all transactions in the block into a single slice.
|
|
numInputs := 0
|
|
var txs []*dcrutil.Tx
|
|
|
|
// TxTreeRegular handling.
|
|
if txTree {
|
|
txs = block.Transactions()
|
|
} else { // TxTreeStake
|
|
txs = block.STransactions()
|
|
}
|
|
|
|
for _, tx := range txs {
|
|
numInputs += len(tx.MsgTx().TxIn)
|
|
}
|
|
txValItems := make([]*txValidateItem, 0, numInputs)
|
|
for _, tx := range txs {
|
|
// Skip revocations if the automatic ticket revocations agenda is active and
|
|
// the transaction version is greater than or equal to 2. This is allowed
|
|
// since consensus rules enforce that revocations MUST pay to the addresses
|
|
// specified by the original commitments in the ticket.
|
|
msgTx := tx.MsgTx()
|
|
if isAutoRevocationsEnabled && !txTree &&
|
|
msgTx.Version >= stake.TxVersionAutoRevocations &&
|
|
stake.IsSSRtx(msgTx, isAutoRevocationsEnabled) {
|
|
|
|
continue
|
|
}
|
|
|
|
for txInIdx, txIn := range msgTx.TxIn {
|
|
// Skip coinbases.
|
|
if txIn.PreviousOutPoint.Index == math.MaxUint32 {
|
|
continue
|
|
}
|
|
|
|
txVI := &txValidateItem{
|
|
txInIndex: txInIdx,
|
|
txIn: txIn,
|
|
tx: tx,
|
|
}
|
|
txValItems = append(txValItems, txVI)
|
|
}
|
|
}
|
|
|
|
// Validate all of the inputs.
|
|
return newTxValidator(utxoView, scriptFlags, sigCache).Validate(txValItems)
|
|
}
|