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:
Chris Stewart 2019-07-08 08:33:45 -05:00 committed by GitHub
parent d73322a76e
commit d00dff5645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 20 deletions

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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) =>