Remove conflict-checking logic from JdbcLedgerDao [DPP-808] (#12555)

* Remove conflict-checking logic from JdbcLedgerDao

changelog_begin
changelog_end

* Remove configuration conflict-checking logic and testing

changelog_begin
changelog_end

* Addressed review comments
* Removed one metric
* Removed Rejections transformation used in sandbox-classic and unit tests
This commit is contained in:
tudor-da 2022-01-27 11:14:52 +01:00 committed by GitHub
parent 4038d0a7e3
commit 89ce7d0245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 37 additions and 1882 deletions

View File

@ -204,15 +204,9 @@ object domain {
override val description: String = "DuplicateKey: contract key is not unique"
}
final case class PartiesNotKnownOnLedger(parties: Set[String]) extends RejectionReason {
override val description: String = "Some parties are unallocated"
}
/** The ledger time of the submission violated some constraint on the ledger time. */
final case class InvalidLedgerTime(description: String) extends RejectionReason
final case class LedgerConfigNotFound(description: String) extends RejectionReason
/** The transaction relied on contracts being active that were no longer
* active at the point where it was sequenced.
*/

View File

@ -400,7 +400,6 @@ final class Metrics(val registry: MetricRegistry) {
private val Prefix: MetricName = index.Prefix :+ "db"
val storePartyEntry: Timer = registry.timer(Prefix :+ "store_party_entry")
val storeInitialState: Timer = registry.timer(Prefix :+ "store_initial_state")
val storePackageEntry: Timer = registry.timer(Prefix :+ "store_package_entry")
val storeTransaction: Timer = registry.timer(Prefix :+ "store_ledger_entry")
@ -473,7 +472,6 @@ final class Metrics(val registry: MetricRegistry) {
val prepareBatches: Timer = registry.timer(dbPrefix :+ "prepare_batches")
// in order within SQL transaction
val commitValidation: Timer = registry.timer(dbPrefix :+ "commit_validation")
val eventsBatch: Timer = registry.timer(dbPrefix :+ "events_batch")
val deleteContractWitnessesBatch: Timer =
registry.timer(dbPrefix :+ "delete_contract_witnesses_batch")
@ -490,9 +488,6 @@ final class Metrics(val registry: MetricRegistry) {
val storeRejectionDbMetrics: DatabaseMetrics = createDbMetrics(
"store_rejection"
) // FIXME Base name conflicts with storeRejection
val storeInitialStateFromScenario: DatabaseMetrics = createDbMetrics(
"store_initial_state_from_scenario"
)
val loadParties: DatabaseMetrics = createDbMetrics("load_parties")
val loadAllParties: DatabaseMetrics = createDbMetrics("load_all_parties")
val loadPackages: DatabaseMetrics = createDbMetrics("load_packages")

View File

@ -59,7 +59,6 @@ compile_deps = [
"//libs-scala/build-info",
"//libs-scala/contextualized-logging",
"//libs-scala/concurrent",
"//libs-scala/grpc-utils",
"//libs-scala/logging-entries",
"//libs-scala/ports",
"//libs-scala/resources",

View File

@ -5,26 +5,18 @@ package com.daml.platform.store
import anorm.Column.nonNull
import anorm._
import com.daml.error.ContextualizedErrorLogger
import com.daml.error.definitions.LedgerApiErrors
import com.daml.grpc.GrpcStatus
import com.daml.ledger.api.domain
import com.daml.ledger.offset.Offset
import com.daml.ledger.participant.state.v2.Update.CommandRejected
import com.daml.ledger.participant.state.{v2 => 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 com.daml.platform.server.api.validation.ErrorFactories
import spray.json.DefaultJsonProtocol._
import spray.json._
import scala.util.Try
import java.io.BufferedReader
import java.sql.{PreparedStatement, SQLNonTransientException, Types}
import java.util.stream.Collectors
import scala.annotation.nowarn
import scala.util.Try
// TODO append-only: split this file on cleanup, and move anorm/db conversion related stuff to the right place
@ -390,67 +382,4 @@ private[platform] object Conversions {
override val sqlType: String = ParameterMetaData.StringParameterMetaData.sqlType
override val jdbcType: Int = ParameterMetaData.StringParameterMetaData.jdbcType
}
implicit class RejectionReasonOps(rejectionReason: domain.RejectionReason) {
@nowarn("msg=deprecated")
def toParticipantStateRejectionReason(
errorFactories: ErrorFactories
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): state.Update.CommandRejected.RejectionReasonTemplate =
rejectionReason match {
case domain.RejectionReason.ContractsNotFound(missingContractIds) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.contractsNotFound(missingContractIds)
)
case domain.RejectionReason.Inconsistent(reason) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.inconsistent(reason)
)
case domain.RejectionReason.InconsistentContractKeys(lookupResult, currentResult) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections
.inconsistentContractKeys(lookupResult, currentResult)
)
case rejection @ domain.RejectionReason.DuplicateContractKey(key) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections
.duplicateContractKey(rejection.description, key)
)
case domain.RejectionReason.Disputed(reason) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.Deprecated.disputed(reason)
)
case domain.RejectionReason.OutOfQuota(reason) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.Deprecated.outOfQuota(reason)
)
case domain.RejectionReason.PartiesNotKnownOnLedger(parties) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.partiesNotKnownToLedger(parties)
)
case domain.RejectionReason.PartyNotKnownOnLedger(description) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.partyNotKnownOnLedger(description)
)
case domain.RejectionReason.SubmitterCannotActViaParticipant(reason) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.submitterCannotActViaParticipant(reason)
)
case domain.RejectionReason.InvalidLedgerTime(reason) =>
CommandRejected.FinalReason(
errorFactories.CommandRejections.invalidLedgerTime(reason)
)
case domain.RejectionReason.LedgerConfigNotFound(description) =>
// This rejection is returned only for V2 error codes already so we don't need to
// wrap it in ErrorFactories (see [[com.daml.platform.sandbox.stores.ledger.Rejection.NoLedgerConfiguration]]
CommandRejected.FinalReason(
GrpcStatus.toProto(
LedgerApiErrors.RequestValidation.NotFound.LedgerConfiguration
.RejectWithMessage(description)
.asGrpcStatusFromContext
)
)
}
}
}

View File

@ -3,6 +3,7 @@
package com.daml.platform.store.appendonlydao
import akka.NotUsed
import akka.stream.Materializer
import akka.stream.scaladsl.Source
import com.daml.daml_lf_dev.DamlLf.Archive
import com.daml.error.DamlContextualizedErrorLogger
@ -26,9 +27,8 @@ import com.daml.lf.transaction.{BlindingInfo, CommittedTransaction}
import com.daml.logging.LoggingContext.withEnrichedLoggingContext
import com.daml.logging.entries.LoggingEntry
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.metrics.{Metrics, Timed}
import com.daml.metrics.Metrics
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.store.Conversions._
import com.daml.platform.store._
import com.daml.platform.store.appendonlydao.events._
import com.daml.platform.store.backend.ParameterStorageBackend.LedgerEnd
@ -38,17 +38,9 @@ import com.daml.platform.store.backend.{
ReadStorageBackend,
}
import com.daml.platform.store.cache.LedgerEndCache
import com.daml.platform.store.entries.{
ConfigurationEntry,
LedgerEntry,
PackageLedgerEntry,
PartyLedgerEntry,
}
import com.daml.platform.store.entries.{ConfigurationEntry, PackageLedgerEntry, PartyLedgerEntry}
import com.daml.platform.store.interning.StringInterning
import com.daml.platform.store.utils.QueueBasedConcurrencyLimiter
import java.sql.Connection
import akka.stream.Materializer
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
@ -63,10 +55,8 @@ private class JdbcLedgerDao(
acsContractFetchingParallelism: Int,
acsGlobalParallelism: Int,
acsIdQueueLimit: Int,
performPostCommitValidation: Boolean,
metrics: Metrics,
lfValueTranslationCache: LfValueTranslationCache.Cache,
validatePartyAllocation: Boolean,
enricher: Option[ValueEnricher],
sequentialIndexer: SequentialWriteDao,
participantId: Ref.ParticipantId,
@ -157,47 +147,12 @@ private class JdbcLedgerDao(
dbDispatcher.executeSql(
metrics.daml.index.db.storeConfigurationEntryDbMetrics
) { implicit conn =>
val optCurrentConfig =
readStorageBackend.configurationStorageBackend.ledgerConfiguration(conn)
val optExpectedGeneration: Option[Long] =
optCurrentConfig.map { case (_, c) => c.generation + 1 }
val finalRejectionReason: Option[String] =
optExpectedGeneration match {
case Some(expGeneration)
if rejectionReason.isEmpty && expGeneration != configuration.generation =>
// If we're not storing a rejection and the new generation is not succ of current configuration, then
// we store a rejection. This code path is only expected to be taken in sandbox. This follows the same
// pattern as with transactions.
Some(
s"Generation mismatch: expected=$expGeneration, actual=${configuration.generation}"
)
case _ =>
// Rejection reason was set, or we have no previous configuration generation, in which case we accept any
// generation.
rejectionReason
}
val update = finalRejectionReason match {
case None =>
state.Update.ConfigurationChanged(
recordTime = recordedAt,
submissionId = Ref.SubmissionId.assertFromString(submissionId),
participantId =
Ref.ParticipantId.assertFromString("1"), // not used for DbDto generation
newConfiguration = configuration,
)
case Some(reason) =>
state.Update.ConfigurationChangeRejected(
recordTime = recordedAt,
submissionId = Ref.SubmissionId.assertFromString(submissionId),
participantId =
Ref.ParticipantId.assertFromString("1"), // not used for DbDto generation
proposedConfiguration = configuration,
rejectionReason = reason,
)
}
val update = state.Update.ConfigurationChanged(
recordTime = recordedAt,
submissionId = Ref.SubmissionId.assertFromString(submissionId),
participantId = Ref.ParticipantId.assertFromString("1"), // not used for DbDto generation
newConfiguration = configuration,
)
sequentialIndexer.store(conn, offset, Some(update))
PersistenceResponse.Ok
@ -271,20 +226,6 @@ private class JdbcLedgerDao(
}
}
private def validate(
ledgerEffectiveTime: Timestamp,
transaction: CommittedTransaction,
divulged: Iterable[state.DivulgedContract],
)(implicit connection: Connection): Option[PostCommitValidation.Rejection] =
Timed.value(
metrics.daml.index.db.storeTransactionDbMetrics.commitValidation,
postCommitValidation.validate(
transaction = transaction,
transactionLedgerEffectiveTime = ledgerEffectiveTime,
divulged = divulged.iterator.map(_.contractId).toSet,
),
)
override def storeRejection(
completionInfo: Option[state.CompletionInfo],
recordTime: Timestamp,
@ -307,88 +248,6 @@ private class JdbcLedgerDao(
PersistenceResponse.Ok
}
override def storeInitialState(
ledgerEntries: Vector[(Offset, LedgerEntry)],
newLedgerEnd: Offset,
)(implicit loggingContext: LoggingContext): Future[Unit] = {
logger.info("Storing initial state")
dbDispatcher.executeSql(metrics.daml.index.db.storeInitialStateFromScenario) {
implicit connection =>
ledgerEntries.foreach { case (offset, entry) =>
entry match {
case tx: LedgerEntry.Transaction =>
val completionInfo = for {
actAs <- if (tx.actAs.isEmpty) None else Some(tx.actAs)
applicationId <- tx.applicationId
commandId <- tx.commandId
submissionId <- tx.submissionId
} yield state.CompletionInfo(
actAs,
applicationId,
commandId,
None,
Some(submissionId),
None, // TODO Ledger Metering
)
sequentialIndexer.store(
connection,
offset,
Some(
state.Update.TransactionAccepted(
optCompletionInfo = completionInfo,
transactionMeta = state.TransactionMeta(
ledgerEffectiveTime = tx.ledgerEffectiveTime,
workflowId = tx.workflowId,
submissionTime = null, // not used for DbDto generation
submissionSeed = null, // not used for DbDto generation
optUsedPackages = None, // not used for DbDto generation
optNodeSeeds = None, // not used for DbDto generation
optByKeyNodes = None, // not used for DbDto generation
),
transaction = tx.transaction,
transactionId = tx.transactionId,
recordTime = tx.recordedAt,
divulgedContracts = Nil,
blindingInfo = None,
)
),
)
case LedgerEntry.Rejection(
recordTime,
commandId,
applicationId,
submissionId,
actAs,
reason,
) =>
sequentialIndexer.store(
connection,
offset,
Some(
state.Update.CommandRejected(
recordTime = recordTime,
completionInfo = state
.CompletionInfo(
actAs,
applicationId,
commandId,
None,
submissionId,
None, // TODO Ledger Metering
),
reasonTemplate = reason.toParticipantStateRejectionReason(errorFactories)(
new DamlContextualizedErrorLogger(logger, loggingContext, submissionId)
),
)
),
)
}
}
sequentialIndexer.store(connection, newLedgerEnd, None)
}
}
private val PageSize = 100
override def getParties(
@ -688,16 +547,6 @@ private class JdbcLedgerDao(
metrics,
)
private val postCommitValidation =
if (performPostCommitValidation)
new PostCommitValidation.BackedBy(
readStorageBackend.partyStorageBackend,
readStorageBackend.contractStorageBackend,
validatePartyAllocation,
)
else
PostCommitValidation.Skip
/** This is a combined store transaction method to support sandbox-classic and tests
* !!! Usage of this is discouraged, with the removal of sandbox-classic this will be removed
*/
@ -718,48 +567,29 @@ private class JdbcLedgerDao(
sequentialIndexer.store(
conn,
offset,
validate(ledgerEffectiveTime, transaction, divulgedContracts) match {
case None =>
Some(
state.Update.TransactionAccepted(
optCompletionInfo = completionInfo,
transactionMeta = state.TransactionMeta(
ledgerEffectiveTime = ledgerEffectiveTime,
workflowId = workflowId,
submissionTime = null, // not used for DbDto generation
submissionSeed = null, // not used for DbDto generation
optUsedPackages = None, // not used for DbDto generation
optNodeSeeds = None, // not used for DbDto generation
optByKeyNodes = None, // not used for DbDto generation
),
transaction = transaction,
transactionId = transactionId,
recordTime = recordTime,
divulgedContracts = divulgedContracts.toList,
blindingInfo = blindingInfo,
)
)
case Some(reason) =>
completionInfo.map(info =>
state.Update.CommandRejected(
recordTime = recordTime,
completionInfo = info,
reasonTemplate = reason.toStateV2RejectionReason(errorFactories)(
new DamlContextualizedErrorLogger(
logger,
loggingContext,
info.submissionId,
)
),
)
)
},
Some(
state.Update.TransactionAccepted(
optCompletionInfo = completionInfo,
transactionMeta = state.TransactionMeta(
ledgerEffectiveTime = ledgerEffectiveTime,
workflowId = workflowId,
submissionTime = null, // not used for DbDto generation
submissionSeed = null, // not used for DbDto generation
optUsedPackages = None, // not used for DbDto generation
optNodeSeeds = None, // not used for DbDto generation
optByKeyNodes = None, // not used for DbDto generation
),
transaction = transaction,
transactionId = transactionId,
recordTime = recordTime,
divulgedContracts = divulgedContracts.toList,
blindingInfo = blindingInfo,
)
),
)
PersistenceResponse.Ok
}
}
}
private[platform] object JdbcLedgerDao {
@ -802,10 +632,8 @@ private[platform] object JdbcLedgerDao {
acsContractFetchingParallelism,
acsGlobalParallelism,
acsIdQueueLimit,
false,
metrics,
lfValueTranslationCache,
false,
enricher,
SequentialWriteDao.noop,
participantId,
@ -849,10 +677,8 @@ private[platform] object JdbcLedgerDao {
acsContractFetchingParallelism,
acsGlobalParallelism,
acsIdQueueLimit,
false,
metrics,
lfValueTranslationCache,
false,
enricher,
sequentialWriteDao,
participantId,
@ -865,54 +691,6 @@ private[platform] object JdbcLedgerDao {
metrics,
)
def validatingWrite(
dbSupport: DbSupport,
sequentialWriteDao: SequentialWriteDao,
eventsPageSize: Int,
eventsProcessingParallelism: Int,
acsIdPageSize: Int,
acsIdFetchingParallelism: Int,
acsContractFetchingParallelism: Int,
acsGlobalParallelism: Int,
acsIdQueueLimit: Int,
servicesExecutionContext: ExecutionContext,
metrics: Metrics,
lfValueTranslationCache: LfValueTranslationCache.Cache,
validatePartyAllocation: Boolean = false,
enricher: Option[ValueEnricher],
participantId: Ref.ParticipantId,
errorFactories: ErrorFactories,
ledgerEndCache: LedgerEndCache,
stringInterning: StringInterning,
materializer: Materializer,
): LedgerDao =
new MeteredLedgerDao(
new JdbcLedgerDao(
dbSupport.dbDispatcher,
servicesExecutionContext,
eventsPageSize,
eventsProcessingParallelism,
acsIdPageSize,
acsIdFetchingParallelism,
acsContractFetchingParallelism,
acsGlobalParallelism,
acsIdQueueLimit,
true,
metrics,
lfValueTranslationCache,
validatePartyAllocation,
enricher,
sequentialWriteDao,
participantId,
dbSupport.storageBackendFactory.readStorageBackend(ledgerEndCache, stringInterning),
dbSupport.storageBackendFactory.createParameterStorageBackend,
dbSupport.storageBackendFactory.createDeduplicationStorageBackend,
errorFactories,
materializer,
),
metrics,
)
val acceptType = "accept"
val rejectType = "reject"
}

View File

@ -26,12 +26,7 @@ import com.daml.lf.transaction.{BlindingInfo, CommittedTransaction}
import com.daml.logging.LoggingContext
import com.daml.platform.store.appendonlydao.events.{ContractStateEvent, FilterRelation}
import com.daml.platform.store.backend.ParameterStorageBackend.LedgerEnd
import com.daml.platform.store.entries.{
ConfigurationEntry,
LedgerEntry,
PackageLedgerEntry,
PartyLedgerEntry,
}
import com.daml.platform.store.entries.{ConfigurationEntry, PackageLedgerEntry, PartyLedgerEntry}
import com.daml.platform.store.interfaces.{LedgerDaoContractsReader, TransactionLogUpdate}
import scala.concurrent.Future
@ -222,6 +217,8 @@ private[platform] trait LedgerReadDao extends ReportsHealth {
): Future[Unit]
}
// TODO sandbox-classic clean-up: This interface and its implementation is only used in the JdbcLedgerDao suite
// It should be removed when the assertions in that suite are covered by other suites
private[platform] trait LedgerWriteDao extends ReportsHealth {
/** Initializes the database with the given ledger identity.
@ -249,19 +246,6 @@ private[platform] trait LedgerWriteDao extends ReportsHealth {
reason: state.Update.CommandRejected.RejectionReasonTemplate,
)(implicit loggingContext: LoggingContext): Future[PersistenceResponse]
/** !!! Please kindly not use this.
* !!! This method is solely for supporting sandbox-classic. Targeted for removal as soon sandbox classic is removed.
* Stores the initial ledger state, e.g., computed by the scenario loader.
* Must be called at most once, before any call to storeLedgerEntry.
*
* @param ledgerEntries the list of LedgerEntries to save
* @return Ok when the operation was successful
*/
def storeInitialState(
ledgerEntries: Vector[(Offset, LedgerEntry)],
newLedgerEnd: Offset,
)(implicit loggingContext: LoggingContext): Future[Unit]
/** Stores a party allocation or rejection thereof.
*
* @param Offset Pair of previous offset and the offset to store the party entry at

View File

@ -18,12 +18,7 @@ import com.daml.lf.transaction.{BlindingInfo, CommittedTransaction}
import com.daml.logging.LoggingContext
import com.daml.metrics.{Metrics, Timed}
import com.daml.platform.store.backend.ParameterStorageBackend.LedgerEnd
import com.daml.platform.store.entries.{
ConfigurationEntry,
LedgerEntry,
PackageLedgerEntry,
PartyLedgerEntry,
}
import com.daml.platform.store.entries.{ConfigurationEntry, PackageLedgerEntry, PartyLedgerEntry}
import com.daml.platform.store.interfaces.LedgerDaoContractsReader
import scala.concurrent.Future
@ -151,15 +146,6 @@ private[platform] class MeteredLedgerDao(ledgerDao: LedgerDao, metrics: Metrics)
ledgerDao.storeRejection(completionInfo, recordTime, offset, reason),
)
override def storeInitialState(
ledgerEntries: Vector[(Offset, LedgerEntry)],
newLedgerEnd: Offset,
)(implicit loggingContext: LoggingContext): Future[Unit] =
Timed.future(
metrics.daml.index.db.storeInitialState,
ledgerDao.storeInitialState(ledgerEntries, newLedgerEnd),
)
override def initialize(
ledgerId: LedgerId,
participantId: ParticipantId,

View File

@ -1,382 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store.appendonlydao.events
import java.sql.Connection
import com.daml.error.{ContextualizedErrorLogger, ErrorCodesVersionSwitcher}
import com.daml.ledger.api.domain
import com.daml.ledger.participant.state.v2
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.transaction.Transaction.ChildrenRecursion
import com.daml.lf.transaction.{CommittedTransaction, GlobalKey}
import com.daml.platform.apiserver.execution.MissingContracts
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.store.appendonlydao.events.PostCommitValidation._
import com.daml.platform.store.backend.{ContractStorageBackend, PartyStorageBackend}
import scala.util.{Failure, Success}
/** Performs post-commit validation on transactions for Sandbox Classic.
* This is intended exclusively as a temporary replacement for
* [[com.daml.platform.store.ActiveLedgerState]] and [[com.daml.platform.store.ActiveLedgerStateManager]]
* 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 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
*/
private[appendonlydao] sealed trait PostCommitValidation {
def validate(
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Timestamp,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[Rejection]
}
private[appendonlydao] object PostCommitValidation {
/** Accept unconditionally a transaction.
*
* Designed to be used by a ledger integration that
* already performs post-commit validation.
*/
object Skip extends PostCommitValidation {
@inline override def validate(
committedTransaction: CommittedTransaction,
transactionLedgerEffectiveTime: Timestamp,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[Rejection] =
None
}
final class BackedBy(
partyStorageBackend: PartyStorageBackend,
contractStorageBackend: ContractStorageBackend,
validatePartyAllocation: Boolean,
) extends PostCommitValidation {
def validate(
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Timestamp,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[Rejection] = {
val causalMonotonicityViolation =
validateCausalMonotonicity(transaction, transactionLedgerEffectiveTime, divulged)
val invalidKeyUsage = validateKeyUsages(transaction)
val unallocatedParties =
if (validatePartyAllocation)
validateParties(transaction)
else
None
unallocatedParties.orElse(invalidKeyUsage.orElse(causalMonotonicityViolation))
}
/** Do all exercise, fetch and lookup-by-key nodes
* 1. exist, and
* 2. refer exclusively to contracts with a ledger effective time smaller than or equal to the transaction's?
*/
private def validateCausalMonotonicity(
transaction: CommittedTransaction,
transactionLedgerEffectiveTime: Timestamp,
divulged: Set[ContractId],
)(implicit connection: Connection): Option[Rejection] = {
val referredContracts = collectReferredContracts(transaction, divulged)
if (referredContracts.isEmpty) {
None
} else
contractStorageBackend.maximumLedgerTime(referredContracts)(connection) match {
case Failure(MissingContracts(missingContractIds)) =>
Some(Rejection.UnknownContracts(missingContractIds.map(_.coid)))
case Failure(_) => Some(Rejection.MaximumLedgerTimeLookupFailure)
case Success(value) => validateCausalMonotonicity(value, transactionLedgerEffectiveTime)
}
}
private def validateCausalMonotonicity(
maximumLedgerEffectiveTime: Option[Timestamp],
transactionLedgerEffectiveTime: Timestamp,
): Option[Rejection] =
maximumLedgerEffectiveTime
.filter(_ > transactionLedgerEffectiveTime)
.fold(Option.empty[Rejection])(contractLedgerEffectiveTime => {
Some(
Rejection.CausalMonotonicityViolation(
contractLedgerEffectiveTime = contractLedgerEffectiveTime,
transactionLedgerEffectiveTime = transactionLedgerEffectiveTime,
)
)
})
private def validateParties(
transaction: CommittedTransaction
)(implicit connection: Connection): Option[Rejection] = {
val informees = transaction.informees
val allocatedInformees =
partyStorageBackend.parties(informees.toSeq)(connection).iterator.map(_.party).toSet
if (allocatedInformees == informees)
None
else
Some(
Rejection.UnallocatedParties((informees diff allocatedInformees).toSet)
)
}
private def collectReferredContracts(
transaction: CommittedTransaction,
divulged: Set[ContractId],
): Set[ContractId] = {
transaction.inputContracts.diff(divulged)
}
private def validateKeyUsages(
transaction: CommittedTransaction
)(implicit connection: Connection): Option[Rejection] =
transaction
.foldInExecutionOrder[Result](Right(State.empty(contractStorageBackend)))(
exerciseBegin = (acc, _, exe) => {
val newAcc = acc.flatMap(validateKeyUsages(exe, _))
(newAcc, ChildrenRecursion.DoRecurse)
},
exerciseEnd = (acc, _, _) => acc,
rollbackBegin = (acc, _, _) => (acc.map(_.beginRollback()), ChildrenRecursion.DoRecurse),
rollbackEnd = (acc, _, _) => acc.map(_.endRollback()),
leaf = (acc, _, leaf) => acc.flatMap(validateKeyUsages(leaf, _)),
)
.fold(Some(_), _ => None)
private def validateKeyUsages(
node: Node,
state: State,
)(implicit connection: Connection): Result =
node match {
case c: Create =>
state.validateCreate(c.key.map(convert(c.templateId, _)), c.coid)
case l: LookupByKey =>
state.validateLookupByKey(convert(l.templateId, l.key), l.result)
case e: Exercise if e.consuming =>
state.removeKeyIfDefined(e.key.map(convert(e.templateId, _)))
case _ =>
// fetch and non-consuming exercise nodes don't need to validate
// anything with regards to contract keys and do not alter the
// state in a way which is relevant for the validation of
// subsequent nodes
Right(state)
}
}
private type Result = Either[Rejection, State]
/** The active ledger key state during validation.
* After a rollback node, we restore the state at the
* beginning of the rollback.
*
* @param contracts Active contracts created in
* the current transaction that have a key indexed
* by a hash of their key.
* @param removed Hashes of contract keys that are known to
* to be archived. Note that a later create with the same
* key will remove the entry again.
*/
private final case class ActiveState(
contracts: Map[Hash, ContractId],
removed: Set[Hash],
) {
def add(key: Key, id: ContractId): ActiveState =
copy(
contracts = contracts.updated(key.hash, id),
removed = removed - key.hash,
)
def remove(key: Key): ActiveState =
copy(
contracts = contracts - key.hash,
removed = removed + key.hash,
)
}
/** 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 currentState The current active ledger state.
* @param rollbackStack Stack of states at the beginning of rollback nodes so we can
* restore the state at the end of the rollback. The most recent rollback
* comes first.
* @param contractStorageBackend For getting committed contracts for post-commit validation purposes.
* This is never changed during the traversal of the transaction.
*/
private final case class State(
private val currentState: ActiveState,
private val rollbackStack: List[ActiveState],
private val contractStorageBackend: ContractStorageBackend,
) {
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(Rejection.DuplicateKey(key)))
}
// `causalMonotonicity` already reports unknown contracts, no need to check it here
def removeKeyIfDefined(maybeKey: Option[Key]): Result =
Right(maybeKey.fold(this)(remove))
def validateLookupByKey(
key: Key,
expectation: Option[ContractId],
)(implicit connection: Connection): Result = {
val result = lookup(key)
if (result == expectation) Right(this)
else Left(Rejection.MismatchingLookup(expectation, result))
}
def beginRollback(): State =
copy(
rollbackStack = currentState :: rollbackStack
)
def endRollback(): State = rollbackStack match {
case Nil =>
throw new IllegalStateException(
"Internal error: rollback ended but rollbackStack was empty"
)
case head :: tail =>
copy(
currentState = head,
rollbackStack = tail,
)
}
private def add(key: Key, id: ContractId): State =
copy(currentState = currentState.add(key, id))
private def remove(key: Key): State =
copy(
currentState = currentState.remove(key)
)
private def lookup(key: Key)(implicit connection: Connection): Option[ContractId] =
currentState.contracts.get(key.hash).orElse {
if (currentState.removed(key.hash)) None
else contractStorageBackend.contractKeyGlobally(key)(connection)
}
}
private object State {
def empty(
contractStorageBackend: ContractStorageBackend
): State =
State(ActiveState(Map.empty, Set.empty), Nil, contractStorageBackend)
}
sealed trait Rejection {
def description: String
def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate
}
object Rejection {
import com.daml.platform.store.Conversions.RejectionReasonOps
object MaximumLedgerTimeLookupFailure extends Rejection {
override val description = "An unhandled failure occurred during ledger time lookup."
override def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
.Disputed(description)
.toParticipantStateRejectionReason(ErrorFactories(new ErrorCodesVersionSwitcher(false)))
}
final case class UnknownContracts(
missingContractIds: Set[String]
) extends Rejection {
override val description =
s"Unknown contracts: ${missingContractIds.mkString("[", ", ", "]")}"
override def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
// TODO error codes: Return specialized error codes
.ContractsNotFound(missingContractIds)
.toParticipantStateRejectionReason(errorFactories)
}
final case class DuplicateKey(key: GlobalKey) extends Rejection {
override val description =
"DuplicateKey: contract key is not unique"
override def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
.DuplicateContractKey(key)
.toParticipantStateRejectionReason(errorFactories)
}
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 toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
.InconsistentContractKeys(expectation, result)
.toParticipantStateRejectionReason(errorFactories)
}
final case class CausalMonotonicityViolation(
contractLedgerEffectiveTime: Timestamp,
transactionLedgerEffectiveTime: Timestamp,
) extends Rejection {
override lazy val description: String =
s"Encountered contract with LET [$contractLedgerEffectiveTime] greater than the LET of the transaction [$transactionLedgerEffectiveTime]"
override def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
.InvalidLedgerTime(description)
.toParticipantStateRejectionReason(errorFactories)
}
final case class UnallocatedParties(
unallocatedParties: Set[String]
) extends Rejection {
override def description: String = "Some parties are unallocated"
override def toStateV2RejectionReason(errorFactories: ErrorFactories)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): v2.Update.CommandRejected.RejectionReasonTemplate =
domain.RejectionReason
.PartiesNotKnownOnLedger(unallocatedParties)
.toParticipantStateRejectionReason(errorFactories)
}
}
}

View File

@ -191,7 +191,6 @@ trait CompletionStorageBackend {
}
trait ContractStorageBackend {
def contractKeyGlobally(key: Key)(connection: Connection): Option[ContractId]
def maximumLedgerTime(ids: Set[ContractId])(connection: Connection): Try[Option[Timestamp]]
def keyState(key: Key, validAt: Long)(connection: Connection): KeyState
def contractState(contractId: ContractId, before: Long)(

View File

@ -33,16 +33,6 @@ class ContractStorageBackendTemplate(
) extends ContractStorageBackend {
import com.daml.platform.store.Conversions.ArrayColumnToIntArray._
override def contractKeyGlobally(key: Key)(connection: Connection): Option[ContractId] =
contractKey(
resultColumns = List("contract_id"),
resultParser = contractId("contract_id"),
)(
readers = None,
key = key,
validAt = ledgerEndCache()._2,
)(connection)
private def emptyContractIds: Throwable =
new IllegalArgumentException(
"Cannot lookup the maximum ledger time for an empty set of contract identifiers"

View File

@ -40,39 +40,24 @@ trait JdbcLedgerDaoConfigurationAppendOnlySpec {
)
config2 <- ledgerDao.lookupLedgerConfiguration().map(_.map(_._2).get)
// Submission with mismatching generation is rejected
// Submission with unique submissionId and correct generation is accepted.
offset2 = nextOffset()
offsetString2 = offset2.toLong
resp2 <- storeConfigurationEntry(
offset2,
s"refuse-config-$offsetString2",
config0,
)
// Submission with unique submissionId and correct generation is accepted.
offset3 = nextOffset()
offsetString3 = offset3.toLong
lastConfig = config1.copy(generation = config1.generation + 2)
resp3 <- storeConfigurationEntry(offset3, s"refuse-config-$offsetString3", lastConfig)
resp3 <- storeConfigurationEntry(offset2, s"refuse-config-$offsetString2", lastConfig)
lastConfigActual <- ledgerDao.lookupLedgerConfiguration().map(_.map(_._2).get)
entries <- ledgerDao.getConfigurationEntries(startExclusive, offset3).runWith(Sink.seq)
entries <- ledgerDao.getConfigurationEntries(startExclusive, offset2).runWith(Sink.seq)
} yield {
resp0 shouldEqual PersistenceResponse.Ok
resp1 shouldEqual PersistenceResponse.Ok
resp2 shouldEqual PersistenceResponse.Ok
resp3 shouldEqual PersistenceResponse.Ok
lastConfig shouldEqual lastConfigActual
entries.toList shouldEqual List(
offset0 -> ConfigurationEntry.Accepted(s"refuse-config-$offsetString0", config1),
/* offset1 is duplicate */
offset1 -> ConfigurationEntry.Accepted(s"refuse-config-$offsetString0", config2),
offset2 -> ConfigurationEntry.Rejected(
s"refuse-config-${offset2.toLong}",
"Generation mismatch: expected=3, actual=0",
config0,
),
offset3 -> ConfigurationEntry.Accepted(s"refuse-config-$offsetString3", lastConfig),
offset2 -> ConfigurationEntry.Accepted(s"refuse-config-$offsetString2", lastConfig),
)
}
}

View File

@ -3,9 +3,7 @@
package com.daml.platform.store.dao
import akka.stream.scaladsl.Sink
import com.daml.platform.store.appendonlydao._
import com.daml.platform.store.entries.ConfigurationEntry
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
@ -35,32 +33,4 @@ trait JdbcLedgerDaoConfigurationSpec {
endingOffset.lastOffset should be > startingOffset.lastOffset
}
}
it should "be able to persist configuration rejection" in {
val startExclusive = nextOffset()
val offset = nextOffset()
val offsetString = offset.toLong
for {
startingConfig <- ledgerDao.lookupLedgerConfiguration().map(_.map(_._2))
proposedConfig = startingConfig.getOrElse(defaultConfig)
response <- storeConfigurationEntry(
offset,
s"config-rejection-$offsetString",
proposedConfig,
Some("bad config"),
)
storedConfig <- ledgerDao.lookupLedgerConfiguration().map(_.map(_._2))
entries <- ledgerDao
.getConfigurationEntries(startExclusive, offset)
.runWith(Sink.seq)
} yield {
response shouldEqual PersistenceResponse.Ok
startingConfig shouldEqual storedConfig
entries shouldEqual List(
offset -> ConfigurationEntry
.Rejected(s"config-rejection-$offsetString", "bad config", proposedConfig)
)
}
}
}

View File

@ -1,235 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store.dao
import java.util.UUID
import com.codahale.metrics.MetricRegistry
import com.daml.ledger.resources.ResourceOwner
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Ref
import com.daml.lf.transaction.BlindingInfo
import com.daml.lf.value.Value.ContractId
import com.daml.logging.LoggingContext
import com.daml.metrics.Metrics
import com.daml.platform.configuration.ServerRole
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.store.{DbSupport, DbType, LfValueTranslationCache}
import com.daml.platform.store.appendonlydao.events.CompressionStrategy
import com.daml.platform.store.appendonlydao.{JdbcLedgerDao, LedgerDao, SequentialWriteDao}
import com.daml.platform.store.backend.StorageBackendFactory
import com.daml.platform.store.interning.StringInterningView
import org.scalatest.LoneElement
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
import scala.concurrent.duration.DurationInt
private[dao] trait JdbcLedgerDaoPostCommitValidationSpec extends LoneElement {
this: AsyncFlatSpec with Matchers with JdbcLedgerDaoSuite =>
override protected def daoOwner(
eventsPageSize: Int,
eventsProcessingParallelism: Int,
acsIdPageSize: Int,
acsIdFetchingParallelism: Int,
acsContractFetchingParallelism: Int,
acsGlobalParallelism: Int,
acsIdQueueLimit: Int,
errorFactories: ErrorFactories,
)(implicit
loggingContext: LoggingContext
): ResourceOwner[LedgerDao] = {
val metrics = new Metrics(new MetricRegistry)
val dbType = DbType.jdbcType(jdbcUrl)
val storageBackendFactory = StorageBackendFactory.of(dbType)
val participantId = Ref.ParticipantId.assertFromString("JdbcLedgerDaoPostCommitValidationSpec")
DbSupport
.migratedOwner(
jdbcUrl = jdbcUrl,
serverRole = ServerRole.Testing(getClass),
connectionPoolSize = dbType.maxSupportedWriteConnections(16),
connectionTimeout = 250.millis,
metrics = metrics,
)
.map { dbSupport =>
val stringInterningStorageBackend =
storageBackendFactory.createStringInterningStorageBackend
val stringInterningView = new StringInterningView(
loadPrefixedEntries = (fromExclusive, toInclusive) =>
implicit loggingContext =>
dbSupport.dbDispatcher.executeSql(metrics.daml.index.db.loadStringInterningEntries) {
stringInterningStorageBackend.loadStringInterningEntries(fromExclusive, toInclusive)
}
)
JdbcLedgerDao.validatingWrite(
dbSupport = dbSupport,
sequentialWriteDao = SequentialWriteDao(
participantId = participantId,
lfValueTranslationCache = LfValueTranslationCache.Cache.none,
metrics = metrics,
compressionStrategy = CompressionStrategy.none(metrics),
ledgerEndCache = ledgerEndCache,
stringInterningView = stringInterningView,
ingestionStorageBackend = storageBackendFactory.createIngestionStorageBackend,
parameterStorageBackend = storageBackendFactory.createParameterStorageBackend,
),
eventsPageSize = eventsPageSize,
eventsProcessingParallelism = eventsProcessingParallelism,
acsIdPageSize = acsIdPageSize,
acsIdFetchingParallelism = acsIdFetchingParallelism,
acsContractFetchingParallelism = acsContractFetchingParallelism,
acsGlobalParallelism = acsGlobalParallelism,
acsIdQueueLimit = acsIdQueueLimit,
servicesExecutionContext = executionContext,
metrics = metrics,
lfValueTranslationCache = LfValueTranslationCache.Cache.none,
enricher = None,
participantId = participantId,
errorFactories = errorFactories,
ledgerEndCache = ledgerEndCache,
stringInterning = stringInterningView,
materializer = materializer,
)
}
}
private val ok = io.grpc.Status.Code.OK.value()
private val alreadyExists = io.grpc.Status.Code.ALREADY_EXISTS.value()
private val failedPrecondition = io.grpc.Status.Code.FAILED_PRECONDITION.value()
private val notFound = io.grpc.Status.Code.NOT_FOUND.value()
behavior of "JdbcLedgerDao (post-commit validation)"
it should "refuse to serialize duplicate contract keys" in {
val keyValue = s"duplicate-key"
// Scenario: Two concurrent commands create the same contract key.
// At command interpretation time, the keys do not exist yet.
// At serialization time, the ledger should refuse to serialize one of them.
for {
from <- ledgerDao.lookupLedgerEnd()
original @ (_, originalAttempt) = txCreateContractWithKey(alice, keyValue)
duplicate @ (_, duplicateAttempt) = txCreateContractWithKey(alice, keyValue)
_ <- store(original)
_ <- store(duplicate)
to <- ledgerDao.lookupLedgerEnd()
completions <- getCompletions(from.lastOffset, to.lastOffset, defaultAppId, Set(alice))
} yield {
completions should contain.allOf(
originalAttempt.commandId.get -> ok,
duplicateAttempt.commandId.get -> alreadyExists,
)
}
}
it should "refuse to serialize invalid negative lookupByKey" in {
val keyValue = s"no-invalid-negative-lookup"
// Scenario: Two concurrent commands: one create and one lookupByKey.
// At command interpretation time, the lookupByKey does not find any contract.
// At serialization time, it should be rejected because now the key is there.
for {
from <- ledgerDao.lookupLedgerEnd()
(_, create) <- store(txCreateContractWithKey(alice, keyValue))
(_, lookup) <- store(txLookupByKey(alice, keyValue, None))
to <- ledgerDao.lookupLedgerEnd()
completions <- getCompletions(from.lastOffset, to.lastOffset, defaultAppId, Set(alice))
} yield {
completions should contain.allOf(
create.commandId.get -> ok,
lookup.commandId.get -> failedPrecondition,
)
}
}
it should "refuse to serialize invalid positive lookupByKey" in {
val keyValue = s"no-invalid-positive-lookup"
// Scenario: Two concurrent commands: one exercise and one lookupByKey.
// At command interpretation time, the lookupByKey finds a contract.
// At serialization time, it should be rejected because now the contract was archived.
for {
from <- ledgerDao.lookupLedgerEnd()
(_, create) <- store(txCreateContractWithKey(alice, keyValue))
createdContractId = nonTransient(create).loneElement
(_, archive) <- store(txArchiveContract(alice, createdContractId -> Some(keyValue)))
(_, lookup) <- store(txLookupByKey(alice, keyValue, Some(createdContractId)))
to <- ledgerDao.lookupLedgerEnd()
completions <- getCompletions(from.lastOffset, to.lastOffset, defaultAppId, Set(alice))
} yield {
completions should contain.allOf(
create.commandId.get -> ok,
archive.commandId.get -> ok,
lookup.commandId.get -> failedPrecondition,
)
}
}
it should "refuse to serialize invalid fetch" in {
val keyValue = s"no-invalid-fetch"
// Scenario: Two concurrent commands: one exercise and one fetch.
// At command interpretation time, the fetch finds a contract.
// At serialization time, it should be rejected because now the contract was archived.
for {
from <- ledgerDao.lookupLedgerEnd()
(_, create) <- store(txCreateContractWithKey(alice, keyValue))
createdContractId = nonTransient(create).loneElement
(_, archive) <- store(txArchiveContract(alice, createdContractId -> Some(keyValue)))
(_, fetch) <- store(txFetch(alice, createdContractId))
to <- ledgerDao.lookupLedgerEnd()
completions <- getCompletions(from.lastOffset, to.lastOffset, defaultAppId, Set(alice))
} yield {
completions should contain.allOf(
create.commandId.get -> ok,
archive.commandId.get -> ok,
fetch.commandId.get -> notFound,
)
}
}
it should "be able to use divulged contract in later transaction" in {
val divulgedContractId: ContractId =
ContractId.V1(Hash.hashPrivateKey(UUID.randomUUID.toString))
val divulgedContracts =
Map((divulgedContractId, someVersionedContractInstance) -> Set(alice))
val blindingInfo = BlindingInfo(
disclosure = Map.empty,
divulgence = Map(divulgedContractId -> Set(alice)),
)
for {
from <- ledgerDao.lookupLedgerEnd()
(_, fetch1) <- store(txFetch(alice, divulgedContractId))
(_, divulgence) <- store(
divulgedContracts,
blindingInfo = Some(blindingInfo),
emptyTransaction(alice),
)
(_, fetch2) <- store(txFetch(alice, divulgedContractId))
to <- ledgerDao.lookupLedgerEnd()
completions <- getCompletions(from.lastOffset, to.lastOffset, defaultAppId, Set(alice))
} yield {
completions should contain.allOf(
fetch1.commandId.get -> notFound,
divulgence.commandId.get -> ok,
fetch2.commandId.get -> ok,
)
}
}
it should "do not refuse to insert entries with conflicting transaction ids" in {
val original = txCreateContractWithKey(alice, "some-key", Some("1337"))
val duplicateTxId = txCreateContractWithKey(alice, "another-key", Some("1337"))
// Post-commit validation does not prevent duplicate transaction ids
for {
_ <- store(original)
_ <- store(duplicateTxId)
} yield succeed
}
}

View File

@ -1,163 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store
import com.daml.error.{
ContextualizedErrorLogger,
DamlContextualizedErrorLogger,
ErrorCodesVersionSwitcher,
}
import com.daml.ledger.api.domain
import com.daml.ledger.api.domain.RejectionReason
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Ref
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.value.Value.{ContractId, ValueText}
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.store.Conversions._
import io.grpc.Status
import org.scalatest.Assertion
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
import scala.annotation.nowarn
@nowarn("msg=deprecated")
class ConversionsSpec extends AsyncWordSpec with Matchers {
private implicit val contextualizedErrorLogger: ContextualizedErrorLogger =
new DamlContextualizedErrorLogger(
ContextualizedLogger.get(getClass),
LoggingContext.ForTesting,
None,
)
private def cid(key: String): ContractId = ContractId.V1(Hash.hashPrivateKey(key))
"converting rejection reasons" should {
"convert an 'Inconsistent' rejection reason" in {
assertConversion(domain.RejectionReason.Inconsistent("This was not very consistent."))(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage = "Inconsistent: This was not very consistent.",
v2expectedCode = Status.Code.FAILED_PRECONDITION.value(),
v2expectedMessage = "INCONSISTENT(9,0): Inconsistent: This was not very consistent.",
)
}
"convert a 'ContractsNotFound' rejection reason" in {
assertConversion(domain.RejectionReason.ContractsNotFound(Set("cId_1", "cId_2")))(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage = "Inconsistent: Could not lookup contracts: [cId_1, cId_2]",
v2expectedCode = Status.Code.NOT_FOUND.value(),
v2expectedMessage = "CONTRACT_NOT_FOUND(11,0): Unknown contracts: [cId_1, cId_2]",
)
}
"convert an 'InconsistentContractKeys' rejection reason" in {
val cId = cid("#cId1")
assertConversion(
domain.RejectionReason
.InconsistentContractKeys(Some(cId), None)
)(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage =
s"Inconsistent: Contract key lookup with different results: expected [Some($cId)], actual [$None]",
v2expectedCode = Status.Code.FAILED_PRECONDITION.value(),
v2expectedMessage =
s"INCONSISTENT_CONTRACT_KEY(9,0): Contract key lookup with different results: expected [Some($cId)], actual [$None]",
)
}
"convert a 'DuplicateContractKey' rejection reason" in {
val key = GlobalKey.assertBuild(
Ref.Identifier.assertFromString("some:template:value"),
ValueText("value"),
)
assertConversion(domain.RejectionReason.DuplicateContractKey(key))(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage = "Inconsistent: DuplicateKey: contract key is not unique",
v2expectedCode = Status.Code.ALREADY_EXISTS.value(),
v2expectedMessage = "DUPLICATE_CONTRACT_KEY(10,0): DuplicateKey: contract key is not unique",
)
}
"convert a 'Disputed' rejection reason" in {
assertConversion(domain.RejectionReason.Disputed("I dispute that."))(
v1expectedCode = Status.Code.INVALID_ARGUMENT.value(),
v1expectedMessage = "Disputed: I dispute that.",
v2expectedCode = Status.Code.INTERNAL.value(),
v2expectedMessage =
"An error occurred. Please contact the operator and inquire about the request <no-correlation-id>",
)
}
"convert an 'OutOfQuota' rejection reason" in {
assertConversion(domain.RejectionReason.OutOfQuota("Insert coins to continue."))(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage = "Resources exhausted: Insert coins to continue.",
v2expectedCode = Status.Code.ABORTED.value(),
v2expectedMessage = "OUT_OF_QUOTA(2,0): Insert coins to continue.",
)
}
"convert a 'PartiesNotKnownOnLedger' rejection reason" in {
assertConversion(domain.RejectionReason.PartiesNotKnownOnLedger(Set("Alice")))(
v1expectedCode = Status.Code.INVALID_ARGUMENT.value(),
v1expectedMessage = "Parties not known on ledger: [Alice]",
v2expectedCode = Status.Code.NOT_FOUND.value(),
v2expectedMessage = "PARTY_NOT_KNOWN_ON_LEDGER(11,0): Parties not known on ledger: [Alice]",
)
}
"convert a 'PartyNotKnownOnLedger' rejection reason" in {
assertConversion(domain.RejectionReason.PartyNotKnownOnLedger("reason"))(
v1expectedCode = Status.Code.INVALID_ARGUMENT.value(),
v1expectedMessage = "Parties not known on ledger: reason",
v2expectedCode = Status.Code.NOT_FOUND.value(),
v2expectedMessage = "PARTY_NOT_KNOWN_ON_LEDGER(11,0): Party not known on ledger: reason",
)
}
"convert a 'SubmitterCannotActViaParticipant' rejection reason" in {
assertConversion(domain.RejectionReason.SubmitterCannotActViaParticipant("Wrong box."))(
v1expectedCode = Status.Code.PERMISSION_DENIED.value(),
v1expectedMessage = "Submitted cannot act via participant: Wrong box.",
v2expectedCode = Status.Code.PERMISSION_DENIED.value(),
v2expectedMessage =
"An error occurred. Please contact the operator and inquire about the request <no-correlation-id>",
)
}
"convert an 'InvalidLedgerTime' rejection reason" in {
assertConversion(domain.RejectionReason.InvalidLedgerTime("Too late."))(
v1expectedCode = Status.Code.ABORTED.value(),
v1expectedMessage = "Invalid ledger time: Too late.",
v2expectedCode = Status.Code.FAILED_PRECONDITION.value(),
v2expectedMessage = "INVALID_LEDGER_TIME(9,0): Too late.",
)
}
}
private def assertConversion(actualRejectionReason: RejectionReason)(
v1expectedCode: Int,
v1expectedMessage: String,
v2expectedCode: Int,
v2expectedMessage: String,
): Assertion = {
val errorFactoriesV1 = ErrorFactories(
new ErrorCodesVersionSwitcher(enableSelfServiceErrorCodes = false)
)
val errorFactoriesV2 = ErrorFactories(
new ErrorCodesVersionSwitcher(enableSelfServiceErrorCodes = true)
)
val convertedV1 = actualRejectionReason.toParticipantStateRejectionReason(errorFactoriesV1)
convertedV1.code shouldBe v1expectedCode
convertedV1.message shouldBe v1expectedMessage
val convertedV2 = actualRejectionReason.toParticipantStateRejectionReason(errorFactoriesV2)
convertedV2.code shouldBe v2expectedCode
convertedV2.message shouldBe v2expectedMessage
}
}

View File

@ -1,642 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store.appendonlydao.events
import com.daml.ledger.api.domain.PartyDetails
import com.daml.ledger.offset.Offset
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Ref
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.transaction.test.{TransactionBuilder => TxBuilder}
import com.daml.lf.value.Value.ValueText
import com.daml.platform.apiserver.execution.MissingContracts
import com.daml.platform.store.backend.{ContractStorageBackend, PartyStorageBackend}
import com.daml.platform.store.entries.PartyLedgerEntry
import com.daml.platform.store.interfaces.LedgerDaoContractsReader.KeyState
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import java.sql.Connection
import java.time.Instant
import java.util.UUID
import scala.util.{Failure, Success, Try}
final class PostCommitValidationSpec extends AnyWordSpec with Matchers {
import PostCommitValidation._
import PostCommitValidationSpec._
"PostCommitValidation" when {
"run without prior history" should {
val fixture = noCommittedContract(parties = List.empty)
val store = new PostCommitValidation.BackedBy(
fixture,
fixture,
validatePartyAllocation = false,
)
"accept a create with a key" in {
val createWithKey = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(createWithKey),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept a create without a key" in {
val createWithoutKey = genTestCreate().copy(key = None)
val error = store.validate(
transaction = TxBuilder.justCommitted(createWithoutKey),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept an exercise of a contract created within the transaction" in {
val createContract = genTestCreate()
val exerciseContract = genTestExercise(createContract)
val error = store.validate(
transaction = TxBuilder.justCommitted(createContract, exerciseContract),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept an exercise of a contract divulged in the current transaction" in {
val divulgedContract = genTestCreate()
val exerciseContract = genTestExercise(divulgedContract)
val error = store.validate(
transaction = TxBuilder.justCommitted(exerciseContract),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set(divulgedContract.coid),
)
error shouldBe None
}
"reject an exercise of a contract not created in this transaction" in {
val missingCreate = genTestCreate()
val exerciseContract = genTestExercise(missingCreate)
val error = store.validate(
transaction = TxBuilder.justCommitted(exerciseContract),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.UnknownContracts(Set(missingCreate.coid.coid)))
}
"accept a fetch of a contract created within the transaction" in {
val createContract = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(createContract, txBuilder.fetch(createContract)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept a fetch of a contract divulged in the current transaction" in {
val divulgedContract = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.fetch(divulgedContract)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set(divulgedContract.coid),
)
error shouldBe None
}
"reject a fetch of a contract not created in this transaction" in {
val missingCreate = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.fetch(missingCreate)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.UnknownContracts(Set(missingCreate.coid.coid)))
}
"accept a successful lookup of a contract created in this transaction" in {
val createContract = genTestCreate()
val error = store.validate(
transaction = TxBuilder
.justCommitted(createContract, txBuilder.lookupByKey(createContract, found = true)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"reject a successful lookup of a missing contract" in {
val missingCreate = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.lookupByKey(missingCreate, found = true)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(
Rejection.MismatchingLookup(expectation = Some(missingCreate.coid), result = None)
)
}
"accept a failed lookup of a missing contract" in {
val missingContract = genTestCreate()
val error = store.validate(
transaction =
TxBuilder.justCommitted(txBuilder.lookupByKey(missingContract, found = false)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept a create in a rollback node" in {
val createContract = genTestCreate()
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(createContract, rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept a create after a rolled back create with the same key" in {
val createContract = genTestCreate()
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(createContract, rollback)
builder.add(createContract)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"reject a create in a rollback after a create with the same key" in {
val createContract = genTestCreate()
val builder = TxBuilder()
builder.add(createContract)
val rollback = builder.add(builder.rollback())
builder.add(createContract, rollback)
val duplicateKey =
GlobalKey.assertBuild(createContract.templateId, createContract.key.get.key)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.DuplicateKey(duplicateKey))
}
"reject a create after a rolled back archive of a contract with the same key" in {
val createContract = genTestCreate()
val builder = TxBuilder()
builder.add(createContract)
val rollback = builder.add(builder.rollback())
builder.add(genTestExercise(createContract), rollback)
builder.add(createContract)
val duplicateKey =
GlobalKey.assertBuild(createContract.templateId, createContract.key.get.key)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.DuplicateKey(duplicateKey))
}
"accept a failed lookup in a rollback" in {
val createContract = genTestCreate()
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(builder.lookupByKey(createContract, found = false), rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
}
"run with one committed contract with a key" should {
val committedContract = genTestCreate()
val exerciseOnCommittedContract = genTestExercise(committedContract)
val committedContractLedgerEffectiveTime =
Timestamp.assertFromInstant(Instant.ofEpochMilli(1000))
val fixture = committedContracts(
parties = List.empty,
contractFixture = committed(
id = committedContract.coid.coid,
ledgerEffectiveTime = committedContractLedgerEffectiveTime,
key = committedContract.key.map(x =>
GlobalKey.assertBuild(committedContract.templateId, x.key)
),
),
)
val store = new PostCommitValidation.BackedBy(
fixture,
fixture,
validatePartyAllocation = false,
)
"reject a create that would introduce a duplicate key" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(committedContract),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
val duplicateKey =
GlobalKey.assertBuild(committedContract.templateId, committedContract.key.get.key)
error shouldBe Some(Rejection.DuplicateKey(duplicateKey))
}
"accept an exercise on the committed contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(exerciseOnCommittedContract),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe None
}
"reject an exercise pre-dating the committed contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(exerciseOnCommittedContract),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.addMicros(-1),
divulged = Set.empty,
)
error shouldBe Some(
Rejection.CausalMonotonicityViolation(
contractLedgerEffectiveTime = committedContractLedgerEffectiveTime,
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.addMicros(-1),
)
)
}
"accept a fetch on the committed contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.fetch(committedContract)),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe None
}
"reject a fetch pre-dating the committed contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.fetch(committedContract)),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.addMicros(-1),
divulged = Set.empty,
)
error shouldBe Some(
Rejection.CausalMonotonicityViolation(
contractLedgerEffectiveTime = committedContractLedgerEffectiveTime,
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime.addMicros(-1),
)
)
}
"accept a successful lookup of the committed contract" in {
val error = store.validate(
transaction =
TxBuilder.justCommitted(txBuilder.lookupByKey(committedContract, found = true)),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe None
}
"reject a failed lookup of the committed contract" in {
val error = store.validate(
transaction =
TxBuilder.justCommitted(txBuilder.lookupByKey(committedContract, found = false)),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe Some(
Rejection.MismatchingLookup(
expectation = None,
result = Some(committedContract.coid),
)
)
}
"reject a create in a rollback" in {
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(committedContract, rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
val duplicateKey =
GlobalKey.assertBuild(committedContract.templateId, committedContract.key.get.key)
error shouldBe Some(Rejection.DuplicateKey(duplicateKey))
}
"reject a failed lookup in a rollback" in {
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(builder.lookupByKey(committedContract, found = false), rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe Some(
Rejection.MismatchingLookup(
result = Some(committedContract.coid),
expectation = None,
)
)
}
"accept a successful lookup in a rollback" in {
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(builder.lookupByKey(committedContract, found = true), rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
error shouldBe None
}
"reject a create after a rolled back archive" in {
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(genTestExercise(committedContract), rollback)
builder.add(committedContract)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = committedContractLedgerEffectiveTime,
divulged = Set.empty,
)
val duplicateKey =
GlobalKey.assertBuild(committedContract.templateId, committedContract.key.get.key)
error shouldBe Some(Rejection.DuplicateKey(duplicateKey))
}
}
"run with one divulged contract" should {
val divulgedContract = genTestCreate()
val exerciseOnDivulgedContract = genTestExercise(divulgedContract)
val fixture = committedContracts(
parties = List.empty,
contractFixture = divulged(divulgedContract.coid.coid),
)
val store = new PostCommitValidation.BackedBy(
fixture,
fixture,
validatePartyAllocation = false,
)
"accept an exercise on the divulged contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(exerciseOnDivulgedContract),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
"accept a fetch on the divulged contract" in {
val error = store.validate(
transaction = TxBuilder.justCommitted(txBuilder.fetch(divulgedContract)),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe None
}
}
"run with unallocated parties" should {
val store = new PostCommitValidation.BackedBy(
noCommittedContract(List.empty),
noCommittedContract(List.empty),
validatePartyAllocation = true,
)
"reject" in {
val createWithKey = genTestCreate()
val error = store.validate(
transaction = TxBuilder.justCommitted(createWithKey),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.UnallocatedParties(Set("Alice")))
}
"reject if party is used in rollback" in {
val createWithKey = genTestCreate()
val builder = TxBuilder()
val rollback = builder.add(builder.rollback())
builder.add(createWithKey, rollback)
val error = store.validate(
transaction = builder.buildCommitted(),
transactionLedgerEffectiveTime = Timestamp.now(),
divulged = Set.empty,
)
error shouldBe Some(Rejection.UnallocatedParties(Set("Alice")))
}
}
}
}
object PostCommitValidationSpec {
import TxBuilder.Implicits._
// Very dirty hack to have a contract store fixture without persistence
private implicit val connection: Connection = null
private val txBuilder = TxBuilder()
private def genTestCreate(): Create =
txBuilder.create(
id = ContractId.V1(Hash.hashPrivateKey(UUID.randomUUID.toString)),
templateId = "bar:baz",
argument = TxBuilder.record("field" -> "value"),
signatories = Set("Alice"),
observers = Set.empty,
key = Some(ValueText("key")),
)
private def genTestExercise(create: Create): Exercise =
txBuilder.exercise(
contract = create,
choice = "SomeChoice",
consuming = true,
actingParties = Set("Alice"),
argument = TxBuilder.record("field" -> "value"),
)
private final case class ContractFixture private (
id: ContractId,
ledgerEffectiveTime: Option[Timestamp],
key: Option[Key],
)
private final case class ContractStoreFixture private (
contracts: Set[ContractFixture],
parties: List[PartyDetails],
) extends PartyStorageBackend
with ContractStorageBackend {
override def contractKeyGlobally(key: Key)(connection: Connection): Option[ContractId] =
contracts.find(c => c.key.contains(key)).map(_.id)
override def maximumLedgerTime(
ids: Set[ContractId]
)(connection: Connection): Try[Option[Timestamp]] = {
val lookup = contracts.collect {
case c if ids.contains(c.id) => c.ledgerEffectiveTime
}
if (lookup.isEmpty) Failure(notFound(ids))
else Success(lookup.fold[Option[Timestamp]](None)(pickTheGreatest))
}
override def keyState(key: Key, validAt: Long)(connection: Connection): KeyState =
notImplemented()
override def contractState(contractId: ContractId, before: Long)(
connection: Connection
): Option[ContractStorageBackend.RawContractState] = notImplemented()
override def activeContractWithArgument(readers: Set[Ref.Party], contractId: ContractId)(
connection: Connection
): Option[ContractStorageBackend.RawContract] = notImplemented()
override def activeContractWithoutArgument(readers: Set[Ref.Party], contractId: ContractId)(
connection: Connection
): Option[String] = notImplemented()
override def contractKey(readers: Set[Ref.Party], key: Key)(
connection: Connection
): Option[ContractId] = notImplemented()
override def contractStateEvents(startExclusive: Long, endInclusive: Long)(
connection: Connection
): Vector[ContractStorageBackend.RawContractStateEvent] = notImplemented()
override def partyEntries(
startExclusive: Offset,
endInclusive: Offset,
pageSize: Int,
queryOffset: Long,
)(connection: Connection): Vector[(Offset, PartyLedgerEntry)] = notImplemented()
override def parties(parties: Seq[Party])(connection: Connection): List[PartyDetails] =
this.parties.filter { party =>
parties.contains(party.party)
}
override def knownParties(connection: Connection): List[PartyDetails] = notImplemented()
// PostCommitValidation only uses a small subset of (PartyStorageBackend with ContractStorageBackend)
private def notImplemented() =
throw new RuntimeException(
"This method is not implemented because it's not used by PostCommitValidation"
)
}
private def pickTheGreatest(l: Option[Timestamp], r: Option[Timestamp]): Option[Timestamp] =
l.fold(r)(left => r.fold(l)(right => if (left > right) l else r))
private def notFound(contractIds: Set[ContractId]): Throwable =
MissingContracts(contractIds)
private def noCommittedContract(parties: List[PartyDetails]): ContractStoreFixture =
ContractStoreFixture(
contracts = Set.empty,
parties = parties,
)
private def committedContracts(
parties: List[PartyDetails],
contractFixture: ContractFixture,
contractFixtures: ContractFixture*
): ContractStoreFixture =
ContractStoreFixture(
contracts = (contractFixture +: contractFixtures).toSet,
parties = parties,
)
private def committed(
id: String,
ledgerEffectiveTime: Timestamp,
key: Option[Key],
): ContractFixture =
ContractFixture(
id = ContractId.assertFromString(id),
ledgerEffectiveTime = Some(ledgerEffectiveTime),
key = key,
)
private def divulged(id: String): ContractFixture =
ContractFixture(
id = ContractId.assertFromString(id),
ledgerEffectiveTime = None,
key = None,
)
}

View File

@ -1,16 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store.dao
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
// Aggregate all specs in a single run to not start a new database fixture for each one
final class JdbcLedgerDaoValidatedH2DatabaseSpec
extends AsyncFlatSpec
with Matchers
with JdbcLedgerDaoSuite
with JdbcLedgerDaoBackendH2Database
with JdbcLedgerDaoPostCommitValidationSpec
with JdbcAppendOnlyTransactionInsertion

View File

@ -1,16 +0,0 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store.dao
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
// Aggregate all specs in a single run to not start a new database fixture for each one
final class JdbcLedgerDaoValidatedPostgresqlSpec
extends AsyncFlatSpec
with Matchers
with JdbcLedgerDaoSuite
with JdbcLedgerDaoBackendPostgresql
with JdbcLedgerDaoPostCommitValidationSpec
with JdbcAppendOnlyTransactionInsertion