dcrd/gcs/blockcf2/blockcf.go
Dave Collins 36fbba3e22
gcs: Prepare v3.0.0.
This updates the gcs module dependencies, the copyright year in the
files modified since the previous release, and serves as a base for
gcs/v3.0.0.

The updated direct dependencies in this commit are as follows:

- github.com/decred/dcrd/blockchain/stake/v4@v4.0.0

The full list of updated direct dependencies since the previous
gcs/v2.1.0 release are as follows:

- github.com/dchest/siphash@v1.2.2
- github.com/decred/dcrd/blockchain/stake/v4@v4.0.0
- github.com/decred/dcrd/chaincfg/chainhash@v1.0.3
- github.com/decred/dcrd/txscript/v4@v4.0.0
- github.com/decred/dcrd/wire@v1.5.0

Finally, all modules in the repository that depend on the module are
tidied to ensure they are updated to use the latest versions hoisted
forward as a result.
2021-11-22 10:37:05 -06:00

467 lines
17 KiB
Go

// Copyright (c) 2019-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
/*
Package blockcf2 provides functions for building committed filters for blocks
using Golomb-coded sets in a way that is useful for light clients such as SPV
wallets.
Committed filters are a reversal of how bloom filters are typically used by a
light client: a consensus-validating full node commits to filters for every
block with a predetermined collision probability and light clients match against
the filters locally rather than uploading personal data to other nodes. If a
filter matches, the light client should fetch the entire block and further
inspect it for relevant transactions.
*/
package blockcf2
import (
"fmt"
"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/gcs/v3"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/wire"
)
const (
// B is bits parameter for constructing GCS filters and results in the
// tunable parameter that is essentially used as the bin size in the
// underlying Golomb coding having a value of 2^B.
B = 19
// M is the inverse of the target false positive rate for constructing GCS
// filters. This is the optimal value of M to minimize the size of the
// filter for B = 19.
M = 784931
)
const (
// 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
)
// PrevScriptError represents an error when looking up a previous output script.
// The caller can use type assertions to determine the specific details of the
// previous outpoint and the transaction input that references it.
type PrevScriptError struct {
PrevOut wire.OutPoint
TxHash chainhash.Hash
TxInIdx int
}
// Error returns the previous script error as a human-readable string and
// satisfies the error interface.
func (e PrevScriptError) Error() string {
return fmt.Sprintf("unable to find output script %s:%d referenced by %s:%d",
e.PrevOut, e.PrevOut.Tree, e.TxHash, e.TxInIdx)
}
// Entries describes all of the filter entries used to create a GCS filter and
// provides methods for appending data structures found in blocks.
type Entries [][]byte
// AddRegularPkScript adds the regular tx output script to an entries slice.
// Empty scripts are ignored.
func (e *Entries) AddRegularPkScript(script []byte) {
if len(script) == 0 {
return
}
*e = append(*e, script)
}
// AddStakePkScript adds the output script without the stake opcode tag to an
// entries slice. Empty scripts are ignored.
func (e *Entries) AddStakePkScript(script []byte) {
if len(script) == 0 {
return
}
*e = append(*e, script[1:])
}
// Key creates a block filter key by truncating the Merkle root of a block to
// the key size.
func Key(merkleRoot *chainhash.Hash) [gcs.KeySize]byte {
var key [gcs.KeySize]byte
copy(key[:], merkleRoot[:])
return key
}
// isStakeOutput returns true if a script output is a stake type.
func isStakeOutput(scriptVersion uint16, pkScript []byte) bool {
return stake.IsTicketPurchaseScript(scriptVersion, pkScript) ||
stake.IsVoteScript(scriptVersion, pkScript) ||
stake.IsRevocationScript(scriptVersion, pkScript) ||
stake.IsStakeChangeScript(scriptVersion, pkScript) ||
stake.IsTreasuryGenScript(scriptVersion, pkScript)
}
// 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
}
// commitmentConverter provides the ability to convert the commitment output
// script of a ticket purchase to either a pay-to-pubkey-hash or a
// pay-to-script-hash script according to the commitment while making use of a
// single backing array that helps reduce the number of allocations necessary
// to make the conversion.
type commitmentConverter struct {
allScripts []byte
}
// commitmentToPaymentScript converts a commitment output script of a ticket
// purchase to either a pay-to-pubkey-hash or a pay-to-script-hash script
// according to the commitment.
func (c *commitmentConverter) paymentScript(commitmentScript []byte) []byte {
commitmentHash := extractTicketCommitHash(commitmentScript)
if isTicketCommitP2SH(commitmentScript) {
// A pay-to-script-hash script is of the form:
// OP_HASH160 <20-byte hash> OP_EQUAL
startIdx := len(c.allScripts)
c.allScripts = append(c.allScripts, txscript.OP_HASH160,
txscript.OP_DATA_20)
c.allScripts = append(c.allScripts, commitmentHash...)
c.allScripts = append(c.allScripts, txscript.OP_EQUAL)
endIdx := len(c.allScripts)
return c.allScripts[startIdx:endIdx:endIdx]
}
// A pay-to-pubkey-hash script for ecdsa+secp256k1 is of the form:
// OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG
startIdx := len(c.allScripts)
c.allScripts = append(c.allScripts, txscript.OP_DUP, txscript.OP_HASH160,
txscript.OP_DATA_20)
c.allScripts = append(c.allScripts, commitmentHash...)
c.allScripts = append(c.allScripts, txscript.OP_EQUALVERIFY,
txscript.OP_CHECKSIG)
endIdx := len(c.allScripts)
return c.allScripts[startIdx:endIdx:endIdx]
}
// makeCommitmentConverter returns a commitment converter with a pre-allocated
// backing array for performing the conversions based on the provided number
// of tickets.
func makeCommitmentConverter(numTickets uint8) commitmentConverter {
// The vast majority of ticket purchases only contain commitments to a
// couple of outputs and p2pkh is the larger of the possible converted
// scripts, so use that information to calculate a reasonable size hint for
// all of the converted commitment scripts.
const p2pkhScriptLen = 25
sizeHint := int32(numTickets) * p2pkhScriptLen * 2
return commitmentConverter{
allScripts: make([]byte, 0, sizeHint),
}
}
// excludeFromFilter returns whether the passed script version and public key
// script combination should be excluded from filters. Scripts that are empty
// or larger than the max allowed length and all script versions other than 0
// are excluded.
func excludeFromFilter(scriptVersion uint16, script []byte) bool {
return scriptVersion != 0 || len(script) == 0 || len(script) >
txscript.MaxScriptSize
}
// PrevScripter defines an interface that provides access to scripts and their
// associated version keyed by an outpoint. It is used within this package as a
// generic means to provide the scripts referenced by all of the inputs to
// transactions within a block that are needed to construct a filter. The
// boolean return indicates whether or not the script and version for the
// provided outpoint was found.
type PrevScripter interface {
PrevScript(*wire.OutPoint) (uint16, []byte, bool)
}
// Regular builds a GCS filter from a block and the previous output scripts it
// references as inputs. The filter will be keyed by the merkle root of the
// block.
//
// The following section describes the items that will be added to the filter,
// however, there are a few special cases that apply:
//
// - Scripts that are not version 0, empty, or otherwise provably unspendable
// are NOT included
// - Output scripts for transactions in the stake tree do NOT include the
// initial stake opcode tag (OP_SS*)
// - This allows users of the filter to only match against a normal P2PKH or
// P2SH script, instead of many extra matches for each tag
//
// Considering the aforementioned exceptions, the filter will contain the
// following items for transactions in the regular tree:
// - Previous output scripts referenced by the transaction inputs with the
// exception of the coinbase
// - Output scripts in the transaction outputs
//
// In addition, also considering the aforementioned exceptions, the filter will
// contain the following items for transactions in the stake tree:
// - For ticket purchases:
// - Previous output scripts referenced by the transaction inputs
// - Commitment output scripts (converted from the commitment hash and type)
// - Change output scripts except those that are provably unspendable
// - For votes:
// - Subsidy generation output scripts
// - Output scripts that pay the original ticket commitments
// - For revocations:
// - Output scripts that pay the original ticket commitments
func Regular(block *wire.MsgBlock, prevScripts PrevScripter) (*gcs.FilterV2, error) {
// There will typically be data entries for at least one output and one
// input per regular transaction in the block, excepting the coinbase, and
// an average of two per stake transaction, though stake transactions vary
// wildly.
//
// In practice, most transactions will have more inputs and outputs than
// this value, but pre-allocate enough space in the backing array for a
// reasonable minimum value to reduce the number of allocations.
numEntriesHint := len(block.Transactions)*2 + len(block.STransactions)
data := make(Entries, 0, numEntriesHint)
// For regular transactions, add all referenced previous output scripts,
// except the coinbase, and all output scripts.
for i, tx := range block.Transactions {
for _, txOut := range tx.TxOut {
// Don't add scripts that are empty, larger than the max allowed
// length, or for an unsupported script version. Notice that
// provably unspendable OP_RETURN scripts are included.
if excludeFromFilter(txOut.Version, txOut.PkScript) {
continue
}
data.AddRegularPkScript(txOut.PkScript)
}
// Skip coinbase since it does not have any inputs.
if i == 0 {
continue
}
for txInIdx, txIn := range tx.TxIn {
prevOut := &txIn.PreviousOutPoint
scriptVer, prevOutScript, ok := prevScripts.PrevScript(prevOut)
if !ok {
return nil, PrevScriptError{
PrevOut: *prevOut,
TxHash: tx.TxHash(),
TxInIdx: txInIdx,
}
}
// Don't add scripts that are empty, larger than the max allowed
// length, or for an unsupported script version.
if excludeFromFilter(scriptVer, prevOutScript) {
continue
}
// Add the script to be included in the filter while stripping the
// initial stake opcode for those in the stake tree.
isStakeTree := prevOut.Tree == wire.TxTreeStake
isStakeOut := isStakeTree && isStakeOutput(scriptVer, prevOutScript)
if isStakeOut {
data.AddStakePkScript(prevOutScript)
} else {
data.AddRegularPkScript(prevOutScript)
}
}
}
// Add data from stake transactions. For each class of stake transaction,
// the following data is committed to the regular filter:
//
// - Ticket purchases:
// - Previous output scripts referenced by the transaction inputs
// - Commitment output scripts (converted from the commitment hash and
// type)
// - Change output scripts except those that are provably unspendable
//
// - Votes:
// - Subsidy generation output scripts
// - Output scripts that pay the original ticket commitments
//
// - Revocations:
// - Output scripts that pay the original ticket commitments
//
// - Treasury add:
// - Input scripts that are payments to the treasury
// - Output script for the second output if it exists (tadd change)
//
// - Treasury spends:
// - Output scripts that make payments
//
// Output scripts are handled specially for stake transactions by slicing
// off the stake opcode tag (OP_SS*). This tag always appears as the first
// byte of the script and removing it allows users of the filter to only
// match against a normal P2PKH or P2SH script, instead of many extra
// matches for each tag.
converter := makeCommitmentConverter(block.Header.FreshStake)
for _, tx := range block.STransactions {
// It is ok to set isTreasuryEnabled to true. This code relies
// on consensus to make sure treasury opcodes never make it
// here. Setting it to true also will automatically enable the
// opcodes once the treasury agenda is voted in.
//
// Pass false for whether the automatic ticket revocations agenda is active.
// This is okay since the auto revocations flag just adds additional rules
// that revocations must have an empty signature script for its input and
// must have zero fee. Revocations that don't follow those rules will still
// be rejected by consensus.
switch stake.DetermineTxType(tx, true, false) {
case stake.TxTypeSStx:
for txInIdx, txIn := range tx.TxIn {
prevOut := &txIn.PreviousOutPoint
scriptVer, prevOutScript, ok := prevScripts.PrevScript(prevOut)
if !ok {
return nil, PrevScriptError{
PrevOut: *prevOut,
TxHash: tx.TxHash(),
TxInIdx: txInIdx,
}
}
// Don't add scripts that are empty, larger than the max allowed
// length, or for an unsupported script version.
if excludeFromFilter(scriptVer, prevOutScript) {
continue
}
// Add the script to be included in the filter while stripping
// the initial stake opcode for those in the stake tree.
isStakeTree := prevOut.Tree == wire.TxTreeStake
isStakeOut := isStakeTree && isStakeOutput(scriptVer, prevOutScript)
if isStakeOut {
data.AddStakePkScript(prevOutScript)
} else {
data.AddRegularPkScript(prevOutScript)
}
}
for txOutIdx, txOut := range tx.TxOut {
// Don't commit to the voting rights output. This is done to
// help thwart attempts at SPV voting. An SPV client does not
// have access to the full blocks, so they can't reasonably vote
// on their validity.
if txOutIdx == 0 {
continue
}
// Commit to the change outputs with the exception of those that
// are provably unspendable.
isChangeOutput := txOutIdx%2 == 0
if isChangeOutput {
if txOut.Value == 0 {
continue
}
data.AddStakePkScript(txOut.PkScript)
continue
}
// Commit to the final payment scripts that are required by the
// ticket commitment outputs.
if len(txOut.PkScript) == 0 {
continue
}
paymentScript := converter.paymentScript(txOut.PkScript)
data.AddRegularPkScript(paymentScript)
}
case stake.TxTypeSSGen:
// The first two outputs are skipped because they indicate the block
// voted on and the vote choices, respectively.
for _, txOut := range tx.TxOut[2:] {
data.AddStakePkScript(txOut.PkScript)
}
case stake.TxTypeSSRtx:
for _, txOut := range tx.TxOut {
data.AddStakePkScript(txOut.PkScript)
}
// The treasury opcodes can be added to cfilter2 for two reasons:
// 1. Nothing changes from the viewpoint of the wallet.
// 2. Consensus disallows treasury opcodes prior to activation.
case stake.TxTypeTAdd:
// All inputs from TAdd are burnt as they enter the treasury account.
for txInIdx, txIn := range tx.TxIn {
prevOut := &txIn.PreviousOutPoint
scriptVer, prevOutScript, ok := prevScripts.PrevScript(prevOut)
if !ok {
return nil, PrevScriptError{
PrevOut: *prevOut,
TxHash: tx.TxHash(),
TxInIdx: txInIdx,
}
}
// Don't add scripts that are empty, larger than the max allowed
// length, or for an unsupported script version.
if excludeFromFilter(scriptVer, prevOutScript) {
continue
}
// Add the script to be included in the filter while stripping
// the initial stake opcode for those in the stake tree.
isStakeTree := prevOut.Tree == wire.TxTreeStake
isStakeOut := isStakeTree && isStakeOutput(scriptVer, prevOutScript)
if isStakeOut {
data.AddStakePkScript(prevOutScript)
} else {
data.AddRegularPkScript(prevOutScript)
}
}
// Handle stake change. Stake change is always output 1 and output 1
// only exists if there is change. Check for 0 value just to be sure.
if len(tx.TxOut) == 2 && tx.TxOut[1].Value != 0 {
data.AddStakePkScript(tx.TxOut[1].PkScript)
}
case stake.TxTypeTSpend:
// Skip OP_RETURN and then strip all OP_TGEN from the remaining
// outputs.
for _, txOut := range tx.TxOut[1:] {
data.AddStakePkScript(txOut.PkScript)
}
}
}
// Create the key by truncating the block's merkle root and use it to create
// the filter.
key := Key(&block.Header.MerkleRoot)
return gcs.NewFilterV2(B, M, key, data)
}