Skip to content

Commit

Permalink
Introduce locally-defined Rejection reasons instead of the particip…
Browse files Browse the repository at this point in the history
…ant-state rejection reason type. [KVL-1002] (#10376)

* participant-integration-api: Reduce usage of the state RejectionReason.

Use a local `Rejection` enumeration instead.

This also changes an implicit conversion to be a somewhat-explicit
conversion.

CHANGELOG_BEGIN
CHANGELOG_END

* sandbox-classic: Internalize rejection reasons.

Instead of using RejectionReasonV0, we can declare them locally.

* kvutils: Create internal rejection reasons in the committer.

These can be converted to state rejection reasons later.

By using internal rejection representations, we're not coupled to the
state API.

* sandbox-classic: Remove unused code from `Rejection`.

* participant-integration-api: Push the rejection reason conversion down.

Just a little bit further.

* participant-integration-api: Add tests for converting rejection reasons.

* participant-integration-api: Remove an unused test dependency.
  • Loading branch information
SamirTalwar authored Jul 23, 2021
1 parent 96f0483 commit d7077e1
Show file tree
Hide file tree
Showing 19 changed files with 480 additions and 228 deletions.
2 changes: 1 addition & 1 deletion ledger/participant-integration-api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ da_scala_test_suite(
"@maven//:com_typesafe_akka_akka_stream",
"@maven//:com_typesafe_akka_akka_stream_testkit",
"@maven//:org_mockito_mockito_scala",
"@maven//:org_playframework_anorm_anorm",
"@maven//:org_scalacheck_scalacheck",
"@maven//:org_scalactic_scalactic",
"@maven//:org_scalatest_scalatest",
Expand Down Expand Up @@ -260,7 +261,6 @@ da_scala_test_suite(
"//ledger/metrics:metrics-test-lib",
"//ledger/participant-state",
"//ledger/participant-state-index",
"//ledger/participant-state/kvutils",
"//ledger/test-common",
"//ledger/test-common:dar-files-stable-lib",
"//libs-scala/concurrent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.daml.ledger.api.v1.ledger_offset.LedgerOffset
import com.daml.ledger.offset.Offset
import com.daml.lf.data.Ref
import com.daml.platform.ApiOffset.ApiOffsetConverter
import com.daml.platform.store.Conversions.domainRejectionReasonToErrorCode
import com.daml.platform.store.Conversions.RejectionReasonOps
import com.daml.platform.store.entries.LedgerEntry
import com.google.rpc.status.Status

Expand Down Expand Up @@ -56,17 +56,14 @@ private[platform] object CompletionFromTransaction {
checkpoint = toApiCheckpoint(recordTime, offset),
Seq(Completion(commandId, Some(Status()), transactionId)),
)

case (offset, LedgerEntry.Rejection(recordTime, commandId, `appId`, actAs, reason))
if actAs.exists(parties) =>
val stateReason = reason.toParticipantStateRejectionReason
val status = Status(stateReason.code.value, stateReason.description)
offset -> CompletionStreamResponse(
checkpoint = toApiCheckpoint(recordTime, offset),
Seq(
Completion(
commandId,
Some(Status(reason.value, reason.description)),
)
),
Seq(Completion(commandId, Some(status))),
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@ import anorm.Column.nonNull
import anorm._
import com.daml.ledger.api.domain
import com.daml.ledger.offset.Offset
import com.daml.ledger.participant.state.v1.RejectionReasonV0
import com.daml.ledger.participant.state.v1.RejectionReasonV0._
import com.daml.ledger.participant.state.{v1 => state}
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Ref
import com.daml.lf.ledger.EventId
import com.daml.lf.value.Value
import io.grpc.Status.Code
import spray.json.DefaultJsonProtocol._
import spray.json._

import scala.language.implicitConversions

// TODO append-only: split this file on cleanup, and move anorm/db conversion related stuff to the right place

private[platform] object OracleArrayConversions {
Expand Down Expand Up @@ -340,22 +336,21 @@ private[platform] object Conversions {
override val jdbcType: Int = ParameterMetaData.StringParameterMetaData.jdbcType
}

// RejectionReason
implicit def domainRejectionReasonToErrorCode(reason: domain.RejectionReason): Code =
domainRejectionReasonToParticipantRejectionReason(
reason
).code

implicit def domainRejectionReasonToParticipantRejectionReason(
reason: domain.RejectionReason
): RejectionReasonV0 =
reason match {
case r: domain.RejectionReason.Inconsistent => Inconsistent(r.description)
case r: domain.RejectionReason.Disputed => Disputed(r.description)
case r: domain.RejectionReason.OutOfQuota => ResourcesExhausted(r.description)
case r: domain.RejectionReason.PartyNotKnownOnLedger => PartyNotKnownOnLedger(r.description)
case r: domain.RejectionReason.SubmitterCannotActViaParticipant =>
SubmitterCannotActViaParticipant(r.description)
case r: domain.RejectionReason.InvalidLedgerTime => InvalidLedgerTime(r.description)
}
implicit class RejectionReasonOps(rejectionReason: domain.RejectionReason) {
def toParticipantStateRejectionReason: state.RejectionReason =
rejectionReason match {
case domain.RejectionReason.Inconsistent(reason) =>
state.RejectionReasonV0.Inconsistent(reason)
case domain.RejectionReason.Disputed(reason) =>
state.RejectionReasonV0.Disputed(reason)
case domain.RejectionReason.OutOfQuota(reason) =>
state.RejectionReasonV0.ResourcesExhausted(reason)
case domain.RejectionReason.PartyNotKnownOnLedger(reason) =>
state.RejectionReasonV0.PartyNotKnownOnLedger(reason)
case domain.RejectionReason.SubmitterCannotActViaParticipant(reason) =>
state.RejectionReasonV0.SubmitterCannotActViaParticipant(reason)
case domain.RejectionReason.InvalidLedgerTime(reason) =>
state.RejectionReasonV0.InvalidLedgerTime(reason)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ private class JdbcLedgerDao(
ledgerEffectiveTime: Instant,
transaction: CommittedTransaction,
divulged: Iterable[DivulgedContract],
)(implicit connection: Connection): Option[RejectionReason] =
)(implicit connection: Connection): Option[PostCommitValidation.Rejection] =
Timed.value(
metrics.daml.index.db.storeTransactionDbMetrics.commitValidation,
postCommitValidation.validate(
Expand Down Expand Up @@ -427,7 +427,7 @@ private class JdbcLedgerDao(
Update.CommandRejected(
recordTime = Time.Timestamp.assertFromInstant(recordTime),
submitterInfo = SubmitterInfo(actAs, applicationId, commandId, Instant.EPOCH),
reason = reason,
reason = reason.toParticipantStateRejectionReason,
)
),
)
Expand Down Expand Up @@ -686,12 +686,12 @@ private class JdbcLedgerDao(
)
)

case Some(error) =>
case Some(reason) =>
submitterInfo.map(someSubmitterInfo =>
Update.CommandRejected(
recordTime = Time.Timestamp.assertFromInstant(recordTime),
submitterInfo = someSubmitterInfo,
reason = error,
reason = reason.toStateV1RejectionReason,
)
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package com.daml.platform.store.appendonlydao.events
import java.sql.Connection
import java.time.Instant

import com.daml.ledger.participant.state.v1.RejectionReasonV0
import com.daml.ledger.participant.state.v1
import com.daml.lf.transaction.CommittedTransaction
import com.daml.platform.store.appendonlydao.events.PostCommitValidation._
import com.daml.platform.store.backend.{ContractStorageBackend, PartyStorageBackend}

/** Performs post-commit validation on transactions for Sandbox Classic.
Expand All @@ -31,7 +32,7 @@ private[appendonlydao] sealed trait PostCommitValidation {
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Instant,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[RejectionReasonV0]
)(implicit connection: Connection): Option[Rejection]

}

Expand All @@ -47,7 +48,7 @@ private[appendonlydao] object PostCommitValidation {
committedTransaction: CommittedTransaction,
transactionLedgerEffectiveTime: Instant,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[RejectionReasonV0] =
)(implicit connection: Connection): Option[Rejection] =
None
}

Expand All @@ -60,7 +61,7 @@ private[appendonlydao] object PostCommitValidation {
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Instant,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[RejectionReasonV0] = {
)(implicit connection: Connection): Option[Rejection] = {

val causalMonotonicityViolation =
validateCausalMonotonicity(transaction, transactionLedgerEffectiveTime, divulged)
Expand All @@ -84,27 +85,27 @@ private[appendonlydao] object PostCommitValidation {
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Instant,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[RejectionReasonV0] = {
)(implicit connection: Connection): Option[Rejection] = {
val referredContracts = collectReferredContracts(transaction, divulged)
if (referredContracts.isEmpty) {
None
} else {
dao
.maximumLedgerTime(referredContracts)(connection)
.map(validateCausalMonotonicity(_, transactionLedgerEffectiveTime))
.getOrElse(Some(UnknownContract))
.getOrElse(Some(Rejection.UnknownContract))
}
}

private def validateCausalMonotonicity(
maximumLedgerEffectiveTime: Option[Instant],
transactionLedgerEffectiveTime: Instant,
): Option[RejectionReasonV0] =
): Option[Rejection] =
maximumLedgerEffectiveTime
.filter(_.isAfter(transactionLedgerEffectiveTime))
.fold(Option.empty[RejectionReasonV0])(contractLedgerEffectiveTime => {
.fold(Option.empty[Rejection])(contractLedgerEffectiveTime => {
Some(
CausalMonotonicityViolation(
Rejection.CausalMonotonicityViolation(
contractLedgerEffectiveTime = contractLedgerEffectiveTime,
transactionLedgerEffectiveTime = transactionLedgerEffectiveTime,
)
Expand All @@ -113,13 +114,13 @@ private[appendonlydao] object PostCommitValidation {

private def validateParties(
transaction: CommittedTransaction
)(implicit connection: Connection): Option[RejectionReasonV0] = {
)(implicit connection: Connection): Option[Rejection] = {
val informees = transaction.informees
val allocatedInformees = dao.parties(informees.toSeq)(connection).map(_.party)
if (allocatedInformees.toSet == informees)
None
else
Some(RejectionReasonV0.PartyNotKnownOnLedger("Some parties are unallocated"))
Some(Rejection.UnallocatedParties)
}

private def collectReferredContracts(
Expand All @@ -131,7 +132,7 @@ private[appendonlydao] object PostCommitValidation {

private def validateKeyUsages(
transaction: CommittedTransaction
)(implicit connection: Connection): Option[RejectionReasonV0] =
)(implicit connection: Connection): Option[Rejection] =
transaction
.foldInExecutionOrder[Result](Right(State.empty(dao)))(
exerciseBegin = (acc, _, exe) => {
Expand All @@ -148,7 +149,7 @@ private[appendonlydao] object PostCommitValidation {
private def validateKeyUsages(
node: Node,
state: State,
)(implicit connection: Connection): Either[RejectionReasonV0, State] =
)(implicit connection: Connection): Result =
node match {
case c: Create =>
state.validateCreate(c.versionedKey.map(convert(c.versionedCoinst.template, _)), c.coid)
Expand All @@ -166,7 +167,7 @@ private[appendonlydao] object PostCommitValidation {

}

private type Result = Either[RejectionReasonV0, State]
private type Result = Either[Rejection, State]

/** The active ledger key state during validation.
* After a rollback node, we restore the state at the
Expand Down Expand Up @@ -214,23 +215,25 @@ private[appendonlydao] object PostCommitValidation {
private val dao: PartyStorageBackend with ContractStorageBackend,
) {

def validateCreate(maybeKey: Option[Key], id: ContractId)(implicit
connection: Connection
): Either[RejectionReasonV0, State] =
def validateCreate(
maybeKey: Option[Key],
id: ContractId,
)(implicit connection: Connection): Result =
maybeKey.fold[Result](Right(this)) { key =>
lookup(key).fold[Result](Right(add(key, id)))(_ => Left(DuplicateKey))
lookup(key).fold[Result](Right(add(key, id)))(_ => Left(Rejection.DuplicateKey))
}

// `causalMonotonicity` already reports unknown contracts, no need to check it here
def removeKeyIfDefined(maybeKey: Option[Key]): Right[RejectionReasonV0, State] =
def removeKeyIfDefined(maybeKey: Option[Key]): Result =
Right(maybeKey.fold(this)(remove))

def validateLookupByKey(key: Key, expectation: Option[ContractId])(implicit
connection: Connection
): Either[RejectionReasonV0, State] = {
def validateLookupByKey(
key: Key,
expectation: Option[ContractId],
)(implicit connection: Connection): Result = {
val result = lookup(key)
if (result == expectation) Right(this)
else Left(MismatchingLookup(expectation, result))
else Left(Rejection.MismatchingLookup(expectation, result))
}

def beginRollback(): State =
Expand Down Expand Up @@ -271,25 +274,57 @@ private[appendonlydao] object PostCommitValidation {
State(ActiveState(Map.empty, Set.empty), Nil, dao)
}

private[events] val DuplicateKey: RejectionReasonV0 =
RejectionReasonV0.Inconsistent("DuplicateKey: contract key is not unique")
sealed trait Rejection {
def description: String

private[events] def MismatchingLookup(
expectation: Option[ContractId],
result: Option[ContractId],
): RejectionReasonV0 =
RejectionReasonV0.Inconsistent(
s"Contract key lookup with different results: expected [$expectation], actual [$result]"
)
def toStateV1RejectionReason: v1.RejectionReason
}

private[events] val UnknownContract: RejectionReasonV0 =
RejectionReasonV0.Inconsistent("Unknown contract")
object Rejection {
object UnknownContract extends Rejection {
override val description =
"Unknown contract"

private[events] def CausalMonotonicityViolation(
contractLedgerEffectiveTime: Instant,
transactionLedgerEffectiveTime: Instant,
): RejectionReasonV0 =
RejectionReasonV0.InvalidLedgerTime(
s"Encountered contract with LET [$contractLedgerEffectiveTime] greater than the LET of the transaction [$transactionLedgerEffectiveTime]"
)
override def toStateV1RejectionReason: v1.RejectionReason =
v1.RejectionReasonV0.Inconsistent(description)
}

object DuplicateKey extends Rejection {
override val description =
"DuplicateKey: contract key is not unique"

override def toStateV1RejectionReason: v1.RejectionReason =
v1.RejectionReasonV0.Inconsistent(description)
}

final case class MismatchingLookup(
expectation: Option[ContractId],
result: Option[ContractId],
) extends Rejection {
override lazy val description: String =
s"Contract key lookup with different results: expected [$expectation], actual [$result]"

override def toStateV1RejectionReason: v1.RejectionReason =
v1.RejectionReasonV0.Inconsistent(description)
}

final case class CausalMonotonicityViolation(
contractLedgerEffectiveTime: Instant,
transactionLedgerEffectiveTime: Instant,
) extends Rejection {
override lazy val description: String =
s"Encountered contract with LET [$contractLedgerEffectiveTime] greater than the LET of the transaction [$transactionLedgerEffectiveTime]"

override def toStateV1RejectionReason: v1.RejectionReason =
v1.RejectionReasonV0.InvalidLedgerTime(description)
}

object UnallocatedParties extends Rejection {
override def description: String =
"Some parties are unallocated"

override def toStateV1RejectionReason: v1.RejectionReason =
v1.RejectionReasonV0.PartyNotKnownOnLedger(description)
}
}
}
Loading

0 comments on commit d7077e1

Please sign in to comment.