Skip to content

Commit

Permalink
PM-2931: Ledger data structure (#17)
Browse files Browse the repository at this point in the history
* PM-2931: Basic ledger.

* PM-2931: Use a Vector, maybe we can save some time on sorting for deterministic hashes.

* PM-2931: Testing the update method.

* PM-2931: Add Ledger.hash using RLP.

* PM-2931: Test RLP roundtrip for a ledger.

* PM-2931: RLPCodecsSpec with example for Ledger.

* PM-2931: Simplify ledger update check.

* PM-2931: Simplify property declaration.
  • Loading branch information
aakoshh authored Apr 5, 2021
1 parent 949e3f3 commit 6a88c7e
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class MetronomeModule(val crossScalaVersion: String) extends CrossScalaModule {
)

override def ivyDeps = super.ivyDeps() ++ Agg(
ivy"io.iohk::scalanet:${VersionOf.scalanet}"
ivy"io.iohk::mantis-rlp:${VersionOf.mantis}"
)

object test extends TestModule
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.ethereum.rlp
import io.iohk.metronome.core.Validated
import io.iohk.metronome.checkpointing.interpreter.models.Transaction
import io.iohk.metronome.crypto.hash.{Hash, Keccak256}

/** Current state of the ledger after applying all previous blocks.
*
* Basically it's the last checkpoint, plus any accumulated proposer blocks
* since then. Initially the last checkpoint is empty; conceptually it could
* the the genesis block of the PoW chain, but we don't know what that is
* until we talk to the interpreter, and we also can't produce it on our
* own since it's opaque data.
*/
case class Ledger(
maybeLastCheckpoint: Option[Transaction.CheckpointCandidate],
proposerBlocks: Vector[Transaction.ProposerBlock]
) {

/** Calculate the hash of the ledger so we can put it in blocks
* and refer to it when syncing state between federation members.
*/
lazy val hash: Hash = Ledger.hash(this)

/** Apply a validated transaction to produce the next ledger state.
*
* The transaction should have been validated against the PoW ledger
* by this point, so we know for example that the new checkpoint is
* a valid extension of the previous one.
*/
def update(transaction: Validated[Transaction]): Ledger =
(transaction: Transaction) match {
case t @ Transaction.ProposerBlock(_) =>
if (proposerBlocks.contains(t))
this
else
copy(proposerBlocks = proposerBlocks :+ t)

case t @ Transaction.CheckpointCandidate(_) =>
Ledger(Some(t), Vector.empty)
}
}

object Ledger {
val empty = Ledger(None, Vector.empty)

def hash(ledger: Ledger): Hash = {
import RLPCodecs._
Keccak256(rlp.encode(ledger))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.ethereum.rlp.RLPCodec
import io.iohk.ethereum.rlp.RLPCodec.Ops
import io.iohk.ethereum.rlp.RLPImplicitDerivations._
import io.iohk.ethereum.rlp.RLPImplicits._
import io.iohk.metronome.checkpointing.interpreter.models.Transaction
import scodec.bits.{BitVector, ByteVector}

object RLPCodecs {
implicit val rlpBitVector: RLPCodec[BitVector] =
implicitly[RLPCodec[Array[Byte]]].xmap(BitVector(_), _.toByteArray)

implicit val rlpByteVector: RLPCodec[ByteVector] =
implicitly[RLPCodec[Array[Byte]]].xmap(ByteVector(_), _.toArray)

implicit val rlpProposerBlock: RLPCodec[Transaction.ProposerBlock] =
deriveLabelledGenericRLPCodec

implicit val rlpCheckpointCandidate
: RLPCodec[Transaction.CheckpointCandidate] =
deriveLabelledGenericRLPCodec

implicit def rlpVector[T: RLPCodec]: RLPCodec[Vector[T]] =
seqEncDec[T]().xmap(_.toVector, _.toSeq)

implicit val rlpLedger: RLPCodec[Ledger] =
deriveLabelledGenericRLPCodec
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.metronome.checkpointing.interpreter.models.Transaction
import org.scalacheck._
import org.scalacheck.Arbitrary.arbitrary
import scodec.bits.BitVector

object ArbitraryInstances {
implicit val arbBitVector: Arbitrary[BitVector] =
Arbitrary {
arbitrary[Array[Byte]].map(BitVector(_))
}

implicit val arbProposerBlock: Arbitrary[Transaction.ProposerBlock] =
Arbitrary {
arbitrary[BitVector].map(Transaction.ProposerBlock(_))
}

implicit val arbCheckpointCandidate
: Arbitrary[Transaction.CheckpointCandidate] =
Arbitrary {
arbitrary[BitVector].map(Transaction.CheckpointCandidate(_))
}

implicit val arbTransaction: Arbitrary[Transaction] =
Arbitrary {
Gen.frequency(
4 -> arbitrary[Transaction.ProposerBlock],
1 -> arbitrary[Transaction.CheckpointCandidate]
)
}

implicit val arbLeger: Arbitrary[Ledger] =
Arbitrary {
for {
mcp <- arbitrary[Option[Transaction.CheckpointCandidate]]
pbs <- arbitrary[Set[Transaction.ProposerBlock]].map(_.toVector)
} yield Ledger(mcp, pbs)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.metronome.core.Validated
import io.iohk.metronome.checkpointing.interpreter.models.Transaction
import org.scalacheck._
import org.scalacheck.Prop.forAll

object LedgerProps extends Properties("Ledger") {
import ArbitraryInstances._

property("update") = forAll { (ledger: Ledger, transaction: Transaction) =>
val updated = ledger.update(Validated[Transaction](transaction))

transaction match {
case _: Transaction.ProposerBlock
if ledger.proposerBlocks.contains(transaction) =>
updated == ledger

case _: Transaction.ProposerBlock =>
updated.proposerBlocks.last == transaction &&
updated.proposerBlocks.distinct == updated.proposerBlocks &&
updated.maybeLastCheckpoint == ledger.maybeLastCheckpoint

case _: Transaction.CheckpointCandidate =>
updated.maybeLastCheckpoint == Some(transaction) &&
updated.proposerBlocks.isEmpty
}
}

property("hash") = forAll { (ledger1: Ledger, ledger2: Ledger) =>
ledger1 == ledger2 && ledger1.hash == ledger2.hash ||
ledger1 != ledger2 && ledger1.hash != ledger2.hash
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.ethereum.rlp
import io.iohk.ethereum.rlp.RLPCodec
import org.scalacheck._
import org.scalacheck.Prop.forAll
import scala.reflect.ClassTag

object RLPCodecsProps extends Properties("RLPCodecs") {
import ArbitraryInstances._
import RLPCodecs._

/** Test that encoding to and decoding from RLP preserves the value. */
def propRoundTrip[T: RLPCodec: Arbitrary: ClassTag] =
property(implicitly[ClassTag[T]].runtimeClass.getSimpleName) = forAll {
(value0: T) =>
val bytes = rlp.encode(value0)
val value1 = rlp.decode[T](bytes)
value0 == value1
}

propRoundTrip[Ledger]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.iohk.metronome.checkpointing.service.models

import io.iohk.ethereum.rlp._
import io.iohk.metronome.checkpointing.interpreter.models.Transaction
import org.scalactic.Equality
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scala.reflect.ClassTag
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary.arbitrary

/** Concrete examples of RLP encoding, so we can make sure the structure is what we expect.
*
* Complements `RLPCodecsProps` which works with arbitrary data.
*/
class RLPCodecsSpec extends AnyFlatSpec with Matchers {
import ArbitraryInstances._
import RLPCodecs._

def sample[T: Arbitrary] = arbitrary[T].sample.get

// Structrual equality checker for RLPEncodeable.
// It has different wrappers for items based on whether it was hand crafted or generated
// by codecs, and the RLPValue has mutable arrays inside.
implicit val eqRLPList = new Equality[RLPEncodeable] {
override def areEqual(a: RLPEncodeable, b: Any): Boolean =
(a, b) match {
case (a: RLPList, b: RLPList) =>
a.items.size == b.items.size && a.items.zip(b.items).forall {
case (a, b) =>
areEqual(a, b)
}
case (a: RLPValue, b: RLPValue) =>
a.bytes.sameElements(b.bytes)
case other =>
false
}
}

abstract class Example[T: RLPCodec: ClassTag] {
def decoded: T
def encoded: RLPEncodeable

def name =
s"RLPCodec[${implicitly[ClassTag[T]].runtimeClass.getSimpleName}]"

def encode = RLPEncoder.encode(decoded)
def decode = RLPDecoder.decode[T](encoded)
}

def exampleBehavior[T](example: Example[T]) = {
it should "encode the example value to the expected RLP data" in {
example.encode shouldEqual example.encoded
}

it should "decode the example RLP data to the expected value" in {
example.decode shouldEqual example.decoded
}
}

def test[T](example: Example[T]) = {
example.name should behave like exampleBehavior(example)
}

test {
new Example[Ledger] {
val ledger = Ledger(
maybeLastCheckpoint = Some(
sample[Transaction.CheckpointCandidate]
),
proposerBlocks = Vector(
sample[Transaction.ProposerBlock],
sample[Transaction.ProposerBlock]
)
)

override val decoded: Ledger = ledger

override val encoded: RLPEncodeable =
RLPList( // Ledger
RLPList( // Option
RLPList( // CheckpointCandidate
RLPValue(ledger.maybeLastCheckpoint.get.value.toByteArray)
)
),
RLPList( // Vector
RLPList( // ProposerBlock
RLPValue(ledger.proposerBlocks(0).value.toByteArray)
),
RLPList(RLPValue(ledger.proposerBlocks(1).value.toByteArray))
)
)
}
}
}

0 comments on commit 6a88c7e

Please sign in to comment.