Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pkg/maintainer/spv/bitcoin_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,43 @@ package spv
import (
"bytes"
"fmt"
"math/big"
"sync"

"github.com/btcsuite/btcd/blockchain"
"github.com/keep-network/keep-core/pkg/bitcoin"
)

// populateBlockHeaders adds headers for [fromHeight, toHeight] inclusive using
// difficultyAt(height) for each block's Bits-derived difficulty.
func populateBlockHeaders(
lbc *localBitcoinChain,
fromHeight, toHeight uint,
difficultyAt func(uint) *big.Int,
) error {
for h := fromHeight; h <= toHeight; h++ {
header := blockHeaderWithDifficulty(difficultyAt(h))
if err := lbc.addBlockHeader(h, header); err != nil {
return err
}
}
return nil
}

// blockHeaderWithDifficulty returns a header whose Difficulty() matches the
// given value (within Bitcoin compact encoding precision). Powers of two and
// small values round-trip exactly.
func blockHeaderWithDifficulty(difficulty *big.Int) *bitcoin.BlockHeader {
maxTarget := new(big.Int)
maxTarget.SetString(
"ffff0000000000000000000000000000000000000000000000000000",
16,
)
target := new(big.Int).Div(maxTarget, difficulty)
bits := blockchain.BigToCompact(target)
return &bitcoin.BlockHeader{Bits: bits}
}

type localBitcoinChain struct {
mutex sync.Mutex

Expand Down
186 changes: 74 additions & 112 deletions pkg/maintainer/spv/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ var logger = log.Logger("keep-maintainer-spv")
// The length of the Bitcoin difficulty epoch in blocks.
const difficultyEpochLength = 2016

// The maximum number of block headers allowed in a single SPV proof. Bounds
// the forward walk over headers when computing required confirmations
// (relevant on testnet4 where long runs of minimum-difficulty blocks occur).
const maxProofHeaders = 144

func Initialize(
ctx context.Context,
config Config,
Expand Down Expand Up @@ -339,135 +344,92 @@ func getProofInfo(
)
}

// Calculate the starting block of the proof and the difficulty epoch number
// it belongs to.
currentEpochDifficulty, previousEpochDifficulty, err :=
btcDiffChain.GetCurrentAndPrevEpochDifficulty()
if err != nil {
return false, 0, 0, fmt.Errorf(
"failed to get Bitcoin epoch difficulties: [%v]",
err,
)
}

// Calculate the starting block of the proof.
proofStartBlock := uint64(latestBlockHeight - accumulatedConfirmations + 1)
proofStartEpoch := proofStartBlock / difficultyEpochLength

// Calculate the ending block of the proof and the difficulty epoch number
// it belongs to.
proofEndBlock := proofStartBlock + txProofDifficultyFactor.Uint64() - 1
proofEndEpoch := proofEndBlock / difficultyEpochLength
// Walk the header chain forward, mirroring the Bridge's
// BitcoinTx.determineRequestedDifficulty and evaluateProofDifficulty
// behavior:
// - minimum-difficulty (DIFF1) headers are skipped while looking for the
// decisive header, but only when both relay epoch difficulties are
// above minimum (testnet4 BIP94 blocks in real epochs),
// - the first decisive header must match the relay's current or previous
// epoch difficulty; that value becomes the requested difficulty,
// - headers are accumulated until their total observed difficulty reaches
// requested difficulty times the transaction proof difficulty factor.
one := big.NewInt(1)
skipMinDifficulty := currentEpochDifficulty.Cmp(one) > 0 &&
previousEpochDifficulty.Cmp(one) > 0

var requestedDiff *big.Int
observedDiff := big.NewInt(0)
headerCount := uint(0)

// Get the current difficulty epoch number as seen by the relay. Subtract
// one to get the previous epoch number.
currentEpoch, err := btcDiffChain.CurrentEpoch()
if err != nil {
return false, 0, 0, fmt.Errorf("failed to get current epoch: [%v]", err)
}
previousEpoch := currentEpoch - 1

// There are only three possible valid combinations of the proof's block
// headers range: the proof must either be entirely in the previous epoch,
// must be entirely in the current epoch or must span the previous and
// current epochs.

// If the proof is entirely within the current epoch, required confirmations
// does not need to be adjusted.
if proofStartEpoch == currentEpoch &&
proofEndEpoch == currentEpoch {
return true, accumulatedConfirmations, uint(txProofDifficultyFactor.Uint64()), nil
}
for {
if headerCount >= maxProofHeaders {
// Could not find a decisive header or accumulate enough
// difficulty within a sane number of headers. Skip the
// transaction; it may become provable later.
return false, 0, 0, nil
}

// If the proof is entirely within the previous epoch, required confirmations
// does not need to be adjusted.
if proofStartEpoch == previousEpoch &&
proofEndEpoch == previousEpoch {
return true, accumulatedConfirmations, uint(txProofDifficultyFactor.Uint64()), nil
}
blockHeight := proofStartBlock + uint64(headerCount)
if blockHeight > uint64(latestBlockHeight) {
// Not enough mined blocks yet to assemble the proof. Report the
// number of headers needed so far plus one more; the caller will
// see accumulated < required and skip the transaction for now.
return true, accumulatedConfirmations, headerCount + 1, nil
}

// If the proof spans the previous and current difficulty epochs, the
// required confirmations may have to be adjusted. The reason for this is
// that there may be a drop in the value of difficulty between the current
// and the previous epochs. Example:
// Let's assume the transaction was done near the end of an epoch, so that
// part of the proof (let's say two block headers) is in the previous epoch
// and part of it is in the current epoch.
// If the previous epoch difficulty is 50 and the current epoch difficulty
// is 30, the total required difficulty of the proof will be transaction
// difficulty factor times previous difficulty: 6 * 50 = 300.
// However, if we simply use transaction difficulty factor to get the number
// of blocks we will end up with the difficulty sum that is too low:
// 50 + 50 + 30 + 30 + 30 + 30 = 220. To calculate the correct number of
// block headers needed we need to find how much difficulty needs to come
// from from the current epoch block headers: 300 - 2*50 = 200 and divide
// it by the current difficulty: 200 / 30 = 6 and add 1, because there
// was a remainder. So the number of block headers from the current epoch
// would be 7. The total number of block headers would be 9 and the sum
// of their difficulties would be: 50 + 50 + 30 + 30 + 30 + 30 + 30 + 30 +
// 30 = 310 which is enough to prove the transaction.
if proofStartEpoch == previousEpoch &&
proofEndEpoch == currentEpoch {
currentEpochDifficulty, previousEpochDifficulty, err :=
btcDiffChain.GetCurrentAndPrevEpochDifficulty()
header, err := btcChain.GetBlockHeader(uint(blockHeight))
if err != nil {
return false, 0, 0, fmt.Errorf(
"failed to get Bitcoin epoch difficulties: [%v]",
"failed to get block header at height [%v]: [%v]",
blockHeight,
err,
)
}

// Calculate the total difficulty that is required for the proof. The
// proof begins in the previous difficulty epoch, therefore the total
// required difficulty will be the previous epoch difficulty times
// transaction proof difficulty factor.
totalDifficultyRequired := new(big.Int).Mul(
previousEpochDifficulty,
txProofDifficultyFactor,
)
headerDiff := header.Difficulty()
headerCount++
observedDiff.Add(observedDiff, headerDiff)

// Calculate the number of block headers in the proof that will come
// from the previous difficulty epoch.
numberOfBlocksPreviousEpoch :=
uint64(difficultyEpochLength - proofStartBlock%difficultyEpochLength)

// Calculate how much difficulty the blocks from the previous epoch part
// of the proof have in total.
totalDifficultyPreviousEpoch := new(big.Int).Mul(
big.NewInt(int64(numberOfBlocksPreviousEpoch)),
previousEpochDifficulty,
)
if requestedDiff == nil {
// Still looking for the decisive header.
if skipMinDifficulty && headerDiff.Cmp(one) == 0 {
continue
}

// Calculate how much difficulty must come from the current epoch.
totalDifficultyCurrentEpoch := new(big.Int).Sub(
totalDifficultyRequired,
totalDifficultyPreviousEpoch,
)
if headerDiff.Cmp(currentEpochDifficulty) == 0 {
requestedDiff = currentEpochDifficulty
} else if headerDiff.Cmp(previousEpochDifficulty) == 0 {
requestedDiff = previousEpochDifficulty
} else {
// The Bridge would revert with "Not at current or previous
// difficulty". The transaction is either too fresh (its epoch
// is not yet proven in the relay) or too old. Skip it; it may
// be proven in the future.
return false, 0, 0, nil
}
}

// Calculate how many blocks from the current epoch we need.
remainder := new(big.Int)
numberOfBlocksCurrentEpoch, remainder := new(big.Int).DivMod(
totalDifficultyCurrentEpoch,
currentEpochDifficulty,
remainder,
totalDifficultyRequired := new(big.Int).Mul(
requestedDiff,
txProofDifficultyFactor,
)
// If there is a remainder, it means there is still some amount of
// difficulty missing that is less than one block difficulty. We need to
// account for that by adding one additional block.
if remainder.Cmp(big.NewInt(0)) > 0 {
numberOfBlocksCurrentEpoch.Add(
numberOfBlocksCurrentEpoch,
big.NewInt(1),
)
if observedDiff.Cmp(totalDifficultyRequired) >= 0 {
return true, accumulatedConfirmations, headerCount, nil
}

// The total required number of confirmations is the sum of blocks from
// the previous and current epochs.
requiredConfirmations := numberOfBlocksPreviousEpoch +
numberOfBlocksCurrentEpoch.Uint64()

return true, accumulatedConfirmations, uint(requiredConfirmations), nil
}

// If we entered here, it means that the proof's block headers range goes
// outside the previous or current difficulty epochs as seen by the relay.
// The reason for this is most likely that transaction entered the Bitcoin
// blockchain within the very new difficulty epoch that is not yet proven in
// the relay. In that case the transaction will be proven in the future.
// The other case could be that the transaction is older than the last two
// Bitcoin difficulty epochs. In that case the transaction will soon leave
// the sliding window of recent transactions.
return false, 0, 0, nil
}

// walletEvent is a type constraint representing wallet-related chain events.
Expand Down
Loading
Loading