mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
Adjust PostCommitValidation (#5774)
* Adjust PostCommitValidation
- do not require a party for validation (to validate divulged contracts)
- stop validation after first error and don't accumulate rejection reasons
- address 01da7393b3 (r416719399)
changelog_begin
changelog_end
* Use participant state rejection reasons, drop unused submitter parameter from validation
This commit is contained in:
parent
cebc26af88
commit
c709f91a73
@ -3,10 +3,9 @@
|
||||
|
||||
package com.daml.platform.store.dao.events
|
||||
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
|
||||
import anorm.SqlParser.{binaryStream, int, str}
|
||||
import anorm.SqlParser.{binaryStream, str}
|
||||
import anorm.{Row, RowParser, SimpleSql, SqlStringInterpolation, ~}
|
||||
import com.daml.ledger.participant.state.index.v2.ContractStore
|
||||
import com.daml.platform.store.Conversions._
|
||||
@ -15,36 +14,18 @@ import com.daml.platform.store.dao.DbDispatcher
|
||||
import com.daml.platform.store.serialization.ValueSerializer.{deserializeValue => deserialize}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* @see [[ContractsTable]]
|
||||
*/
|
||||
private[dao] sealed abstract class ContractsReader(
|
||||
val committedContracts: PostCommitValidationData,
|
||||
dispatcher: DbDispatcher,
|
||||
executionContext: ExecutionContext,
|
||||
) extends ContractStore {
|
||||
|
||||
import ContractsReader._
|
||||
|
||||
object InTransaction extends PostCommitValidationData {
|
||||
|
||||
override def lookupContractKey(submitter: Party, key: Key)(
|
||||
implicit connection: Connection): Option[ContractId] =
|
||||
lookupContractKeyQuery(submitter, key).as(contractId("contract_id").singleOpt)
|
||||
|
||||
override def lookupMaximumLedgerTime(ids: Set[ContractId])(
|
||||
implicit connection: Connection): Try[Option[Instant]] =
|
||||
SQL"select max(create_ledger_effective_time) as max_create_ledger_effective_time, count(*) as num_contracts from participant_contracts where participant_contracts.contract_id in ($ids)"
|
||||
.as(
|
||||
(instant("max_create_ledger_effective_time").? ~ int("num_contracts")).single
|
||||
.map {
|
||||
case result ~ numContracts if numContracts == ids.size => Success(result)
|
||||
case _ => Failure(notFound(ids))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
protected def lookupContractKeyQuery(submitter: Party, key: Key): SimpleSql[Row]
|
||||
|
||||
override def lookupActiveContract(
|
||||
@ -61,13 +42,13 @@ private[dao] sealed abstract class ContractsReader(
|
||||
key: Key,
|
||||
): Future[Option[ContractId]] =
|
||||
dispatcher.executeSql("lookup_contract_by_key") { implicit connection =>
|
||||
InTransaction.lookupContractKey(submitter, key)
|
||||
lookupContractKeyQuery(submitter, key).as(contractId("contract_id").singleOpt)
|
||||
}
|
||||
|
||||
override def lookupMaximumLedgerTime(ids: Set[ContractId]): Future[Option[Instant]] =
|
||||
dispatcher
|
||||
.executeSql("lookup_maximum_ledger_time") { implicit connection =>
|
||||
InTransaction.lookupMaximumLedgerTime(ids)
|
||||
committedContracts.lookupMaximumLedgerTime(ids)
|
||||
}
|
||||
.map(_.get)(executionContext)
|
||||
|
||||
@ -79,14 +60,19 @@ object ContractsReader {
|
||||
dispatcher: DbDispatcher,
|
||||
executionContext: ExecutionContext,
|
||||
dbType: DbType,
|
||||
): ContractsReader =
|
||||
): ContractsReader = {
|
||||
val table = ContractsTable(dbType)
|
||||
dbType match {
|
||||
case DbType.Postgres => new Postgresql(dispatcher, executionContext)
|
||||
case DbType.H2Database => new H2Database(dispatcher, executionContext)
|
||||
case DbType.Postgres => new Postgresql(table, dispatcher, executionContext)
|
||||
case DbType.H2Database => new H2Database(table, dispatcher, executionContext)
|
||||
}
|
||||
}
|
||||
|
||||
private final class Postgresql(dispatcher: DbDispatcher, executionContext: ExecutionContext)
|
||||
extends ContractsReader(dispatcher, executionContext) {
|
||||
private final class Postgresql(
|
||||
table: ContractsTable,
|
||||
dispatcher: DbDispatcher,
|
||||
executionContext: ExecutionContext,
|
||||
) extends ContractsReader(table, dispatcher, executionContext) {
|
||||
override protected def lookupContractKeyQuery(
|
||||
submitter: Party,
|
||||
key: Key,
|
||||
@ -94,8 +80,11 @@ object ContractsReader {
|
||||
SQL"select participant_contracts.contract_id from #$contractsTable where $submitter =ANY(create_stakeholders) and contract_witness = $submitter and create_key_hash = ${key.hash}"
|
||||
}
|
||||
|
||||
private final class H2Database(dispatcher: DbDispatcher, executionContext: ExecutionContext)
|
||||
extends ContractsReader(dispatcher, executionContext) {
|
||||
private final class H2Database(
|
||||
table: ContractsTable,
|
||||
dispatcher: DbDispatcher,
|
||||
executionContext: ExecutionContext,
|
||||
) extends ContractsReader(table, dispatcher, executionContext) {
|
||||
override protected def lookupContractKeyQuery(
|
||||
submitter: Party,
|
||||
key: Key,
|
||||
@ -121,14 +110,4 @@ object ContractsReader {
|
||||
|
||||
private val contractsTable = "participant_contracts natural join participant_contract_witnesses"
|
||||
|
||||
private def emptyContractIds: Throwable =
|
||||
new IllegalArgumentException(
|
||||
"Cannot lookup the maximum ledger time for an empty set of contract identifiers"
|
||||
)
|
||||
|
||||
private def notFound(contractIds: Set[ContractId]): Throwable =
|
||||
new IllegalArgumentException(
|
||||
s"One or more of the following contract identifiers has been found: ${contractIds.map(_.coid).mkString(", ")}"
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -3,14 +3,18 @@
|
||||
|
||||
package com.daml.platform.store.dao.events
|
||||
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
|
||||
import anorm.{BatchSql, NamedParameter}
|
||||
import anorm.SqlParser.int
|
||||
import anorm.{BatchSql, NamedParameter, SqlStringInterpolation, ~}
|
||||
import com.daml.platform.store.Conversions._
|
||||
import com.daml.platform.store.DbType
|
||||
import com.daml.platform.store.serialization.ValueSerializer.{serializeValue => serialize}
|
||||
|
||||
private[events] sealed abstract class ContractsTable {
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
private[events] sealed abstract class ContractsTable extends PostCommitValidationData {
|
||||
|
||||
protected val insertContractQuery: String
|
||||
|
||||
@ -143,6 +147,25 @@ private[events] sealed abstract class ContractsTable {
|
||||
|
||||
}
|
||||
|
||||
override final def lookupContractKeyGlobally(key: Key)(
|
||||
implicit connection: Connection): Option[ContractId] =
|
||||
SQL"select participant_contracts.contract_id from participant_contracts where create_key_hash = ${key.hash}"
|
||||
.as(contractId("contract_id").singleOpt)
|
||||
|
||||
override final def lookupMaximumLedgerTime(ids: Set[ContractId])(
|
||||
implicit connection: Connection): Try[Option[Instant]] =
|
||||
if (ids.isEmpty) {
|
||||
Failure(ContractsTable.emptyContractIds)
|
||||
} else {
|
||||
SQL"select max(create_ledger_effective_time) as max_create_ledger_effective_time, count(*) as num_contracts from participant_contracts where participant_contracts.contract_id in ($ids)"
|
||||
.as(
|
||||
(instant("max_create_ledger_effective_time").? ~ int("num_contracts")).single
|
||||
.map {
|
||||
case result ~ numContracts if numContracts == ids.size => Success(result)
|
||||
case _ => Failure(ContractsTable.notFound(ids))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private[events] object ContractsTable {
|
||||
@ -163,4 +186,14 @@ private[events] object ContractsTable {
|
||||
s"merge into participant_contracts using dual on contract_id = {contract_id} when not matched then insert (contract_id, template_id, create_argument, create_ledger_effective_time, create_key_hash, create_stakeholders) values ({contract_id}, {template_id}, {create_argument}, {create_ledger_effective_time}, {create_key_hash}, {create_stakeholders})"
|
||||
}
|
||||
|
||||
private def emptyContractIds: Throwable =
|
||||
new IllegalArgumentException(
|
||||
"Cannot lookup the maximum ledger time for an empty set of contract identifiers"
|
||||
)
|
||||
|
||||
private def notFound(contractIds: Set[ContractId]): Throwable =
|
||||
new IllegalArgumentException(
|
||||
s"One or more of the following contract identifiers has been found: ${contractIds.map(_.coid).mkString(", ")}"
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ package com.daml.platform.store.dao.events
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.ledger.api.domain.RejectionReason
|
||||
import com.daml.ledger.participant.state.v1.RejectionReason
|
||||
|
||||
/**
|
||||
* Performs post-commit validation on transactions for Sandbox Classic.
|
||||
@ -15,10 +15,12 @@ import com.daml.ledger.api.domain.RejectionReason
|
||||
* so that the old post-commit validation backed by the old participant schema can be
|
||||
* dropped and the DAML-on-X-backed implementation of the Sandbox can skip it entirely.
|
||||
*
|
||||
* Post-commit validation is relevant for two reasons:
|
||||
* Post-commit validation is relevant for three reasons:
|
||||
* - keys can be referenced by two concurrent interpretations, potentially leading to
|
||||
* either create nodes with duplicate active keys or lookup-by-key nodes referring to
|
||||
* inactive keys
|
||||
* - contracts may have been consumed by a concurrent interpretation, potentially leading
|
||||
* to double spends
|
||||
* - the transaction's ledger effective time is determined after interpretation,
|
||||
* meaning that causal monotonicity cannot be verified while interpreting a command
|
||||
*/
|
||||
@ -28,8 +30,7 @@ sealed trait PostCommitValidation {
|
||||
transaction: Transaction,
|
||||
transactionLedgerEffectiveTime: Instant,
|
||||
divulged: Set[ContractId],
|
||||
submitter: Party,
|
||||
)(implicit connection: Connection): Set[RejectionReason]
|
||||
)(implicit connection: Connection): Option[RejectionReason]
|
||||
|
||||
}
|
||||
|
||||
@ -42,13 +43,12 @@ object PostCommitValidation {
|
||||
* already performs post-commit validation.
|
||||
*/
|
||||
object Skip extends PostCommitValidation {
|
||||
override def validate(
|
||||
@inline override def validate(
|
||||
transaction: Transaction,
|
||||
transactionLedgerEffectiveTime: Instant,
|
||||
divulged: Set[ContractId],
|
||||
submitter: Party,
|
||||
)(implicit connection: Connection): Set[RejectionReason] =
|
||||
Set.empty
|
||||
)(implicit connection: Connection): Option[RejectionReason] =
|
||||
None
|
||||
}
|
||||
|
||||
final class BackedBy(data: PostCommitValidationData) extends PostCommitValidation {
|
||||
@ -57,16 +57,14 @@ object PostCommitValidation {
|
||||
transaction: Transaction,
|
||||
transactionLedgerEffectiveTime: Instant,
|
||||
divulged: Set[ContractId],
|
||||
submitter: Party,
|
||||
)(implicit connection: Connection): Set[RejectionReason] = {
|
||||
)(implicit connection: Connection): Option[RejectionReason] = {
|
||||
|
||||
val causalMonotonicityRejection =
|
||||
val causalMonotonicityViolation =
|
||||
validateCausalMonotonicity(transaction, transactionLedgerEffectiveTime, divulged)
|
||||
|
||||
val invalidKeyUsageRejection =
|
||||
validateKeyUsages(transaction, submitter)
|
||||
val invalidKeyUsage = validateKeyUsages(transaction)
|
||||
|
||||
causalMonotonicityRejection.union(invalidKeyUsageRejection)
|
||||
invalidKeyUsage.orElse(causalMonotonicityViolation)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,27 +76,27 @@ object PostCommitValidation {
|
||||
transaction: Transaction,
|
||||
transactionLedgerEffectiveTime: Instant,
|
||||
divulged: Set[ContractId],
|
||||
)(implicit connection: Connection): Set[RejectionReason] = {
|
||||
)(implicit connection: Connection): Option[RejectionReason] = {
|
||||
val referredContracts = collectReferredContracts(transaction, divulged)
|
||||
if (referredContracts.isEmpty) {
|
||||
Set.empty
|
||||
None
|
||||
} else {
|
||||
data
|
||||
.lookupMaximumLedgerTime(referredContracts)
|
||||
.map(validateCausalMonotonicity(_, transactionLedgerEffectiveTime))
|
||||
.getOrElse(Set(UnknownContract))
|
||||
.getOrElse(Some(UnknownContract))
|
||||
}
|
||||
}
|
||||
|
||||
private def validateCausalMonotonicity(
|
||||
maximumLedgerEffectiveTime: Option[Instant],
|
||||
transactionLedgerEffectiveTime: Instant,
|
||||
): Set[RejectionReason] =
|
||||
): Option[RejectionReason] =
|
||||
maximumLedgerEffectiveTime
|
||||
.filter(_.isAfter(transactionLedgerEffectiveTime))
|
||||
.fold(Set.empty[RejectionReason])(
|
||||
.fold(Option.empty[RejectionReason])(
|
||||
contractLedgerEffectiveTime => {
|
||||
Set(
|
||||
Some(
|
||||
CausalMonotonicityViolation(
|
||||
contractLedgerEffectiveTime = contractLedgerEffectiveTime,
|
||||
transactionLedgerEffectiveTime = transactionLedgerEffectiveTime,
|
||||
@ -117,8 +115,6 @@ object PostCommitValidation {
|
||||
(created + c.coid, ids)
|
||||
case ((created, ids), (_, e: Exercise)) if !divulged(e.targetCoid) =>
|
||||
(created, ids + e.targetCoid)
|
||||
case ((created, ids), (_, e: Exercise)) if !divulged(e.targetCoid) =>
|
||||
(created, ids + e.targetCoid)
|
||||
case ((created, ids), (_, f: Fetch)) if !divulged(f.coid) =>
|
||||
(created, ids + f.coid)
|
||||
case ((created, ids), (_, l: LookupByKey)) =>
|
||||
@ -128,20 +124,19 @@ object PostCommitValidation {
|
||||
referred.diff(createdInTransaction)
|
||||
}
|
||||
|
||||
private def validateKeyUsages(transaction: Transaction, submitter: Party)(
|
||||
implicit connection: Connection): Set[RejectionReason] = {
|
||||
private def validateKeyUsages(transaction: Transaction)(
|
||||
implicit connection: Connection): Option[RejectionReason] =
|
||||
transaction
|
||||
.fold(State.empty(data, submitter)) {
|
||||
case (state, (_, node)) => validateKeyUsages(node, state)
|
||||
.fold[Result](Right(State.empty(data))) {
|
||||
case (Right(state), (_, node)) => validateKeyUsages(node, state)
|
||||
case (rejection, _) => rejection
|
||||
}
|
||||
.errors
|
||||
}
|
||||
.fold(Some(_), _ => None)
|
||||
|
||||
private def validateKeyUsages(
|
||||
node: Node,
|
||||
state: State,
|
||||
)(implicit connection: Connection): State = {
|
||||
|
||||
)(implicit connection: Connection): Either[RejectionReason, State] =
|
||||
node match {
|
||||
case c: Create =>
|
||||
state.validateCreate(c.key.map(convert(c.coinst.template, _)), c.coid)
|
||||
@ -154,45 +149,45 @@ object PostCommitValidation {
|
||||
// anything with regards to contract keys and do not alter the
|
||||
// state in a way which is relevant for the validation of
|
||||
// subsequent nodes
|
||||
state
|
||||
Right(state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private type Result = Either[RejectionReason, State]
|
||||
|
||||
/**
|
||||
* Represents the state of an ongoing validation.
|
||||
* It must be carried over as the transaction is
|
||||
* validated one node at a time in pre-order
|
||||
* traversal for this to make sense.
|
||||
*
|
||||
* @param errors Accumulates errors retrieved so far while validating
|
||||
* @param contracts All contracts created as part of the current transaction
|
||||
* @param removed Ensures indexed contracts are not referred to by key if they are removed in the current transaction
|
||||
* @param submitter The submitter of the current transaction
|
||||
* @param data Data about committed contracts for post-commit validation purposes
|
||||
*/
|
||||
private final case class State(
|
||||
errors: Set[RejectionReason],
|
||||
private val contracts: Map[Hash, ContractId],
|
||||
private val removed: Set[Hash],
|
||||
private val submitter: Party,
|
||||
private val data: PostCommitValidationData,
|
||||
) {
|
||||
|
||||
def validateCreate(maybeKey: Option[Key], id: ContractId)(
|
||||
implicit connection: Connection): State =
|
||||
maybeKey.fold(this) { key =>
|
||||
lookup(key).fold(add(key, id))(_ => error(DuplicateKey))
|
||||
implicit connection: Connection): Either[RejectionReason, State] =
|
||||
maybeKey.fold[Result](Right(this)) { key =>
|
||||
lookup(key).fold[Result](Right(add(key, id)))(_ => Left(DuplicateKey))
|
||||
}
|
||||
|
||||
// `causalMonotonicity` already reports unknown contracts, no need to check it here
|
||||
def removeKeyIfDefined(maybeKey: Option[Key])(implicit connection: Connection): State =
|
||||
maybeKey.fold(this)(remove)
|
||||
def removeKeyIfDefined(maybeKey: Option[Key])(
|
||||
implicit connection: Connection): Right[RejectionReason, State] =
|
||||
Right(maybeKey.fold(this)(remove))
|
||||
|
||||
def validateLookupByKey(key: Key, expectation: Option[ContractId])(
|
||||
implicit connection: Connection): State = {
|
||||
implicit connection: Connection): Either[RejectionReason, State] = {
|
||||
val result = lookup(key)
|
||||
if (result == expectation) this
|
||||
else copy(errors = errors + MismatchingLookup(expectation, result))
|
||||
if (result == expectation) Right(this)
|
||||
else Left(MismatchingLookup(expectation, result))
|
||||
}
|
||||
|
||||
private def add(key: Key, id: ContractId): State =
|
||||
@ -207,20 +202,17 @@ object PostCommitValidation {
|
||||
removed = removed + key.hash,
|
||||
)
|
||||
|
||||
private def error(reason: RejectionReason): State =
|
||||
copy(errors = errors + reason)
|
||||
|
||||
private def lookup(key: Key)(implicit connection: Connection): Option[ContractId] =
|
||||
contracts.get(key.hash).orElse {
|
||||
if (removed(key.hash)) None
|
||||
else data.lookupContractKey(submitter, key)
|
||||
else data.lookupContractKeyGlobally(key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private object State {
|
||||
def empty(data: PostCommitValidationData, submitter: Party): State =
|
||||
State(Set.empty, Map.empty, Set.empty, submitter, data)
|
||||
def empty(data: PostCommitValidationData): State =
|
||||
State(Map.empty, Set.empty, data)
|
||||
}
|
||||
|
||||
private[events] val DuplicateKey: RejectionReason =
|
||||
@ -235,7 +227,7 @@ object PostCommitValidation {
|
||||
)
|
||||
|
||||
private[events] val UnknownContract: RejectionReason =
|
||||
RejectionReason.Inconsistent(s"Could not lookup contract")
|
||||
RejectionReason.Inconsistent
|
||||
|
||||
private[events] def CausalMonotonicityViolation(
|
||||
contractLedgerEffectiveTime: Instant,
|
||||
|
@ -10,8 +10,7 @@ import scala.util.Try
|
||||
|
||||
private[events] trait PostCommitValidationData {
|
||||
|
||||
def lookupContractKey(submitter: Party, key: Key)(
|
||||
implicit connection: Connection): Option[ContractId]
|
||||
def lookupContractKeyGlobally(key: Key)(implicit connection: Connection): Option[ContractId]
|
||||
|
||||
def lookupMaximumLedgerTime(ids: Set[ContractId])(
|
||||
implicit connection: Connection): Try[Option[Instant]]
|
||||
|
@ -27,15 +27,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val createWithKey = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(createWithKey),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -43,15 +42,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val createWithoutKey = genTestCreate().copy(key = None)
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(createWithoutKey),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -60,15 +58,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
val createContract = genTestCreate()
|
||||
val exerciseContract = genTestExercise(createContract)
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(createContract, exerciseContract),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -77,15 +74,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
val divulgedContract = genTestCreate()
|
||||
val exerciseContract = genTestExercise(divulgedContract)
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(exerciseContract),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set(divulgedContract.coid),
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -94,15 +90,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
val missingCreate = genTestCreate()
|
||||
val exerciseContract = genTestExercise(missingCreate)
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(exerciseContract),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain theSameElementsAs Seq(UnknownContract)
|
||||
error shouldBe Some(UnknownContract)
|
||||
|
||||
}
|
||||
|
||||
@ -110,15 +105,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val createContract = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(createContract, fetch(createContract)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -126,15 +120,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val divulgedContract = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(fetch(divulgedContract)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set(divulgedContract.coid),
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -142,15 +135,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val missingCreate = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(fetch(missingCreate)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain theSameElementsAs Seq(UnknownContract)
|
||||
error shouldBe Some(UnknownContract)
|
||||
|
||||
}
|
||||
|
||||
@ -158,15 +150,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val createContract = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(createContract, lookupByKey(createContract, found = true)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -174,15 +165,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val missingCreate = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(lookupByKey(missingCreate, found = true)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain allElementsOf Seq(
|
||||
error shouldBe Some(
|
||||
MismatchingLookup(
|
||||
expectation = Some(missingCreate.coid),
|
||||
result = None,
|
||||
@ -195,15 +185,14 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val missingContract = genTestCreate()
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(lookupByKey(missingContract, found = false)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
@ -220,7 +209,6 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
committed(
|
||||
id = committedContract.coid.coid,
|
||||
ledgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
witnesses = Set("Alice"),
|
||||
key = committedContract.key.map(convert(committedContract.coinst.template, _))
|
||||
)
|
||||
)
|
||||
@ -228,43 +216,40 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
"reject a create that would introduce a duplicate key" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(committedContract),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain theSameElementsAs Seq(DuplicateKey)
|
||||
error shouldBe Some(DuplicateKey)
|
||||
|
||||
}
|
||||
|
||||
"accept an exercise on the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(exerciseOnCommittedContract),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
"reject an exercise pre-dating the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(exerciseOnCommittedContract),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.minusNanos(1),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain theSameElementsAs Seq(
|
||||
error shouldBe Some(
|
||||
CausalMonotonicityViolation(
|
||||
contractLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.minusNanos(1),
|
||||
@ -275,29 +260,27 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
"accept a fetch on the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(fetch(committedContract)),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
"reject a fetch pre-dating the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(fetch(committedContract)),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.minusNanos(1),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain theSameElementsAs Seq(
|
||||
error shouldBe Some(
|
||||
CausalMonotonicityViolation(
|
||||
contractLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.minusNanos(1),
|
||||
@ -308,29 +291,27 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
"accept a successful lookup of the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(lookupByKey(committedContract, found = true)),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
"reject a failed lookup of the committed contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(lookupByKey(committedContract, found = false)),
|
||||
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation should contain allElementsOf Seq(
|
||||
error shouldBe Some(
|
||||
MismatchingLookup(
|
||||
result = Some(committedContract.coid),
|
||||
expectation = None,
|
||||
@ -348,38 +329,33 @@ final class PostCommitValidationSpec extends WordSpec with Matchers {
|
||||
|
||||
val store = new PostCommitValidation.BackedBy(
|
||||
committedContracts(
|
||||
divulged(
|
||||
id = divulgedContract.coid.coid,
|
||||
witnesses = Set("Alice"),
|
||||
)
|
||||
divulged(divulgedContract.coid.coid),
|
||||
)
|
||||
)
|
||||
|
||||
"accept an exercise on the divulged contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(exerciseOnDivulgedContract),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
|
||||
"accept a fetch on the divulged contract" in {
|
||||
|
||||
val validation =
|
||||
val error =
|
||||
store.validate(
|
||||
transaction = just(fetch(divulgedContract)),
|
||||
transactionLedgerEffectiveTime = Instant.now(),
|
||||
divulged = Set.empty,
|
||||
submitter = Party.assertFromString("Alice"),
|
||||
)
|
||||
|
||||
validation shouldBe Set.empty
|
||||
error shouldBe None
|
||||
|
||||
}
|
||||
}
|
||||
@ -412,7 +388,6 @@ object PostCommitValidationSpec {
|
||||
private final case class ContractFixture private (
|
||||
id: ContractId,
|
||||
ledgerEffectiveTime: Option[Instant],
|
||||
witnesses: Set[Party],
|
||||
key: Option[Key],
|
||||
)
|
||||
|
||||
@ -422,9 +397,9 @@ object PostCommitValidationSpec {
|
||||
private final case class ContractStoreFixture private (contracts: Set[ContractFixture])
|
||||
extends PostCommitValidationData {
|
||||
|
||||
override def lookupContractKey(submitter: Party, key: Key)(
|
||||
override def lookupContractKeyGlobally(key: Key)(
|
||||
implicit connection: Connection = null): Option[ContractId] =
|
||||
contracts.find(c => c.key.contains(key) && c.witnesses.contains(submitter)).map(_.id)
|
||||
contracts.find(c => c.key.contains(key)).map(_.id)
|
||||
|
||||
override def lookupMaximumLedgerTime(ids: Set[ContractId])(
|
||||
implicit connection: Connection = null): Try[Option[Instant]] = {
|
||||
@ -432,11 +407,11 @@ object PostCommitValidationSpec {
|
||||
case c if ids.contains(c.id) => c.ledgerEffectiveTime
|
||||
}
|
||||
if (lookup.isEmpty) Failure(notFound(ids))
|
||||
else Success(lookup.fold[Option[Instant]](None)(pickTheGreater))
|
||||
else Success(lookup.fold[Option[Instant]](None)(pickTheGreatest))
|
||||
}
|
||||
}
|
||||
|
||||
private def pickTheGreater(l: Option[Instant], r: Option[Instant]): Option[Instant] =
|
||||
private def pickTheGreatest(l: Option[Instant], r: Option[Instant]): Option[Instant] =
|
||||
l.fold(r)(left => r.fold(l)(right => if (left.isAfter(right)) l else r))
|
||||
|
||||
private def notFound(contractIds: Set[ContractId]): Throwable =
|
||||
@ -456,21 +431,18 @@ object PostCommitValidationSpec {
|
||||
private def committed(
|
||||
id: String,
|
||||
ledgerEffectiveTime: Instant,
|
||||
witnesses: Set[String],
|
||||
key: Option[Key] = None,
|
||||
): ContractFixture =
|
||||
ContractFixture(
|
||||
ContractId.assertFromString(id),
|
||||
Some(ledgerEffectiveTime),
|
||||
witnesses.map(Party.assertFromString),
|
||||
key,
|
||||
)
|
||||
|
||||
private def divulged(id: String, witnesses: Set[String]): ContractFixture =
|
||||
private def divulged(id: String): ContractFixture =
|
||||
ContractFixture(
|
||||
ContractId.assertFromString(id),
|
||||
None,
|
||||
witnesses.map(Party.assertFromString),
|
||||
None,
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user