mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
4038d0a7e3
commit
89ce7d0245
@ -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.
|
||||
*/
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)(
|
||||
|
@ -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"
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user