diff --git a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala index 2d6eab58ea..c4730607e6 100644 --- a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala +++ b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala @@ -1,10 +1,14 @@ package org.bitcoins.chain.blockchain -import org.bitcoins.chain.{ChainCallbacks, OnBlockHeaderConnected} import org.bitcoins.chain.pow.Pow +import org.bitcoins.chain.{ChainCallbacks, OnBlockHeaderConnected} import org.bitcoins.core.api.chain.ChainApi import org.bitcoins.core.api.chain.ChainQueryApi.FilterResponse -import org.bitcoins.core.api.chain.db.{BlockHeaderDb, BlockHeaderDbHelper} +import org.bitcoins.core.api.chain.db.{ + BlockHeaderDb, + BlockHeaderDbHelper, + CompactFilterHeaderDb +} import org.bitcoins.core.gcs.{BlockFilter, FilterHeader} import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.p2p.CompactFilterMessage @@ -244,6 +248,75 @@ class ChainHandlerTest extends ChainDbUnitTest { } } + it must "verify a batch of filter headers" in { chainHandler: ChainHandler => + val goodBatch = Vector( + ChainUnitTest.genesisFilterHeaderDb, + CompactFilterHeaderDb( + hashBE = DoubleSha256DigestBE.fromHex( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"), + previousFilterHeaderBE = ChainUnitTest.genesisFilterHeaderDb.hashBE, + height = 1, + filterHashBE = DoubleSha256DigestBE.fromHex( + "555152535455565758595a5b5c5d5e5f555152535455565758595a5b5c5d5e5f"), + blockHashBE = DoubleSha256DigestBE.fromHex( + "aaa1a2a3a4a5a6a7a8a9aaabacadaeafaaa1a2a3a4a5a6a7a8a9aaabacadaeaf") + ) + ) + + val invalidGenesisFilterHeaderBatch = Vector( + ChainUnitTest.genesisFilterHeaderDb.copy( + hashBE = ChainUnitTest.genesisFilterHeaderDb.previousFilterHeaderBE, + previousFilterHeaderBE = ChainUnitTest.genesisFilterHeaderDb.hashBE + ) + ) + + val invalidFilterHeaderBatch = Vector( + ChainUnitTest.genesisFilterHeaderDb.copy(height = 1) + ) + + val selfReferenceFilterHeaderBatch = Vector( + ChainUnitTest.genesisFilterHeaderDb, + CompactFilterHeaderDb( + hashBE = DoubleSha256DigestBE.fromHex( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"), + previousFilterHeaderBE = DoubleSha256DigestBE.fromHex( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"), + height = 1, + filterHashBE = DoubleSha256DigestBE.fromHex( + "555152535455565758595a5b5c5d5e5f555152535455565758595a5b5c5d5e5f"), + blockHashBE = DoubleSha256DigestBE.fromHex( + "aaa1a2a3a4a5a6a7a8a9aaabacadaeafaaa1a2a3a4a5a6a7a8a9aaabacadaeaf") + ) + ) + + val unknownFilterHeaderBatch = Vector( + ChainUnitTest.genesisFilterHeaderDb, + CompactFilterHeaderDb( + hashBE = DoubleSha256DigestBE.fromHex( + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"), + previousFilterHeaderBE = DoubleSha256DigestBE.fromHex( + "555152535455565758595a5b5c5d5e5f555152535455565758595a5b5c5d5e5f"), + height = 1, + filterHashBE = DoubleSha256DigestBE.fromHex( + "555152535455565758595a5b5c5d5e5f555152535455565758595a5b5c5d5e5f"), + blockHashBE = DoubleSha256DigestBE.fromHex( + "aaa1a2a3a4a5a6a7a8a9aaabacadaeafaaa1a2a3a4a5a6a7a8a9aaabacadaeaf") + ) + ) + + for { + _ <- chainHandler.verifyFilterHeaders(goodBatch) + _ <- recoverToSucceededIf[IllegalArgumentException]( + chainHandler.verifyFilterHeaders(invalidGenesisFilterHeaderBatch)) + _ <- recoverToSucceededIf[IllegalArgumentException]( + chainHandler.verifyFilterHeaders(invalidFilterHeaderBatch)) + _ <- recoverToSucceededIf[IllegalArgumentException]( + chainHandler.verifyFilterHeaders(selfReferenceFilterHeaderBatch)) + _ <- recoverToSucceededIf[IllegalArgumentException]( + chainHandler.verifyFilterHeaders(unknownFilterHeaderBatch)) + } yield succeed + } + it must "get the highest filter" in { chainHandler: ChainHandler => { for { diff --git a/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala b/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala index 2717b013ce..fcb5bf5abb 100644 --- a/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala +++ b/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala @@ -352,27 +352,7 @@ class ChainHandler( for { filterHeadersToCreate <- filterHeadersToCreateF - _ <- - if ( - filterHeadersToCreate.nonEmpty && filterHeadersToCreate.head.height > 0 - ) { - val firstFilter = filterHeadersToCreate.head - val filterHashFOpt = filterHeaderDAO - .findByHash(firstFilter.previousFilterHeaderBE) - filterHashFOpt.map { - case Some(prevHeader) => - require( - prevHeader.height == firstFilter.height - 1, - s"Unexpected previous header's height: ${prevHeader.height} != ${filterHeadersToCreate.head.height - 1}" - ) - case None => - // If the previous filter header doesn't exist it must be for the genesis block - require( - firstFilter.previousFilterHeaderBE == DoubleSha256DigestBE.empty && firstFilter.height == 0, - s"Previous filter header does not exist: $firstFilter" - ) - } - } else Future.unit + _ <- verifyFilterHeaders(filterHeadersToCreate) _ <- filterHeaderDAO.createAll(filterHeadersToCreate) } yield { val minHeightOpt = filterHeadersToCreate.minByOption(_.height) @@ -437,6 +417,66 @@ class ChainHandler( } } + /** Verifies if the previous headers exist either in the batch [[filterHeaders]] + * or in the database, throws if it doesn't + */ + def verifyFilterHeaders( + filterHeaders: Vector[CompactFilterHeaderDb]): Future[Unit] = { + val byHash = filterHeaders.foldLeft( + Map.empty[DoubleSha256DigestBE, CompactFilterHeaderDb])((acc, fh) => + acc.updated(fh.hashBE, fh)) + val verify = checkFilterHeader(byHash)(_) + FutureUtil.sequentially(filterHeaders)(verify).map(_ => ()) + } + + private def checkFilterHeader( + filtersByHash: Map[DoubleSha256DigestBE, CompactFilterHeaderDb])( + filterHeader: CompactFilterHeaderDb): Future[Unit] = { + + def checkHeight( + filterHeader: CompactFilterHeaderDb, + prevHeader: CompactFilterHeaderDb): Unit = { + require( + prevHeader.height == filterHeader.height - 1, + s"Unexpected previous filter header's height: ${prevHeader.height} != ${filterHeader.height - 1}" + ) + } + + if (filterHeader.hashBE == filterHeader.previousFilterHeaderBE) { + Future.failed( + new IllegalArgumentException( + s"Filter header cannot reference to itself: ${filterHeader}")) + } else if (filterHeader.height == 0) { + Future { + require( + filterHeader.previousFilterHeaderBE == DoubleSha256DigestBE.empty, + s"Previous filter header hash for the genesis block must be empty: ${filterHeader}") + } + } else { + if (filterHeader.previousFilterHeaderBE == DoubleSha256DigestBE.empty) { + Future.failed(new IllegalArgumentException( + s"Previous filter header hash for a regular block must not be empty: ${filterHeader}")) + } else { + filtersByHash.get(filterHeader.previousFilterHeaderBE) match { + case Some(prevHeader) => + Future { + checkHeight(filterHeader, prevHeader) + } + case None => + val filterHashFOpt = filterHeaderDAO + .findByHash(filterHeader.previousFilterHeaderBE) + filterHashFOpt.map { + case Some(prevHeader) => + checkHeight(filterHeader, prevHeader) + case None => + throw new IllegalArgumentException( + s"Previous filter header does not exist: $filterHeader") + } + } + } + } + } + private def findFilterDbFromMessage( filterHeader: CompactFilterHeaderDb, messagesByBlockHash: Map[