mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-25 09:02:11 +01:00
Reorg handling in chain project (#548)
* Implement 'IndexedSeqLike' on Blockchain data structure to give us access to the scala collection methods we know and love * Implement ability to handle reorgs in Blockchain.connectTip() * Add another test case for where block B and C are at the same height as each other, with B being the best block hash. When block D comes in, it is built on top of block C so that means our best block hash should switch to C and then to D after D is connected * Add more unit tests to Blockchain reorg handling * Address torkel's code review
This commit is contained in:
parent
d73322a76e
commit
d00dff5645
4 changed files with 186 additions and 20 deletions
|
@ -1,8 +1,13 @@
|
|||
package org.bitcoins.chain.blockchain
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.chain.api.ChainApi
|
||||
import org.bitcoins.chain.config.ChainAppConfig
|
||||
import org.bitcoins.chain.models.BlockHeaderDbHelper
|
||||
import org.bitcoins.chain.models.{
|
||||
BlockHeaderDAO,
|
||||
BlockHeaderDb,
|
||||
BlockHeaderDbHelper
|
||||
}
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.util.FileUtil
|
||||
import org.bitcoins.testkit.chain.fixture.ChainFixtureTag
|
||||
|
@ -35,6 +40,7 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
withChainHandler(test)
|
||||
|
||||
val genesis = ChainUnitTest.genesisHeaderDb
|
||||
behavior of "ChainHandler"
|
||||
|
||||
it must "process a new valid block header, and then be able to fetch that header" in {
|
||||
|
@ -124,6 +130,84 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
}
|
||||
}
|
||||
|
||||
it must "handle a very basic reorg where one chain is one block behind the best chain" in {
|
||||
chainHandler: ChainHandler =>
|
||||
val reorgFixtureF = buildChainHandlerCompetingHeaders(chainHandler)
|
||||
val chainHandlerCF = reorgFixtureF.map(_.chainApi)
|
||||
// header B, best hash ATM
|
||||
val newHeaderBF = reorgFixtureF.map(_.headerDb1)
|
||||
// header C, same height as B but was seen later
|
||||
val newHeaderCF = reorgFixtureF.map(_.headerDb2)
|
||||
|
||||
// check that header B is the leader
|
||||
val assertBBestHashF = for {
|
||||
chainHandler <- chainHandlerCF
|
||||
headerB <- newHeaderBF
|
||||
bestHash <- chainHandler.getBestBlockHash
|
||||
} yield {
|
||||
assert(bestHash == headerB.hashBE)
|
||||
}
|
||||
|
||||
// build a new header D off of C which was seen later
|
||||
// but has the same height as B
|
||||
val newHeaderDF =
|
||||
newHeaderCF.map(h => BlockHeaderHelper.buildNextHeader(h))
|
||||
|
||||
val chainHandlerDF = for {
|
||||
_ <- assertBBestHashF
|
||||
newHeaderD <- newHeaderDF
|
||||
chainHandler <- chainHandlerCF
|
||||
chainHandlerD <- chainHandler.processHeader(newHeaderD.blockHeader)
|
||||
} yield chainHandlerD
|
||||
|
||||
for {
|
||||
chainHandler <- chainHandlerDF
|
||||
newHeaderD <- newHeaderDF
|
||||
hash <- chainHandler.getBestBlockHash
|
||||
} yield {
|
||||
// assert that header D overtook header B
|
||||
assert(hash == newHeaderD.hashBE)
|
||||
}
|
||||
}
|
||||
|
||||
it must "NOT reorg to a shorter chain that just received a new block" in {
|
||||
chainHandler: ChainHandler =>
|
||||
val reorgFixtureF = buildChainHandlerCompetingHeaders(chainHandler)
|
||||
val chainHandlerCF = reorgFixtureF.map(_.chainApi)
|
||||
val newHeaderBF = reorgFixtureF.map(_.headerDb1)
|
||||
val newHeaderCF = reorgFixtureF.map(_.headerDb2)
|
||||
|
||||
//we are going to generate two new blocks on chain C
|
||||
val chainHandlerEWithHeaderF: Future[(ChainApi, BlockHeaderDb)] = for {
|
||||
newHeaderC <- newHeaderCF
|
||||
chainHandler <- chainHandlerCF
|
||||
headerD = BlockHeaderHelper.buildNextHeader(newHeaderC)
|
||||
headerE = BlockHeaderHelper.buildNextHeader(headerD)
|
||||
chainHandlerE <- chainHandler.processHeaders(
|
||||
Vector(headerD.blockHeader, headerE.blockHeader))
|
||||
} yield (chainHandlerE, headerE)
|
||||
|
||||
val chainHandlerEF = chainHandlerEWithHeaderF.map(_._1)
|
||||
|
||||
val headerEF = chainHandlerEWithHeaderF.map(_._2)
|
||||
//now we are going to attempt to generate a block on top of B
|
||||
//we should _not_ reorg to a new best tip after adding block F ontop of B
|
||||
//the best hash should still be header E's best hash.
|
||||
|
||||
val chainHandlerFF = for {
|
||||
chainHandler <- chainHandlerEF
|
||||
headerB <- newHeaderBF
|
||||
headerF = BlockHeaderHelper.buildNextHeader(headerB)
|
||||
chainHandlerF <- chainHandler.processHeader(headerF.blockHeader)
|
||||
} yield chainHandlerF
|
||||
|
||||
for {
|
||||
chainHandlerF <- chainHandlerFF
|
||||
headerE <- headerEF
|
||||
bestHash <- chainHandlerF.getBestBlockHash
|
||||
} yield assert(bestHash == headerE.hashBE)
|
||||
}
|
||||
|
||||
final def processHeaders(
|
||||
processorF: Future[ChainHandler],
|
||||
remainingHeaders: List[BlockHeader],
|
||||
|
@ -141,4 +225,39 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
case Nil => succeed
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds two competing headers that are built from the same parent */
|
||||
private def buildCompetingHeaders(
|
||||
parent: BlockHeaderDb): (BlockHeader, BlockHeader) = {
|
||||
val newHeaderB =
|
||||
BlockHeaderHelper.buildNextHeader(parent)
|
||||
|
||||
val newHeaderC =
|
||||
BlockHeaderHelper.buildNextHeader(parent)
|
||||
|
||||
(newHeaderB.blockHeader, newHeaderC.blockHeader)
|
||||
}
|
||||
|
||||
case class ReorgFixture(
|
||||
chainApi: ChainApi,
|
||||
headerDb1: BlockHeaderDb,
|
||||
headerDb2: BlockHeaderDb,
|
||||
oldBestBlockHeader: BlockHeaderDb) {
|
||||
lazy val header1: BlockHeader = headerDb1.blockHeader
|
||||
lazy val header2: BlockHeader = headerDb2.blockHeader
|
||||
}
|
||||
|
||||
/** Builds two competing headers off of the [[ChainHandler.getBestBlockHash best chain tip]] */
|
||||
private def buildChainHandlerCompetingHeaders(
|
||||
chainHandler: ChainHandler): Future[ReorgFixture] = {
|
||||
for {
|
||||
oldBestTip <- chainHandler.getBestBlockHeader
|
||||
(newHeaderB, newHeaderC) = buildCompetingHeaders(oldBestTip)
|
||||
newChainApi <- chainHandler.processHeaders(Vector(newHeaderB, newHeaderC))
|
||||
newHeaderDbB <- newChainApi.getHeader(newHeaderB.hashBE)
|
||||
newHeaderDbC <- newChainApi.getHeader(newHeaderC.hashBE)
|
||||
} yield {
|
||||
ReorgFixture(newChainApi, newHeaderDbB.get, newHeaderDbC.get, oldBestTip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,4 +46,19 @@ trait ChainApi {
|
|||
/** Gets the hash of the block that is what we consider "best" */
|
||||
def getBestBlockHash(
|
||||
implicit ec: ExecutionContext): Future[DoubleSha256DigestBE]
|
||||
|
||||
/** Gets the best block header we have */
|
||||
def getBestBlockHeader(
|
||||
implicit ec: ExecutionContext): Future[BlockHeaderDb] = {
|
||||
for {
|
||||
hash <- getBestBlockHash
|
||||
headerOpt <- getHeader(hash)
|
||||
} yield
|
||||
headerOpt match {
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
s"We found best hash=${hash.hex} but could not retrieve the full header!!!")
|
||||
case Some(header) => header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package org.bitcoins.chain.blockchain
|
||||
|
||||
import org.bitcoins.chain.models.{BlockHeaderDAO, BlockHeaderDb}
|
||||
import org.bitcoins.chain.validation.TipUpdateResult.BadPreviousBlockHash
|
||||
import org.bitcoins.chain.validation.{TipUpdateResult, TipValidation}
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
|
||||
import scala.collection.{IndexedSeqLike, IterableLike, SeqLike, mutable}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
|
@ -19,8 +21,25 @@ import scala.concurrent.{ExecutionContext, Future}
|
|||
* }}}
|
||||
*
|
||||
*/
|
||||
case class Blockchain(headers: Vector[BlockHeaderDb]) extends BitcoinSLogger {
|
||||
case class Blockchain(headers: Vector[BlockHeaderDb])
|
||||
extends IndexedSeqLike[BlockHeaderDb, Vector[BlockHeaderDb]]
|
||||
with BitcoinSLogger {
|
||||
val tip: BlockHeaderDb = headers.head
|
||||
|
||||
/** @inheritdoc */
|
||||
override def newBuilder: mutable.Builder[
|
||||
BlockHeaderDb,
|
||||
Vector[BlockHeaderDb]] = Vector.newBuilder[BlockHeaderDb]
|
||||
|
||||
/** @inheritdoc */
|
||||
override def seq: IndexedSeq[BlockHeaderDb] = headers
|
||||
|
||||
/** @inheritdoc */
|
||||
override def length: Int = headers.length
|
||||
|
||||
/** @inheritdoc */
|
||||
override def apply(idx: Int): BlockHeaderDb = headers(idx)
|
||||
|
||||
}
|
||||
|
||||
object Blockchain extends BitcoinSLogger {
|
||||
|
@ -53,28 +72,41 @@ object Blockchain extends BitcoinSLogger {
|
|||
blockchains =>
|
||||
val nested: Vector[Future[BlockchainUpdate]] = blockchains.map {
|
||||
blockchain =>
|
||||
val tip = blockchain.tip
|
||||
logger.debug(
|
||||
s"Attempting to add new tip=${header.hashBE.hex} with prevhash=${header.previousBlockHashBE.hex} to chain with current tips=${tip.hashBE.hex}")
|
||||
val tipResultF = TipValidation.checkNewTip(newPotentialTip = header,
|
||||
currentTip = tip,
|
||||
blockHeaderDAO =
|
||||
blockHeaderDAO)
|
||||
val prevBlockHeaderOpt =
|
||||
blockchain.find(_.hashBE == header.previousBlockHashBE)
|
||||
prevBlockHeaderOpt match {
|
||||
case None =>
|
||||
logger.debug(
|
||||
s"No common ancestor found in the chain to connect to ${header.hashBE}")
|
||||
val err = TipUpdateResult.BadPreviousBlockHash(header)
|
||||
val failed = BlockchainUpdate.Failed(blockchain = blockchain,
|
||||
failedHeader = header,
|
||||
tipUpdateFailure = err)
|
||||
Future.successful(failed)
|
||||
|
||||
tipResultF.map { tipResult =>
|
||||
tipResult match {
|
||||
case TipUpdateResult.Success(headerDb) =>
|
||||
val newChain =
|
||||
Blockchain.fromHeaders(headerDb +: blockchain.headers)
|
||||
BlockchainUpdate.Successful(newChain, headerDb)
|
||||
case fail: TipUpdateResult.Failure =>
|
||||
BlockchainUpdate.Failed(blockchain, header, fail)
|
||||
}
|
||||
case Some(prevBlockHeader) =>
|
||||
//found a header to connect to!
|
||||
logger.debug(
|
||||
s"Attempting to add new tip=${header.hashBE.hex} with prevhash=${header.previousBlockHashBE.hex} to chain")
|
||||
val tipResultF =
|
||||
TipValidation.checkNewTip(newPotentialTip = header,
|
||||
currentTip = prevBlockHeader,
|
||||
blockHeaderDAO = blockHeaderDAO)
|
||||
|
||||
tipResultF.map { tipResult =>
|
||||
tipResult match {
|
||||
case TipUpdateResult.Success(headerDb) =>
|
||||
val newChain =
|
||||
Blockchain.fromHeaders(headerDb +: blockchain.headers)
|
||||
BlockchainUpdate.Successful(newChain, headerDb)
|
||||
case fail: TipUpdateResult.Failure =>
|
||||
BlockchainUpdate.Failed(blockchain, header, fail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parseSuccessOrFailure(nested = nested)
|
||||
}
|
||||
|
||||
tipResultF
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ case class ChainHandler(
|
|||
val createdF = blockHeaderDAO.create(updatedHeader)
|
||||
createdF.map { header =>
|
||||
logger.debug(
|
||||
s"Connected new header to blockchain, heigh=${header.height} hash=${header.hashBE}")
|
||||
s"Connected new header to blockchain, height=${header.height} hash=${header.hashBE}")
|
||||
ChainHandler(blockHeaderDAO, chainConfig)
|
||||
}
|
||||
case BlockchainUpdate.Failed(_, _, reason) =>
|
||||
|
|
Loading…
Add table
Reference in a new issue