Skip to content

Commit

Permalink
[ETCM-969] simple Branch implementation (#1039)
Browse files Browse the repository at this point in the history
* create Branch class with getBlockByNumber and getHashByBlockNumber functions

* make getBlockByNumber private in blockchain reader

* Create EmptyBlochainBranch and make getBestChain return it if there no known best block

* Rephrase scaladoc comment and remove TODO
  • Loading branch information
Aurélien Richez authored Jul 16, 2021
1 parent 2940e56 commit 2e078af
Show file tree
Hide file tree
Showing 27 changed files with 175 additions and 49 deletions.
6 changes: 3 additions & 3 deletions src/it/scala/io/iohk/ethereum/sync/RegularSyncItSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class RegularSyncItSpec extends FreeSpecBase with Matchers with BeforeAndAfterAl
_ <- peer2.importBlocksUntil(30)(IdentityUpdate)
_ <- peer1.startRegularSync()
_ <- peer2.startRegularSync()
_ <- peer2.addCheckpointedBlock(peer2.blockchainReader.getBlockByNumber(25).get)
_ <- peer2.addCheckpointedBlock(peer2.blockchainReader.getBestBranch().getBlockByNumber(25).get)
_ <- peer2.waitForRegularSyncLoadLastBlock(length)
_ <- peer1.connectToPeers(Set(peer2.node))
_ <- peer1.waitForRegularSyncLoadLastBlock(length)
Expand Down Expand Up @@ -181,8 +181,8 @@ class RegularSyncItSpec extends FreeSpecBase with Matchers with BeforeAndAfterAl
)
)
(
peer1.blockchainReader.getBlockByNumber(blockNumer + 1),
peer2.blockchainReader.getBlockByNumber(blockNumer + 1)
peer1.blockchainReader.getBestBranch().getBlockByNumber(blockNumer + 1),
peer2.blockchainReader.getBestBranch().getBlockByNumber(blockNumer + 1)
) match {
case (Some(blockP1), Some(blockP2)) =>
assert(peer1.bl.getChainWeightByHash(blockP1.hash) == peer2.bl.getChainWeightByHash(blockP2.hash))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ object RegularSyncItSpecUtils {
Task(blockNumber match {
case Some(bNumber) =>
blockchainReader
.getBestBranch()
.getBlockByNumber(bNumber)
.getOrElse(throw new RuntimeException(s"block by number: $bNumber doesn't exist"))
case None => blockchainReader.getBestBlock().get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.iohk.ethereum.db.storage.pruning.PruningMode
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefEmpty
import io.iohk.ethereum.domain.Blockchain
import io.iohk.ethereum.domain._
import io.iohk.ethereum.domain.branch.Branch
import io.iohk.ethereum.jsonrpc.ProofService.EmptyStorageValueProof
import io.iohk.ethereum.jsonrpc.ProofService.StorageProof
import io.iohk.ethereum.jsonrpc.ProofService.StorageProofKey
Expand Down Expand Up @@ -101,7 +102,9 @@ object DumpChainApp

val blockchain: Blockchain = new BlockchainMock(genesisHash)
val blockchainReader: BlockchainReader = mock[BlockchainReader]
(blockchainReader.getHashByBlockNumber _).expects(*).returning(Some(genesisHash))
val bestChain: Branch = mock[Branch]
(blockchainReader.getBestBranch _).expects().anyNumberOfTimes().returning(bestChain)
(bestChain.getHashByBlockNumber _).expects(*).returning(Some(genesisHash))

val nodeStatus: NodeStatus =
NodeStatus(key = nodeKey, serverStatus = ServerStatus.NotListening, discoveryStatus = ServerStatus.NotListening)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ trait OmmersValidator {
): Either[OmmersError, OmmersValid] = {

val getBlockHeaderByHash: ByteString => Option[BlockHeader] = blockchainReader.getBlockHeaderByHash
val bestBranch = blockchainReader.getBestBranch()
val getNBlocksBack: (ByteString, Int) => List[Block] =
(_, n) => ((blockNumber - n) until blockNumber).toList.flatMap(blockchainReader.getBlockByNumber)
(_, n) =>
((blockNumber - n) until blockNumber).toList
.flatMap(nb => bestBranch.getBlockByNumber(nb))

validate(parentHash, blockNumber, ommers, getBlockHeaderByHash, getNBlocksBack)
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/io/iohk/ethereum/domain/Blockchain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class BlockchainImpl(
override def isInChain(hash: ByteString): Boolean =
(for {
header <- blockchainReader.getBlockHeaderByHash(hash) if header.number <= blockchainReader.getBestBlockNumber()
hash <- blockchainReader.getHashByBlockNumber(header.number)
hash <- blockchainReader.getBestBranch().getHashByBlockNumber(header.number)
} yield header.hash == hash).getOrElse(false)

override def getChainWeightByHash(blockhash: ByteString): Option[ChainWeight] = chainWeightStorage.get(blockhash)
Expand Down Expand Up @@ -223,7 +223,7 @@ class BlockchainImpl(
val latestCheckpointNumber = getLatestCheckpointBlockNumber()

val blockNumberMappingUpdates =
if (blockchainReader.getHashByBlockNumber(block.number).contains(blockHash))
if (blockchainReader.getBestBranch().getHashByBlockNumber(block.number).contains(blockHash))
removeBlockNumberMapping(block.number)
else blockNumberMappingStorage.emptyBatchUpdate

Expand Down Expand Up @@ -292,7 +292,7 @@ class BlockchainImpl(
): BigInt =
if (blockNumberToCheck > 0) {
val maybePreviousCheckpointBlockNumber = for {
currentBlock <- blockchainReader.getBlockByNumber(blockNumberToCheck)
currentBlock <- blockchainReader.getBestBranch().getBlockByNumber(blockNumberToCheck)
if currentBlock.hasCheckpoint &&
currentBlock.number < latestCheckpointBlockNumber
} yield currentBlock.number
Expand Down
53 changes: 34 additions & 19 deletions src/main/scala/io/iohk/ethereum/domain/BlockchainReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import io.iohk.ethereum.db.storage.BlockHeadersStorage
import io.iohk.ethereum.db.storage.BlockNumberMappingStorage
import io.iohk.ethereum.db.storage.ReceiptStorage
import io.iohk.ethereum.db.storage.StateStorage
import io.iohk.ethereum.domain.branch.BestBranch
import io.iohk.ethereum.domain.branch.Branch
import io.iohk.ethereum.domain.branch.EmptyBranch$
import io.iohk.ethereum.mpt.MptNode
import io.iohk.ethereum.utils.Logger

Expand Down Expand Up @@ -48,31 +51,12 @@ class BlockchainReader(
body <- getBlockBodyByHash(hash)
} yield Block(header, body)

/** Returns a block hash given a block number
*
* @param number Number of the searchead block
* @return Block hash if found
*/
def getHashByBlockNumber(number: BigInt): Option[ByteString] =
blockNumberMappingStorage.get(number)

def getBlockHeaderByNumber(number: BigInt): Option[BlockHeader] =
for {
hash <- getHashByBlockNumber(number)
header <- getBlockHeaderByHash(hash)
} yield header

/** Allows to query for a block based on it's number
*
* @param number Block number
* @return Block if it exists
*/
def getBlockByNumber(number: BigInt): Option[Block] =
for {
hash <- getHashByBlockNumber(number)
block <- getBlockByHash(hash)
} yield block

/** Returns MPT node searched by it's hash
* @param hash Node Hash
* @return MPT node
Expand All @@ -86,6 +70,18 @@ class BlockchainReader(
*/
def getReceiptsByHash(blockhash: ByteString): Option[Seq[Receipt]] = receiptStorage.get(blockhash)

/** get the current best stored branch */
def getBestBranch(): Branch =
getBestBlock()
.map { block =>
new BestBranch(
block.header,
blockNumberMappingStorage,
this
)
}
.getOrElse(EmptyBranch$)

def getBestBlockNumber(): BigInt = {
val bestSavedBlockNumber = appStateStorage.getBestBlockNumber()
val bestKnownBlockNumber = blockchainMetadata.bestKnownBlockAndLatestCheckpoint.get().bestBlockNumber
Expand Down Expand Up @@ -116,6 +112,25 @@ class BlockchainReader(

def genesisBlock: Block =
getBlockByNumber(0).get

/** Allows to query for a block based on it's number
*
* @param number Block number
* @return Block if it exists
*/
private def getBlockByNumber(number: BigInt): Option[Block] =
for {
hash <- getHashByBlockNumber(number)
block <- getBlockByHash(hash)
} yield block

/** Returns a block hash given a block number
*
* @param number Number of the searchead block
* @return Block hash if found
*/
private def getHashByBlockNumber(number: BigInt): Option[ByteString] =
blockNumberMappingStorage.get(number)
}

object BlockchainReader {
Expand Down
35 changes: 35 additions & 0 deletions src/main/scala/io/iohk/ethereum/domain/branch/BestBranch.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.iohk.ethereum.domain.branch
import akka.util.ByteString

import io.iohk.ethereum.db.storage.BlockNumberMappingStorage
import io.iohk.ethereum.domain.Block
import io.iohk.ethereum.domain.BlockHeader
import io.iohk.ethereum.domain.BlockchainReader

/** A Branch instance which only works for the best canonical branch or a subset of this branch.
* This implementation uses the existing storage indexes to access blocks by number more efficiently.
*/
class BestBranch(
tipBlockHeader: BlockHeader,
bestChainBlockNumberMappingStorage: BlockNumberMappingStorage,
blockchainReader: BlockchainReader
) extends Branch {

/* The following assumptions are made in this class :
* - The whole branch exists in storage
* - The various metadata and index are consistent
*/

override def getBlockByNumber(number: BigInt): Option[Block] =
if (tipBlockHeader.number >= number && number >= 0) {
for {
hash <- getHashByBlockNumber(number)
block <- blockchainReader.getBlockByHash(hash)
} yield block
} else None

override def getHashByBlockNumber(number: BigInt): Option[ByteString] =
if (tipBlockHeader.number >= number && number >= 0) {
bestChainBlockNumberMappingStorage.get(number)
} else None
}
16 changes: 16 additions & 0 deletions src/main/scala/io/iohk/ethereum/domain/branch/Branch.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.iohk.ethereum.domain.branch

import akka.util.ByteString

import io.iohk.ethereum.domain.Block

/** An interface to manipulate blockchain branches */
trait Branch {

/** Returns a block inside this branch based on its number */
def getBlockByNumber(number: BigInt): Option[Block]

/** Returns a block hash for the block at the given height if any */
def getHashByBlockNumber(number: BigInt): Option[ByteString]

}
11 changes: 11 additions & 0 deletions src/main/scala/io/iohk/ethereum/domain/branch/EmptyBranch$.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.iohk.ethereum.domain.branch
import akka.util.ByteString

import io.iohk.ethereum.domain.Block

object EmptyBranch$ extends Branch {

override def getBlockByNumber(number: BigInt): Option[Block] = None

override def getHashByBlockNumber(number: BigInt): Option[ByteString] = None
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CheckpointingService(
req.parentCheckpoint.forall(blockchainReader.getBlockHeaderByHash(_).exists(_.number < blockToReturnNum))

Task {
blockchainReader.getBlockByNumber(blockToReturnNum)
blockchainReader.getBestBranch().getBlockByNumber(blockToReturnNum)
}.flatMap {
case Some(b) if isValidParent =>
Task.now(Right(GetLatestBlockResponse(Some(BlockInfo(b.hash, b.number)))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class EthProofService(
private def resolveBlock(blockParam: BlockParam): Either[JsonRpcError, ResolvedBlock] = {
def getBlock(number: BigInt): Either[JsonRpcError, Block] =
blockchainReader
.getBestBranch()
.getBlockByNumber(number)
.toRight(JsonRpcError.InvalidParams(s"Block $number not found"))

Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/io/iohk/ethereum/jsonrpc/EthTxService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ class EthTxService(
val bestBlock = blockchainReader.getBestBlockNumber()

Task {
val bestBranch = blockchainReader.getBestBranch()
val gasPrice = ((bestBlock - blockDifference) to bestBlock)
.flatMap(blockchainReader.getBlockByNumber)
.flatMap(nb => bestBranch.getBlockByNumber(nb))
.flatMap(_.body.transactionList)
.map(_.tx.gasPrice)
if (gasPrice.nonEmpty) {
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/io/iohk/ethereum/jsonrpc/ResolveBlock.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ trait ResolveBlock {

private def getBlock(number: BigInt): Either[JsonRpcError, Block] =
blockchainReader
.getBestBranch()
.getBlockByNumber(number)
.toRight(JsonRpcError.InvalidParams(s"Block $number not found"))

Expand Down
7 changes: 5 additions & 2 deletions src/main/scala/io/iohk/ethereum/jsonrpc/TestService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ class TestService(

val blockOpt = request.parameters.blockHashOrNumber
.fold(
number => blockchainReader.getBlockByNumber(number),
number => blockchainReader.getBestBranch().getBlockByNumber(number),
blockHash => blockchainReader.getBlockByHash(blockHash)
)

Expand Down Expand Up @@ -411,7 +411,10 @@ class TestService(
def storageRangeAt(request: StorageRangeRequest): ServiceResponse[StorageRangeResponse] = {

val blockOpt = request.parameters.blockHashOrNumber
.fold(number => blockchainReader.getBlockByNumber(number), hash => blockchainReader.getBlockByHash(hash))
.fold(
number => blockchainReader.getBestBranch().getBlockByNumber(number),
hash => blockchainReader.getBlockByHash(hash)
)

(for {
block <- blockOpt.toRight(StorageRangeResponse(complete = false, Map.empty, None))
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/io/iohk/ethereum/ledger/BlockImport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class BlockImport(
private def removeBlocksUntil(parent: ByteString, fromNumber: BigInt): List[BlockData] = {
@tailrec
def removeBlocksUntil(parent: ByteString, fromNumber: BigInt, acc: List[BlockData]): List[BlockData] =
blockchainReader.getBlockByNumber(fromNumber) match {
blockchainReader.getBestBranch().getBlockByNumber(fromNumber) match {
case Some(block) if block.header.hash == parent || fromNumber == 0 =>
acc

Expand Down
4 changes: 3 additions & 1 deletion src/main/scala/io/iohk/ethereum/ledger/BlockValidation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class BlockValidation(
val remaining = n - queuedBlocks.length - 1

val numbers = (block.header.number - remaining) until block.header.number
val blocks = (numbers.toList.flatMap(blockchainReader.getBlockByNumber) :+ block) ::: queuedBlocks
val bestBranch = blockchainReader.getBestBranch()
val blocks =
(numbers.toList.flatMap(nb => bestBranch.getBlockByNumber(nb)) :+ block) ::: queuedBlocks
blocks
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/main/scala/io/iohk/ethereum/ledger/BranchResolution.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ class BranchResolution(blockchain: Blockchain, blockchainReader: BlockchainReade
}
}

private def getTopBlocksFromNumber(from: BigInt): List[Block] =
private def getTopBlocksFromNumber(from: BigInt): List[Block] = {
val bestBranch = blockchainReader.getBestBranch()
(from to blockchainReader.getBestBlockNumber())
.flatMap(blockchainReader.getBlockByNumber)
.flatMap(nb => bestBranch.getBlockByNumber(nb))
.toList
}
}

sealed trait BranchResolutionResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ class TestEthBlockServiceWrapper(
.getBlockByNumber(request)
.map(
_.map { blockByBlockResponse =>
val fullBlock = blockchainReader.getBlockByNumber(blockByBlockResponse.blockResponse.get.number).get
val bestBranch = blockchainReader.getBestBranch()
val fullBlock = bestBranch.getBlockByNumber(blockByBlockResponse.blockResponse.get.number).get
BlockByNumberResponse(blockByBlockResponse.blockResponse.map(response => toEthResponse(fullBlock, response)))
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class TransactionHistoryService(
val getLastCheckpoint = Task(blockchain.getLatestCheckpointBlockNumber()).memoizeOnSuccess
val txnsFromBlocks = Observable
.from(fromBlocks.reverse)
.mapParallelOrdered(10)(blockNr => Task(blockchainReader.getBlockByNumber(blockNr)))(
.mapParallelOrdered(10)(blockNr => Task(blockchainReader.getBestBranch().getBlockByNumber(blockNr)))(
OverflowStrategy.Unbounded
)
.collect { case Some(block) => block }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ class StdOmmersValidatorSpec extends AnyFlatSpec with Matchers with ScalaCheckPr

it should "report failure if there is an ommer which that is not parent of an ancestor" in new BlockUtils {
val getNBlocksBack: (ByteString, Int) => List[Block] =
(_, n) => ((ommersBlockNumber - n) until ommersBlockNumber).toList.flatMap(blockchainReader.getBlockByNumber)
(_, n) =>
((ommersBlockNumber - n) until ommersBlockNumber).toList
.flatMap(nb => blockchainReader.getBestBranch().getBlockByNumber(nb))

ommersValidator.validateOmmersAncestors(
ommersBlockParentHash,
Expand Down Expand Up @@ -462,6 +464,7 @@ class StdOmmersValidatorSpec extends AnyFlatSpec with Matchers with ScalaCheckPr
.and(blockchainWriter.storeBlock(block95))
.and(blockchainWriter.storeBlock(block96))
.commit()
blockchain.saveBestKnownBlocks(block96.number)

}
}
9 changes: 6 additions & 3 deletions src/test/scala/io/iohk/ethereum/domain/BlockchainSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
it should "be able to store a block and retrieve it by number" in new EphemBlockchainTestSetup {
val validBlock = Fixtures.Blocks.ValidBlock.block
blockchainWriter.storeBlock(validBlock).commit()
val block = blockchainReader.getBlockByNumber(validBlock.header.number)
blockchain.saveBestKnownBlocks(validBlock.number)
val block = blockchainReader.getBestBranch().getBlockByNumber(validBlock.header.number)
block.isDefined should ===(true)
validBlock should ===(block.get)
}
Expand All @@ -67,8 +68,10 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
}

it should "not return a value if not stored" in new EphemBlockchainTestSetup {
blockchainReader.getBlockByNumber(Fixtures.Blocks.ValidBlock.header.number).isEmpty should ===(true)
blockchainReader.getBlockByHash(Fixtures.Blocks.ValidBlock.header.hash).isEmpty should ===(true)
blockchainReader
.getBestBranch()
.getBlockByNumber(Fixtures.Blocks.ValidBlock.header.number) shouldBe None
blockchainReader.getBlockByHash(Fixtures.Blocks.ValidBlock.header.hash) shouldBe None
}

it should "be able to store a block with checkpoint and retrieve it and checkpoint" in new EphemBlockchainTestSetup {
Expand Down
Loading

0 comments on commit 2e078af

Please sign in to comment.