Rework NodeTestUtil to use a specific bestBlockHash (#5332)

* Rework NodeTestUtil to use a specific bestBlockHash, this is useful in reorg situations

Use param in reorg test, modify scaladoc

WIP

* Cleanup

* Try to fix usage of stopHashBE

* Cleanup

* WIP: Fix getCompactFilterStartHeight()

* Revert logback-test.xml

* Fix bug with isFiltersSynced() in reorg situations

* scalafmt, fix compile

* Fix another bug with isFiltersSynced()

* Fix compile
This commit is contained in:
Chris Stewart 2023-12-31 17:38:12 -06:00 committed by GitHub
parent 5e81ec5ed2
commit b39736fb8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 47 deletions

View File

@ -1181,6 +1181,8 @@ case class CompactFilterHeadersMessage(
filterHashes: Vector[DoubleSha256Digest]) filterHashes: Vector[DoubleSha256Digest])
extends DataPayload { extends DataPayload {
val stopHashBE: DoubleSha256DigestBE = stopHash.flip
/** The number of hashes in this message */ /** The number of hashes in this message */
val filterHashesLength: CompactSizeUInt = CompactSizeUInt( val filterHashesLength: CompactSizeUInt = CompactSizeUInt(
UInt64(filterHashes.length)) UInt64(filterHashes.length))

View File

@ -389,20 +389,26 @@ class NeutrinoNodeTest extends NodeTestWithCachedBitcoindPair {
val bitcoind0 = bitcoinds(0) val bitcoind0 = bitcoinds(0)
val bitcoind1 = bitcoinds(1) val bitcoind1 = bitcoinds(1)
for { for {
_ <- NodeTestUtil.awaitAllSync(node, bitcoind0) _ <- NodeTestUtil.awaitSyncAndIBD(node, bitcoind0)
//disconnect bitcoind1 as we don't need it //disconnect bitcoind1 as we don't need it
nodeUri1 <- NodeTestUtil.getNodeURIFromBitcoind(bitcoind1) nodeUri1 <- NodeTestUtil.getNodeURIFromBitcoind(bitcoind1)
_ <- bitcoind1.disconnectNode(nodeUri1) _ <- bitcoind1.disconnectNode(nodeUri1)
bestBlockHash0 <- bitcoind0.getBestBlockHash() bestBlockHash0 <- bitcoind0.getBestBlockHash()
//invalidate blockhash to force a reorg when next block is generated
_ <- bitcoind0.invalidateBlock(bestBlockHash0) _ <- bitcoind0.invalidateBlock(bestBlockHash0)
_ <- AsyncUtil.retryUntilSatisfiedF(
() => node.getConnectionCount.map(_ == 1),
1.second)
//now generate a block, make sure we sync with them //now generate a block, make sure we sync with them
hashes <- bitcoind0.generate(1) hashes0 <- bitcoind0.generate(1)
chainApi <- node.chainApiFromDb() chainApi <- node.chainApiFromDb()
_ <- AsyncUtil.retryUntilSatisfiedF(() => _ <- AsyncUtil.retryUntilSatisfiedF(() =>
chainApi.getHeader(hashes.head).map(_.isDefined)) chainApi.getHeader(hashes0.head).map(_.isDefined))
//generate another block to make sure the reorg is complete //generate another block to make sure the reorg is complete
_ <- bitcoind0.generate(1) hashes1 <- bitcoind0.generate(1)
_ <- NodeTestUtil.awaitAllSync(node, bitcoind0) _ <- NodeTestUtil.awaitAllSync(node = node,
bitcoind = bitcoind0,
bestBlockHashBE = Some(hashes1.head))
} yield succeed } yield succeed
} }
} }

View File

@ -74,7 +74,7 @@ case class PeerManager(
private def syncCompactFilters( private def syncCompactFilters(
bestFilterHeader: CompactFilterHeaderDb, bestFilterHeader: CompactFilterHeaderDb,
chainApi: ChainApi, chainApi: ChainApi,
compactFilterStartHeight: Int, compactFilterStartHeightOpt: Option[Int],
nodeState: SyncNodeState)(implicit nodeState: SyncNodeState)(implicit
chainAppConfig: ChainAppConfig): Future[Unit] = { chainAppConfig: ChainAppConfig): Future[Unit] = {
val syncPeer = nodeState.syncPeer val syncPeer = nodeState.syncPeer
@ -97,15 +97,13 @@ case class PeerManager(
sendCompactFilterHeaderMsgF.flatMap { isSyncFilterHeaders => sendCompactFilterHeaderMsgF.flatMap { isSyncFilterHeaders =>
// If we have started syncing filters // If we have started syncing filters
if ( if (!isSyncFilterHeaders) {
!isSyncFilterHeaders && compactFilterStartHeight < bestFilterHeader.height
) {
PeerManager PeerManager
.sendNextGetCompactFilterCommand( .sendNextGetCompactFilterCommand(
peerMessageSenderApi = peerMsgSender, peerMessageSenderApi = peerMsgSender,
chainApi = chainApi, chainApi = chainApi,
filterBatchSize = chainAppConfig.filterBatchSize, filterBatchSize = chainAppConfig.filterBatchSize,
startHeightOpt = Some(compactFilterStartHeight), startHeightOpt = compactFilterStartHeightOpt,
stopBlockHash = bestFilterHeader.blockHashBE, stopBlockHash = bestFilterHeader.blockHashBE,
peer = syncPeer peer = syncPeer
) )
@ -927,19 +925,19 @@ case class PeerManager(
} else { } else {
syncCompactFilters(bestFilterHeader = bestFilterHeader, syncCompactFilters(bestFilterHeader = bestFilterHeader,
chainApi = chainApi, chainApi = chainApi,
compactFilterStartHeight = bestFilter.height, compactFilterStartHeightOpt = None,
nodeState = nodeState) nodeState = nodeState)
} }
case (Some(bestFilterHeader), None) => case (Some(bestFilterHeader), None) =>
val compactFilterStartHeightF = val compactFilterStartHeightOptF =
PeerManager.getCompactFilterStartHeight(chainApi, PeerManager.getCompactFilterStartHeight(chainApi,
walletCreationTimeOpt) walletCreationTimeOpt)
for { for {
compactFilterStartHeight <- compactFilterStartHeightF compactFilterStartHeightOpt <- compactFilterStartHeightOptF
_ <- syncCompactFilters(bestFilterHeader = bestFilterHeader, _ <- syncCompactFilters(bestFilterHeader = bestFilterHeader,
chainApi = chainApi, chainApi = chainApi,
compactFilterStartHeight = compactFilterStartHeightOpt =
compactFilterStartHeight, compactFilterStartHeightOpt,
nodeState = nodeState) nodeState = nodeState)
} yield () } yield ()
@ -1112,11 +1110,10 @@ object PeerManager extends Logging {
chainApi.nextFilterHeaderBatchRange(stopBlockHash = stopBlockHash, chainApi.nextFilterHeaderBatchRange(stopBlockHash = stopBlockHash,
batchSize = filterBatchSize, batchSize = filterBatchSize,
startHeightOpt = startHeightOpt) startHeightOpt = startHeightOpt)
_ = logger.info(
s"Requesting compact filters from $filterSyncMarkerOpt with peer=$peer startHeightOpt=$startHeightOpt stopBlockHashBE=$stopBlockHash")
res <- filterSyncMarkerOpt match { res <- filterSyncMarkerOpt match {
case Some(filterSyncMarker) => case Some(filterSyncMarker) =>
logger.info(
s"Requesting compact filters from $filterSyncMarker with peer=$peer")
peerMessageSenderApi peerMessageSenderApi
.sendGetCompactFiltersMessage(filterSyncMarker) .sendGetCompactFiltersMessage(filterSyncMarker)
.map(_ => true) .map(_ => true)
@ -1167,11 +1164,11 @@ object PeerManager extends Logging {
def getCompactFilterStartHeight( def getCompactFilterStartHeight(
chainApi: ChainApi, chainApi: ChainApi,
walletCreationTimeOpt: Option[Instant])(implicit walletCreationTimeOpt: Option[Instant])(implicit
ec: ExecutionContext): Future[Int] = { ec: ExecutionContext): Future[Option[Int]] = {
chainApi.getBestFilter().flatMap { chainApi.getBestFilter().flatMap {
case Some(f) => case Some(_) =>
//we have already started syncing filters, return the height of the last filter seen //we have already started syncing filters, return the height of the last filter seen
Future.successful(f.height + 1) Future.successful(None)
case None => case None =>
walletCreationTimeOpt match { walletCreationTimeOpt match {
case Some(instant) => case Some(instant) =>
@ -1189,10 +1186,11 @@ object PeerManager extends Logging {
//want to choose the maximum out of these too //want to choose the maximum out of these too
//if our internal chainstate filter count is > creationTimeHeight //if our internal chainstate filter count is > creationTimeHeight
//we just want to start syncing from our last seen filter //we just want to start syncing from our last seen filter
Math.max(height, filterCount) val result = Math.max(height, filterCount)
Some(result)
} }
case None => case None =>
Future.successful(0) Future.successful(None)
} }
} }

View File

@ -182,9 +182,10 @@ case class DataMessageHandler(
for { for {
sortedBlockFilters <- sortedBlockFiltersF sortedBlockFilters <- sortedBlockFiltersF
sortedFilterMessages = sortedBlockFilters.map(_._2) sortedFilterMessages = sortedBlockFilters.map(_._2)
filterBestBlockHashBE = sortedFilterMessages.lastOption
.map(_.blockHashBE)
_ = logger.debug( _ = logger.debug(
s"Processing ${filterBatch.size} filters bestBlockHashBE=${sortedFilterMessages.lastOption s"Processing ${filterBatch.size} filters bestBlockHashBE=${filterBestBlockHashBE}")
.map(_.blockHashBE)}")
newChainApi <- chainApi.processFilters(sortedFilterMessages) newChainApi <- chainApi.processFilters(sortedFilterMessages)
sortedGolombFilters = sortedBlockFilters.map(x => sortedGolombFilters = sortedBlockFilters.map(x =>
(x._1, x._3)) (x._1, x._3))
@ -524,7 +525,7 @@ case class DataMessageHandler(
private def sendFirstGetCompactFilterCommand( private def sendFirstGetCompactFilterCommand(
peerMessageSenderApi: PeerMessageSenderApi, peerMessageSenderApi: PeerMessageSenderApi,
stopBlockHash: DoubleSha256DigestBE, stopBlockHash: DoubleSha256DigestBE,
startHeight: Int, startHeightOpt: Option[Int],
syncNodeState: SyncNodeState): Future[Option[NodeState.FilterSync]] = { syncNodeState: SyncNodeState): Future[Option[NodeState.FilterSync]] = {
logger.info(s"Beginning to sync filters to stopBlockHashBE=$stopBlockHash") logger.info(s"Beginning to sync filters to stopBlockHashBE=$stopBlockHash")
@ -538,7 +539,7 @@ case class DataMessageHandler(
} }
sendNextGetCompactFilterCommand(peerMessageSenderApi = peerMessageSenderApi, sendNextGetCompactFilterCommand(peerMessageSenderApi = peerMessageSenderApi,
startHeightOpt = Some(startHeight), startHeightOpt = startHeightOpt,
stopBlockHash = stopBlockHash, stopBlockHash = stopBlockHash,
fs = fs) fs = fs)
} }
@ -756,7 +757,9 @@ case class DataMessageHandler(
val recoveredStateF: Future[NodeState] = getHeadersF.recoverWith { val recoveredStateF: Future[NodeState] = getHeadersF.recoverWith {
case _: DuplicateHeaders => case _: DuplicateHeaders =>
logger.warn(s"Received duplicate headers from ${peer} in state=$state") logger.warn(s"Received duplicate headers from ${peer} in state=$state")
Future.successful(headerSyncState) val d = DoneSyncing(headerSyncState.peersWithServices,
headerSyncState.waitingForDisconnection)
Future.successful(d)
case _: InvalidBlockHeader => case _: InvalidBlockHeader =>
logger.warn( logger.warn(
s"Invalid headers of count $count sent from ${peer} in state=$state") s"Invalid headers of count $count sent from ${peer} in state=$state")
@ -792,8 +795,7 @@ case class DataMessageHandler(
val blockCountF = chainApi.getBlockCount() val blockCountF = chainApi.getBlockCount()
val bestBlockHashF = chainApi.getBestBlockHash() val bestBlockHashF = chainApi.getBestBlockHash()
for { for {
_ <- chainApi.processFilterHeaders(filterHeaders, _ <- chainApi.processFilterHeaders(filterHeaders, filterHeader.stopHashBE)
filterHeader.stopHash.flip)
filterHeaderCount <- chainApi.getFilterHeaderCount() filterHeaderCount <- chainApi.getFilterHeaderCount()
blockCount <- blockCountF blockCount <- blockCountF
bestBlockHash <- bestBlockHashF bestBlockHash <- bestBlockHashF
@ -804,18 +806,17 @@ case class DataMessageHandler(
sendNextGetCompactFilterHeadersCommand( sendNextGetCompactFilterHeadersCommand(
peerMessageSenderApi = peerMessageSenderApi, peerMessageSenderApi = peerMessageSenderApi,
syncPeer = peer, syncPeer = peer,
prevStopHash = filterHeader.stopHash.flip, prevStopHash = filterHeader.stopHashBE,
stopHash = bestBlockHash).map(_ => filterHeaderSync) stopHash = bestBlockHash).map(_ => filterHeaderSync)
} else { } else {
for { for {
startHeight <- PeerManager.getCompactFilterStartHeight( startHeightOpt <- PeerManager.getCompactFilterStartHeight(
chainApi, chainApi,
walletCreationTimeOpt) walletCreationTimeOpt)
bestBlockHash <- bestBlockHashF
filterSyncStateOpt <- sendFirstGetCompactFilterCommand( filterSyncStateOpt <- sendFirstGetCompactFilterCommand(
peerMessageSenderApi = peerMessageSenderApi, peerMessageSenderApi = peerMessageSenderApi,
stopBlockHash = bestBlockHash, stopBlockHash = filterHeader.stopHashBE,
startHeight = startHeight, startHeightOpt = startHeightOpt,
syncNodeState = filterHeaderSync) syncNodeState = filterHeaderSync)
} yield { } yield {
filterSyncStateOpt match { filterSyncStateOpt match {

View File

@ -79,6 +79,29 @@ abstract class NodeTestUtil extends P2PLogger {
} }
} }
private def isSameBestFilter(
node: NeutrinoNode,
rpc: BitcoindRpcClient,
bestBlockHashBEOpt: Option[DoubleSha256DigestBE])(implicit
ec: ExecutionContext): Future[Boolean] = {
val bestBlockHashBEF = bestBlockHashBEOpt match {
case Some(bestBlockHash) => Future.successful(bestBlockHash)
case None => rpc.getBestBlockHash()
}
for {
bestBlockHashBE <- bestBlockHashBEF
chainApi <- node.chainApiFromDb()
bestFilterOpt <- chainApi.getBestFilter()
} yield {
bestFilterOpt match {
case Some(bestFilter) => bestFilter.blockHashBE == bestBlockHashBE
case None => false
}
}
}
def isSameBestFilterHeight(node: NeutrinoNode, rpc: BitcoindRpcClient)( def isSameBestFilterHeight(node: NeutrinoNode, rpc: BitcoindRpcClient)(
implicit ec: ExecutionContext): Future[Boolean] = { implicit ec: ExecutionContext): Future[Boolean] = {
val rpcCountF = rpc.getBlockCount() val rpcCountF = rpc.getBlockCount()
@ -137,24 +160,35 @@ abstract class NodeTestUtil extends P2PLogger {
} }
/** Awaits sync between the given node and bitcoind client */ /** Awaits sync between the given node and bitcoind client */
def awaitCompactFiltersSync(node: NeutrinoNode, rpc: BitcoindRpcClient)( def awaitCompactFiltersSync(
implicit sys: ActorSystem): Future[Unit] = { node: NeutrinoNode,
rpc: BitcoindRpcClient,
bestBlockHashBEOpt: Option[DoubleSha256DigestBE] = None)(implicit
sys: ActorSystem): Future[Unit] = {
import sys.dispatcher import sys.dispatcher
TestAsyncUtil TestAsyncUtil
.retryUntilSatisfiedF(() => isSameBestFilterHeight(node, rpc), .retryUntilSatisfiedF(
1.second, () => isSameBestFilter(node, rpc, bestBlockHashBEOpt),
maxTries = syncTries) 1.second,
maxTries = syncTries)
} }
/** The future doesn't complete until the nodes best hash is the given hash */ /** The future doesn't complete until the nodes best hash is the given hash */
def awaitBestHash(node: Node, bitcoind: BitcoindRpcClient)(implicit def awaitBestHash(
node: Node,
bitcoind: BitcoindRpcClient,
bestHashOpt: Option[DoubleSha256DigestBE] = None)(implicit
system: ActorSystem): Future[Unit] = { system: ActorSystem): Future[Unit] = {
import system.dispatcher import system.dispatcher
def bestBitcoinSHashF: Future[DoubleSha256DigestBE] = { def bestBitcoinSHashF: Future[DoubleSha256DigestBE] = {
node.chainApiFromDb().flatMap(_.getBestBlockHash()) node.chainApiFromDb().flatMap(_.getBestBlockHash())
} }
def bestBitcoindHashF: Future[DoubleSha256DigestBE] = def bestBitcoindHashF: Future[DoubleSha256DigestBE] = {
bitcoind.getBestBlockHash() bestHashOpt match {
case Some(bestHash) => Future.successful(bestHash)
case None => bitcoind.getBestBlockHash()
}
}
TestAsyncUtil.retryUntilSatisfiedF( TestAsyncUtil.retryUntilSatisfiedF(
() => { () => {
@ -168,14 +202,22 @@ abstract class NodeTestUtil extends P2PLogger {
) )
} }
/** Awaits header, filter header and filter sync between the neutrino node and rpc client */ /** Awaits header, filter header and filter sync between the neutrino node and rpc client
def awaitAllSync(node: NeutrinoNode, bitcoind: BitcoindRpcClient)(implicit * @param the node we are syncing
* @param bitcoind the node we are syncing against
* @param bestBlockHashBE the best block hash we are expected to sync to, this is useful for reorg situations.
* If None given, we use bitcoind's best block header
*/
def awaitAllSync(
node: NeutrinoNode,
bitcoind: BitcoindRpcClient,
bestBlockHashBE: Option[DoubleSha256DigestBE] = None)(implicit
system: ActorSystem): Future[Unit] = { system: ActorSystem): Future[Unit] = {
import system.dispatcher import system.dispatcher
for { for {
_ <- NodeTestUtil.awaitBestHash(node, bitcoind) _ <- NodeTestUtil.awaitBestHash(node, bitcoind, bestBlockHashBE)
_ <- NodeTestUtil.awaitCompactFilterHeadersSync(node, bitcoind) _ <- NodeTestUtil.awaitCompactFilterHeadersSync(node, bitcoind)
_ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind) _ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind, bestBlockHashBE)
} yield () } yield ()
} }