Skip to content

Commit 4f5d57f

Browse files
authored
Merge pull request #942 from input-output-hk/feature/ETCM-655
Feature/etcm 655
2 parents 04208b0 + 11a0b77 commit 4f5d57f

File tree

9 files changed

+178
-54
lines changed

9 files changed

+178
-54
lines changed

src/main/resources/application.conf

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,17 +410,15 @@ mantis {
410410
# we found a fork. To resolve it, we need to query the same peer for previous headers, to find a common ancestor.
411411
branch-resolution-request-size = 30
412412

413-
# TODO investigate proper value to handle ETC reorgs correctly
414413
# threshold for storing non-main-chain blocks in queue.
415414
# if: current_best_block_number - block_number > max-queued-block-number-behind
416415
# then: the block will not be queued (such already queued blocks will be removed)
417-
max-queued-block-number-behind = 100
416+
max-queued-block-number-behind = 1000
418417

419-
# TODO investigate proper value to handle ETC reorgs correctly
420418
# threshold for storing non-main-chain blocks in queue.
421419
# if: block_number - current_best_block_number > max-queued-block-number-ahead
422420
# then: the block will not be queued (such already queued blocks will be removed)
423-
max-queued-block-number-ahead = 100
421+
max-queued-block-number-ahead = 1000
424422

425423
# Maximum number of blocks, after which block hash from NewBlockHashes packet is considered ancient
426424
# and peer sending it is blacklisted
@@ -623,7 +621,8 @@ akka {
623621
loglevel = "INFO"
624622
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
625623
logger-startup-timeout = 30s
626-
log-dead-letters = off
624+
log-dead-letters-during-shutdown = off
625+
log-dead-letters = 5
627626

628627
coordinated-shutdown.phases {
629628
actor-system-terminate {

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockFetcher.scala

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.iohk.ethereum.blockchain.sync.regular
22

33
import akka.actor.Status.Failure
4-
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
4+
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props}
55
import akka.pattern.{ask, pipe}
6+
import akka.stream.scaladsl.{Keep, Sink, Source, SourceQueue}
7+
import akka.stream.{Attributes, DelayOverflowStrategy, OverflowStrategy}
68
import akka.util.{ByteString, Timeout}
79
import cats.data.NonEmptyList
810
import cats.instances.option._
@@ -45,8 +47,24 @@ class BlockFetcher(
4547
import BlockFetcher._
4648

4749
implicit val ec: MonixScheduler = MonixScheduler(context.dispatcher)
50+
implicit val sys: ActorSystem = context.system
4851
implicit val timeout: Timeout = syncConfig.peerResponseTimeout + 2.second // some margin for actor communication
4952

53+
private val queue: SourceQueue[BlockFetcherState] = {
54+
val cap = 1000
55+
val numberOfElements = 1
56+
Source
57+
.queue[BlockFetcherState](cap, OverflowStrategy.backpressure)
58+
.delay(5.seconds, DelayOverflowStrategy.dropHead)
59+
.addAttributes(Attributes.inputBuffer(numberOfElements, numberOfElements))
60+
.map { s =>
61+
log.debug("Resuming fetching with the latest state")
62+
fetchBlocks(s.withResumedFetching)
63+
}
64+
.toMat(Sink.ignore)(Keep.left)
65+
.run()
66+
}
67+
5068
override def receive: Receive = idle()
5169

5270
override def postStop(): Unit = {
@@ -103,7 +121,7 @@ class BlockFetcher(
103121
}
104122

105123
private def handleHeadersMessages(state: BlockFetcherState): Receive = {
106-
case Response(peer, BlockHeaders(headers)) if state.isFetchingHeaders =>
124+
case Response(_, BlockHeaders(headers)) if state.isFetchingHeaders =>
107125
val newState =
108126
if (state.fetchingHeadersState == AwaitingHeadersToBeIgnored) {
109127
log.debug(
@@ -153,7 +171,7 @@ class BlockFetcher(
153171
state.withBodiesFetchReceived.handleRequestedBlocks(newBlocks, peer.id)
154172
}
155173
val waitingHeadersDequeued = state.waitingHeaders.size - newState.waitingHeaders.size
156-
log.debug(s"Processed ${waitingHeadersDequeued} new blocks from received block bodies")
174+
log.debug(s"Processed $waitingHeadersDequeued new blocks from received block bodies")
157175
fetchBlocks(newState)
158176
}
159177
case RetryBodiesRequest if state.isFetchingBodies =>
@@ -267,20 +285,32 @@ class BlockFetcher(
267285
}
268286

269287
private def handlePossibleTopUpdate(state: BlockFetcherState): Receive = {
270-
//by handling these type of messages, fetcher can received from network, fresh info about blocks on top
288+
//by handling these type of messages, fetcher can receive from network fresh info about blocks on top
271289
//ex. After a successful handshake, fetcher will receive the info about the header of the peer best block
272290
case MessageFromPeer(BlockHeaders(headers), _) =>
273291
headers.lastOption.map { bh =>
274292
log.debug(s"Candidate for new top at block ${bh.number}, current known top ${state.knownTop}")
275293
val newState = state.withPossibleNewTopAt(bh.number)
276294
fetchBlocks(newState)
277295
}
278-
//keep fetcher state updated in case new checkpoint block or mined block was imported
296+
//keep fetcher state updated in case new mined block was imported
279297
case InternalLastBlockImport(blockNr) =>
280-
log.debug(s"New last block $blockNr imported from the inside")
298+
log.debug(s"New mined block $blockNr imported from the inside")
281299
val newState = state.withLastBlock(blockNr).withPossibleNewTopAt(blockNr)
282300

283301
fetchBlocks(newState)
302+
303+
//keep fetcher state updated in case new checkpoint block was imported
304+
case InternalCheckpointImport(blockNr) =>
305+
log.debug(s"New checkpoint block $blockNr imported from the inside")
306+
307+
val newState = state
308+
.clearQueues()
309+
.withLastBlock(blockNr)
310+
.withPossibleNewTopAt(blockNr)
311+
.withPausedFetching
312+
313+
fetchBlocks(newState)
284314
}
285315

286316
private def handlePickedBlocks(
@@ -296,9 +326,14 @@ class BlockFetcher(
296326
private def fetchBlocks(state: BlockFetcherState): Unit = {
297327
// Remember that tryFetchHeaders and tryFetchBodies can issue a request
298328
// Nice and clean way to express that would be to use SyncIO from cats-effect
299-
val newState = state |> tryFetchHeaders |> tryFetchBodies
300329

301-
context become started(newState)
330+
if (state.pausedFetching) {
331+
queue.offer(state)
332+
context become started(state)
333+
} else {
334+
val newState = state |> tryFetchHeaders |> tryFetchBodies
335+
context become started(newState)
336+
}
302337
}
303338

304339
private def tryFetchHeaders(fetcherState: BlockFetcherState): BlockFetcherState =
@@ -329,7 +364,7 @@ class BlockFetcher(
329364
.filter(!_.isFetchingBodies)
330365
.filter(_.waitingHeaders.nonEmpty)
331366
.tap(fetchBodies)
332-
.map(state => state.withNewBodiesFetch)
367+
.map(_.withNewBodiesFetch)
333368
.getOrElse(fetcherState)
334369

335370
private def fetchBodies(state: BlockFetcherState): Unit = {
@@ -403,13 +438,13 @@ object BlockFetcher {
403438
Props(new BlockFetcher(peersClient, peerEventBus, supervisor, syncConfig, blockValidator))
404439

405440
sealed trait FetchMsg
406-
case class Start(importer: ActorRef, fromBlock: BigInt) extends FetchMsg
407-
case class FetchStateNode(hash: ByteString) extends FetchMsg
408-
case object RetryFetchStateNode extends FetchMsg
409-
case class PickBlocks(amount: Int) extends FetchMsg
410-
case class StrictPickBlocks(from: BigInt, atLEastWith: BigInt) extends FetchMsg
411-
case object PrintStatus extends FetchMsg
412-
case class InvalidateBlocksFrom(fromBlock: BigInt, reason: String, toBlacklist: Option[BigInt]) extends FetchMsg
441+
final case class Start(importer: ActorRef, fromBlock: BigInt) extends FetchMsg
442+
final case class FetchStateNode(hash: ByteString) extends FetchMsg
443+
final case object RetryFetchStateNode extends FetchMsg
444+
final case class PickBlocks(amount: Int) extends FetchMsg
445+
final case class StrictPickBlocks(from: BigInt, atLEastWith: BigInt) extends FetchMsg
446+
final case object PrintStatus extends FetchMsg
447+
final case class InvalidateBlocksFrom(fromBlock: BigInt, reason: String, toBlacklist: Option[BigInt]) extends FetchMsg
413448

414449
object InvalidateBlocksFrom {
415450

@@ -419,12 +454,13 @@ object BlockFetcher {
419454
def apply(from: BigInt, reason: String, toBlacklist: Option[BigInt]): InvalidateBlocksFrom =
420455
new InvalidateBlocksFrom(from, reason, toBlacklist)
421456
}
422-
case class BlockImportFailed(blockNr: BigInt, reason: String) extends FetchMsg
423-
case class InternalLastBlockImport(blockNr: BigInt) extends FetchMsg
424-
case object RetryBodiesRequest extends FetchMsg
425-
case object RetryHeadersRequest extends FetchMsg
457+
final case class BlockImportFailed(blockNr: BigInt, reason: String) extends FetchMsg
458+
final case class InternalLastBlockImport(blockNr: BigInt) extends FetchMsg
459+
final case class InternalCheckpointImport(blockNr: BigInt) extends FetchMsg
460+
final case object RetryBodiesRequest extends FetchMsg
461+
final case object RetryHeadersRequest extends FetchMsg
426462

427463
sealed trait FetchResponse
428-
case class PickedBlocks(blocks: NonEmptyList[Block]) extends FetchResponse
429-
case class FetchedStateNode(stateNode: NodeData) extends FetchResponse
464+
final case class PickedBlocks(blocks: NonEmptyList[Block]) extends FetchResponse
465+
final case class FetchedStateNode(stateNode: NodeData) extends FetchResponse
430466
}

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockFetcherState.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ case class BlockFetcherState(
4141
waitingHeaders: Queue[BlockHeader],
4242
fetchingHeadersState: FetchingHeadersState,
4343
fetchingBodiesState: FetchingBodiesState,
44+
pausedFetching: Boolean = false,
4445
stateNodeFetcher: Option[StateNodeFetcher],
4546
lastBlock: BigInt,
4647
knownTop: BigInt,
@@ -295,6 +296,9 @@ case class BlockFetcherState(
295296
def withNewBodiesFetch: BlockFetcherState = copy(fetchingBodiesState = AwaitingBodies)
296297
def withBodiesFetchReceived: BlockFetcherState = copy(fetchingBodiesState = NotFetchingBodies)
297298

299+
def withPausedFetching: BlockFetcherState = copy(pausedFetching = true)
300+
def withResumedFetching: BlockFetcherState = copy(pausedFetching = false)
301+
298302
def fetchingStateNode(hash: ByteString, requestor: ActorRef): BlockFetcherState =
299303
copy(stateNodeFetcher = Some(StateNodeFetcher(hash, requestor)))
300304

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/BlockImporter.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ class BlockImporter(
184184
): Task[(List[Block], Option[Any])] =
185185
if (blocks.isEmpty) {
186186
importedBlocks.headOption match {
187-
case Some(block) => supervisor ! ProgressProtocol.ImportedBlock(block.number, internally = false)
187+
case Some(block) =>
188+
supervisor ! ProgressProtocol.ImportedBlock(block.number, block.hasCheckpoint, internally = false)
188189
case None => ()
189190
}
190191

@@ -258,15 +259,16 @@ class BlockImporter(
258259
val (blocks, weights) = importedBlocksData.map(data => (data.block, data.weight)).unzip
259260
broadcastBlocks(blocks, weights)
260261
updateTxPool(importedBlocksData.map(_.block), Seq.empty)
261-
supervisor ! ProgressProtocol.ImportedBlock(block.number, internally)
262+
supervisor ! ProgressProtocol.ImportedBlock(block.number, block.hasCheckpoint, internally)
262263
case BlockEnqueued => ()
263264
case DuplicateBlock => ()
264265
case UnknownParent => () // This is normal when receiving broadcast blocks
265266
case ChainReorganised(oldBranch, newBranch, weights) =>
266267
updateTxPool(newBranch, oldBranch)
267268
broadcastBlocks(newBranch, weights)
268269
newBranch.lastOption match {
269-
case Some(newBlock) => supervisor ! ProgressProtocol.ImportedBlock(newBlock.number, internally)
270+
case Some(newBlock) =>
271+
supervisor ! ProgressProtocol.ImportedBlock(newBlock.number, block.hasCheckpoint, internally)
270272
case None => ()
271273
}
272274
case BlockImportFailed(error) =>

src/main/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSync.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import akka.util.ByteString
55
import io.iohk.ethereum.blockchain.sync.SyncProtocol
66
import io.iohk.ethereum.blockchain.sync.SyncProtocol.Status
77
import io.iohk.ethereum.blockchain.sync.SyncProtocol.Status.Progress
8-
import io.iohk.ethereum.blockchain.sync.regular.BlockFetcher.InternalLastBlockImport
8+
import io.iohk.ethereum.blockchain.sync.regular.BlockFetcher.{InternalCheckpointImport, InternalLastBlockImport}
99
import io.iohk.ethereum.blockchain.sync.regular.RegularSync.{NewCheckpoint, ProgressProtocol, ProgressState}
1010
import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
1111
import io.iohk.ethereum.consensus.validators.BlockValidator
@@ -100,10 +100,14 @@ class RegularSync(
100100
log.info(s"Got information about new block [number = $blockNumber]")
101101
val newState = progressState.copy(bestKnownNetworkBlock = blockNumber)
102102
context become running(newState)
103-
case ProgressProtocol.ImportedBlock(blockNumber, internally) =>
103+
case ProgressProtocol.ImportedBlock(blockNumber, isCheckpoint, internally) =>
104104
log.info(s"Imported new block [number = $blockNumber, internally = $internally]")
105-
val newState = progressState.copy(currentBlock = blockNumber)
106-
if (internally) {
105+
val newState =
106+
if (isCheckpoint) progressState.copy(currentBlock = blockNumber, bestKnownNetworkBlock = blockNumber)
107+
else progressState.copy(currentBlock = blockNumber)
108+
if (internally && isCheckpoint) {
109+
fetcher ! InternalCheckpointImport(blockNumber)
110+
} else if (internally) {
107111
fetcher ! InternalLastBlockImport(blockNumber)
108112
}
109113
context become running(newState)
@@ -171,6 +175,6 @@ object RegularSync {
171175
case object StartedFetching extends ProgressProtocol
172176
case class StartingFrom(blockNumber: BigInt) extends ProgressProtocol
173177
case class GotNewBlock(blockNumber: BigInt) extends ProgressProtocol
174-
case class ImportedBlock(blockNumber: BigInt, internally: Boolean) extends ProgressProtocol
178+
case class ImportedBlock(blockNumber: BigInt, isCheckpoint: Boolean, internally: Boolean) extends ProgressProtocol
175179
}
176180
}

src/main/scala/io/iohk/ethereum/jsonrpc/CheckpointingJsonMethodsImplicits.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ object CheckpointingJsonMethodsImplicits extends JsonMethodsImplicits {
1717
params: Option[JsonAST.JArray]
1818
): Either[JsonRpcError, GetLatestBlockRequest] =
1919
params match {
20-
case Some(JArray(JInt(chkpInterval) :: Nil)) =>
20+
case Some(JArray(JInt(chkpInterval) :: second :: Nil)) =>
2121
if (chkpInterval > 0 && chkpInterval <= Int.MaxValue)
22-
Right(GetLatestBlockRequest(chkpInterval.toInt))
22+
second match {
23+
case JString(blockHash) =>
24+
for {
25+
hash <- extractHash(blockHash)
26+
} yield GetLatestBlockRequest(chkpInterval.toInt, Some(hash))
27+
case JNull =>
28+
Right(GetLatestBlockRequest(chkpInterval.toInt, None))
29+
case _ =>
30+
Left(InvalidParams("Not supported type for parentCheckpoint"))
31+
}
2332
else
2433
Left(InvalidParams("Expected positive integer"))
2534
case _ =>

src/main/scala/io/iohk/ethereum/jsonrpc/CheckpointingService.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ class CheckpointingService(
1818
def getLatestBlock(req: GetLatestBlockRequest): ServiceResponse[GetLatestBlockResponse] = {
1919
lazy val bestBlockNum = blockchain.getBestBlockNumber()
2020
lazy val blockToReturnNum = bestBlockNum - bestBlockNum % req.checkpointingInterval
21+
lazy val isValidParent = req.parentCheckpoint.forall(blockchain.getBlockHeaderByHash(_).isDefined)
2122

2223
Task {
2324
blockchain.getBlockByNumber(blockToReturnNum)
2425
}.flatMap {
25-
case Some(b) =>
26-
val resp = GetLatestBlockResponse(b.hash, b.number)
27-
Task.now(Right(resp))
26+
case Some(b) if isValidParent =>
27+
Task.now(Right(GetLatestBlockResponse(Some(BlockInfo(b.hash, b.number)))))
28+
29+
case Some(_) =>
30+
log.debug("Parent checkpoint is not found in a local blockchain")
31+
Task.now(Right(GetLatestBlockResponse(None)))
2832

2933
case None =>
3034
log.error(
@@ -42,8 +46,9 @@ class CheckpointingService(
4246
}
4347

4448
object CheckpointingService {
45-
case class GetLatestBlockRequest(checkpointingInterval: Int)
46-
case class GetLatestBlockResponse(hash: ByteString, number: BigInt)
49+
case class GetLatestBlockRequest(checkpointingInterval: Int, parentCheckpoint: Option[ByteString])
50+
case class GetLatestBlockResponse(block: Option[BlockInfo])
51+
case class BlockInfo(hash: ByteString, number: BigInt)
4752

4853
case class PushCheckpointRequest(hash: ByteString, signatures: List[ECDSASignature])
4954
case class PushCheckpointResponse()

src/test/scala/io/iohk/ethereum/jsonrpc/CheckpointingJRCSpec.scala

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,38 @@ class CheckpointingJRCSpec
3131
import Req._
3232

3333
"CheckpointingJRC" should "getLatestBlock" in new TestSetup {
34-
val request = getLatestBlockRequestBuilder(JArray(JInt(4) :: Nil))
35-
val servResp = GetLatestBlockResponse(block.hash, block.number)
34+
val request = getLatestBlockRequestBuilder(JArray(JInt(4) :: JNull :: Nil))
35+
val servResp = GetLatestBlockResponse(Some(BlockInfo(block.hash, block.number)))
3636
(checkpointingService.getLatestBlock _)
37-
.expects(GetLatestBlockRequest(4))
37+
.expects(GetLatestBlockRequest(4, None))
3838
.returning(Task.now(Right(servResp)))
3939

4040
val expectedResult = JObject(
41-
"hash" -> JString("0x" + ByteStringUtils.hash2string(block.hash)),
42-
"number" -> JInt(block.number)
43-
)
41+
"block" -> JObject(
42+
"hash" -> JString("0x" + ByteStringUtils.hash2string(block.hash)),
43+
"number" -> JInt(block.number)
44+
))
4445

4546
val response = jsonRpcController.handleRequest(request).runSyncUnsafe()
4647
response should haveResult(expectedResult)
4748
}
4849

50+
it should "return invalid params when checkpoint parent is of the wrong type" in new TestSetup {
51+
val request = getLatestBlockRequestBuilder(JArray(JInt(1) :: JBool(true) :: Nil))
52+
53+
val response = jsonRpcController.handleRequest(request).runSyncUnsafe()
54+
response should haveError(notSupportedTypeError)
55+
}
56+
4957
it should "return invalid params when checkpoint interval is not positive (getLatestBlock)" in new TestSetup {
50-
val request = getLatestBlockRequestBuilder(JArray(JInt(-1) :: Nil))
58+
val request = getLatestBlockRequestBuilder(JArray(JInt(-1) :: JNull :: Nil))
5159

5260
val response = jsonRpcController.handleRequest(request).runSyncUnsafe()
5361
response should haveError(expectedPositiveIntegerError)
5462
}
5563

5664
it should "return invalid params when checkpoint interval is too big (getLatestBlock)" in new TestSetup {
57-
val request = getLatestBlockRequestBuilder(JArray(JInt(BigInt(Int.MaxValue) + 1) :: Nil))
65+
val request = getLatestBlockRequestBuilder(JArray(JInt(BigInt(Int.MaxValue) + 1) :: JNull :: Nil))
5866

5967
val response = jsonRpcController.handleRequest(request).runSyncUnsafe()
6068
response should haveError(expectedPositiveIntegerError)
@@ -191,6 +199,7 @@ class CheckpointingJRCSpec
191199
)
192200

193201
val expectedPositiveIntegerError = InvalidParams("Expected positive integer")
202+
val notSupportedTypeError = InvalidParams("Not supported type for parentCheckpoint")
194203

195204
def pushCheckpointRequestBuilder(json: JArray) = JsonRpcRequest(
196205
"2.0",

0 commit comments

Comments
 (0)