Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fea(iris): ATL-1923 Implement in-memory ledger #43

Merged
merged 3 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions iris/service/build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Dependencies._
import sbt.Keys.testFrameworks
import sbtghpackages.GitHubPackagesPlugin.autoImport._

ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "3.1.3"
Expand All @@ -11,13 +13,23 @@ ThisBuild / apiBaseDirectory := baseDirectory.value / "../api"
// Project definitions
lazy val root = project
.in(file("."))
.aggregate(core, sql, `api-server`)
.aggregate(core, sql, server)

lazy val core = project
.in(file("core"))
.settings(
name := "iris-core",
libraryDependencies ++= coreDependencies
githubTokenSource := TokenSource.Environment("ATALA_GITHUB_TOKEN"),
resolvers += Resolver
.githubPackages("input-output-hk", "atala-prism-sdk"),
// Needed for Kotlin coroutines that support new memory management mode
resolvers +=
"JetBrains Space Maven Repository" at "https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven",
libraryDependencies ++= coreDependencies,
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"),
// gRPC settings
Compile / PB.targets := Seq(scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"),
Compile / PB.protoSources := Seq(apiBaseDirectory.value / "grpc")
)

lazy val sql = project
Expand All @@ -28,13 +40,10 @@ lazy val sql = project
)
.dependsOn(core)

lazy val `api-server` = project
.in(file("api-server"))
lazy val server = project
.in(file("server"))
.settings(
name := "iris-api-server",
name := "iris-server",
libraryDependencies ++= apiServerDependencies,
// gRPC settings
Compile / PB.targets := Seq(scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"),
Compile / PB.protoSources := Seq(apiBaseDirectory.value / "grpc")
)
.dependsOn(core, sql)
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ final case class IrisNotification(foo: String)

final case class IrisOperationId(id: String)
final case class IrisOperation(foo: String)
final case class SignedIrisOperation(foo: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.iohk.atala.iris.core.model.ledger

case class Funds(lovelaces: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.iohk.atala.iris.core.model.ledger

import enumeratum.{Enum, EnumEntry}

import scala.collection.immutable.ArraySeq

sealed trait Ledger extends EnumEntry

object Ledger extends Enum[Ledger] {
val values = ArraySeq(InMemory, CardanoMainnet, CardanoTestnet)

case object InMemory extends Ledger
case object CardanoMainnet extends Ledger
case object CardanoTestnet extends Ledger
pva701 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.iohk.atala.iris.core.model.ledger

case class TransactionDetails(id: TransactionId, status: TransactionStatus)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.iohk.atala.iris.core.model.ledger

import com.typesafe.config.ConfigMemorySize
import io.iohk.atala.shared.{HashValue, HashValueConfig, HashValueFrom}

import scala.collection.immutable.ArraySeq

class TransactionId(bytes: ArraySeq[Byte]) extends HashValue {
override def value: ArraySeq[Byte] = bytes
}

object TransactionId extends HashValueFrom[TransactionId] {

override val config: HashValueConfig = HashValueConfig(
ConfigMemorySize.ofBytes(32)
)

override protected def constructor(value: ArraySeq[Byte]): TransactionId =
new TransactionId(value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.iohk.atala.iris.core.model.ledger

import enumeratum.{Enum, EnumEntry}
import enumeratum.EnumEntry.Snakecase
import scala.collection.immutable.ArraySeq

sealed trait TransactionStatus extends EnumEntry with Snakecase

object TransactionStatus extends Enum[TransactionStatus] {
val values = ArraySeq(InMempool, Submitted, Expired, InLedger)

case object InMempool extends TransactionStatus
case object Submitted extends TransactionStatus
case object Expired extends TransactionStatus
case object InLedger extends TransactionStatus
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.iohk.atala.iris.core.repository

import io.iohk.atala.iris.core.model.{IrisOperation, IrisOperationId, SignedIrisOperation}
import io.iohk.atala.iris.core.model as model
import zio.*

// TODO: replace with actual implementation
trait OperationsRepository[F[_]] {
def getOperation(id: IrisOperationId): F[IrisOperation]
def saveOperations(ops: Seq[SignedIrisOperation]): F[Unit]
def getOperation(id: model.IrisOperationId): F[model.IrisOperation]
def saveOperations(ops: Seq[model.IrisOperation]): F[Unit]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package io.iohk.atala.iris.core.service

import io.iohk.atala.iris.core.model.ledger.TransactionStatus.{InLedger, InMempool}
import io.iohk.atala.iris.core.model.ledger.{Funds, TransactionDetails, TransactionId}
import io.iohk.atala.iris.core.service.InmemoryUnderlyingLedgerService.{CardanoBlock, CardanoTransaction, Config}
import io.iohk.atala.iris.proto.dlt as proto
import io.iohk.atala.prism.crypto.Sha256
import zio.stm.*
import zio.*

object InmemoryUnderlyingLedgerService {
case class Config(blockEvery: Duration, initialFunds: Funds, txFee: Funds)

case class CardanoTransaction(operations: Seq[proto.IrisOperation]) {
lazy val transactionId: TransactionId = {
val objectBytes = proto.AtalaObject(operations).toByteArray
val hash = Sha256.compute(objectBytes)
TransactionId
.from(hash.getValue)
.getOrElse(throw new RuntimeException("Unexpected invalid hash"))
}
}

case class CardanoBlock(txs: Seq[CardanoTransaction])

def layer(config: Config): ULayer[InmemoryUnderlyingLedgerService] = ZLayer.fromZIO {
for {
mempoolRef <- TRef.make(Vector[CardanoTransaction]()).commit
blocksRef <- TRef.make(Vector[CardanoBlock]()).commit
initialBalance <- TRef.make(config.initialFunds).commit
srv = InmemoryUnderlyingLedgerService(config, mempoolRef, blocksRef, initialBalance)
_ <- srv.startBackgroundProcess()
} yield srv
}
}

class InmemoryUnderlyingLedgerService(
pva701 marked this conversation as resolved.
Show resolved Hide resolved
config: Config,
mempoolRef: TRef[Vector[CardanoTransaction]],
blocksRef: TRef[Vector[CardanoBlock]],
balanceRef: TRef[Funds]
) extends UnderlyingLedgerService {

override def publish(operations: Seq[proto.IrisOperation]): IO[LedgerError, Unit] =
STM.atomically {
for {
curFunds <- balanceRef.get
newFunds <- STM.cond(
curFunds.lovelaces >= config.txFee.lovelaces,
Funds(curFunds.lovelaces - config.txFee.lovelaces),
LedgerError("Insufficient wallet balance")
)
_ <- balanceRef.set(newFunds)
_ <- mempoolRef.update(_.appended(CardanoTransaction(operations)))
} yield ()
}

override def getTransactionDetails(transactionId: TransactionId): IO[LedgerError, TransactionDetails] =
STM.atomically {
for {
mempool <- mempoolRef.get
blockchain <- blocksRef.get
tdetails <- STM.fromOption {
mempool.find(_.transactionId == transactionId)
.map(_ => TransactionDetails(transactionId, InMempool))
}.orElse {
STM.fromOption {
blockchain.find(block => block.txs.exists(t => t.transactionId == transactionId))
.map(_ => TransactionDetails(transactionId, InLedger))
}
}
.orElseFail(LedgerError(s"Couldn't find tx $transactionId"))
} yield tdetails
}

override def deleteTransaction(transactionId: TransactionId): IO[LedgerError, Unit] = STM.atomically {
for {
mempool <- mempoolRef.get
_ <- STM.cond(mempool.exists(_.transactionId == transactionId),
(),
LedgerError(s"Transaction $transactionId not found in the mempool")
)
_ <- mempoolRef.update(m => m.filter(_.transactionId != transactionId))
_ <- balanceRef.update(b => Funds(b.lovelaces + config.txFee.lovelaces))
} yield ()
}

override def getWalletBalance: IO[LedgerError, Funds] = balanceRef.get.commit

def getMempool: UIO[List[CardanoTransaction]] = mempoolRef.get.commit.map(_.toList)

def getBlocks: UIO[List[CardanoBlock]] = blocksRef.get.commit.map(_.toList)

private[service] def startBackgroundProcess(): UIO[Unit] = STM.atomically {
for {
// Craft a new block from mempool transactions
txs <- mempoolRef.modify(old => (old, Vector.empty))
_ <- blocksRef.update(_.appended(CardanoBlock(txs)))
} yield ()
}
.repeat(Schedule.spaced(config.blockEvery))
.fork.map(_ => ())
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package io.iohk.atala.iris.core.service

import io.iohk.atala.iris.core.model.SignedIrisOperation
import io.iohk.atala.iris.proto.dlt as proto
import zio.*

// TODO: replace with actual implementation
trait PublishingService {
def publishOperations(op: SignedIrisOperation): UIO[Unit]
def publishOperation(op: proto.IrisOperation): UIO[Unit]
}

object MockPublishingService {
val layer: ULayer[PublishingService] = ZLayer.succeed {
new PublishingService {
override def publishOperations(op: SignedIrisOperation): UIO[Unit] = ZIO.unit
override def publishOperation(op: proto.IrisOperation): UIO[Unit] = ZIO.unit
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.iohk.atala.iris.core.service

import io.iohk.atala.iris.proto.dlt as proto
import io.iohk.atala.iris.core.model.IrisOperation
import io.iohk.atala.iris.core.model.ledger.{Funds, TransactionDetails, TransactionId}
import zio.{IO, UIO}

case class LedgerError(msg: String) extends RuntimeException(msg)

trait UnderlyingLedgerService {
// def getType: Ledger

def publish(operations: Seq[proto.IrisOperation]): IO[LedgerError, Unit]

def getTransactionDetails(transactionId: TransactionId): IO[LedgerError, TransactionDetails]

def deleteTransaction(transactionId: TransactionId): IO[LedgerError, Unit]

def getWalletBalance: IO[LedgerError, Funds]
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package io.iohk.atala.iris.core.worker

import io.iohk.atala.iris.core.model.SignedIrisOperation
import io.iohk.atala.iris.core.service.PublishingService
import io.iohk.atala.iris.proto.dlt as proto
import zio.{UIO, ULayer, ZIO, ZLayer}

trait PublishingScheduler {
def scheduleOperations(op: SignedIrisOperation): UIO[Unit]
def scheduleOperations(op: proto.IrisOperation): UIO[Unit]
}

object MockPublishingScheduler {
val layer: ULayer[PublishingScheduler] = ZLayer.succeed {
new PublishingScheduler {
def scheduleOperations(op: SignedIrisOperation): UIO[Unit] = ZIO.unit
def scheduleOperations(op: proto.IrisOperation): UIO[Unit] = ZIO.unit
}
}
}
Loading