Skip to content

Commit

Permalink
Merge pull request #1931 from Crypt-iQ/export_header_funcs
Browse files Browse the repository at this point in the history
blockchain: refactor and export header validation checks
  • Loading branch information
Roasbeef committed Jun 29, 2023
2 parents f9cbff0 + ee6c0e1 commit f5eeb10
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 76 deletions.
64 changes: 61 additions & 3 deletions blockchain/blockindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,60 @@ func (node *blockNode) Ancestor(height int32) *blockNode {
return n
}

// Height returns the blockNode's height in the chain.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Height() int32 {
return node.height
}

// Bits returns the blockNode's nBits.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Bits() uint32 {
return node.bits
}

// Timestamp returns the blockNode's timestamp.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Timestamp() int64 {
return node.timestamp
}

// Parent returns the blockNode's parent.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Parent() HeaderCtx {
if node.parent == nil {
// This is required since node.parent is a *blockNode and if we
// do not explicitly return nil here, the caller may fail when
// nil-checking this.
return nil
}

return node.parent
}

// RelativeAncestorCtx returns the blockNode's ancestor that is distance blocks
// before it in the chain. This is equivalent to the RelativeAncestor function
// below except that the return type is different.
//
// This function is safe for concurrent access.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) RelativeAncestorCtx(distance int32) HeaderCtx {
ancestor := node.RelativeAncestor(distance)
if ancestor == nil {
// This is required since RelativeAncestor returns a *blockNode
// and if we do not explicitly return nil here, the caller may
// fail when nil-checking this.
return nil
}

return ancestor
}

// RelativeAncestor returns the ancestor block node a relative 'distance' blocks
// before this node. This is equivalent to calling Ancestor with the node's
// height minus provided distance.
Expand All @@ -182,17 +236,17 @@ func (node *blockNode) RelativeAncestor(distance int32) *blockNode {
// prior to, and including, the block node.
//
// This function is safe for concurrent access.
func (node *blockNode) CalcPastMedianTime() time.Time {
func CalcPastMedianTime(node HeaderCtx) time.Time {
// Create a slice of the previous few block timestamps used to calculate
// the median per the number defined by the constant medianTimeBlocks.
timestamps := make([]int64, medianTimeBlocks)
numNodes := 0
iterNode := node
for i := 0; i < medianTimeBlocks && iterNode != nil; i++ {
timestamps[i] = iterNode.timestamp
timestamps[i] = iterNode.Timestamp()
numNodes++

iterNode = iterNode.parent
iterNode = iterNode.Parent()
}

// Prune the slice to the actual number of available timestamps which
Expand All @@ -217,6 +271,10 @@ func (node *blockNode) CalcPastMedianTime() time.Time {
return time.Unix(medianTimestamp, 0)
}

// A compile-time assertion to ensure blockNode implements the HeaderCtx
// interface.
var _ HeaderCtx = (*blockNode)(nil)

// blockIndex provides facilities for keeping track of an in-memory index of the
// block chain. Although the name block chain suggests a single chain of
// blocks, it is actually a tree-shaped structure where any node can have
Expand Down
7 changes: 4 additions & 3 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func (b *BlockChain) calcSequenceLock(node *blockNode, tx *btcutil.Tx, utxoView
prevInputHeight = 0
}
blockNode := node.Ancestor(prevInputHeight)
medianTime := blockNode.CalcPastMedianTime()
medianTime := CalcPastMedianTime(blockNode)

// Time based relative time-locks as defined by BIP 68
// have a time granularity of RelativeLockSeconds, so
Expand Down Expand Up @@ -595,7 +595,8 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
blockSize := uint64(block.MsgBlock().SerializeSize())
blockWeight := uint64(GetBlockWeight(block))
state := newBestState(node, blockSize, blockWeight, numTxns,
curTotalTxns+numTxns, node.CalcPastMedianTime())
curTotalTxns+numTxns, CalcPastMedianTime(node),
)

// Atomically insert info into the database.
err = b.db.Update(func(dbTx database.Tx) error {
Expand Down Expand Up @@ -708,7 +709,7 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block *btcutil.Block, view
blockWeight := uint64(GetBlockWeight(prevBlock))
newTotalTxns := curTotalTxns - uint64(len(block.MsgBlock().Transactions))
state := newBestState(prevNode, blockSize, blockWeight, numTxns,
newTotalTxns, prevNode.CalcPastMedianTime())
newTotalTxns, CalcPastMedianTime(prevNode))

err = b.db.Update(func(dbTx database.Tx) error {
// Update best block state.
Expand Down
4 changes: 2 additions & 2 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ func TestCalcSequenceLock(t *testing.T) {
// Obtain the median time past from the PoV of the input created above.
// The MTP for the input is the MTP from the PoV of the block *prior*
// to the one that included it.
medianTime := node.RelativeAncestor(5).CalcPastMedianTime().Unix()
medianTime := CalcPastMedianTime(node.RelativeAncestor(5)).Unix()

// The median time calculated from the PoV of the best block in the
// test chain. For unconfirmed inputs, this value will be used since
// the MTP will be calculated from the PoV of the yet-to-be-mined
// block.
nextMedianTime := node.CalcPastMedianTime().Unix()
nextMedianTime := CalcPastMedianTime(node).Unix()
nextBlockHeight := int32(numBlocksToActivate) + 1

// Add an additional transaction which will serve as our unconfirmed
Expand Down
2 changes: 1 addition & 1 deletion blockchain/chainio.go
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ func (b *BlockChain) initChainState() error {
blockWeight := uint64(GetBlockWeight(btcutil.NewBlock(&block)))
numTxns := uint64(len(block.Transactions))
b.stateSnapshot = newBestState(tip, blockSize, blockWeight,
numTxns, state.totalTxns, tip.CalcPastMedianTime())
numTxns, state.totalTxns, CalcPastMedianTime(tip))

return nil
})
Expand Down
73 changes: 36 additions & 37 deletions blockchain/difficulty.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,117 +193,116 @@ func (b *BlockChain) calcEasiestDifficulty(bits uint32, duration time.Duration)

// findPrevTestNetDifficulty returns the difficulty of the previous block which
// did not have the special testnet minimum difficulty rule applied.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) findPrevTestNetDifficulty(startNode *blockNode) uint32 {
func findPrevTestNetDifficulty(startNode HeaderCtx, c ChainCtx) uint32 {
// Search backwards through the chain for the last block without
// the special rule applied.
iterNode := startNode
for iterNode != nil && iterNode.height%b.blocksPerRetarget != 0 &&
iterNode.bits == b.chainParams.PowLimitBits {
for iterNode != nil && iterNode.Height()%c.BlocksPerRetarget() != 0 &&
iterNode.Bits() == c.ChainParams().PowLimitBits {

iterNode = iterNode.parent
iterNode = iterNode.Parent()
}

// Return the found difficulty or the minimum difficulty if no
// appropriate block was found.
lastBits := b.chainParams.PowLimitBits
lastBits := c.ChainParams().PowLimitBits
if iterNode != nil {
lastBits = iterNode.bits
lastBits = iterNode.Bits()
}
return lastBits
}

// calcNextRequiredDifficulty calculates the required difficulty for the block
// after the passed previous block node based on the difficulty retarget rules.
// after the passed previous HeaderCtx based on the difficulty retarget rules.
// This function differs from the exported CalcNextRequiredDifficulty in that
// the exported version uses the current best chain as the previous block node
// while this function accepts any block node.
func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
newBlockTime time.Time) (uint32, error) {
// the exported version uses the current best chain as the previous HeaderCtx
// while this function accepts any block node. This function accepts a ChainCtx
// parameter that gives the necessary difficulty context variables.
func calcNextRequiredDifficulty(lastNode HeaderCtx, newBlockTime time.Time,
c ChainCtx) (uint32, error) {

// Emulate the same behavior as Bitcoin Core that for regtest there is
// no difficulty retargeting.
if b.chainParams.PoWNoRetargeting {
return b.chainParams.PowLimitBits, nil
if c.ChainParams().PoWNoRetargeting {
return c.ChainParams().PowLimitBits, nil
}

// Genesis block.
if lastNode == nil {
return b.chainParams.PowLimitBits, nil
return c.ChainParams().PowLimitBits, nil
}

// Return the previous block's difficulty requirements if this block
// is not at a difficulty retarget interval.
if (lastNode.height+1)%b.blocksPerRetarget != 0 {
if (lastNode.Height()+1)%c.BlocksPerRetarget() != 0 {
// For networks that support it, allow special reduction of the
// required difficulty once too much time has elapsed without
// mining a block.
if b.chainParams.ReduceMinDifficulty {
if c.ChainParams().ReduceMinDifficulty {
// Return minimum difficulty when more than the desired
// amount of time has elapsed without mining a block.
reductionTime := int64(b.chainParams.MinDiffReductionTime /
reductionTime := int64(c.ChainParams().MinDiffReductionTime /
time.Second)
allowMinTime := lastNode.timestamp + reductionTime
allowMinTime := lastNode.Timestamp() + reductionTime
if newBlockTime.Unix() > allowMinTime {
return b.chainParams.PowLimitBits, nil
return c.ChainParams().PowLimitBits, nil
}

// The block was mined within the desired timeframe, so
// return the difficulty for the last block which did
// not have the special minimum difficulty rule applied.
return b.findPrevTestNetDifficulty(lastNode), nil
return findPrevTestNetDifficulty(lastNode, c), nil
}

// For the main network (or any unrecognized networks), simply
// return the previous block's difficulty requirements.
return lastNode.bits, nil
return lastNode.Bits(), nil
}

// Get the block node at the previous retarget (targetTimespan days
// worth of blocks).
firstNode := lastNode.RelativeAncestor(b.blocksPerRetarget - 1)
firstNode := lastNode.RelativeAncestorCtx(c.BlocksPerRetarget() - 1)
if firstNode == nil {
return 0, AssertError("unable to obtain previous retarget block")
}

// Limit the amount of adjustment that can occur to the previous
// difficulty.
actualTimespan := lastNode.timestamp - firstNode.timestamp
actualTimespan := lastNode.Timestamp() - firstNode.Timestamp()
adjustedTimespan := actualTimespan
if actualTimespan < b.minRetargetTimespan {
adjustedTimespan = b.minRetargetTimespan
} else if actualTimespan > b.maxRetargetTimespan {
adjustedTimespan = b.maxRetargetTimespan
if actualTimespan < c.MinRetargetTimespan() {
adjustedTimespan = c.MinRetargetTimespan()
} else if actualTimespan > c.MaxRetargetTimespan() {
adjustedTimespan = c.MaxRetargetTimespan()
}

// Calculate new target difficulty as:
// currentDifficulty * (adjustedTimespan / targetTimespan)
// The result uses integer division which means it will be slightly
// rounded down. Bitcoind also uses integer division to calculate this
// result.
oldTarget := CompactToBig(lastNode.bits)
oldTarget := CompactToBig(lastNode.Bits())
newTarget := new(big.Int).Mul(oldTarget, big.NewInt(adjustedTimespan))
targetTimeSpan := int64(b.chainParams.TargetTimespan / time.Second)
targetTimeSpan := int64(c.ChainParams().TargetTimespan / time.Second)
newTarget.Div(newTarget, big.NewInt(targetTimeSpan))

// Limit new value to the proof of work limit.
if newTarget.Cmp(b.chainParams.PowLimit) > 0 {
newTarget.Set(b.chainParams.PowLimit)
if newTarget.Cmp(c.ChainParams().PowLimit) > 0 {
newTarget.Set(c.ChainParams().PowLimit)
}

// Log new target difficulty and return it. The new target logging is
// intentionally converting the bits back to a number instead of using
// newTarget since conversion to the compact representation loses
// precision.
newTargetBits := BigToCompact(newTarget)
log.Debugf("Difficulty retarget at block height %d", lastNode.height+1)
log.Debugf("Old target %08x (%064x)", lastNode.bits, oldTarget)
log.Debugf("Difficulty retarget at block height %d", lastNode.Height()+1)
log.Debugf("Old target %08x (%064x)", lastNode.Bits(), oldTarget)
log.Debugf("New target %08x (%064x)", newTargetBits, CompactToBig(newTargetBits))
log.Debugf("Actual timespan %v, adjusted timespan %v, target timespan %v",
time.Duration(actualTimespan)*time.Second,
time.Duration(adjustedTimespan)*time.Second,
b.chainParams.TargetTimespan)
c.ChainParams().TargetTimespan)

return newTargetBits, nil
}
Expand All @@ -315,7 +314,7 @@ func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
// This function is safe for concurrent access.
func (b *BlockChain) CalcNextRequiredDifficulty(timestamp time.Time) (uint32, error) {
b.chainLock.Lock()
difficulty, err := b.calcNextRequiredDifficulty(b.bestChain.Tip(), timestamp)
difficulty, err := calcNextRequiredDifficulty(b.bestChain.Tip(), timestamp, b)
b.chainLock.Unlock()
return difficulty, err
}
55 changes: 55 additions & 0 deletions blockchain/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package blockchain

import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)

// ChainCtx is an interface that abstracts away blockchain parameters.
type ChainCtx interface {
// ChainParams returns the chain's configured chaincfg.Params.
ChainParams() *chaincfg.Params

// BlocksPerRetarget returns the number of blocks before retargeting
// occurs.
BlocksPerRetarget() int32

// MinRetargetTimespan returns the minimum amount of time to use in the
// difficulty calculation.
MinRetargetTimespan() int64

// MaxRetargetTimespan returns the maximum amount of time to use in the
// difficulty calculation.
MaxRetargetTimespan() int64

// VerifyCheckpoint returns whether the passed height and hash match
// the checkpoint data. Not all instances of VerifyCheckpoint will use
// this function for validation.
VerifyCheckpoint(height int32, hash *chainhash.Hash) bool

// FindPreviousCheckpoint returns the most recent checkpoint that we
// have validated. Not all instances of FindPreviousCheckpoint will use
// this function for validation.
FindPreviousCheckpoint() (HeaderCtx, error)
}

// HeaderCtx is an interface that describes information about a block. This is
// used so that external libraries can provide their own context (the header's
// parent, bits, etc.) when attempting to contextually validate a header.
type HeaderCtx interface {
// Height returns the header's height.
Height() int32

// Bits returns the header's bits.
Bits() uint32

// Timestamp returns the header's timestamp.
Timestamp() int64

// Parent returns the header's parent.
Parent() HeaderCtx

// RelativeAncestorCtx returns the header's ancestor that is distance
// blocks before it in the chain.
RelativeAncestorCtx(distance int32) HeaderCtx
}
2 changes: 1 addition & 1 deletion blockchain/thresholdstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, e

blockNode := newBlockNode(blockHeader, prevNode)

return blockNode.CalcPastMedianTime(), nil
return CalcPastMedianTime(blockNode), nil
}

// thresholdStateTransition given a state, a previous node, and a toeholds
Expand Down
Loading

0 comments on commit f5eeb10

Please sign in to comment.