From 597b68c79e40c3e6bb63b5d5a0622273222c52f2 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 13:41:52 +0900 Subject: [PATCH 01/12] blockchain, workmath: refactor functions to workmath package Some of the functions in difficulty.go are not dependent on any external functions and they are needed to introduce testing code for the invalidateblock and reconsiderblock methods that are to be added on in later commits. Having the workmath package let's us reuse the code and avoid dependency cycles. The existing functions that were exported already (HashToBig, CompactToBig, BigToCompact, CalcWork) are still kept in difficulty.go to avoid breaking external code that depends on those exported functions. --- blockchain/difficulty.go | 95 +---------- blockchain/internal/workmath/README.md | 15 ++ blockchain/internal/workmath/difficulty.go | 153 ++++++++++++++++++ .../workmath}/difficulty_test.go | 2 +- 4 files changed, 174 insertions(+), 91 deletions(-) create mode 100644 blockchain/internal/workmath/README.md create mode 100644 blockchain/internal/workmath/difficulty.go rename blockchain/{ => internal/workmath}/difficulty_test.go (98%) diff --git a/blockchain/difficulty.go b/blockchain/difficulty.go index 1fa850cc37..b1e39b9d62 100644 --- a/blockchain/difficulty.go +++ b/blockchain/difficulty.go @@ -8,31 +8,14 @@ import ( "math/big" "time" + "github.com/btcsuite/btcd/blockchain/internal/workmath" "github.com/btcsuite/btcd/chaincfg/chainhash" ) -var ( - // bigOne is 1 represented as a big.Int. It is defined here to avoid - // the overhead of creating it multiple times. - bigOne = big.NewInt(1) - - // oneLsh256 is 1 shifted left 256 bits. It is defined here to avoid - // the overhead of creating it multiple times. - oneLsh256 = new(big.Int).Lsh(bigOne, 256) -) - // HashToBig converts a chainhash.Hash into a big.Int that can be used to // perform math comparisons. func HashToBig(hash *chainhash.Hash) *big.Int { - // A Hash is in little-endian, but the big package wants the bytes in - // big-endian, so reverse them. - buf := *hash - blen := len(buf) - for i := 0; i < blen/2; i++ { - buf[i], buf[blen-1-i] = buf[blen-1-i], buf[i] - } - - return new(big.Int).SetBytes(buf[:]) + return workmath.HashToBig(hash) } // CompactToBig converts a compact representation of a whole number N to an @@ -60,31 +43,7 @@ func HashToBig(hash *chainhash.Hash) *big.Int { // which represent difficulty targets, thus there really is not a need for a // sign bit, but it is implemented here to stay consistent with bitcoind. func CompactToBig(compact uint32) *big.Int { - // Extract the mantissa, sign bit, and exponent. - mantissa := compact & 0x007fffff - isNegative := compact&0x00800000 != 0 - exponent := uint(compact >> 24) - - // Since the base for the exponent is 256, the exponent can be treated - // as the number of bytes to represent the full 256-bit number. So, - // treat the exponent as the number of bytes and shift the mantissa - // right or left accordingly. This is equivalent to: - // N = mantissa * 256^(exponent-3) - var bn *big.Int - if exponent <= 3 { - mantissa >>= 8 * (3 - exponent) - bn = big.NewInt(int64(mantissa)) - } else { - bn = big.NewInt(int64(mantissa)) - bn.Lsh(bn, 8*(exponent-3)) - } - - // Make it negative if the sign bit is set. - if isNegative { - bn = bn.Neg(bn) - } - - return bn + return workmath.CompactToBig(compact) } // BigToCompact converts a whole number N to a compact representation using @@ -92,41 +51,7 @@ func CompactToBig(compact uint32) *big.Int { // of precision, so values larger than (2^23 - 1) only encode the most // significant digits of the number. See CompactToBig for details. func BigToCompact(n *big.Int) uint32 { - // No need to do any work if it's zero. - if n.Sign() == 0 { - return 0 - } - - // Since the base for the exponent is 256, the exponent can be treated - // as the number of bytes. So, shift the number right or left - // accordingly. This is equivalent to: - // mantissa = mantissa / 256^(exponent-3) - var mantissa uint32 - exponent := uint(len(n.Bytes())) - if exponent <= 3 { - mantissa = uint32(n.Bits()[0]) - mantissa <<= 8 * (3 - exponent) - } else { - // Use a copy to avoid modifying the caller's original number. - tn := new(big.Int).Set(n) - mantissa = uint32(tn.Rsh(tn, 8*(exponent-3)).Bits()[0]) - } - - // When the mantissa already has the sign bit set, the number is too - // large to fit into the available 23-bits, so divide the number by 256 - // and increment the exponent accordingly. - if mantissa&0x00800000 != 0 { - mantissa >>= 8 - exponent++ - } - - // Pack the exponent, sign bit, and mantissa into an unsigned 32-bit - // int and return it. - compact := uint32(exponent<<24) | mantissa - if n.Sign() < 0 { - compact |= 0x00800000 - } - return compact + return workmath.BigToCompact(n) } // CalcWork calculates a work value from difficulty bits. Bitcoin increases @@ -140,17 +65,7 @@ func BigToCompact(n *big.Int) uint32 { // potential division by zero and really small floating point numbers, the // result adds 1 to the denominator and multiplies the numerator by 2^256. func CalcWork(bits uint32) *big.Int { - // Return a work value of zero if the passed difficulty bits represent - // a negative number. Note this should not happen in practice with valid - // blocks, but an invalid block could trigger it. - difficultyNum := CompactToBig(bits) - if difficultyNum.Sign() <= 0 { - return big.NewInt(0) - } - - // (1 << 256) / (difficultyNum + 1) - denominator := new(big.Int).Add(difficultyNum, bigOne) - return new(big.Int).Div(oneLsh256, denominator) + return workmath.CalcWork(bits) } // calcEasiestDifficulty calculates the easiest possible difficulty that a block diff --git a/blockchain/internal/workmath/README.md b/blockchain/internal/workmath/README.md new file mode 100644 index 0000000000..879b2dcfd7 --- /dev/null +++ b/blockchain/internal/workmath/README.md @@ -0,0 +1,15 @@ +workmath +========== + +[![Build Status](https://github.com/btcsuite/btcd/workflows/Build%20and%20Test/badge.svg)](https://github.com/btcsuite/btcd/actions) +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/btcsuite/btcd/workmath) + +Package workmath provides utility functions that are related with calculating +the work from difficulty bits. This package was introduced to avoid import +cycles in btcd. + +## License + +Package workmath is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/blockchain/internal/workmath/difficulty.go b/blockchain/internal/workmath/difficulty.go new file mode 100644 index 0000000000..8ff7adad1c --- /dev/null +++ b/blockchain/internal/workmath/difficulty.go @@ -0,0 +1,153 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package workmath + +import ( + "math/big" + + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +var ( + // bigOne is 1 represented as a big.Int. It is defined here to avoid + // the overhead of creating it multiple times. + bigOne = big.NewInt(1) + + // oneLsh256 is 1 shifted left 256 bits. It is defined here to avoid + // the overhead of creating it multiple times. + oneLsh256 = new(big.Int).Lsh(bigOne, 256) +) + +// HashToBig converts a chainhash.Hash into a big.Int that can be used to +// perform math comparisons. +func HashToBig(hash *chainhash.Hash) *big.Int { + // A Hash is in little-endian, but the big package wants the bytes in + // big-endian, so reverse them. + buf := *hash + blen := len(buf) + for i := 0; i < blen/2; i++ { + buf[i], buf[blen-1-i] = buf[blen-1-i], buf[i] + } + + return new(big.Int).SetBytes(buf[:]) +} + +// CompactToBig converts a compact representation of a whole number N to an +// unsigned 32-bit number. The representation is similar to IEEE754 floating +// point numbers. +// +// Like IEEE754 floating point, there are three basic components: the sign, +// the exponent, and the mantissa. They are broken out as follows: +// +// - the most significant 8 bits represent the unsigned base 256 exponent +// - bit 23 (the 24th bit) represents the sign bit +// - the least significant 23 bits represent the mantissa +// +// ------------------------------------------------- +// | Exponent | Sign | Mantissa | +// ------------------------------------------------- +// | 8 bits [31-24] | 1 bit [23] | 23 bits [22-00] | +// ------------------------------------------------- +// +// The formula to calculate N is: +// +// N = (-1^sign) * mantissa * 256^(exponent-3) +// +// This compact form is only used in bitcoin to encode unsigned 256-bit numbers +// which represent difficulty targets, thus there really is not a need for a +// sign bit, but it is implemented here to stay consistent with bitcoind. +func CompactToBig(compact uint32) *big.Int { + // Extract the mantissa, sign bit, and exponent. + mantissa := compact & 0x007fffff + isNegative := compact&0x00800000 != 0 + exponent := uint(compact >> 24) + + // Since the base for the exponent is 256, the exponent can be treated + // as the number of bytes to represent the full 256-bit number. So, + // treat the exponent as the number of bytes and shift the mantissa + // right or left accordingly. This is equivalent to: + // N = mantissa * 256^(exponent-3) + var bn *big.Int + if exponent <= 3 { + mantissa >>= 8 * (3 - exponent) + bn = big.NewInt(int64(mantissa)) + } else { + bn = big.NewInt(int64(mantissa)) + bn.Lsh(bn, 8*(exponent-3)) + } + + // Make it negative if the sign bit is set. + if isNegative { + bn = bn.Neg(bn) + } + + return bn +} + +// BigToCompact converts a whole number N to a compact representation using +// an unsigned 32-bit number. The compact representation only provides 23 bits +// of precision, so values larger than (2^23 - 1) only encode the most +// significant digits of the number. See CompactToBig for details. +func BigToCompact(n *big.Int) uint32 { + // No need to do any work if it's zero. + if n.Sign() == 0 { + return 0 + } + + // Since the base for the exponent is 256, the exponent can be treated + // as the number of bytes. So, shift the number right or left + // accordingly. This is equivalent to: + // mantissa = mantissa / 256^(exponent-3) + var mantissa uint32 + exponent := uint(len(n.Bytes())) + if exponent <= 3 { + mantissa = uint32(n.Bits()[0]) + mantissa <<= 8 * (3 - exponent) + } else { + // Use a copy to avoid modifying the caller's original number. + tn := new(big.Int).Set(n) + mantissa = uint32(tn.Rsh(tn, 8*(exponent-3)).Bits()[0]) + } + + // When the mantissa already has the sign bit set, the number is too + // large to fit into the available 23-bits, so divide the number by 256 + // and increment the exponent accordingly. + if mantissa&0x00800000 != 0 { + mantissa >>= 8 + exponent++ + } + + // Pack the exponent, sign bit, and mantissa into an unsigned 32-bit + // int and return it. + compact := uint32(exponent<<24) | mantissa + if n.Sign() < 0 { + compact |= 0x00800000 + } + return compact +} + +// CalcWork calculates a work value from difficulty bits. Bitcoin increases +// the difficulty for generating a block by decreasing the value which the +// generated hash must be less than. This difficulty target is stored in each +// block header using a compact representation as described in the documentation +// for CompactToBig. The main chain is selected by choosing the chain that has +// the most proof of work (highest difficulty). Since a lower target difficulty +// value equates to higher actual difficulty, the work value which will be +// accumulated must be the inverse of the difficulty. Also, in order to avoid +// potential division by zero and really small floating point numbers, the +// result adds 1 to the denominator and multiplies the numerator by 2^256. +func CalcWork(bits uint32) *big.Int { + // Return a work value of zero if the passed difficulty bits represent + // a negative number. Note this should not happen in practice with valid + // blocks, but an invalid block could trigger it. + difficultyNum := CompactToBig(bits) + if difficultyNum.Sign() <= 0 { + return big.NewInt(0) + } + + // (1 << 256) / (difficultyNum + 1) + denominator := new(big.Int).Add(difficultyNum, bigOne) + return new(big.Int).Div(oneLsh256, denominator) +} diff --git a/blockchain/difficulty_test.go b/blockchain/internal/workmath/difficulty_test.go similarity index 98% rename from blockchain/difficulty_test.go rename to blockchain/internal/workmath/difficulty_test.go index c4d8fb6ef5..bed4d1f13f 100644 --- a/blockchain/difficulty_test.go +++ b/blockchain/internal/workmath/difficulty_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package blockchain +package workmath import ( "math/big" From 337d7f6be8cddca2c75589e5d9c5edcc5cce42fb Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 13:54:53 +0900 Subject: [PATCH 02/12] fullblocktests, testhelper: refactor out spendableOut spendableOut and the functions related to it are is moved to package testhelper and are exported. This is done to make the code reusable without introducing an import cycle when the testing code for invalidateblock and reconsiderblock are added in follow up commits. --- blockchain/fullblocktests/generate.go | 68 ++++++++---------------- blockchain/internal/testhelper/README.md | 16 ++++++ blockchain/internal/testhelper/common.go | 31 +++++++++++ 3 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 blockchain/internal/testhelper/README.md create mode 100644 blockchain/internal/testhelper/common.go diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index fe36bfe136..5f35d3e72b 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -19,6 +19,7 @@ import ( "time" "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/blockchain/internal/testhelper" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -165,31 +166,6 @@ type BlockDisconnectExpectUTXO struct { // This implements the TestInstance interface. func (b BlockDisconnectExpectUTXO) FullBlockTestInstance() {} -// spendableOut represents a transaction output that is spendable along with -// additional metadata such as the block its in and how much it pays. -type spendableOut struct { - prevOut wire.OutPoint - amount btcutil.Amount -} - -// makeSpendableOutForTx returns a spendable output for the given transaction -// and transaction output index within the transaction. -func makeSpendableOutForTx(tx *wire.MsgTx, txOutIndex uint32) spendableOut { - return spendableOut{ - prevOut: wire.OutPoint{ - Hash: tx.TxHash(), - Index: txOutIndex, - }, - amount: btcutil.Amount(tx.TxOut[txOutIndex].Value), - } -} - -// makeSpendableOut returns a spendable output for the given block, transaction -// index within the block, and transaction output index within the transaction. -func makeSpendableOut(block *wire.MsgBlock, txIndex, txOutIndex uint32) spendableOut { - return makeSpendableOutForTx(block.Transactions[txIndex], txOutIndex) -} - // testGenerator houses state used to easy the process of generating test blocks // that build from one another along with housing other useful things such as // available spendable outputs used throughout the tests. @@ -203,7 +179,7 @@ type testGenerator struct { blockHeights map[string]int32 // Used for tracking spendable coinbase outputs. - spendableOuts []spendableOut + spendableOuts []testhelper.SpendableOut prevCollectedHash chainhash.Hash // Common key for any tests which require signed transactions. @@ -449,14 +425,14 @@ func additionalTx(tx *wire.MsgTx) func(*wire.MsgBlock) { // transaction ends up with a unique hash. The script is a simple OP_TRUE // script which avoids the need to track addresses and signature scripts in the // tests. -func createSpendTx(spend *spendableOut, fee btcutil.Amount) *wire.MsgTx { +func createSpendTx(spend *testhelper.SpendableOut, fee btcutil.Amount) *wire.MsgTx { spendTx := wire.NewMsgTx(1) spendTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: spend.prevOut, + PreviousOutPoint: spend.PrevOut, Sequence: wire.MaxTxInSequenceNum, SignatureScript: nil, }) - spendTx.AddTxOut(wire.NewTxOut(int64(spend.amount-fee), + spendTx.AddTxOut(wire.NewTxOut(int64(spend.Amount-fee), opTrueScript)) spendTx.AddTxOut(wire.NewTxOut(0, uniqueOpReturnScript())) @@ -469,7 +445,7 @@ func createSpendTx(spend *spendableOut, fee btcutil.Amount) *wire.MsgTx { // is a simple OP_TRUE script which avoids the need to track addresses and // signature scripts in the tests. The signature script is nil. func createSpendTxForTx(tx *wire.MsgTx, fee btcutil.Amount) *wire.MsgTx { - spend := makeSpendableOutForTx(tx, 0) + spend := testhelper.MakeSpendableOutForTx(tx, 0) return createSpendTx(&spend, fee) } @@ -492,7 +468,7 @@ func createSpendTxForTx(tx *wire.MsgTx, fee btcutil.Amount) *wire.MsgTx { // applied after all munge functions have been invoked: // - The merkle root will be recalculated unless it was manually changed // - The block will be solved unless the nonce was changed -func (g *testGenerator) nextBlock(blockName string, spend *spendableOut, mungers ...func(*wire.MsgBlock)) *wire.MsgBlock { +func (g *testGenerator) nextBlock(blockName string, spend *testhelper.SpendableOut, mungers ...func(*wire.MsgBlock)) *wire.MsgBlock { // Create coinbase transaction for the block using any additional // subsidy if specified. nextHeight := g.tipHeight + 1 @@ -594,7 +570,7 @@ func (g *testGenerator) setTip(blockName string) { // oldestCoinbaseOuts removes the oldest coinbase output that was previously // saved to the generator and returns the set as a slice. -func (g *testGenerator) oldestCoinbaseOut() spendableOut { +func (g *testGenerator) oldestCoinbaseOut() testhelper.SpendableOut { op := g.spendableOuts[0] g.spendableOuts = g.spendableOuts[1:] return op @@ -603,7 +579,7 @@ func (g *testGenerator) oldestCoinbaseOut() spendableOut { // saveTipCoinbaseOut adds the coinbase tx output in the current tip block to // the list of spendable outputs. func (g *testGenerator) saveTipCoinbaseOut() { - g.spendableOuts = append(g.spendableOuts, makeSpendableOut(g.tip, 0, 0)) + g.spendableOuts = append(g.spendableOuts, testhelper.MakeSpendableOut(g.tip, 0, 0)) g.prevCollectedHash = g.tip.BlockHash() } @@ -947,7 +923,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { tests = append(tests, testInstances) // Collect spendable outputs. This simplifies the code below. - var outs []*spendableOut + var outs []*testhelper.SpendableOut for i := uint16(0); i < coinbaseMaturity; i++ { op := g.oldestCoinbaseOut() outs = append(outs, &op) @@ -985,7 +961,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // \-> b3(1) g.setTip("b1") g.nextBlock("b3", outs[1]) - b3Tx1Out := makeSpendableOut(g.tip, 1, 0) + b3Tx1Out := testhelper.MakeSpendableOut(g.tip, 1, 0) acceptedToSideChainWithExpectedTip("b2") // Extend b3 fork to make the alternative chain longer and force reorg. @@ -1300,7 +1276,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { g.setTip("b35") doubleSpendTx := createSpendTx(outs[11], lowFee) g.nextBlock("b37", outs[11], additionalTx(doubleSpendTx)) - b37Tx1Out := makeSpendableOut(g.tip, 1, 0) + b37Tx1Out := testhelper.MakeSpendableOut(g.tip, 1, 0) rejected(blockchain.ErrMissingTxOut) g.setTip("b35") @@ -1356,7 +1332,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { for i := 0; i < txnsNeeded; i++ { // Create a signed transaction that spends from the // associated p2sh output in b39. - spend := makeSpendableOutForTx(b39.Transactions[i+2], 2) + spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) tx := createSpendTx(&spend, lowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) @@ -1387,7 +1363,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { g.nextBlock("b41", outs[12], func(b *wire.MsgBlock) { txnsNeeded := (maxBlockSigOps / redeemScriptSigOps) for i := 0; i < txnsNeeded; i++ { - spend := makeSpendableOutForTx(b39.Transactions[i+2], 2) + spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) tx := createSpendTx(&spend, lowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) @@ -1719,7 +1695,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // \-> b59(17) g.setTip("b57") g.nextBlock("b59", outs[17], func(b *wire.MsgBlock) { - b.Transactions[1].TxOut[0].Value = int64(outs[17].amount) + 1 + b.Transactions[1].TxOut[0].Value = int64(outs[17].Amount) + 1 }) rejected(blockchain.ErrSpendTooHigh) @@ -2003,14 +1979,14 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // NOTE: The createSpendTx func adds the OP_RETURN output. zeroFee := btcutil.Amount(0) for i := uint32(0); i < numAdditionalOutputs; i++ { - spend := makeSpendableOut(b, 1, i+2) + spend := testhelper.MakeSpendableOut(b, 1, i+2) tx := createSpendTx(&spend, zeroFee) b.AddTransaction(tx) } }) g.assertTipBlockNumTxns(6) g.assertTipBlockTxOutOpReturn(5, 1) - b75OpReturnOut := makeSpendableOut(g.tip, 5, 1) + b75OpReturnOut := testhelper.MakeSpendableOut(g.tip, 5, 1) accepted() // Reorg to a side chain that does not contain the OP_RETURNs. @@ -2043,7 +2019,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // An OP_RETURN output doesn't have any value and the default behavior // of nextBlock is to assign a fee of one, so increment the amount here // to effective negate that behavior. - b75OpReturnOut.amount++ + b75OpReturnOut.Amount++ g.nextBlock("b80", &b75OpReturnOut) rejected(blockchain.ErrMissingTxOut) @@ -2075,7 +2051,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { g.nextBlock("b82a", outs[28]) accepted() - b82aTx1Out0 := makeSpendableOut(g.tip, 1, 0) + b82aTx1Out0 := testhelper.MakeSpendableOut(g.tip, 1, 0) g.nextBlock("b83a", &b82aTx1Out0) accepted() @@ -2097,17 +2073,17 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // We expect b82a output to now be a utxo since b83a was spending it and it was // removed from the main chain. blockDisconnectExpectUTXO("b82aTx1Out0", - true, b82aTx1Out0.prevOut, g.blocksByName["b83a"].BlockHash()) + true, b82aTx1Out0.PrevOut, g.blocksByName["b83a"].BlockHash()) // We expect the output from b82 to not exist once b82a itself has been removed // from the main chain. blockDisconnectExpectUTXO("b82aTx1Out0", - false, b82aTx1Out0.prevOut, g.blocksByName["b82a"].BlockHash()) + false, b82aTx1Out0.PrevOut, g.blocksByName["b82a"].BlockHash()) // The output that was being spent in b82a should exist after the removal of // b82a. blockDisconnectExpectUTXO("outs[28]", - true, outs[28].prevOut, g.blocksByName["b82a"].BlockHash()) + true, outs[28].PrevOut, g.blocksByName["b82a"].BlockHash()) // Create block 84 and reorg out the sidechain with b83a as the tip. // diff --git a/blockchain/internal/testhelper/README.md b/blockchain/internal/testhelper/README.md new file mode 100644 index 0000000000..40b339bf79 --- /dev/null +++ b/blockchain/internal/testhelper/README.md @@ -0,0 +1,16 @@ +testhelper +========== + +[![Build Status](https://github.com/btcsuite/btcd/workflows/Build%20and%20Test/badge.svg)](https://github.com/btcsuite/btcd/actions) +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/btcsuite/btcd/blockchain/testhelper) + +Package testhelper provides functions that are used internally in the +btcd/blockchain and btcd/blockchain/fullblocktests package to test consensus +validation rules. Mainly provided to avoid dependency cycles internally among +the different packages in btcd. + +## License + +Package testhelper is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go new file mode 100644 index 0000000000..e7f5a2622e --- /dev/null +++ b/blockchain/internal/testhelper/common.go @@ -0,0 +1,31 @@ +package testhelper + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" +) + +// SpendableOut represents a transaction output that is spendable along with +// additional metadata such as the block its in and how much it pays. +type SpendableOut struct { + PrevOut wire.OutPoint + Amount btcutil.Amount +} + +// MakeSpendableOutForTx returns a spendable output for the given transaction +// and transaction output index within the transaction. +func MakeSpendableOutForTx(tx *wire.MsgTx, txOutIndex uint32) SpendableOut { + return SpendableOut{ + PrevOut: wire.OutPoint{ + Hash: tx.TxHash(), + Index: txOutIndex, + }, + Amount: btcutil.Amount(tx.TxOut[txOutIndex].Value), + } +} + +// MakeSpendableOut returns a spendable output for the given block, transaction +// index within the block, and transaction output index within the transaction. +func MakeSpendableOut(block *wire.MsgBlock, txIndex, txOutIndex uint32) SpendableOut { + return MakeSpendableOutForTx(block.Transactions[txIndex], txOutIndex) +} From d4644dff10faf9fea7de02f219c0490723985394 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 14:01:05 +0900 Subject: [PATCH 03/12] fullblocktests, testhelper: move solveBlock to testhelper solveBlock is moved to testhelper and is exported. This is done so that the code can be reused without introducing import cycles. The testing code to be added in alter commits for invalidateblock and reconsider block will use SolveBlock. --- blockchain/fullblocktests/generate.go | 68 +---------------------- blockchain/internal/testhelper/common.go | 69 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 67 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 5f35d3e72b..00beedaaa2 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -14,7 +14,6 @@ import ( "encoding/binary" "errors" "fmt" - "math" "runtime" "time" @@ -303,71 +302,6 @@ func calcMerkleRoot(txns []*wire.MsgTx) chainhash.Hash { return blockchain.CalcMerkleRoot(utilTxns, false) } -// solveBlock attempts to find a nonce which makes the passed block header hash -// to a value less than the target difficulty. When a successful solution is -// found true is returned and the nonce field of the passed header is updated -// with the solution. False is returned if no solution exists. -// -// NOTE: This function will never solve blocks with a nonce of 0. This is done -// so the 'nextBlock' function can properly detect when a nonce was modified by -// a munge function. -func solveBlock(header *wire.BlockHeader) bool { - // sbResult is used by the solver goroutines to send results. - type sbResult struct { - found bool - nonce uint32 - } - - // solver accepts a block header and a nonce range to test. It is - // intended to be run as a goroutine. - targetDifficulty := blockchain.CompactToBig(header.Bits) - quit := make(chan bool) - results := make(chan sbResult) - solver := func(hdr wire.BlockHeader, startNonce, stopNonce uint32) { - // We need to modify the nonce field of the header, so make sure - // we work with a copy of the original header. - for i := startNonce; i >= startNonce && i <= stopNonce; i++ { - select { - case <-quit: - return - default: - hdr.Nonce = i - hash := hdr.BlockHash() - if blockchain.HashToBig(&hash).Cmp( - targetDifficulty) <= 0 { - - results <- sbResult{true, i} - return - } - } - } - results <- sbResult{false, 0} - } - - startNonce := uint32(1) - stopNonce := uint32(math.MaxUint32) - numCores := uint32(runtime.NumCPU()) - noncesPerCore := (stopNonce - startNonce) / numCores - for i := uint32(0); i < numCores; i++ { - rangeStart := startNonce + (noncesPerCore * i) - rangeStop := startNonce + (noncesPerCore * (i + 1)) - 1 - if i == numCores-1 { - rangeStop = stopNonce - } - go solver(*header, rangeStart, rangeStop) - } - for i := uint32(0); i < numCores; i++ { - result := <-results - if result.found { - close(quit) - header.Nonce = result.nonce - return true - } - } - - return false -} - // additionalCoinbase returns a function that itself takes a block and // modifies it by adding the provided amount to coinbase subsidy. func additionalCoinbase(amount btcutil.Amount) func(*wire.MsgBlock) { @@ -523,7 +457,7 @@ func (g *testGenerator) nextBlock(blockName string, spend *testhelper.SpendableO // Only solve the block if the nonce wasn't manually changed by a munge // function. - if block.Header.Nonce == curNonce && !solveBlock(&block.Header) { + if block.Header.Nonce == curNonce && !testhelper.SolveBlock(&block.Header) { panic(fmt.Sprintf("Unable to solve block at height %d", nextHeight)) } diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index e7f5a2622e..518a679218 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -1,6 +1,10 @@ package testhelper import ( + "math" + "runtime" + + "github.com/btcsuite/btcd/blockchain/internal/workmath" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" ) @@ -29,3 +33,68 @@ func MakeSpendableOutForTx(tx *wire.MsgTx, txOutIndex uint32) SpendableOut { func MakeSpendableOut(block *wire.MsgBlock, txIndex, txOutIndex uint32) SpendableOut { return MakeSpendableOutForTx(block.Transactions[txIndex], txOutIndex) } + +// SolveBlock attempts to find a nonce which makes the passed block header hash +// to a value less than the target difficulty. When a successful solution is +// found true is returned and the nonce field of the passed header is updated +// with the solution. False is returned if no solution exists. +// +// NOTE: This function will never solve blocks with a nonce of 0. This is done +// so the 'nextBlock' function can properly detect when a nonce was modified by +// a munge function. +func SolveBlock(header *wire.BlockHeader) bool { + // sbResult is used by the solver goroutines to send results. + type sbResult struct { + found bool + nonce uint32 + } + + // solver accepts a block header and a nonce range to test. It is + // intended to be run as a goroutine. + targetDifficulty := workmath.CompactToBig(header.Bits) + quit := make(chan bool) + results := make(chan sbResult) + solver := func(hdr wire.BlockHeader, startNonce, stopNonce uint32) { + // We need to modify the nonce field of the header, so make sure + // we work with a copy of the original header. + for i := startNonce; i >= startNonce && i <= stopNonce; i++ { + select { + case <-quit: + return + default: + hdr.Nonce = i + hash := hdr.BlockHash() + if workmath.HashToBig(&hash).Cmp( + targetDifficulty) <= 0 { + + results <- sbResult{true, i} + return + } + } + } + results <- sbResult{false, 0} + } + + startNonce := uint32(1) + stopNonce := uint32(math.MaxUint32) + numCores := uint32(runtime.NumCPU()) + noncesPerCore := (stopNonce - startNonce) / numCores + for i := uint32(0); i < numCores; i++ { + rangeStart := startNonce + (noncesPerCore * i) + rangeStop := startNonce + (noncesPerCore * (i + 1)) - 1 + if i == numCores-1 { + rangeStop = stopNonce + } + go solver(*header, rangeStart, rangeStop) + } + for i := uint32(0); i < numCores; i++ { + result := <-results + if result.found { + close(quit) + header.Nonce = result.nonce + return true + } + } + + return false +} From 62790ac065fc4da5e09da89fce9f9e93ae78f0eb Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 14:06:13 +0900 Subject: [PATCH 04/12] fullblocktests, testhelper: move opTrueScript and lowFee to testhelper The variables are moved to testhelper so that they can be reused in the blockchain package without introducing an import cycle. The testing code for invalidateblock and reconsiderblock that will be added in later commits will be using these variables. --- blockchain/fullblocktests/generate.go | 46 ++++++++++-------------- blockchain/internal/testhelper/common.go | 11 ++++++ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 00beedaaa2..2416bd1b31 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -43,16 +43,6 @@ const ( numLargeReorgBlocks = 1088 ) -var ( - // opTrueScript is simply a public key script that contains the OP_TRUE - // opcode. It is defined here to reduce garbage creation. - opTrueScript = []byte{txscript.OP_TRUE} - - // lowFee is a single satoshi and exists to make the test code more - // readable. - lowFee = btcutil.Amount(1) -) - // TestInstance is an interface that describes a specific test instance returned // by the tests generated in this package. It should be type asserted to one // of the concrete test instance types in order to test accordingly. @@ -283,7 +273,7 @@ func (g *testGenerator) createCoinbaseTx(blockHeight int32) *wire.MsgTx { }) tx.AddTxOut(&wire.TxOut{ Value: blockchain.CalcBlockSubsidy(blockHeight, g.params), - PkScript: opTrueScript, + PkScript: testhelper.OpTrueScript, }) return tx } @@ -367,7 +357,7 @@ func createSpendTx(spend *testhelper.SpendableOut, fee btcutil.Amount) *wire.Msg SignatureScript: nil, }) spendTx.AddTxOut(wire.NewTxOut(int64(spend.Amount-fee), - opTrueScript)) + testhelper.OpTrueScript)) spendTx.AddTxOut(wire.NewTxOut(0, uniqueOpReturnScript())) return spendTx @@ -1208,7 +1198,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // \-> b38(b37.tx[1]) // g.setTip("b35") - doubleSpendTx := createSpendTx(outs[11], lowFee) + doubleSpendTx := createSpendTx(outs[11], testhelper.LowFee) g.nextBlock("b37", outs[11], additionalTx(doubleSpendTx)) b37Tx1Out := testhelper.MakeSpendableOut(g.tip, 1, 0) rejected(blockchain.ErrMissingTxOut) @@ -1246,7 +1236,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { txnsNeeded := (maxBlockSigOps / redeemScriptSigOps) + 1 prevTx := b.Transactions[1] for i := 0; i < txnsNeeded; i++ { - prevTx = createSpendTxForTx(prevTx, lowFee) + prevTx = createSpendTxForTx(prevTx, testhelper.LowFee) prevTx.TxOut[0].Value -= 2 prevTx.AddTxOut(wire.NewTxOut(2, p2shScript)) b.AddTransaction(prevTx) @@ -1267,7 +1257,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // Create a signed transaction that spends from the // associated p2sh output in b39. spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) - tx := createSpendTx(&spend, lowFee) + tx := createSpendTx(&spend, testhelper.LowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) if err != nil { @@ -1283,7 +1273,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // the block one over the max allowed. fill := maxBlockSigOps - (txnsNeeded * redeemScriptSigOps) + 1 finalTx := b.Transactions[len(b.Transactions)-1] - tx := createSpendTxForTx(finalTx, lowFee) + tx := createSpendTxForTx(finalTx, testhelper.LowFee) tx.TxOut[0].PkScript = repeatOpcode(txscript.OP_CHECKSIG, fill) b.AddTransaction(tx) }) @@ -1298,7 +1288,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { txnsNeeded := (maxBlockSigOps / redeemScriptSigOps) for i := 0; i < txnsNeeded; i++ { spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) - tx := createSpendTx(&spend, lowFee) + tx := createSpendTx(&spend, testhelper.LowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) if err != nil { @@ -1317,7 +1307,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { return } finalTx := b.Transactions[len(b.Transactions)-1] - tx := createSpendTxForTx(finalTx, lowFee) + tx := createSpendTxForTx(finalTx, testhelper.LowFee) tx.TxOut[0].PkScript = repeatOpcode(txscript.OP_CHECKSIG, fill) b.AddTransaction(tx) }) @@ -1347,7 +1337,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // ... -> b43(13) // \-> b44(14) g.nextBlock("b44", nil, func(b *wire.MsgBlock) { - nonCoinbaseTx := createSpendTx(outs[14], lowFee) + nonCoinbaseTx := createSpendTx(outs[14], testhelper.LowFee) b.Transactions[0] = nonCoinbaseTx }) rejected(blockchain.ErrFirstTxNotCoinbase) @@ -1552,7 +1542,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { g.setTip("b55") b57 := g.nextBlock("b57", outs[16], func(b *wire.MsgBlock) { tx2 := b.Transactions[1] - tx3 := createSpendTxForTx(tx2, lowFee) + tx3 := createSpendTxForTx(tx2, testhelper.LowFee) b.AddTransaction(tx3) }) g.assertTipBlockNumTxns(3) @@ -1597,7 +1587,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // in the block. spendTx := b.Transactions[1] for i := 0; i < 4; i++ { - spendTx = createSpendTxForTx(spendTx, lowFee) + spendTx = createSpendTxForTx(spendTx, testhelper.LowFee) b.AddTransaction(spendTx) } @@ -1734,7 +1724,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // ... b64(18) -> b65(19) g.setTip("b64") g.nextBlock("b65", outs[19], func(b *wire.MsgBlock) { - tx3 := createSpendTxForTx(b.Transactions[1], lowFee) + tx3 := createSpendTxForTx(b.Transactions[1], testhelper.LowFee) b.AddTransaction(tx3) }) accepted() @@ -1744,8 +1734,8 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // ... -> b65(19) // \-> b66(20) g.nextBlock("b66", nil, func(b *wire.MsgBlock) { - tx2 := createSpendTx(outs[20], lowFee) - tx3 := createSpendTxForTx(tx2, lowFee) + tx2 := createSpendTx(outs[20], testhelper.LowFee) + tx3 := createSpendTxForTx(tx2, testhelper.LowFee) b.AddTransaction(tx3) b.AddTransaction(tx2) }) @@ -1759,8 +1749,8 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { g.setTip("b65") g.nextBlock("b67", outs[20], func(b *wire.MsgBlock) { tx2 := b.Transactions[1] - tx3 := createSpendTxForTx(tx2, lowFee) - tx4 := createSpendTxForTx(tx2, lowFee) + tx3 := createSpendTxForTx(tx2, testhelper.LowFee) + tx4 := createSpendTxForTx(tx2, testhelper.LowFee) b.AddTransaction(tx3) b.AddTransaction(tx4) }) @@ -1883,7 +1873,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { txscript.OP_ELSE, txscript.OP_TRUE, txscript.OP_ENDIF} g.nextBlock("b74", outs[23], replaceSpendScript(script), func(b *wire.MsgBlock) { tx2 := b.Transactions[1] - tx3 := createSpendTxForTx(tx2, lowFee) + tx3 := createSpendTxForTx(tx2, testhelper.LowFee) tx3.TxIn[0].SignatureScript = []byte{txscript.OP_FALSE} b.AddTransaction(tx3) }) @@ -1904,7 +1894,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { const zeroCoin = int64(0) spendTx := b.Transactions[1] for i := 0; i < numAdditionalOutputs; i++ { - spendTx.AddTxOut(wire.NewTxOut(zeroCoin, opTrueScript)) + spendTx.AddTxOut(wire.NewTxOut(zeroCoin, testhelper.OpTrueScript)) } // Add transactions spending from the outputs added above that diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index 518a679218..15fc5fd75c 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -6,9 +6,20 @@ import ( "github.com/btcsuite/btcd/blockchain/internal/workmath" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) +var ( + // OpTrueScript is simply a public key script that contains the OP_TRUE + // opcode. It is defined here to reduce garbage creation. + OpTrueScript = []byte{txscript.OP_TRUE} + + // LowFee is a single satoshi and exists to make the test code more + // readable. + LowFee = btcutil.Amount(1) +) + // SpendableOut represents a transaction output that is spendable along with // additional metadata such as the block its in and how much it pays. type SpendableOut struct { From 9093243d8bcc40c46ad612e1921e7556cecd636f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 14:13:07 +0900 Subject: [PATCH 05/12] fullblocktests, testhelper: move uniqueOpReturnScript to testhelper uniqueOpReturnScript is moved to testhelper and is exported so that the code and be reused in package blockchain without introducing import cycles. The test code for invalidateblock and reconsiderblock that are gonna be added in later commits uses the functions. --- blockchain/fullblocktests/generate.go | 35 ++++++------------------ blockchain/internal/testhelper/common.go | 25 +++++++++++++++++ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 2416bd1b31..242de64734 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -228,30 +228,6 @@ func standardCoinbaseScript(blockHeight int32, extraNonce uint64) ([]byte, error AddInt64(int64(extraNonce)).Script() } -// opReturnScript returns a provably-pruneable OP_RETURN script with the -// provided data. -func opReturnScript(data []byte) []byte { - builder := txscript.NewScriptBuilder() - script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script() - if err != nil { - panic(err) - } - return script -} - -// uniqueOpReturnScript returns a standard provably-pruneable OP_RETURN script -// with a random uint64 encoded as the data. -func uniqueOpReturnScript() []byte { - rand, err := wire.RandomUint64() - if err != nil { - panic(err) - } - - data := make([]byte, 8) - binary.LittleEndian.PutUint64(data[0:8], rand) - return opReturnScript(data) -} - // createCoinbaseTx returns a coinbase transaction paying an appropriate // subsidy based on the passed block height. The coinbase signature script // conforms to the requirements of version 2 blocks. @@ -358,7 +334,11 @@ func createSpendTx(spend *testhelper.SpendableOut, fee btcutil.Amount) *wire.Msg }) spendTx.AddTxOut(wire.NewTxOut(int64(spend.Amount-fee), testhelper.OpTrueScript)) - spendTx.AddTxOut(wire.NewTxOut(0, uniqueOpReturnScript())) + opRetScript, err := testhelper.UniqueOpReturnScript() + if err != nil { + panic(err) + } + spendTx.AddTxOut(wire.NewTxOut(0, opRetScript)) return spendTx } @@ -1959,7 +1939,10 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { const zeroCoin = int64(0) spendTx := b.Transactions[1] for i := 0; i < numAdditionalOutputs; i++ { - opRetScript := uniqueOpReturnScript() + opRetScript, err := testhelper.UniqueOpReturnScript() + if err != nil { + panic(err) + } spendTx.AddTxOut(wire.NewTxOut(zeroCoin, opRetScript)) } }) diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index 15fc5fd75c..49f80347e1 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -1,6 +1,7 @@ package testhelper import ( + "encoding/binary" "math" "runtime" @@ -20,6 +21,30 @@ var ( LowFee = btcutil.Amount(1) ) +// OpReturnScript returns a provably-pruneable OP_RETURN script with the +// provided data. +func OpReturnScript(data []byte) ([]byte, error) { + builder := txscript.NewScriptBuilder() + script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script() + if err != nil { + return nil, err + } + return script, nil +} + +// UniqueOpReturnScript returns a standard provably-pruneable OP_RETURN script +// with a random uint64 encoded as the data. +func UniqueOpReturnScript() ([]byte, error) { + rand, err := wire.RandomUint64() + if err != nil { + return nil, err + } + + data := make([]byte, 8) + binary.LittleEndian.PutUint64(data[0:8], rand) + return OpReturnScript(data) +} + // SpendableOut represents a transaction output that is spendable along with // additional metadata such as the block its in and how much it pays. type SpendableOut struct { From 59c7d105073717a9f1b7363c673a49496d76cfdd Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 16:09:50 +0900 Subject: [PATCH 06/12] fullblocktests, testhelper: move standardCoinbaseScript to testhelper standardCoinbaseScript is moved to testhelper and is exported. This allows test code in package blockchain to reuse the code without introducing an import cycle. This code is used in the testing code for invalidateblock and reconsiderblock that's added in the later commits. --- blockchain/fullblocktests/generate.go | 10 +--------- blockchain/internal/testhelper/common.go | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 242de64734..7bd838a5e0 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -220,20 +220,12 @@ func pushDataScript(items ...[]byte) []byte { return script } -// standardCoinbaseScript returns a standard script suitable for use as the -// signature script of the coinbase transaction of a new block. In particular, -// it starts with the block height that is required by version 2 blocks. -func standardCoinbaseScript(blockHeight int32, extraNonce uint64) ([]byte, error) { - return txscript.NewScriptBuilder().AddInt64(int64(blockHeight)). - AddInt64(int64(extraNonce)).Script() -} - // createCoinbaseTx returns a coinbase transaction paying an appropriate // subsidy based on the passed block height. The coinbase signature script // conforms to the requirements of version 2 blocks. func (g *testGenerator) createCoinbaseTx(blockHeight int32) *wire.MsgTx { extraNonce := uint64(0) - coinbaseScript, err := standardCoinbaseScript(blockHeight, extraNonce) + coinbaseScript, err := testhelper.StandardCoinbaseScript(blockHeight, extraNonce) if err != nil { panic(err) } diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index 49f80347e1..7d16c2f49d 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -21,6 +21,14 @@ var ( LowFee = btcutil.Amount(1) ) +// StandardCoinbaseScript returns a standard script suitable for use as the +// signature script of the coinbase transaction of a new block. In particular, +// it starts with the block height that is required by version 2 blocks. +func StandardCoinbaseScript(blockHeight int32, extraNonce uint64) ([]byte, error) { + return txscript.NewScriptBuilder().AddInt64(int64(blockHeight)). + AddInt64(int64(extraNonce)).Script() +} + // OpReturnScript returns a provably-pruneable OP_RETURN script with the // provided data. func OpReturnScript(data []byte) ([]byte, error) { From 8ab27b92451d8aff04f1da62ac80daa796202641 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 16:32:28 +0900 Subject: [PATCH 07/12] fullblocktests, testhelper: move createCoinbaseTx to testhelper createCoinbaseTx's code is refactored out and placed in testhelper package and is exported so that callers in package blockchain can reuse the code without introducing import cycles. The test code for invalidateblock and reconsiderblock that'll be added in later commits make use of this code. --- blockchain/fullblocktests/generate.go | 22 ++----------------- blockchain/internal/testhelper/common.go | 27 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 7bd838a5e0..2f092fdea1 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -224,26 +224,8 @@ func pushDataScript(items ...[]byte) []byte { // subsidy based on the passed block height. The coinbase signature script // conforms to the requirements of version 2 blocks. func (g *testGenerator) createCoinbaseTx(blockHeight int32) *wire.MsgTx { - extraNonce := uint64(0) - coinbaseScript, err := testhelper.StandardCoinbaseScript(blockHeight, extraNonce) - if err != nil { - panic(err) - } - - tx := wire.NewMsgTx(1) - tx.AddTxIn(&wire.TxIn{ - // Coinbase transactions have no inputs, so previous outpoint is - // zero hash and max index. - PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, - wire.MaxPrevOutIndex), - Sequence: wire.MaxTxInSequenceNum, - SignatureScript: coinbaseScript, - }) - tx.AddTxOut(&wire.TxOut{ - Value: blockchain.CalcBlockSubsidy(blockHeight, g.params), - PkScript: testhelper.OpTrueScript, - }) - return tx + return testhelper.CreateCoinbaseTx( + blockHeight, blockchain.CalcBlockSubsidy(blockHeight, g.params)) } // calcMerkleRoot creates a merkle tree from the slice of transactions and diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index 7d16c2f49d..0c45122c6d 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/blockchain/internal/workmath" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) @@ -21,6 +22,32 @@ var ( LowFee = btcutil.Amount(1) ) +// CreateCoinbaseTx returns a coinbase transaction paying an appropriate +// subsidy based on the passed block height and the block subsidy. The +// coinbase signature script conforms to the requirements of version 2 blocks. +func CreateCoinbaseTx(blockHeight int32, blockSubsidy int64) *wire.MsgTx { + extraNonce := uint64(0) + coinbaseScript, err := StandardCoinbaseScript(blockHeight, extraNonce) + if err != nil { + panic(err) + } + + tx := wire.NewMsgTx(1) + tx.AddTxIn(&wire.TxIn{ + // Coinbase transactions have no inputs, so previous outpoint is + // zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{}, + wire.MaxPrevOutIndex), + Sequence: wire.MaxTxInSequenceNum, + SignatureScript: coinbaseScript, + }) + tx.AddTxOut(&wire.TxOut{ + Value: blockSubsidy, + PkScript: OpTrueScript, + }) + return tx +} + // StandardCoinbaseScript returns a standard script suitable for use as the // signature script of the coinbase transaction of a new block. In particular, // it starts with the block height that is required by version 2 blocks. From 5df14376c11aa6e043a6832df206a9cc4b4a091f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 16:42:41 +0900 Subject: [PATCH 08/12] fullblocktests, testhelper: move createSpendTx to testhelper createSpendTx is moved to testhelper so that the function can be used for callers in package blockchain without introducing import cycles. The test code for invalidateblock and reconsiderblock that are going to be added in later commits make use of this code. --- blockchain/fullblocktests/generate.go | 39 +++++------------------- blockchain/internal/testhelper/common.go | 23 ++++++++++++++ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 2f092fdea1..3f53c2b9f1 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -294,29 +294,6 @@ func additionalTx(tx *wire.MsgTx) func(*wire.MsgBlock) { } } -// createSpendTx creates a transaction that spends from the provided spendable -// output and includes an additional unique OP_RETURN output to ensure the -// transaction ends up with a unique hash. The script is a simple OP_TRUE -// script which avoids the need to track addresses and signature scripts in the -// tests. -func createSpendTx(spend *testhelper.SpendableOut, fee btcutil.Amount) *wire.MsgTx { - spendTx := wire.NewMsgTx(1) - spendTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: spend.PrevOut, - Sequence: wire.MaxTxInSequenceNum, - SignatureScript: nil, - }) - spendTx.AddTxOut(wire.NewTxOut(int64(spend.Amount-fee), - testhelper.OpTrueScript)) - opRetScript, err := testhelper.UniqueOpReturnScript() - if err != nil { - panic(err) - } - spendTx.AddTxOut(wire.NewTxOut(0, opRetScript)) - - return spendTx -} - // createSpendTxForTx creates a transaction that spends from the first output of // the provided transaction and includes an additional unique OP_RETURN output // to ensure the transaction ends up with a unique hash. The public key script @@ -324,7 +301,7 @@ func createSpendTx(spend *testhelper.SpendableOut, fee btcutil.Amount) *wire.Msg // signature scripts in the tests. The signature script is nil. func createSpendTxForTx(tx *wire.MsgTx, fee btcutil.Amount) *wire.MsgTx { spend := testhelper.MakeSpendableOutForTx(tx, 0) - return createSpendTx(&spend, fee) + return testhelper.CreateSpendTx(&spend, fee) } // nextBlock builds a new block that extends the current tip associated with the @@ -364,7 +341,7 @@ func (g *testGenerator) nextBlock(blockName string, spend *testhelper.SpendableO // add it to the list of transactions to include in the block. // The script is a simple OP_TRUE script in order to avoid the // need to track addresses and signature scripts in the tests. - txns = append(txns, createSpendTx(spend, fee)) + txns = append(txns, testhelper.CreateSpendTx(spend, fee)) } // Use a timestamp that is one second after the previous block unless @@ -1152,7 +1129,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // \-> b38(b37.tx[1]) // g.setTip("b35") - doubleSpendTx := createSpendTx(outs[11], testhelper.LowFee) + doubleSpendTx := testhelper.CreateSpendTx(outs[11], testhelper.LowFee) g.nextBlock("b37", outs[11], additionalTx(doubleSpendTx)) b37Tx1Out := testhelper.MakeSpendableOut(g.tip, 1, 0) rejected(blockchain.ErrMissingTxOut) @@ -1211,7 +1188,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // Create a signed transaction that spends from the // associated p2sh output in b39. spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) - tx := createSpendTx(&spend, testhelper.LowFee) + tx := testhelper.CreateSpendTx(&spend, testhelper.LowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) if err != nil { @@ -1242,7 +1219,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { txnsNeeded := (maxBlockSigOps / redeemScriptSigOps) for i := 0; i < txnsNeeded; i++ { spend := testhelper.MakeSpendableOutForTx(b39.Transactions[i+2], 2) - tx := createSpendTx(&spend, testhelper.LowFee) + tx := testhelper.CreateSpendTx(&spend, testhelper.LowFee) sig, err := txscript.RawTxInSignature(tx, 0, redeemScript, txscript.SigHashAll, g.privKey) if err != nil { @@ -1291,7 +1268,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // ... -> b43(13) // \-> b44(14) g.nextBlock("b44", nil, func(b *wire.MsgBlock) { - nonCoinbaseTx := createSpendTx(outs[14], testhelper.LowFee) + nonCoinbaseTx := testhelper.CreateSpendTx(outs[14], testhelper.LowFee) b.Transactions[0] = nonCoinbaseTx }) rejected(blockchain.ErrFirstTxNotCoinbase) @@ -1688,7 +1665,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { // ... -> b65(19) // \-> b66(20) g.nextBlock("b66", nil, func(b *wire.MsgBlock) { - tx2 := createSpendTx(outs[20], testhelper.LowFee) + tx2 := testhelper.CreateSpendTx(outs[20], testhelper.LowFee) tx3 := createSpendTxForTx(tx2, testhelper.LowFee) b.AddTransaction(tx3) b.AddTransaction(tx2) @@ -1858,7 +1835,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { zeroFee := btcutil.Amount(0) for i := uint32(0); i < numAdditionalOutputs; i++ { spend := testhelper.MakeSpendableOut(b, 1, i+2) - tx := createSpendTx(&spend, zeroFee) + tx := testhelper.CreateSpendTx(&spend, zeroFee) b.AddTransaction(tx) } }) diff --git a/blockchain/internal/testhelper/common.go b/blockchain/internal/testhelper/common.go index 0c45122c6d..681097480c 100644 --- a/blockchain/internal/testhelper/common.go +++ b/blockchain/internal/testhelper/common.go @@ -22,6 +22,29 @@ var ( LowFee = btcutil.Amount(1) ) +// CreateSpendTx creates a transaction that spends from the provided spendable +// output and includes an additional unique OP_RETURN output to ensure the +// transaction ends up with a unique hash. The script is a simple OP_TRUE +// script which avoids the need to track addresses and signature scripts in the +// tests. +func CreateSpendTx(spend *SpendableOut, fee btcutil.Amount) *wire.MsgTx { + spendTx := wire.NewMsgTx(1) + spendTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: spend.PrevOut, + Sequence: wire.MaxTxInSequenceNum, + SignatureScript: nil, + }) + spendTx.AddTxOut(wire.NewTxOut(int64(spend.Amount-fee), + OpTrueScript)) + opRetScript, err := UniqueOpReturnScript() + if err != nil { + panic(err) + } + spendTx.AddTxOut(wire.NewTxOut(0, opRetScript)) + + return spendTx +} + // CreateCoinbaseTx returns a coinbase transaction paying an appropriate // subsidy based on the passed block height and the block subsidy. The // coinbase signature script conforms to the requirements of version 2 blocks. From ea39fe090dddf45e3a91aaecd0dd6323ed5e5dc2 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 17:52:34 +0900 Subject: [PATCH 09/12] blockchain: add block generating functions in test code The block generating functions here allow for a test to create mock blocks. This is useful for testing invalidateblock and reconsiderblock methods on blockchain that will be added in later commits. --- blockchain/common_test.go | 94 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/blockchain/common_test.go b/blockchain/common_test.go index 5037c1828e..12badd3ec0 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/btcsuite/btcd/blockchain/internal/testhelper" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -396,3 +397,96 @@ func newFakeNode(parent *blockNode, blockVersion int32, bits uint32, timestamp t } return newBlockNode(header, parent) } + +// addBlock adds a block to the blockchain that succeeds the previous block. +// The blocks spends all the provided spendable outputs. The new block and +// the new spendable outputs created in the block are returned. +func addBlock(chain *BlockChain, prev *btcutil.Block, spends []*testhelper.SpendableOut) ( + *btcutil.Block, []*testhelper.SpendableOut, error) { + + block, outs, err := newBlock(chain, prev, spends) + if err != nil { + return nil, nil, err + } + + _, _, err = chain.ProcessBlock(block, BFNone) + if err != nil { + return nil, nil, err + } + + return block, outs, nil +} + +// calcMerkleRoot creates a merkle tree from the slice of transactions and +// returns the root of the tree. +func calcMerkleRoot(txns []*wire.MsgTx) chainhash.Hash { + if len(txns) == 0 { + return chainhash.Hash{} + } + + utilTxns := make([]*btcutil.Tx, 0, len(txns)) + for _, tx := range txns { + utilTxns = append(utilTxns, btcutil.NewTx(tx)) + } + return CalcMerkleRoot(utilTxns, false) +} + +// newBlock creates a block to the blockchain that succeeds the previous block. +// The blocks spends all the provided spendable outputs. The new block and the +// newly spendable outputs created in the block are returned. +func newBlock(chain *BlockChain, prev *btcutil.Block, + spends []*testhelper.SpendableOut) (*btcutil.Block, []*testhelper.SpendableOut, error) { + + blockHeight := prev.Height() + 1 + txns := make([]*wire.MsgTx, 0, 1+len(spends)) + + // Create and add coinbase tx. + cb := testhelper.CreateCoinbaseTx(blockHeight, CalcBlockSubsidy(blockHeight, chain.chainParams)) + txns = append(txns, cb) + + // Spend all txs to be spent. + for _, spend := range spends { + cb.TxOut[0].Value += int64(testhelper.LowFee) + + spendTx := testhelper.CreateSpendTx(spend, testhelper.LowFee) + txns = append(txns, spendTx) + } + + // Use a timestamp that is one second after the previous block unless + // this is the first block in which case the current time is used. + var ts time.Time + if blockHeight == 1 { + ts = time.Unix(time.Now().Unix(), 0) + } else { + ts = prev.MsgBlock().Header.Timestamp.Add(time.Second) + } + + // Create the block. The nonce will be solved in the below code in + // SolveBlock. + block := btcutil.NewBlock(&wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: *prev.Hash(), + MerkleRoot: calcMerkleRoot(txns), + Bits: chain.chainParams.PowLimitBits, + Timestamp: ts, + Nonce: 0, // To be solved. + }, + Transactions: txns, + }) + block.SetHeight(blockHeight) + + // Solve the block. + if !testhelper.SolveBlock(&block.MsgBlock().Header) { + return nil, nil, fmt.Errorf("Unable to solve block at height %d", blockHeight) + } + + // Create spendable outs to return. + outs := make([]*testhelper.SpendableOut, len(txns)) + for i, tx := range txns { + out := testhelper.MakeSpendableOutForTx(tx, 0) + outs[i] = &out + } + + return block, outs, nil +} From 635ae689578399e2d6e867468f93188fdc52a368 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 17:56:12 +0900 Subject: [PATCH 10/12] blockchain: Add InvalidateBlock() method to BlockChain InvalidateBlock() invalidates a given block and marks all its descendents as invalid as well. The active chain tip changes if the invalidated block is part of the best chain. --- blockchain/chain.go | 138 +++++++++++++++++ blockchain/chain_test.go | 311 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) diff --git a/blockchain/chain.go b/blockchain/chain.go index 7e06e5c77c..8e75b447e9 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1798,6 +1798,144 @@ func (b *BlockChain) LocateHeaders(locator BlockLocator, hashStop *chainhash.Has return headers } +// InvalidateBlock invalidates the requested block and all its descedents. If a block +// in the best chain is invalidated, the active chain tip will be the parent of the +// invalidated block. +// +// This function is safe for concurrent access. +func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { + b.chainLock.Lock() + defer b.chainLock.Unlock() + + node := b.index.LookupNode(hash) + if node == nil { + // Return an error if the block doesn't exist. + return fmt.Errorf("Requested block hash of %s is not found "+ + "and thus cannot be invalidated.", hash) + } + if node.height == 0 { + return fmt.Errorf("Requested block hash of %s is a at height 0 "+ + "and is thus a genesis block and cannot be invalidated.", + node.hash) + } + + // Nothing to do if the given block is already invalid. + if node.status.KnownInvalid() { + return nil + } + + // Set the status of the block being invalidated. + b.index.SetStatusFlags(node, statusValidateFailed) + b.index.UnsetStatusFlags(node, statusValid) + + // If the block we're invalidating is not on the best chain, we simply + // mark the block and all its descendants as invalid and return. + if !b.bestChain.Contains(node) { + // Grab all the tips excluding the active tip. + tips := b.index.InactiveTips(b.bestChain) + for _, tip := range tips { + // Continue if the given inactive tip is not a descendant of the block + // being invalidated. + if !tip.IsAncestor(node) { + continue + } + + // Keep going back until we get to the block being invalidated. + // For each of the parent, we'll unset valid status and set invalid + // ancestor status. + for n := tip; n != nil && n != node; n = n.parent { + // Continue if it's already invalid. + if n.status.KnownInvalid() { + continue + } + b.index.SetStatusFlags(n, statusInvalidAncestor) + b.index.UnsetStatusFlags(n, statusValid) + } + } + + if writeErr := b.index.flushToDB(); writeErr != nil { + return fmt.Errorf("Error flushing block index "+ + "changes to disk: %v", writeErr) + } + + // Return since the block being invalidated is on a side branch. + // Nothing else left to do. + return nil + } + + // If we're here, it means a block from the active chain tip is getting + // invalidated. + // + // Grab all the nodes to detach from the active chain. + detachNodes := list.New() + for n := b.bestChain.Tip(); n != nil && n != node; n = n.parent { + // Continue if it's already invalid. + if n.status.KnownInvalid() { + continue + } + + // Change the status of the block node. + b.index.SetStatusFlags(n, statusInvalidAncestor) + b.index.UnsetStatusFlags(n, statusValid) + detachNodes.PushBack(n) + } + + // Push back the block node being invalidated. + detachNodes.PushBack(node) + + // Reorg back to the parent of the block being invalidated. + // Nothing to attach so just pass an empty list. + err := b.reorganizeChain(detachNodes, list.New()) + if err != nil { + return err + } + + if writeErr := b.index.flushToDB(); writeErr != nil { + log.Warnf("Error flushing block index changes to disk: %v", writeErr) + } + + // Grab all the tips. + tips := b.index.InactiveTips(b.bestChain) + tips = append(tips, b.bestChain.Tip()) + + // Here we'll check if the invalidation of the block in the active tip + // changes the status of the chain tips. If a side branch now has more + // worksum, it becomes the active chain tip. + var bestTip *blockNode + for _, tip := range tips { + // Skip invalid tips as they cannot become the active tip. + if tip.status.KnownInvalid() { + continue + } + + // If we have no best tips, then set this tip as the best tip. + if bestTip == nil { + bestTip = tip + } else { + // If there is an existing best tip, then compare it + // against the current tip. + if tip.workSum.Cmp(bestTip.workSum) == 1 { + bestTip = tip + } + } + } + + // Return if the best tip is the current tip. + if bestTip == b.bestChain.Tip() { + return nil + } + + // Reorganize to the best tip if a side branch is now the most work tip. + detachNodes, attachNodes := b.getReorganizeNodes(bestTip) + err = b.reorganizeChain(detachNodes, attachNodes) + + if writeErr := b.index.flushToDB(); writeErr != nil { + log.Warnf("Error flushing block index changes to disk: %v", writeErr) + } + + return err +} + // IndexManager provides a generic interface that the is called when blocks are // connected and disconnected to and from the tip of the main chain for the // purpose of supporting optional indexes. diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 259a643f3c..57bbd6246c 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -6,10 +6,12 @@ package blockchain import ( "fmt" + "math/rand" "reflect" "testing" "time" + "github.com/btcsuite/btcd/blockchain/internal/testhelper" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -1311,3 +1313,312 @@ func TestIsAncestor(t *testing.T) { branch2Nodes[0].hash.String()) } } + +// randomSelect selects random amount of random elements from a slice and returns a +// new slice. The selected elements are removed. +func randomSelect(input []*testhelper.SpendableOut) ( + []*testhelper.SpendableOut, []*testhelper.SpendableOut) { + + selected := []*testhelper.SpendableOut{} + + // Select random elements from the input slice + amount := rand.Intn(len(input)) + for i := 0; i < amount; i++ { + // Generate a random index + randIdx := rand.Intn(len(input)) + + // Append the selected element to the new slice + selected = append(selected, input[randIdx]) + + // Remove the selected element from the input slice. + // This ensures that each selected element is unique. + input = append(input[:randIdx], input[randIdx+1:]...) + } + + return input, selected +} + +// addBlocks generates new blocks and adds them to the chain. The newly generated +// blocks will spend from the spendable outputs passed in. The returned hases are +// the hashes of the newly generated blocks. +func addBlocks(count int, chain *BlockChain, prevBlock *btcutil.Block, + allSpendableOutputs []*testhelper.SpendableOut) ( + []*chainhash.Hash, [][]*testhelper.SpendableOut, error) { + + blockHashes := make([]*chainhash.Hash, 0, count) + spendablesOuts := make([][]*testhelper.SpendableOut, 0, count) + + // Always spend everything on the first block. This ensures we get unique blocks + // every time. The random select may choose not to spend any and that results + // in getting the same block. + nextSpends := allSpendableOutputs + allSpendableOutputs = allSpendableOutputs[:0] + for b := 0; b < count; b++ { + newBlock, newSpendableOuts, err := addBlock(chain, prevBlock, nextSpends) + if err != nil { + return nil, nil, err + } + prevBlock = newBlock + + blockHashes = append(blockHashes, newBlock.Hash()) + spendablesOuts = append(spendablesOuts, newSpendableOuts) + allSpendableOutputs = append(allSpendableOutputs, newSpendableOuts...) + + // Grab utxos to be spent in the next block. + allSpendableOutputs, nextSpends = randomSelect(allSpendableOutputs) + } + + return blockHashes, spendablesOuts, nil +} + +func TestInvalidateBlock(t *testing.T) { + tests := []struct { + name string + chainGen func() (*BlockChain, []*chainhash.Hash, func()) + }{ + { + name: "one branch, invalidate once", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain( + "TestInvalidateBlock-one-branch-" + + "invalidate-once") + // Grab the tip of the chain. + tip := btcutil.NewBlock(params.GenesisBlock) + + // Create a chain with 11 blocks. + _, _, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Invalidate block 5. + block, err := chain.BlockByHeight(5) + if err != nil { + t.Fatal(err) + } + invalidateHash := block.Hash() + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + { + name: "invalidate twice", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-twice") + // Grab the tip of the chain. + tip := btcutil.NewBlock(params.GenesisBlock) + + // Create a chain with 11 blocks. + _, spendableOuts, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) + //_, _, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Set invalidateHash as block 5. + block, err := chain.BlockByHeight(5) + if err != nil { + t.Fatal(err) + } + invalidateHash := block.Hash() + + // Create a side chain with 7 blocks that builds on block 1. + b1, err := chain.BlockByHeight(1) + if err != nil { + t.Fatal(err) + } + altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) + if err != nil { + t.Fatal(err) + } + + // Grab block at height 5: + // + // b2, b3, b4, b5 + // 0, 1, 2, 3 + invalidateHash2 := altBlockHashes[3] + + // Sanity checking that we grabbed the correct hash. + node := chain.index.LookupNode(invalidateHash) + if node == nil || node.height != 5 { + t.Fatalf("wanted to grab block at height 5 but got height %v", + node.height) + } + + return chain, []*chainhash.Hash{invalidateHash, invalidateHash2}, tearDown + }, + }, + { + name: "invalidate a side branch", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-side-branch") + tip := btcutil.NewBlock(params.GenesisBlock) + + // Grab the tip of the chain. + tip, err := chain.BlockByHash(&chain.bestChain.Tip().hash) + if err != nil { + t.Fatal(err) + } + + // Create a chain with 11 blocks. + _, spendableOuts, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Create a side chain with 7 blocks that builds on block 1. + b1, err := chain.BlockByHeight(1) + if err != nil { + t.Fatal(err) + } + altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) + if err != nil { + t.Fatal(err) + } + + // Grab block at height 4: + // + // b2, b3, b4 + // 0, 1, 2 + invalidateHash := altBlockHashes[2] + + // Sanity checking that we grabbed the correct hash. + node := chain.index.LookupNode(invalidateHash) + if node == nil || node.height != 4 { + t.Fatalf("wanted to grab block at height 4 but got height %v", + node.height) + } + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + } + + for _, test := range tests { + chain, invalidateHashes, tearDown := test.chainGen() + func() { + defer tearDown() + for _, invalidateHash := range invalidateHashes { + chainTipsBefore := chain.ChainTips() + + // Mark if we're invalidating a block that's a part of the best chain. + var bestChainBlock bool + node := chain.index.LookupNode(invalidateHash) + if chain.bestChain.Contains(node) { + bestChainBlock = true + } + + // Actual invalidation. + err := chain.InvalidateBlock(invalidateHash) + if err != nil { + t.Fatal(err) + } + + chainTipsAfter := chain.ChainTips() + + // Create a map for easy lookup. + chainTipMap := make(map[chainhash.Hash]ChainTip, len(chainTipsAfter)) + activeTipCount := 0 + for _, chainTip := range chainTipsAfter { + chainTipMap[chainTip.BlockHash] = chainTip + + if chainTip.Status == StatusActive { + activeTipCount++ + } + } + if activeTipCount != 1 { + t.Fatalf("TestInvalidateBlock fail. Expected "+ + "1 active chain tip but got %d", activeTipCount) + } + + bestTip := chain.bestChain.Tip() + + validForkCount := 0 + for _, tip := range chainTipsBefore { + // If the chaintip was an active tip and we invalidated a block + // in the active tip, assert that it's invalid now. + if bestChainBlock && tip.Status == StatusActive { + gotTip, found := chainTipMap[tip.BlockHash] + if !found { + t.Fatalf("TestInvalidateBlock fail. Expected "+ + "block %s not found in chaintips after "+ + "invalidateblock", tip.BlockHash.String()) + } + + if gotTip.Status != StatusInvalid { + t.Fatalf("TestInvalidateBlock fail. "+ + "Expected block %s to be invalid, got status: %s", + gotTip.BlockHash.String(), gotTip.Status) + } + } + + if !bestChainBlock && tip.Status != StatusActive { + gotTip, found := chainTipMap[tip.BlockHash] + if !found { + t.Fatalf("TestInvalidateBlock fail. Expected "+ + "block %s not found in chaintips after "+ + "invalidateblock", tip.BlockHash.String()) + } + + if gotTip.BlockHash == *invalidateHash && gotTip.Status != StatusInvalid { + t.Fatalf("TestInvalidateBlock fail. "+ + "Expected block %s to be invalid, got status: %s", + gotTip.BlockHash.String(), gotTip.Status) + } + } + + // If we're not invalidating the branch with an active tip, + // we expect the active tip to remain the same. + if !bestChainBlock && tip.Status == StatusActive && tip.BlockHash != bestTip.hash { + t.Fatalf("TestInvalidateBlock fail. Expected block %s as the tip but got %s", + tip.BlockHash.String(), bestTip.hash.String()) + } + + // If this tip is not invalid and not active, it should be + // lighter than the current best tip. + if tip.Status != StatusActive && tip.Status != StatusInvalid && + tip.Height > bestTip.height { + + tipNode := chain.index.LookupNode(&tip.BlockHash) + if bestTip.workSum.Cmp(tipNode.workSum) == -1 { + t.Fatalf("TestInvalidateBlock fail. Expected "+ + "block %s to be the active tip but block %s "+ + "was", tipNode.hash.String(), bestTip.hash.String()) + } + } + + if tip.Status == StatusValidFork { + validForkCount++ + } + } + + // If there are no other valid chain tips besides the active chaintip, + // we expect to have one more chain tip after the invalidate. + if validForkCount == 0 && len(chainTipsAfter) != len(chainTipsBefore)+1 { + t.Fatalf("TestInvalidateBlock fail. Expected %d chaintips but got %d", + len(chainTipsBefore)+1, len(chainTipsAfter)) + } + } + + // Try to invaliate the already invalidated hash. + err := chain.InvalidateBlock(invalidateHashes[0]) + if err != nil { + t.Fatal(err) + } + + // Try to invaliate a genesis block + err = chain.InvalidateBlock(chain.chainParams.GenesisHash) + if err == nil { + t.Fatalf("TestInvalidateBlock fail. Expected to err when trying to" + + "invalidate a genesis block.") + } + + // Try to invaliate a block that doesn't exist. + err = chain.InvalidateBlock(chaincfg.MainNetParams.GenesisHash) + if err == nil { + t.Fatalf("TestInvalidateBlock fail. Expected to err when trying to" + + "invalidate a block that doesn't exist.") + } + }() + } +} From c14546b369dde37d868985f1112468b70d52967f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Apr 2024 18:46:48 +0900 Subject: [PATCH 11/12] blockchain: Refactor reorganizeChain to exclude verification reorganizeChain() used to handle the following: 1: That the blocknodes being disconnected/connected indeed to connect properly without errors. 2: Perform the actual disconnect/connect of the blocknodes. The functionality of 1, the validation that the disconnects/connects can happen without errors are now refactored out into verifyReorganizationValidity. This is an effort made so that ReconsiderBlock() can call verifyReorganizationValidity and set the block status of the reconsidered chain and return nil even when an error returns as it's ok to get an error when reconsidering an invalid branch. --- blockchain/chain.go | 251 +++++++++++++++++++++++--------------------- 1 file changed, 132 insertions(+), 119 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 8e75b447e9..95ef3eac1a 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -870,9 +870,117 @@ func countSpentOutputs(block *btcutil.Block) int { // // This function MUST be called with the chain state lock held (for writes). func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error { + // Check first that the detach and the attach nodes are valid and they + // pass verification. + detachBlocks, attachBlocks, detachSpentTxOuts, + err := b.verifyReorganizationValidity(detachNodes, attachNodes) + if err != nil { + return err + } + + // Track the old and new best chains heads. + tip := b.bestChain.Tip() + oldBest := tip + newBest := tip + + // Reset the view for the actual connection code below. This is + // required because the view was previously modified when checking if + // the reorg would be successful and the connection code requires the + // view to be valid from the viewpoint of each block being disconnected. + view := NewUtxoViewpoint() + view.SetBestHash(&b.bestChain.Tip().hash) + + // Disconnect blocks from the main chain. + for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() { + n := e.Value.(*blockNode) + block := detachBlocks[i] + + // Load all of the utxos referenced by the block that aren't + // already in the view. + err := view.fetchInputUtxos(b.utxoCache, block) + if err != nil { + return err + } + + // Update the view to unspend all of the spent txos and remove + // the utxos created by the block. + err = view.disconnectTransactions(b.db, block, + detachSpentTxOuts[i]) + if err != nil { + return err + } + + // Update the database and chain state. The cache will be flushed + // here before the utxoview modifications happen to the database. + err = b.disconnectBlock(n, block, view) + if err != nil { + return err + } + + newBest = n.parent + } + + // Set the fork point only if there are nodes to attach since otherwise + // blocks are only being disconnected and thus there is no fork point. + var forkNode *blockNode + if attachNodes.Len() > 0 { + forkNode = newBest + } + + // Connect the new best chain blocks using the utxocache directly. It's more + // efficient and since we already checked that the blocks are correct and that + // the transactions connect properly, it's ok to access the cache. If we suddenly + // crash here, we are able to recover as well. + for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() { + n := e.Value.(*blockNode) + block := attachBlocks[i] + + // Update the cache to mark all utxos referenced by the block + // as spent and add all transactions being created by this block + // to it. Also, provide an stxo slice so the spent txout + // details are generated. + stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) + err = b.utxoCache.connectTransactions(block, &stxos) + if err != nil { + return err + } + + // Update the database and chain state. + err = b.connectBlock(n, block, stxos) + if err != nil { + return err + } + + newBest = n + } + + // Log the point where the chain forked and old and new best chain + // heads. + if forkNode != nil { + log.Infof("REORGANIZE: Chain forks at %v (height %v)", forkNode.hash, + forkNode.height) + } + log.Infof("REORGANIZE: Old best chain head was %v (height %v)", + &oldBest.hash, oldBest.height) + log.Infof("REORGANIZE: New best chain head is %v (height %v)", + newBest.hash, newBest.height) + + return nil +} + +// verifyReorganizationValidity will verify that the disconnects and the connects +// that are in the list are able to be processed without mutating the chain. +// +// For the attach nodes, it'll check that each of the blocks are valid and will +// change the status of the block node in the list to invalid if the block fails +// to pass verification. For the detach nodes, it'll check that the blocks being +// detached and their spend journals are present on the database. +func (b *BlockChain) verifyReorganizationValidity(detachNodes, attachNodes *list.List) ( + []*btcutil.Block, []*btcutil.Block, [][]SpentTxOut, error) { + // Nothing to do if no reorganize nodes were provided. if detachNodes.Len() == 0 && attachNodes.Len() == 0 { - return nil + return nil, nil, nil, nil } // Ensure the provided nodes match the current best chain. @@ -880,9 +988,10 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error if detachNodes.Len() != 0 { firstDetachNode := detachNodes.Front().Value.(*blockNode) if firstDetachNode.hash != tip.hash { - return AssertError(fmt.Sprintf("reorganize nodes to detach are "+ - "not for the current best chain -- first detach node %v, "+ - "current chain %v", &firstDetachNode.hash, &tip.hash)) + return nil, nil, nil, + AssertError(fmt.Sprintf("reorganize nodes to detach are "+ + "not for the current best chain -- first detach node %v, "+ + "current chain %v", &firstDetachNode.hash, &tip.hash)) } } @@ -891,17 +1000,14 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error firstAttachNode := attachNodes.Front().Value.(*blockNode) lastDetachNode := detachNodes.Back().Value.(*blockNode) if firstAttachNode.parent.hash != lastDetachNode.parent.hash { - return AssertError(fmt.Sprintf("reorganize nodes do not have the "+ - "same fork point -- first attach parent %v, last detach "+ - "parent %v", &firstAttachNode.parent.hash, - &lastDetachNode.parent.hash)) + return nil, nil, nil, + AssertError(fmt.Sprintf("reorganize nodes do not have the "+ + "same fork point -- first attach parent %v, last detach "+ + "parent %v", &firstAttachNode.parent.hash, + &lastDetachNode.parent.hash)) } } - // Track the old and new best chains heads. - oldBest := tip - newBest := tip - // All of the blocks to detach and related spend journal entries needed // to unspend transaction outputs in the blocks being disconnected must // be loaded from the database during the reorg check phase below and @@ -916,7 +1022,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error // database and using that information to unspend all of the spent txos // and remove the utxos created by the blocks. view := NewUtxoViewpoint() - view.SetBestHash(&oldBest.hash) + view.SetBestHash(&tip.hash) for e := detachNodes.Front(); e != nil; e = e.Next() { n := e.Value.(*blockNode) var block *btcutil.Block @@ -926,19 +1032,20 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error return err }) if err != nil { - return err + return nil, nil, nil, err } if n.hash != *block.Hash() { - return AssertError(fmt.Sprintf("detach block node hash %v (height "+ - "%v) does not match previous parent block hash %v", &n.hash, - n.height, block.Hash())) + return nil, nil, nil, AssertError( + fmt.Sprintf("detach block node hash %v (height "+ + "%v) does not match previous parent block hash %v", + &n.hash, n.height, block.Hash())) } // Load all of the utxos referenced by the block that aren't // already in the view. err = view.fetchInputUtxos(b.utxoCache, block) if err != nil { - return err + return nil, nil, nil, err } // Load all of the spent txos for the block from the spend @@ -949,7 +1056,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error return err }) if err != nil { - return err + return nil, nil, nil, err } // Store the loaded block and spend journal entry for later. @@ -958,17 +1065,8 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error err = view.disconnectTransactions(b.db, block, stxos) if err != nil { - return err + return nil, nil, nil, err } - - newBest = n.parent - } - - // Set the fork point only if there are nodes to attach since otherwise - // blocks are only being disconnected and thus there is no fork point. - var forkNode *blockNode - if attachNodes.Len() > 0 { - forkNode = newBest } // Perform several checks to verify each block that needs to be attached @@ -993,7 +1091,7 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error return err }) if err != nil { - return err + return nil, nil, nil, err } // Store the loaded block for later. @@ -1005,14 +1103,13 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error if b.index.NodeStatus(n).KnownValid() { err = view.fetchInputUtxos(b.utxoCache, block) if err != nil { - return err + return nil, nil, nil, err } err = view.connectTransactions(block, nil) if err != nil { - return err + return nil, nil, nil, err } - newBest = n continue } @@ -1033,96 +1130,12 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error b.index.SetStatusFlags(dn, statusInvalidAncestor) } } - return err + return nil, nil, nil, err } b.index.SetStatusFlags(n, statusValid) - - newBest = n - } - - // Flush the utxo cache for the block disconnect below. The disconnect - // code assumes that it's directly modifying the database so the cache - // will be left in an inconsistent state. It needs to be flushed beforehand - // in order for that to not happen. - err := b.db.Update(func(dbTx database.Tx) error { - return b.utxoCache.flush(dbTx, FlushRequired, b.BestSnapshot()) - }) - if err != nil { - return err - } - - // Reset the view for the actual connection code below. This is - // required because the view was previously modified when checking if - // the reorg would be successful and the connection code requires the - // view to be valid from the viewpoint of each block being disconnected. - view = NewUtxoViewpoint() - view.SetBestHash(&b.bestChain.Tip().hash) - - // Disconnect blocks from the main chain. - for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() { - n := e.Value.(*blockNode) - block := detachBlocks[i] - - // Load all of the utxos referenced by the block that aren't - // already in the view. - err := view.fetchInputUtxos(b.utxoCache, block) - if err != nil { - return err - } - - // Update the view to unspend all of the spent txos and remove - // the utxos created by the block. - err = view.disconnectTransactions(b.db, block, - detachSpentTxOuts[i]) - if err != nil { - return err - } - - // Update the database and chain state. The cache will be flushed - // here before the utxoview modifications happen to the database. - err = b.disconnectBlock(n, block, view) - if err != nil { - return err - } } - // Connect the new best chain blocks using the utxocache directly. It's more - // efficient and since we already checked that the blocks are correct and that - // the transactions connect properly, it's ok to access the cache. If we suddenly - // crash here, we are able to recover as well. - for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() { - n := e.Value.(*blockNode) - block := attachBlocks[i] - - // Update the cache to mark all utxos referenced by the block - // as spent and add all transactions being created by this block - // to it. Also, provide an stxo slice so the spent txout - // details are generated. - stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) - err = b.utxoCache.connectTransactions(block, &stxos) - if err != nil { - return err - } - - // Update the database and chain state. - err = b.connectBlock(n, block, stxos) - if err != nil { - return err - } - } - - // Log the point where the chain forked and old and new best chain - // heads. - if forkNode != nil { - log.Infof("REORGANIZE: Chain forks at %v (height %v)", forkNode.hash, - forkNode.height) - } - log.Infof("REORGANIZE: Old best chain head was %v (height %v)", - &oldBest.hash, oldBest.height) - log.Infof("REORGANIZE: New best chain head is %v (height %v)", - newBest.hash, newBest.height) - - return nil + return detachBlocks, attachBlocks, detachSpentTxOuts, nil } // connectBestChain handles connecting the passed block to the chain while From 7c04bd86ac54fbe404e1085e569a35d83ea370e2 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Apr 2024 19:48:48 +0900 Subject: [PATCH 12/12] blockchain: Add ReconsiderBlock to BlockChain ReconsiderBlock reconsiders the validity of the block for the passed in blockhash. The behavior of the function mimics that of Bitcoin Core. The invalid status of the block nodes are reset and if the chaintip that is being reconsidered has more cumulative work, then we'll validate the blocks and reorganize to it. If the cumulative work is lesser than the current active chain tip, then nothing else will be done. --- blockchain/chain.go | 82 ++++++++++++++ blockchain/chain_test.go | 226 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) diff --git a/blockchain/chain.go b/blockchain/chain.go index 95ef3eac1a..cf4a0e0d32 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1949,6 +1949,88 @@ func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { return err } +// ReconsiderBlock reconsiders the validity of the block with the given hash. +// +// This function is safe for concurrent access. +func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { + b.chainLock.Lock() + defer b.chainLock.Unlock() + + node := b.index.LookupNode(hash) + if node == nil { + // Return an error if the block doesn't exist. + return fmt.Errorf("Requested block hash of %s is not found "+ + "and thus cannot be reconsidered.", hash) + } + + // Nothing to do if the given block is already valid. + if node.status.KnownValid() { + return nil + } + + // Clear the status of the block being reconsidered. + b.index.UnsetStatusFlags(node, statusInvalidAncestor) + b.index.UnsetStatusFlags(node, statusValidateFailed) + + // Grab all the tips. + tips := b.index.InactiveTips(b.bestChain) + tips = append(tips, b.bestChain.Tip()) + + // Go through all the tips and unset the status for all the descendents of the + // block being reconsidered. + var reconsiderTip *blockNode + for _, tip := range tips { + // Continue if the given inactive tip is not a descendant of the block + // being invalidated. + if !tip.IsAncestor(node) { + // Set as the reconsider tip if the block node being reconsidered + // is a tip. + if tip == node { + reconsiderTip = node + } + continue + } + + // Mark the current tip as the tip being reconsidered. + reconsiderTip = tip + + // Unset the status of all the parents up until it reaches the block + // being reconsidered. + for n := tip; n != nil && n != node; n = n.parent { + b.index.UnsetStatusFlags(n, statusInvalidAncestor) + } + } + + // Compare the cumulative work for the branch being reconsidered. + if reconsiderTip.workSum.Cmp(b.bestChain.Tip().workSum) <= 0 { + return nil + } + + // If the reconsider tip has a higher cumulative work, then reorganize + // to it after checking the validity of the nodes. + detachNodes, attachNodes := b.getReorganizeNodes(reconsiderTip) + + // We're checking if the reorganization that'll happen is actually valid. + // While this is called in reorganizeChain, we call it beforehand as the error + // returned from reorganizeChain doesn't differentiate between actual disconnect/ + // connect errors or whether the branch we're trying to fork to is invalid. + // + // The block status changes here without being flushed so we immediately flush + // the blockindex after we call this function. + _, _, _, err := b.verifyReorganizationValidity(detachNodes, attachNodes) + if writeErr := b.index.flushToDB(); writeErr != nil { + log.Warnf("Error flushing block index changes to disk: %v", writeErr) + } + if err != nil { + // If we errored out during the verification of the reorg branch, + // it's ok to return nil as we reconsidered the block and determined + // that it's invalid. + return nil + } + + return b.reorganizeChain(detachNodes, attachNodes) +} + // IndexManager provides a generic interface that the is called when blocks are // connected and disconnected to and from the tip of the main chain for the // purpose of supporting optional indexes. diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 57bbd6246c..b3bccf56f7 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -1622,3 +1622,229 @@ func TestInvalidateBlock(t *testing.T) { }() } } + +func TestReconsiderBlock(t *testing.T) { + tests := []struct { + name string + chainGen func() (*BlockChain, []*chainhash.Hash, func()) + }{ + { + name: "one branch, invalidate once and revalidate", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-one-branch-invalidate-once") + + // Create a chain with 101 blocks. + tip := btcutil.NewBlock(params.GenesisBlock) + _, _, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Invalidate block 5. + block, err := chain.BlockByHeight(5) + if err != nil { + t.Fatal(err) + } + invalidateHash := block.Hash() + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + { + name: "invalidate the active branch with a side branch present and revalidate", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-with-side-branch") + + // Create a chain with 101 blocks. + tip := btcutil.NewBlock(params.GenesisBlock) + _, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Invalidate block 5. + block, err := chain.BlockByHeight(5) + if err != nil { + t.Fatal(err) + } + invalidateHash := block.Hash() + + // Create a side chain with 7 blocks that builds on block 1. + b1, err := chain.BlockByHeight(1) + if err != nil { + t.Fatal(err) + } + _, _, err = addBlocks(6, chain, b1, spendableOuts[0]) + if err != nil { + t.Fatal(err) + } + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + { + name: "invalidate a side branch and revalidate it", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-invalidate-a-side-branch") + + // Create a chain with 101 blocks. + tip := btcutil.NewBlock(params.GenesisBlock) + _, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Create a side chain with 7 blocks that builds on block 1. + b1, err := chain.BlockByHeight(1) + if err != nil { + t.Fatal(err) + } + altBlockHashes, _, err := addBlocks(6, chain, b1, spendableOuts[0]) + if err != nil { + t.Fatal(err) + } + // Grab block at height 4: + // + // b2, b3, b4, b5 + // 0, 1, 2, 3 + invalidateHash := altBlockHashes[2] + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + { + name: "reconsider an invalid side branch with a higher work", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-higher") + + tip := btcutil.NewBlock(params.GenesisBlock) + _, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Select utxos to be spent from the best block and + // modify the amount so that the block will be invalid. + nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1]) + nextSpends[0].Amount += testhelper.LowFee + + // Make an invalid block that best on top of the current tip. + bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash) + if err != nil { + t.Fatal(err) + } + invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends) + invalidateHash := invalidBlock.Hash() + + // The block validation will fail here and we'll mark the + // block as invalid in the block index. + chain.ProcessBlock(invalidBlock, BFNone) + + // Modify the amount again so it's valid. + nextSpends[0].Amount -= testhelper.LowFee + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + { + name: "reconsider an invalid side branch with a lower work", + chainGen: func() (*BlockChain, []*chainhash.Hash, func()) { + chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-lower") + + tip := btcutil.NewBlock(params.GenesisBlock) + _, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + // Select utxos to be spent from the best block and + // modify the amount so that the block will be invalid. + nextSpends, _ := randomSelect(spendableOuts[len(spendableOuts)-1]) + nextSpends[0].Amount += testhelper.LowFee + + // Make an invalid block that best on top of the current tip. + bestBlock, err := chain.BlockByHash(&chain.BestSnapshot().Hash) + if err != nil { + t.Fatal(err) + } + invalidBlock, _, _ := newBlock(chain, bestBlock, nextSpends) + invalidateHash := invalidBlock.Hash() + + // The block validation will fail here and we'll mark the + // block as invalid in the block index. + chain.ProcessBlock(invalidBlock, BFNone) + + // Modify the amount again so it's valid. + nextSpends[0].Amount -= testhelper.LowFee + + // Add more blocks to make the invalid block a + // side chain and not the most pow. + _, _, err = addBlocks(3, chain, bestBlock, []*testhelper.SpendableOut{}) + if err != nil { + t.Fatal(err) + } + + return chain, []*chainhash.Hash{invalidateHash}, tearDown + }, + }, + } + + for _, test := range tests { + chain, invalidateHashes, tearDown := test.chainGen() + func() { + defer tearDown() + for _, invalidateHash := range invalidateHashes { + // Cache the chain tips before the invalidate. Since we'll reconsider + // the invalidated block, we should come back to these tips in the end. + tips := chain.ChainTips() + expectedChainTips := make(map[chainhash.Hash]ChainTip, len(tips)) + for _, tip := range tips { + expectedChainTips[tip.BlockHash] = tip + } + + // Invalidation. + err := chain.InvalidateBlock(invalidateHash) + if err != nil { + t.Fatal(err) + } + + // Reconsideration. + err = chain.ReconsiderBlock(invalidateHash) + if err != nil { + t.Fatal(err) + } + + // Compare the tips aginst the tips we've cached. + gotChainTips := chain.ChainTips() + for _, gotChainTip := range gotChainTips { + testChainTip, found := expectedChainTips[gotChainTip.BlockHash] + if !found { + t.Errorf("TestReconsiderBlock Failed test \"%s\". Couldn't find an expected "+ + "chain tip with height %d, hash %s, branchlen %d, status \"%s\"", + test.name, testChainTip.Height, testChainTip.BlockHash.String(), + testChainTip.BranchLen, testChainTip.Status.String()) + } + + // If the invalid side branch is a lower work, we'll never + // actually process the block again until the branch becomes + // a greater work chain so it'll show up as valid-fork. + if test.name == "reconsider an invalid side branch with a lower work" && + testChainTip.BlockHash == *invalidateHash { + + testChainTip.Status = StatusValidFork + } + + if !reflect.DeepEqual(testChainTip, gotChainTip) { + t.Errorf("TestReconsiderBlock Failed test \"%s\". Expected chain tip with "+ + "height %d, hash %s, branchlen %d, status \"%s\" but got "+ + "height %d, hash %s, branchlen %d, status \"%s\"", test.name, + testChainTip.Height, testChainTip.BlockHash.String(), + testChainTip.BranchLen, testChainTip.Status.String(), + gotChainTip.Height, gotChainTip.BlockHash.String(), + gotChainTip.BranchLen, gotChainTip.Status.String()) + } + } + } + }() + } +}