diff --git a/metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockStorage.scala b/metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockStorage.scala index a2c278a9..4d658e6f 100644 --- a/metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockStorage.scala +++ b/metronome/hotstuff/service/src/io/iohk/metronome/hotstuff/service/storage/BlockStorage.scala @@ -1,9 +1,7 @@ package io.iohk.metronome.hotstuff.service.storage -import cats.implicits._ -import io.iohk.metronome.storage.{KVStore, KVStoreRead, KVCollection} +import io.iohk.metronome.storage.{KVCollection, KVTree} import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, Block} -import scala.collection.immutable.Queue /** Storage for blocks that maintains parent-child relationships as well, * to facilitate tree traversal and pruning. @@ -14,276 +12,22 @@ import scala.collection.immutable.Queue */ class BlockStorage[N, A <: Agreement: Block]( blockColl: KVCollection[N, A#Hash, A#Block], - blockMetaColl: KVCollection[N, A#Hash, BlockStorage.BlockMeta[A]], + blockMetaColl: KVCollection[N, A#Hash, KVTree.NodeMeta[A#Hash]], parentToChildrenColl: KVCollection[N, A#Hash, Set[A#Hash]] -) { - import BlockStorage.BlockMeta - - private implicit val kvn = KVStore.instance[N] - private implicit val kvrn = KVStoreRead.instance[N] - - /** Insert a block into the store, and if the parent still exists, - * then add this block to its children. - */ - def put(block: A#Block): KVStore[N, Unit] = { - val blockHash = Block[A].blockHash(block) - val meta = - BlockMeta(Block[A].parentBlockHash(block), Block[A].height(block)) - - blockColl.put(blockHash, block) >> - blockMetaColl.put(blockHash, meta) >> - parentToChildrenColl.alter(meta.parentBlockHash) { maybeChildren => - maybeChildren orElse Set.empty.some map (_ + blockHash) - } - - } - - /** Retrieve a block by hash, if it exists. */ - def get(blockHash: A#Hash): KVStoreRead[N, Option[A#Block]] = - blockColl.read(blockHash) - - /** Check whether a block is present in the tree. */ - def contains(blockHash: A#Hash): KVStoreRead[N, Boolean] = - blockMetaColl.read(blockHash).map(_.isDefined) - - /** Check how many children the block has in the tree. */ - private def childCount(blockHash: A#Hash): KVStoreRead[N, Int] = - parentToChildrenColl.read(blockHash).map(_.fold(0)(_.size)) - - /** Check whether the parent of the block is present in the tree. */ - private def hasParent(blockHash: A#Hash): KVStoreRead[N, Boolean] = - blockMetaColl.read(blockHash).flatMap { - case None => KVStoreRead[N].pure(false) - case Some(meta) => contains(meta.parentBlockHash) - } - - private def getParentBlockHash( - blockHash: A#Hash - ): KVStoreRead[N, Option[A#Hash]] = - blockMetaColl.read(blockHash).map(_.map(_.parentBlockHash)) - - /** Check whether it's safe to delete a block. - * - * A block is safe to delete if doing so doesn't break up the tree - * into a forest, in which case we may have blocks we cannot reach - * by traversal, leaking space. - * - * This is true if the block has no children, - * or it has no parent and at most one child. - */ - private def canDelete(blockHash: A#Hash): KVStoreRead[N, Boolean] = - (hasParent(blockHash), childCount(blockHash)).mapN { - case (_, 0) => true - case (false, 1) => true - case _ => false - } - - /** Delete a block by hash, if doing so wouldn't break the tree; - * otherwise do nothing. - * - * Return `true` if block has been deleted, `false` if not. - * - * If this is not efficent enough, then move the deletion traversal - * logic into the this class so it can make sure all the invariants - * are maintained, e.g. collect all hashes that can be safely deleted - * and then do so without checks. - */ - def delete(blockHash: A#Hash): KVStore[N, Boolean] = - canDelete(blockHash).lift.flatMap { ok => - deleteUnsafe(blockHash).whenA(ok).as(ok) - } - - /** Delete a block and remove it from any parent-to-child mapping, - * without any checking for the tree structure invariants. - */ - def deleteUnsafe(blockHash: A#Hash): KVStore[N, Unit] = { - def deleteIfEmpty(maybeChildren: Option[Set[A#Hash]]) = - maybeChildren.filter(_.nonEmpty) - - getParentBlockHash(blockHash).lift.flatMap { - case None => - KVStore[N].unit - case Some(parentHash) => - parentToChildrenColl.alter(parentHash) { maybeChildren => - deleteIfEmpty(maybeChildren.map(_ - blockHash)) - } - } >> - blockColl.delete(blockHash) >> - blockMetaColl.delete(blockHash) >> - // Keep the association from existing children, until they last one is deleted. - parentToChildrenColl.alter(blockHash)(deleteIfEmpty) - } - - /** Get the ancestor chain of a block from the root, - * including the block itself. - * - * If the block is not in the tree, the result will be empty, - * otherwise `head` will be the root of the block tree, - * and `last` will be the block itself. - */ - def getPathFromRoot(blockHash: A#Hash): KVStoreRead[N, List[A#Hash]] = { - def loop( - blockHash: A#Hash, - acc: List[A#Hash] - ): KVStoreRead[N, List[A#Hash]] = { - getParentBlockHash(blockHash).flatMap { - case None => - // This block doesn't exist in the tree, so our ancestry is whatever we collected so far. - KVStoreRead[N].pure(acc) - - case Some(parentHash) => - // So at least `blockHash` exists in the tree. - loop(parentHash, blockHash :: acc) - } - } - loop(blockHash, Nil) - } - - /** Get the ancestor chain between two hashes in the chain, if there is one. - * - * If either of the blocks are not in the tree, or there's no path between them, - * return an empty list. This can happen if we have already pruned away the ancestry as well. - */ - def getPathFromAncestor( - ancestorBlockHash: A#Hash, - descendantBlockHash: A#Hash - ): KVStoreRead[N, List[A#Hash]] = { - def loop( - blockHash: A#Hash, - acc: List[A#Hash], - maxDistance: Long - ): KVStoreRead[N, List[A#Hash]] = { - if (blockHash == ancestorBlockHash) { - KVStoreRead[N].pure(blockHash :: acc) - } else if (maxDistance <= 0) { - KVStoreRead[N].pure(Nil) - } else { - blockMetaColl.read(blockHash).flatMap { - case None => - KVStoreRead[N].pure(Nil) - case Some(meta) => - loop(meta.parentBlockHash, blockHash :: acc, maxDistance - 1) - } - } - } - - ( - blockMetaColl.read(ancestorBlockHash), - blockMetaColl.read(descendantBlockHash) - ).mapN((_, _)) - .flatMap { - case (Some(ameta), Some(dmeta)) => - loop( - descendantBlockHash, - Nil, - maxDistance = dmeta.height - ameta.height - ) - case _ => KVStoreRead[N].pure(Nil) - } - } - - /** Collect all descendants of a block, - * including the block itself. - * - * The result will start with the blocks furthest away, - * so it should be safe to delete them in the same order; - * `last` will be the block itself. - * - * The `skip` parameter can be used to avoid traversing - * branches that we want to keep during deletion. - */ - def getDescendants( - blockHash: A#Hash, - skip: Set[A#Hash] = Set.empty - ): KVStoreRead[N, List[A#Hash]] = { - // BFS traversal. - def loop( - queue: Queue[A#Hash], - acc: List[A#Hash] - ): KVStoreRead[N, List[A#Hash]] = { - queue.dequeueOption match { - case None => - KVStoreRead[N].pure(acc) - - case Some((blockHash, queue)) if skip(blockHash) => - loop(queue, acc) - - case Some((blockHash, queue)) => - parentToChildrenColl.read(blockHash).flatMap { - case None => - // Since we're not inserting an empty child set, - // we can't tell here if the block exists or not. - loop(queue, blockHash :: acc) - case Some(children) => - loop(queue ++ children, blockHash :: acc) - } - } - } - - loop(Queue(blockHash), Nil).flatMap { - case result @ List(`blockHash`) => - result.filterA(contains) - case result => - KVStoreRead[N].pure(result) - } - } - - /** Delete all blocks which are not descendants of a given block, - * making it the new root. - * - * Return the list of deleted block hashes. - */ - def pruneNonDescendants(blockHash: A#Hash): KVStore[N, List[A#Hash]] = - getPathFromRoot(blockHash).lift.flatMap { - case Nil => - KVStore[N].pure(Nil) - - case path @ (rootHash :: _) => - // The safe order to delete blocks would be to go down the main chain - // from the root, delete each non-mainchain child, then the parent, - // then descend on the main chain until we hit `blockHash`. - - // A similar effect can be achieved by collecting all descendants - // of the root, then deleting everything that isn't on the main chain, - // from the children towards the root, and finally the main chain itself, - // going from the root towards the children. - val isMainChain = path.toSet - - for { - deleteables <- getDescendants(rootHash, skip = Set(blockHash)).lift - _ <- deleteables.filterNot(isMainChain).traverse(deleteUnsafe(_)) - _ <- path.init.traverse(deleteUnsafe(_)) - } yield deleteables - } - - /** Remove all blocks in a tree, given by a `blockHash` that's in the tree, - * except perhaps a new root (and its descendants) we want to keep. - * - * This is used to delete an old tree when starting a new that's most likely - * not connected to it, and would otherwise result in a forest. - */ - def purgeTree( - blockHash: A#Hash, - keep: Option[A#Hash] - ): KVStore[N, List[A#Hash]] = - getPathFromRoot(blockHash).lift.flatMap { - case Nil => - KVStore[N].pure(Nil) - - case rootHash :: _ => - for { - ds <- getDescendants(rootHash, skip = keep.toSet).lift - // Going from the leaves towards the root. - _ <- ds.reverse.traverse(deleteUnsafe(_)) - } yield ds - } -} +) extends KVTree[N, A#Hash, A#Block]( + blockColl, + blockMetaColl, + parentToChildrenColl + )(BlockStorage.node[A]) object BlockStorage { - - /** Properties about the block that we frequently need for traversal. */ - case class BlockMeta[A <: Agreement]( - parentBlockHash: A#Hash, - height: Long - ) + implicit def node[A <: Agreement: Block]: KVTree.Node[A#Hash, A#Block] = + new KVTree.Node[A#Hash, A#Block] { + override def key(value: A#Block): A#Hash = + Block[A].blockHash(value) + override def parentKey(value: A#Block): A#Hash = + Block[A].parentBlockHash(value) + override def height(value: A#Block): Long = + Block[A].height(value) + } } diff --git a/metronome/hotstuff/service/test/src/io/iohk/metronome/hotstuff/service/storage/BlockStorageProps.scala b/metronome/hotstuff/service/test/src/io/iohk/metronome/hotstuff/service/storage/BlockStorageProps.scala index c67477ef..993755d3 100644 --- a/metronome/hotstuff/service/test/src/io/iohk/metronome/hotstuff/service/storage/BlockStorageProps.scala +++ b/metronome/hotstuff/service/test/src/io/iohk/metronome/hotstuff/service/storage/BlockStorageProps.scala @@ -1,7 +1,7 @@ package io.iohk.metronome.hotstuff.service.storage import cats.implicits._ -import io.iohk.metronome.storage.{KVCollection, KVStoreState} +import io.iohk.metronome.storage.{KVCollection, KVStoreState, KVTree} import io.iohk.metronome.hotstuff.consensus.basic.{Agreement, Block => BlockOps} import org.scalacheck._ import org.scalacheck.Arbitrary.arbitrary @@ -47,9 +47,9 @@ object BlockStorageProps extends Properties("BlockStorage") { object TestBlockStorage extends BlockStorage[Namespace, TestAgreement]( new KVCollection[Namespace, Hash, TestBlock](Namespace.Blocks), - new KVCollection[Namespace, Hash, BlockStorage.BlockMeta[ - TestAgreement - ]](Namespace.BlockMetas), + new KVCollection[Namespace, Hash, KVTree.NodeMeta[Hash]]( + Namespace.BlockMetas + ), new KVCollection[Namespace, Hash, Set[Hash]](Namespace.BlockToChildren) ) @@ -200,8 +200,8 @@ object BlockStorageProps extends Properties("BlockStorage") { val s = data.store.putBlock(block) s(Namespace.Blocks)(block.id) == block s(Namespace.BlockMetas)(block.id) - .asInstanceOf[BlockStorage.BlockMeta[TestAgreement]] - .parentBlockHash == block.parentId + .asInstanceOf[KVTree.NodeMeta[Hash]] + .parentKey == block.parentId } property("put unordered") = forAll { diff --git a/metronome/storage/src/io/iohk/metronome/storage/KVRingBuffer.scala b/metronome/storage/src/io/iohk/metronome/storage/KVRingBuffer.scala index 414506f3..30ed83ab 100644 --- a/metronome/storage/src/io/iohk/metronome/storage/KVRingBuffer.scala +++ b/metronome/storage/src/io/iohk/metronome/storage/KVRingBuffer.scala @@ -3,7 +3,10 @@ package io.iohk.metronome.storage import cats.implicits._ import scodec.{Decoder, Encoder, Codec} -/** Storing the last N items inserted into a collection. */ +/** Storing the last N items inserted into a collection. + * + * This component is currently tested through `LedgerStorage`. + */ class KVRingBuffer[N, K, V]( coll: KVCollection[N, K, V], metaNamespace: N, diff --git a/metronome/storage/src/io/iohk/metronome/storage/KVTree.scala b/metronome/storage/src/io/iohk/metronome/storage/KVTree.scala new file mode 100644 index 00000000..a3a9d91b --- /dev/null +++ b/metronome/storage/src/io/iohk/metronome/storage/KVTree.scala @@ -0,0 +1,293 @@ +package io.iohk.metronome.storage + +import cats.implicits._ +import scala.collection.immutable.Queue + +/** Storage for nodes that maintains parent-child relationships as well, + * to facilitate tree traversal and pruning. + * + * It is assumed that the application maintains some pointers into the tree + * where it can start traversing from, e.g. the last Commit Quorum Certificate + * would point at a block hash which would serve as the entry point. + * + * This component is currently tested through `BlockStorage`. + */ +class KVTree[N, K, V]( + nodeColl: KVCollection[N, K, V], + nodeMetaColl: KVCollection[N, K, KVTree.NodeMeta[K]], + parentToChildrenColl: KVCollection[N, K, Set[K]] +)(implicit ev: KVTree.Node[K, V]) { + import KVTree.NodeMeta + + private implicit val kvn = KVStore.instance[N] + private implicit val kvrn = KVStoreRead.instance[N] + + /** Insert a node into the store, and if the parent still exists, + * then add this node to its children. + */ + def put(value: V): KVStore[N, Unit] = { + val nodeKey = ev.key(value) + val meta = + NodeMeta(ev.parentKey(value), ev.height(value)) + + nodeColl.put(nodeKey, value) >> + nodeMetaColl.put(nodeKey, meta) >> + parentToChildrenColl.alter(meta.parentKey) { maybeChildren => + maybeChildren orElse Set.empty.some map (_ + nodeKey) + } + + } + + /** Retrieve a node by key, if it exists. */ + def get(key: K): KVStoreRead[N, Option[V]] = + nodeColl.read(key) + + /** Check whether a node is present in the tree. */ + def contains(key: K): KVStoreRead[N, Boolean] = + nodeMetaColl.read(key).map(_.isDefined) + + /** Check how many children the node has in the tree. */ + private def childCount(key: K): KVStoreRead[N, Int] = + parentToChildrenColl.read(key).map(_.fold(0)(_.size)) + + /** Check whether the parent of the node is present in the tree. */ + private def hasParent(key: K): KVStoreRead[N, Boolean] = + nodeMetaColl.read(key).flatMap { + case None => KVStoreRead[N].pure(false) + case Some(meta) => contains(meta.parentKey) + } + + private def getParentKey( + key: K + ): KVStoreRead[N, Option[K]] = + nodeMetaColl.read(key).map(_.map(_.parentKey)) + + /** Check whether it's safe to delete a node. + * + * A node is safe to delete if doing so doesn't break up the tree + * into a forest, in which case we may have nodes we cannot reach + * by traversal, leaking space. + * + * This is true if the node has no children, + * or it has no parent and at most one child. + */ + private def canDelete(key: K): KVStoreRead[N, Boolean] = + (hasParent(key), childCount(key)).mapN { + case (_, 0) => true + case (false, 1) => true + case _ => false + } + + /** Delete a node by hash, if doing so wouldn't break the tree; + * otherwise do nothing. + * + * Return `true` if node has been deleted, `false` if not. + * + * If this is not efficent enough, then move the deletion traversal + * logic into the this class so it can make sure all the invariants + * are maintained, e.g. collect all hashes that can be safely deleted + * and then do so without checks. + */ + def delete(key: K): KVStore[N, Boolean] = + canDelete(key).lift.flatMap { ok => + deleteUnsafe(key).whenA(ok).as(ok) + } + + /** Delete a node and remove it from any parent-to-child mapping, + * without any checking for the tree structure invariants. + */ + def deleteUnsafe(key: K): KVStore[N, Unit] = { + def deleteIfEmpty(maybeChildren: Option[Set[K]]) = + maybeChildren.filter(_.nonEmpty) + + getParentKey(key).lift.flatMap { + case None => + KVStore[N].unit + case Some(parentKey) => + parentToChildrenColl.alter(parentKey) { maybeChildren => + deleteIfEmpty(maybeChildren.map(_ - key)) + } + } >> + nodeColl.delete(key) >> + nodeMetaColl.delete(key) >> + // Keep the association from existing children, until they last one is deleted. + parentToChildrenColl.alter(key)(deleteIfEmpty) + } + + /** Get the ancestor chain of a node from the root, including the node itself. + * + * If the node is not in the tree, the result will be empty, + * otherwise `head` will be the root of the node tree, + * and `last` will be the node itself. + */ + def getPathFromRoot(key: K): KVStoreRead[N, List[K]] = { + def loop( + key: K, + acc: List[K] + ): KVStoreRead[N, List[K]] = { + getParentKey(key).flatMap { + case None => + // This node doesn't exist in the tree, so our ancestry is whatever we collected so far. + KVStoreRead[N].pure(acc) + + case Some(parentKey) => + // So at least `key` exists in the tree. + loop(parentKey, key :: acc) + } + } + loop(key, Nil) + } + + /** Get the ancestor chain between two hashes in the chain, if there is one. + * + * If either of the nodes are not in the tree, or there's no path between them, + * return an empty list. This can happen if we have already pruned away the ancestry as well. + */ + def getPathFromAncestor( + ancestorKey: K, + descendantKey: K + ): KVStoreRead[N, List[K]] = { + def loop( + key: K, + acc: List[K], + maxDistance: Long + ): KVStoreRead[N, List[K]] = { + if (key == ancestorKey) { + KVStoreRead[N].pure(key :: acc) + } else if (maxDistance == 0) { + KVStoreRead[N].pure(Nil) + } else { + nodeMetaColl.read(key).flatMap { + case None => + KVStoreRead[N].pure(Nil) + case Some(meta) => + loop(meta.parentKey, key :: acc, maxDistance - 1) + } + } + } + + ( + nodeMetaColl.read(ancestorKey), + nodeMetaColl.read(descendantKey) + ).mapN((_, _)) + .flatMap { + case (Some(ameta), Some(dmeta)) => + loop( + descendantKey, + Nil, + maxDistance = dmeta.height - ameta.height + ) + case _ => KVStoreRead[N].pure(Nil) + } + } + + /** Collect all descendants of a node, including the node itself. + * + * The result will start with the nodes furthest away, + * so it should be safe to delete them in the same order; + * `last` will be the node itself. + * + * The `skip` parameter can be used to avoid traversing + * branches that we want to keep during deletion. + */ + def getDescendants( + key: K, + skip: Set[K] = Set.empty + ): KVStoreRead[N, List[K]] = { + // BFS traversal. + def loop( + queue: Queue[K], + acc: List[K] + ): KVStoreRead[N, List[K]] = { + queue.dequeueOption match { + case None => + KVStoreRead[N].pure(acc) + + case Some((key, queue)) if skip(key) => + loop(queue, acc) + + case Some((key, queue)) => + parentToChildrenColl.read(key).flatMap { + case None => + // Since we're not inserting an empty child set, + // we can't tell here if the node exists or not. + loop(queue, key :: acc) + case Some(children) => + loop(queue ++ children, key :: acc) + } + } + } + + loop(Queue(key), Nil).flatMap { + case result @ List(`key`) => + result.filterA(contains) + case result => + KVStoreRead[N].pure(result) + } + } + + /** Delete all nodes which are not descendants of a given node, making it the new root. + * + * Return the list of deleted node keys. + */ + def pruneNonDescendants(key: K): KVStore[N, List[K]] = + getPathFromRoot(key).lift.flatMap { + case Nil => + KVStore[N].pure(Nil) + + case path @ (rootHash :: _) => + // The safe order to delete nodes would be to go down the main chain + // from the root, delete each non-mainchain child, then the parent, + // then descend on the main chain until we hit `key`. + + // A similar effect can be achieved by collecting all descendants + // of the root, then deleting everything that isn't on the main chain, + // from the children towards the root, and finally the main chain itself, + // going from the root towards the children. + val isMainChain = path.toSet + + for { + deleteables <- getDescendants(rootHash, skip = Set(key)).lift + _ <- deleteables.filterNot(isMainChain).traverse(deleteUnsafe(_)) + _ <- path.init.traverse(deleteUnsafe(_)) + } yield deleteables + } + + /** Remove all nodes in a tree, given by a key that's in the tree, + * except perhaps a new root (and its descendants) we want to keep. + * + * This is used to delete an old tree when starting a new that's most likely + * not connected to it, and would otherwise result in a forest. + */ + def purgeTree( + key: K, + keep: Option[K] + ): KVStore[N, List[K]] = + getPathFromRoot(key).lift.flatMap { + case Nil => + KVStore[N].pure(Nil) + + case rootHash :: _ => + for { + ds <- getDescendants(rootHash, skip = keep.toSet).lift + // Going from the leaves towards the root. + _ <- ds.reverse.traverse(deleteUnsafe(_)) + } yield ds + } +} + +object KVTree { + + /** Type class for the node-like values stored in the tree. */ + trait Node[K, V] { + def key(value: V): K + def parentKey(value: V): K + def height(value: V): Long + } + + /** Properties about the nodes that we frequently need for traversal. */ + case class NodeMeta[K]( + parentKey: K, + height: Long + ) +}