From 597b68c79e40c3e6bb63b5d5a0622273222c52f2 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 2 Apr 2024 13:41:52 +0900 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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.") + } + }() + } +}