-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PM-2931: Ledger data structure (#17)
* 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
Showing
7 changed files
with
274 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...ome/checkpointing/service/src/io/iohk/metronome/checkpointing/service/models/Ledger.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
.../checkpointing/service/src/io/iohk/metronome/checkpointing/service/models/RLPCodecs.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
40 changes: 40 additions & 0 deletions
40
.../service/test/src/io/iohk/metronome/checkpointing/service/models/ArbitraryInstances.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...ointing/service/test/src/io/iohk/metronome/checkpointing/service/models/LedgerProps.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
...ting/service/test/src/io/iohk/metronome/checkpointing/service/models/RLPCodecsProps.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
95 changes: 95 additions & 0 deletions
95
...nting/service/test/src/io/iohk/metronome/checkpointing/service/models/RLPCodecsSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
) | ||
) | ||
} | ||
} | ||
} |