Skip to content

Commit

Permalink
blockchain: intro HeaderCtx, ChainCtx + refactor CheckBlockHeaderContext
Browse files Browse the repository at this point in the history
This change will allow an external program to provide its own HeaderCtx
and ChainCtx and be able to perform contextual block header checks.
  • Loading branch information
Crypt-iQ committed Jun 29, 2023
1 parent f9cbff0 commit 5ecd38f
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 73 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 5ecd38f

Please sign in to comment.