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:
Stefano Baghino 2020-04-29 14:55:16 +02:00 committed by GitHub
parent cebc26af88
commit c709f91a73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 169 deletions

View File

@ -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(", ")}"
)
}

View File

@ -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(", ")}"
)
}

View File

@ -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,

View File

@ -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]]

View File

@ -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,
)