dcrd/blockchain/headercmt.go
Dave Collins 974f80d1e5
multi: Consolidate header proof logic.
Currently, the p2p and rpcserver handlers that deal with compact filters
essentially duplicate the logic for dealing with the associated header
proof and are also required to understand the structure of the proof in
the form of knowing the associated index.  This is not ideal since there
is no guarantee that a given index will always be the same through
consensus changes.

With those points in mind, this reworks the header proof logic slightly
to consolidate it by introducing a new HeaderProof struct to the
blockchain that houses a header commitment inclusion proof and
associated proof index and updating the FilterByBlockHash method to
return the header proof along with the filter and updates the p2p and
rpcserver handlers and associated interfaces and tests accordingly.

The result is that the aforementioned p2p and rpcserver handlers for
obtaining compact filters no longer need to understand anything about
the proofs.  It is also useful for any future header commitments too
since the same logic will hold true for those instances as well.
2022-05-14 17:28:50 -05:00

173 lines
6.3 KiB
Go

// Copyright (c) 2019-2022 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 (
"fmt"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/database/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/gcs/v4"
"github.com/decred/dcrd/wire"
)
const (
// HeaderCmtFilterIndex is the proof index for the filter header commitment.
HeaderCmtFilterIndex = 0
)
// headerCommitmentData houses information the block header commits to via the
// commitment root.
type headerCommitmentData struct {
filter *gcs.FilterV2
}
// CalcCommitmentRootV1 calculates and returns the required v1 block commitment
// root from the filter hash it commits to.
//
// This function is safe for concurrent access.
func CalcCommitmentRootV1(filterHash chainhash.Hash) chainhash.Hash {
// NOTE: The commitment root is actually the merkle root of a merkle tree
// whose leaves are each of the individual commitments. However, since
// there is only currently a single commitment, the merkle root will simply
// be the hash of the sole item, so there is no point in doing extra work.
//
// The callers could certainly simply avoid calling this function and do the
// same thing directly, however, providing versioned functions helps make
// it clear exactly what each header commitment version commits to and makes
// the code more consistent with multiple versions.
return filterHash
}
// FetchUtxoViewParentTemplate loads utxo details from the point of view of just
// having connected the given block, which must be a block template that
// connects to the parent of the tip of the main chain. In other words, the
// given block must be a sibling of the current tip of the main chain.
//
// This should typically only be used by mining code when it is unable to
// generate a template that extends the current tip due to being unable to
// acquire the minimum required number of votes to extend it.
//
// This function is safe for concurrent access however the returned view is NOT.
func (b *BlockChain) FetchUtxoViewParentTemplate(block *wire.MsgBlock) (*UtxoViewpoint, error) {
b.chainLock.Lock()
defer b.chainLock.Unlock()
// The block template must build off the parent of the current tip of the
// main chain.
tip := b.bestChain.Tip()
if tip.parent == nil {
str := fmt.Sprintf("unable to fetch utxos for non-existent parent of "+
"the current tip %s", tip.hash)
return nil, ruleError(ErrInvalidTemplateParent, str)
}
parentHash := block.Header.PrevBlock
if parentHash != tip.parent.hash {
str := fmt.Sprintf("previous block must be the parent of the current "+
"chain tip %s, but got %s", tip.parent.hash, parentHash)
return nil, ruleError(ErrInvalidTemplateParent, str)
}
// Since the block template is building on the parent of the current tip,
// 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 nil, err
}
parent, err := b.fetchMainChainBlockByNode(tip.parent)
if err != nil {
return nil, err
}
// Determine if treasury agenda is active.
isTreasuryEnabled, err := b.isTreasuryAgendaActive(tip.parent)
if err != nil {
return nil, err
}
// Load all of the spent txos for the tip block from the spend journal.
var stxos []spentTxOut
err = b.db.View(func(dbTx database.Tx) error {
stxos, err = dbFetchSpendJournalEntry(dbTx, tipBlock, isTreasuryEnabled)
return err
})
if err != nil {
return nil, 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)
if err != nil {
return nil, err
}
// The view is now from the point of view of the parent of the current tip
// block. However, calculating the commitment root requires the view to
// include outputs created in the candidate block, so update the view to
// mark all utxos referenced by the block as spent and add all transactions
// being created by the block to it. In the case the block votes against
// the parent, also disconnect all of the regular transactions in the parent
// block.
utilBlock := dcrutil.NewBlock(block)
err = view.connectBlock(b.db, utilBlock, parent, nil, isTreasuryEnabled)
if err != nil {
return nil, err
}
return view, nil
}
// HeaderProof houses a merkle tree inclusion proof and associated proof index
// for a header commitment. This information allows clients to efficiently
// prove whether or not the commitment root of a block header commits to
// specific data at the given index.
type HeaderProof struct {
ProofIndex uint32
ProofHashes []chainhash.Hash
}
// FilterByBlockHash returns the version 2 GCS filter for the given block hash
// along with a header commitment inclusion proof when they exist. This
// function returns the filters regardless of whether or not their associated
// block is part of the main chain.
//
// An error that wraps ErrNoFilter will be returned when the filter for the
// given block hash does not exist.
//
// This function is safe for concurrent access.
func (b *BlockChain) FilterByBlockHash(hash *chainhash.Hash) (*gcs.FilterV2, *HeaderProof, error) {
var filter *gcs.FilterV2
err := b.db.View(func(dbTx database.Tx) error {
var err error
filter, err = dbFetchGCSFilter(dbTx, hash)
return err
})
if err != nil {
return nil, nil, err
}
if filter == nil {
str := fmt.Sprintf("no filter available for block %s", hash)
return nil, nil, contextError(ErrNoFilter, str)
}
// NOTE: When more header commitments are added, this will need to load the
// inclusion proof for the filter from the database. However, since there
// is only currently a single commitment, there is only a single leaf in the
// commitment merkle tree, and hence the proof hashes will always be empty
// given there are no siblings. Adding an additional header commitment will
// require a consensus vote anyway and this can be updated at that time.
headerProof := &HeaderProof{
ProofIndex: HeaderCmtFilterIndex,
ProofHashes: nil,
}
return filter, headerProof, nil
}