2020 03 15 chainhandler getnancestor (#1247)

* Rename BlockHeaderDAO.getNChildren() to BlockHeaderDAO.getNAncestor() as that is what it actually does

* This changes the behavior of BlockHeaderDAO.getNAncestors from the previous implementation which just fetched headers in between two heights, to actually validating the headers that are fetched from the database with Blockchain.reconstructFromheaders()

* Run scalafmt

* Replace BlockHeader.getNAncestors call to Blockchain.reconstructFromHeaders() with Blockchain.connectWalkBackwards() so that we don't do full POW validation from the database, just make sure the headers connect to avoid reorg problems. Also add some more informative logs

* Address Roman's code review
This commit is contained in:
Chris Stewart 2020-03-18 05:51:31 -05:00 committed by GitHub
parent 277d62591f
commit 800fdffffb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 198 additions and 23 deletions

View file

@ -1,23 +1,25 @@
package org.bitcoins.chain.blockchain
import akka.actor.ActorSystem
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.chain.models.{BlockHeaderDAO, BlockHeaderDb}
import org.bitcoins.testkit.chain.fixture.ChainFixture
import org.bitcoins.testkit.chain.{BlockHeaderHelper, ChainUnitTest}
import org.scalatest.FutureOutcome
class BlockchainTest extends ChainUnitTest {
import scala.collection.mutable
override type FixtureParam = BlockHeaderDAO
class BlockchainTest extends ChainUnitTest {
override type FixtureParam = ChainFixture
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
withBlockHeaderDAO(test)
withChainFixture(test)
implicit override val system: ActorSystem = ActorSystem("BlockchainTest")
behavior of "Blockchain"
it must "connect a new header to the current tip of a blockchain" in {
bhDAO: BlockHeaderDAO =>
it must "connect a new header to the current tip of a blockchain" inFixtured {
case ChainFixture.Empty =>
val blockchain = Blockchain.fromHeaders(
headers = Vector(ChainUnitTest.genesisHeaderDb)
)
@ -36,4 +38,43 @@ class BlockchainTest extends ChainUnitTest {
assert(false)
}
}
it must "reconstruct a blockchain given a child header correctly" inFixtured {
case ChainFixture.Empty =>
val accum = new mutable.ArrayBuffer[BlockHeaderDb](5)
accum.+=(ChainUnitTest.genesisHeaderDb)
//generate 4 headers
0.until(4).foreach { _ =>
val newHeader = BlockHeaderHelper.buildNextHeader(accum.last)
accum.+=(newHeader)
}
//now given the last header, and the other headers we should reconstruct the blockchain
val headers = accum.dropRight(1).toVector
val tip = accum.last
val reconstructed = Blockchain.reconstructFromHeaders(childHeader = tip,
ancestors = headers)
assert(reconstructed.length == 1)
val chain = reconstructed.head
assert(chain.toVector.length == 5)
assert(chain.tip == accum.last)
assert(chain.last == ChainUnitTest.genesisHeaderDb)
assert(chain.toVector == accum.reverse.toVector)
}
it must "fail to reconstruct a blockchain if we do not have validly connected headers" inFixtured {
case ChainFixture.Empty =>
val missingHeader =
BlockHeaderHelper.buildNextHeader(ChainUnitTest.genesisHeaderDb)
val thirdHeader = BlockHeaderHelper.buildNextHeader(missingHeader)
val reconstructed =
Blockchain.reconstructFromHeaders(thirdHeader,
Vector(ChainUnitTest.genesisHeaderDb))
assert(reconstructed.isEmpty)
}
}

View file

@ -1,6 +1,7 @@
package org.bitcoins.chain.models
import akka.actor.ActorSystem
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.testkit.chain.{BlockHeaderHelper, ChainUnitTest}
import org.scalatest.FutureOutcome
@ -201,4 +202,53 @@ class BlockHeaderDAOTest extends ChainUnitTest {
assert(genesisOpt.contains(genesisHeaderDb))
}
}
it must "implement getNAncestors correctly" in {
blockHeaderDAO: BlockHeaderDAO =>
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
val createdF = blockHeaderDAO.create(blockHeader)
val noGenesisAncestorsF =
blockHeaderDAO.getNAncestors(childHash =
genesisHeaderDb.blockHeader.hashBE,
n = 1)
val foundGenesisF =
blockHeaderDAO.getNAncestors(childHash =
genesisHeaderDb.blockHeader.hashBE,
n = 0)
val emptyAssertion = for {
noGenesisAncestors <- noGenesisAncestorsF
foundGenesis <- foundGenesisF
} yield {
assert(noGenesisAncestors.length == 1)
assert(noGenesisAncestors == Vector(ChainUnitTest.genesisHeaderDb))
assert(foundGenesis.length == 1)
assert(foundGenesis == Vector(ChainUnitTest.genesisHeaderDb))
}
val oneChildF = for {
created <- createdF
children <- blockHeaderDAO.getNAncestors(childHash =
created.blockHeader.hashBE,
n = 1)
} yield {
assert(children.length == 2)
assert(children == Vector(created, genesisHeaderDb))
}
val bashHash = DoubleSha256DigestBE.empty
val hashDoesNotExistF = for {
children <- blockHeaderDAO.getNAncestors(childHash = bashHash, n = 1)
} yield {
assert(children.isEmpty)
}
for {
_ <- emptyAssertion
_ <- oneChildF
lastAssert <- hashDoesNotExistF
} yield lastAssert
}
}

View file

@ -118,7 +118,7 @@ private[blockchain] trait BaseBlockChainCompObject
//found a header to connect to!
val prevBlockHeader = blockchain.headers(prevHeaderIdx)
logger.debug(
s"Attempting to add new tip=${header.hashBE.hex} with prevhash=${header.previousBlockHashBE.hex} to chain")
s"Attempting to add new tip=${header.hashBE.hex} with prevhash=${header.previousBlockHashBE.hex} to chain of ${blockchain.length} headers")
val chain = blockchain.fromValidHeader(prevBlockHeader)
val tipResult =
TipValidation.checkNewTip(newPotentialTip = header, chain)
@ -234,4 +234,62 @@ private[blockchain] trait BaseBlockChainCompObject
}
}
/** Walks backwards from the current header searching through ancestors if [[current.previousBlockHashBE]] is in [[ancestors]]
* This does not validate other things such as POW.
* */
@tailrec
final def connectWalkBackwards(
current: BlockHeaderDb,
ancestors: Vector[BlockHeaderDb],
accum: Vector[BlockHeaderDb] = Vector.empty)(
implicit chainAppConfig: ChainAppConfig): Vector[BlockHeaderDb] = {
val prevHeaderOpt = ancestors.find(_.hashBE == current.previousBlockHashBE)
prevHeaderOpt match {
case Some(h) =>
connectWalkBackwards(current = h,
accum = current +: accum,
ancestors = ancestors)
case None =>
logger.debug(s"No prev found for $current hashBE=${current.hashBE}")
current +: accum
}
}
/** Walks backwards from a child header reconstructing a blockchain
* This validates things like POW, difficulty change etc.
* */
def reconstructFromHeaders(
childHeader: BlockHeaderDb,
ancestors: Vector[BlockHeaderDb])(
implicit chainAppConfig: ChainAppConfig): Vector[Blockchain] = {
//now all hashes are connected correctly forming a
//valid blockchain in term of hashes connected to each other
val orderedHeaders = connectWalkBackwards(current = childHeader,
accum = Vector.empty,
ancestors = ancestors)
val initBlockchainOpt = orderedHeaders match {
case Vector() | _ +: Vector() =>
//for the case of _ +: Vector() this means only our
//child header is in the chain, which means we
//weren't able to form a blockchain
None
case h +: _ =>
//find our first header as we need it's Db representation
//rather than just the raw header
val dbOpt = ancestors.find(_.hashBE == h.hashBE)
Some(Blockchain.fromHeaders(Vector(dbOpt.get)))
}
//now let's connect headers
val blockchainUpdateOpt = initBlockchainOpt.map { initBlockchain =>
Blockchain.connectHeadersToChains(orderedHeaders.tail.map(_.blockHeader),
Vector(initBlockchain))
}
blockchainUpdateOpt match {
case Some(v) => v.map(_.blockchain)
case None => Vector.empty
}
}
}

View file

@ -195,7 +195,7 @@ case class ChainHandler(
val filterHeadersToCreateF: Future[Vector[CompactFilterHeaderDb]] = for {
blockHeaders <- blockHeaderDAO
.getNChildren(ancestorHash = stopHash, n = filterHeaders.size - 1)
.getNAncestors(childHash = stopHash, n = filterHeaders.size - 1)
.map(_.sortBy(_.height))
} yield {
if (blockHeaders.size != filterHeaders.size) {

View file

@ -141,20 +141,29 @@ case class BlockHeaderDAO()(
}
}
/** Gets Block Headers of all children starting with the given block hash (inclusive), could be out of order */
def getNChildren(
ancestorHash: DoubleSha256DigestBE,
/** Gets ancestor block headers starting with the given block hash (inclusive)
* These headers are guaranteed to be in order and a valid chain.
* */
def getNAncestors(
childHash: DoubleSha256DigestBE,
n: Int): Future[Vector[BlockHeaderDb]] = {
logger.debug(s"Getting $n ancestors for blockhash=$childHash")
val headerOptF = findByHash(childHash)
for {
headerOpt <- findByHash(ancestorHash)
res <- headerOpt match {
headerOpt <- headerOptF
headers <- headerOpt match {
case Some(header) =>
getBetweenHeights(Math.max(header.height - n, 0), header.height)
val startHeight = Math.max(header.height - n, 0)
getBetweenHeights(startHeight, header.height)
case None =>
Future.successful(Vector.empty)
}
} yield {
res
headerOpt.map { header =>
val connectedHeaders =
Blockchain.connectWalkBackwards(header, headers)
connectedHeaders.reverse
}.getOrElse(Vector.empty)
}
}

View file

@ -1,10 +1,10 @@
package org.bitcoins.chain.pow
import org.bitcoins.chain.blockchain.Blockchain
import org.bitcoins.chain.models.{BlockHeaderDb}
import org.bitcoins.core.number.UInt32
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.core.protocol.blockchain.{BlockHeader, ChainParams}
import org.bitcoins.chain.models.BlockHeaderDb
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain._
import org.bitcoins.core.util.NumberUtil
/**
@ -42,10 +42,16 @@ sealed abstract class Pow {
nonMinDiffF match {
case Some(bh) => bh.nBits
case None =>
//if we can't find a non min diffulty block, let's just fail
throw new RuntimeException(
s"Could not find non mindiffulty block in chain! hash=${tip.hashBE.hex} height=${currentHeight}")
case None =>
config.chain match {
case RegTestNetChainParams =>
RegTestNetChainParams.compressedPowLimit
case TestNetChainParams | MainNetChainParams =>
//if we can't find a non min diffulty block, let's just fail
throw new RuntimeException(
s"Could not find non mindifficulty block in chain of size=${blockchain.length}! hash=${tip.hashBE.hex} height=${currentHeight}")
}
}
}
} else {

View file

@ -1047,6 +1047,17 @@ case class CompactFilterCheckPointMessage(
def bytes: ByteVector =
RawCompactFilterCheckpointMessageSerializer.write(this)
override def toString: String = {
val headersString = {
if (filterHeaders.isEmpty) {
"empty"
} else {
s"${filterHeaders.head}...${filterHeaders.last}"
}
}
s"CompactFilterCheckPointMessage(filterType=${filterType}, stopHash=${stopHash}, filterHeaders=${headersString})"
}
}
object CompactFilterCheckPointMessage

View file

@ -25,7 +25,7 @@ object BytesToPushOntoStack
override val opCode = num
}
override def operations: Seq[BytesToPushOntoStack] =
override val operations: Seq[BytesToPushOntoStack] =
(for { i <- 0 to 75 } yield BytesToPushOntoStackImpl(i))
def fromNumber(num: Long): BytesToPushOntoStack = {