From 302df1ebd1c5a2fc47f71dd4e010a916539e5652 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 14 Jul 2021 10:05:26 +0200 Subject: [PATCH 01/18] Refine transaction validation CHANGELOG_BEGIN CHANGELOG_END --- .../transaction/TransactionCommitter.scala | 13 +- .../KeyMonotonicityValidation.scala | 49 ---- .../ModelConformanceValidator.scala | 248 +++++++++++------- ... => TransactionConsistencyValidator.scala} | 93 ++++--- .../daml/ledger/validator/TestHelper.scala | 2 +- .../KeyMonotonicityValidationSpec.scala | 79 ------ .../ModelConformanceValidatorSpec.scala | 100 ++++++- ...TransactionConsistencyValidatorSpec.scala} | 17 +- 8 files changed, 316 insertions(+), 285 deletions(-) delete mode 100644 ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidation.scala rename ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/{ContractKeysValidator.scala => TransactionConsistencyValidator.scala} (60%) delete mode 100644 ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidationSpec.scala rename ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/{ContractKeysValidatorSpec.scala => TransactionConsistencyValidatorSpec.scala} (95%) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/TransactionCommitter.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/TransactionCommitter.scala index 617e571f2221..fbee08646d52 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/TransactionCommitter.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/TransactionCommitter.scala @@ -10,9 +10,9 @@ import com.daml.ledger.participant.state.kvutils.DamlKvutils._ import com.daml.ledger.participant.state.kvutils.committer.Committer._ import com.daml.ledger.participant.state.kvutils.committer._ import com.daml.ledger.participant.state.kvutils.committer.transaction.validation.{ - ContractKeysValidator, LedgerTimeValidator, ModelConformanceValidator, + TransactionConsistencyValidator, } import com.daml.ledger.participant.state.kvutils.{Conversions, Err} import com.daml.ledger.participant.state.v1.{Configuration, RejectionReasonV0} @@ -74,10 +74,8 @@ private[kvutils] class TransactionCommitter( "check_informee_parties_allocation" -> checkInformeePartiesAllocation, "deduplicate" -> deduplicateCommand, "validate_ledger_time" -> ledgerTimeValidator.createValidationStep(rejections), - "validate_contract_keys" -> ContractKeysValidator.createValidationStep(rejections), - "validate_model_conformance" -> modelConformanceValidator.createValidationStep( - rejections - ), + "validate_model_conformance" -> modelConformanceValidator.createValidationStep(rejections), + "validate_consistency" -> TransactionConsistencyValidator.createValidationStep(rejections), "blind" -> blind, "trim_unnecessary_nodes" -> trimUnnecessaryNodes, "build_final_log_entry" -> buildFinalLogEntry, @@ -217,9 +215,8 @@ private[kvutils] class TransactionCommitter( case Nil => result case head :: tail => import TransactionOuterClass.Node.NodeTypeCase - val node = nodeMap - .get(head) - .getOrElse(throw Err.InternalError(s"Invalid transaction node id $head")) + val node = + nodeMap.getOrElse(head, throw Err.InternalError(s"Invalid transaction node id $head")) node.getNodeTypeCase match { case NodeTypeCase.CREATE => goNodesToKeep(tail, result + head) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidation.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidation.scala deleted file mode 100644 index 4a4ebf4f04a5..000000000000 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidation.scala +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.ledger.participant.state.kvutils.committer.transaction.validation - -import com.daml.ledger.participant.state.kvutils.Conversions -import com.daml.ledger.participant.state.kvutils.DamlKvutils.{DamlStateKey, DamlStateValue} -import com.daml.ledger.participant.state.kvutils.committer.transaction.{ - DamlTransactionEntrySummary, - Rejections, -} -import com.daml.ledger.participant.state.kvutils.committer.{StepContinue, StepResult} -import com.daml.ledger.participant.state.v1.RejectionReasonV0 -import com.daml.lf.data.Time.Timestamp -import com.daml.logging.LoggingContext - -private[validation] object KeyMonotonicityValidation { - - /** LookupByKey nodes themselves don't actually fetch the contract. - * Therefore we need to do an additional check on all contract keys - * to ensure the referred contract satisfies the causal monotonicity invariant. - * This could be reduced to only validate this for keys referred to by - * NodeLookupByKey. - */ - def checkContractKeysCausalMonotonicity( - recordTime: Option[Timestamp], - keys: Set[DamlStateKey], - damlState: Map[DamlStateKey, DamlStateValue], - transactionEntry: DamlTransactionEntrySummary, - rejections: Rejections, - )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - val causalKeyMonotonicity = keys.forall { key => - val state = damlState(key) - val keyActiveAt = - Conversions - .parseTimestamp(state.getContractKeyState.getActiveAt) - .toInstant - !keyActiveAt.isAfter(transactionEntry.ledgerEffectiveTime.toInstant) - } - if (causalKeyMonotonicity) - StepContinue(transactionEntry) - else - rejections.buildRejectionStep( - transactionEntry, - RejectionReasonV0.InvalidLedgerTime("Causal monotonicity violated"), - recordTime, - ) - } -} diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 79af0d8fbc8d..805512083424 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -5,15 +5,10 @@ package com.daml.ledger.participant.state.kvutils.committer.transaction.validati import com.daml.ledger.participant.state.kvutils.Conversions.{ contractIdToStateKey, - decodeContractId, packageStateKey, parseTimestamp, } -import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ - DamlContractKey, - DamlContractState, - DamlStateValue, -} +import com.daml.ledger.participant.state.kvutils.DamlKvutils.{DamlContractState, DamlStateValue} import com.daml.ledger.participant.state.kvutils.committer.transaction.{ DamlTransactionEntrySummary, Rejections, @@ -24,9 +19,18 @@ import com.daml.ledger.participant.state.kvutils.{Conversions, Err} import com.daml.ledger.participant.state.v1.RejectionReasonV0 import com.daml.lf.archive import com.daml.lf.data.Ref.PackageId -import com.daml.lf.engine.{Engine, Error => LfError} +import com.daml.lf.data.Time.Timestamp +import com.daml.lf.engine.{Engine, Result, Error => LfError} import com.daml.lf.language.Ast +import com.daml.lf.transaction.Transaction.{ + DuplicateKeys, + InconsistentKeys, + KeyActive, + KeyInput, + KeyInputError, +} import com.daml.lf.transaction.{ + GlobalKey, GlobalKeyWithMaintainers, Node, NodeId, @@ -40,99 +44,107 @@ import com.daml.logging.LoggingContext.withEnrichedLoggingContext import com.daml.logging.{ContextualizedLogger, LoggingContext} import com.daml.metrics.Metrics +/** Validates the submission's conformance to the Daml model. + * + * @param engine An [[Engine]] instance to reinterpret and validate the transaction. + * @param metrics A [[Metrics]] instance to record metrics. + */ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Metrics) extends TransactionValidator { import ModelConformanceValidator._ private final val logger = ContextualizedLogger.get(getClass) - /** Creates a committer step that validates the submission's conformance to the Daml model. */ + /** Validates model conformance based on the transaction itself (where it's possible). + * Because fetch nodes don't contain contracts, we still need to get them from the current state ([[CommitContext]]). + * It first reinterprets the transaction to detect a potentially malicious participant or bugs. + * Then, checks the causal monotonicity. + * + * @param rejections A helper object for creating rejection [[Step]]s. + * @return A committer [[Step]] that performs validation. + */ override def createValidationStep(rejections: Rejections): Step = new Step { def apply( commitContext: CommitContext, transactionEntry: DamlTransactionEntrySummary, )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = metrics.daml.kvutils.committer.transaction.interpretTimer.time(() => { - // Pull all keys from referenced contracts. We require this for 'fetchByKey' calls - // which are not evidenced in the transaction itself and hence the contract key state is - // not included in the inputs. - lazy val knownKeys: Map[DamlContractKey, Value.ContractId] = - commitContext.collectInputs { - case (key, Some(value)) - if value.getContractState.hasContractKey - && contractIsActive(transactionEntry, value.getContractState) => - value.getContractState.getContractKey -> Conversions - .stateKeyToContractId(key) - } + val validationResult = engine.validate( + transactionEntry.submitters.toSet, + SubmittedTransaction(transactionEntry.transaction), + transactionEntry.ledgerEffectiveTime, + commitContext.participantId, + transactionEntry.submissionTime, + transactionEntry.submissionSeed, + ) - try { - engine - .validate( - transactionEntry.submitters.toSet, - SubmittedTransaction(transactionEntry.transaction), - transactionEntry.ledgerEffectiveTime, - commitContext.participantId, - transactionEntry.submissionTime, - transactionEntry.submissionSeed, - ) - .consume( - lookupContract(transactionEntry, commitContext), - lookupPackage(commitContext), - lookupKey(commitContext, knownKeys), - ) - .fold( - err => - rejections.buildRejectionStep( - transactionEntry, - rejectionReasonForValidationError(err), - commitContext.recordTime, - ), - _ => StepContinue[DamlTransactionEntrySummary](transactionEntry), - ) - } catch { - case err: Err.MissingInputState => - logger.warn( - "Model conformance validation failed due to a missing input state (most likely due to invalid state on the participant)." - ) - rejections.buildRejectionStep( - transactionEntry, - RejectionReasonV0.Disputed(err.getMessage), - commitContext.recordTime, - ) - } + for { + stepResult <- consumeValidationResult( + validationResult, + transactionEntry, + commitContext, + rejections, + ) + finalStepResult <- validateCausalMonotonicity(stepResult, commitContext, rejections) + } yield finalStepResult }) } - private def contractIsActive( + private def consumeValidationResult( + validationResult: Result[Unit], transactionEntry: DamlTransactionEntrySummary, - contractState: DamlContractState, - ): Boolean = { - val activeAt = Option(contractState.getActiveAt).map(parseTimestamp) - !contractState.hasArchivedAt && activeAt.exists(transactionEntry.ledgerEffectiveTime >= _) + commitContext: CommitContext, + rejections: Rejections, + )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { + try { + val stepResult = for { + contractKeyInputs <- transactionEntry.transaction.contractKeyInputs.left + .map(rejectionForKeyInputError(transactionEntry, commitContext.recordTime, rejections)) + _ <- validationResult + .consume( + lookupContract(commitContext), + lookupPackage(commitContext), + lookupKey(contractKeyInputs), + ) + .left + .map(error => + rejections.buildRejectionStep( + transactionEntry, + rejectionReasonForValidationError(error), + commitContext.recordTime, + ) + ) + } yield () + stepResult.fold(identity, _ => StepContinue(transactionEntry)) + } catch { + case err: Err.MissingInputState => + logger.warn( + "Model conformance validation failed due to a missing input state (most likely due to invalid state on the participant)." + ) + rejections.buildRejectionStep( + transactionEntry, + RejectionReasonV0.Disputed(err.getMessage), + commitContext.recordTime, + ) + } } - // Helper to lookup contract instances. We verify the activeness of - // contract instances here. Since we look up every contract that was + // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. private def lookupContract( - transactionEntry: DamlTransactionEntrySummary, - commitContext: CommitContext, - )( - coid: Value.ContractId - ): Option[Value.ContractInst[Value.VersionedValue[Value.ContractId]]] = { + commitContext: CommitContext + )(coid: Value.ContractId): Option[Value.ContractInst[Value.VersionedValue[Value.ContractId]]] = { val stateKey = contractIdToStateKey(coid) - for { - // Fetch the state of the contract so that activeness can be checked. - // There is the possibility that the reinterpretation of the transaction yields a different - // result in a LookupByKey than the original transaction. This means that the contract state data for the - // contractId pointed to by that contractKey might not have been preloaded into the input state map. - // This is not a problem because after the transaction reinterpretation, we compare the original - // transaction with the reinterpreted one, and the LookupByKey node will not match. - // Additionally, all contract keys are checked to uphold causal monotonicity. - contractState <- commitContext.read(stateKey).map(_.getContractState) - if contractIsActive(transactionEntry, contractState) - contract = Conversions.decodeContractInstance(contractState.getContractInstance) - } yield contract + // There is the possibility that the reinterpretation of the transaction yields a different + // result in a LookupByKey than the original transaction. This means that the contract state data for the + // contractId pointed to by that contractKey might not have been preloaded into the input state map. + // This is not a problem because after the transaction reinterpretation, we compare the original + // transaction with the reinterpreted one, and the LookupByKey node will not match. + commitContext + .read(stateKey) + .map(_.getContractState) + .map(_.getContractInstance) + .map(Conversions.decodeContractInstance) } // Helper to lookup package from the state. The package contents @@ -171,36 +183,70 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me } private def lookupKey( - commitContext: CommitContext, - knownKeys: Map[DamlContractKey, Value.ContractId], + contractKeyInputs: Map[GlobalKey, KeyInput] )(key: GlobalKeyWithMaintainers): Option[Value.ContractId] = { - // we don't check whether the contract is active or not, because in we might not have loaded it earlier. - // this is not a problem, because: - // a) if the lookup was negative and we actually found a contract, - // the transaction validation will fail. - // b) if the lookup was positive and its result is a different contract, - // the transaction validation will fail. - // c) if the lookup was positive and its result is the same contract, - // - the authorization check ensures that the submitter is in fact allowed - // to lookup the contract - // - the separate contract keys check ensures that all contracts pointed to by - // contract keys respect causal monotonicity. - val stateKey = Conversions.globalKeyToStateKey(key.globalKey) - val contractId = for { - stateValue <- commitContext.read(stateKey) - if stateValue.getContractKeyState.getContractId.nonEmpty - } yield decodeContractId(stateValue.getContractKeyState.getContractId) + contractKeyInputs.get(key.globalKey) match { + case Some(KeyActive(cid)) => Some(cid) + case _ => None + } + } + + private[validation] def validateCausalMonotonicity( + transactionEntry: DamlTransactionEntrySummary, + commitContext: CommitContext, + rejections: Rejections, + )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - // If the key was not in state inputs, then we look whether any of the accessed contracts has - // the key we're looking for. This happens with "fetchByKey" where the key lookup is not - // evidenced in the transaction. The activeness of the contract is checked when it is fetched. - contractId.orElse { - knownKeys.get(stateKey.getContractKey) + val inputContracts: Map[Value.ContractId, DamlContractState] = commitContext + .collectInputs { + case (key, Some(value)) if value.hasContractState => + Conversions.stateKeyToContractId(key) -> value.getContractState + } + + val isCasualMonotonicityHeld = transactionEntry.transaction.inputContracts.forall { + contractId => + val inputContractState = inputContracts(contractId) + val activeAt = Option(inputContractState.getActiveAt).map(parseTimestamp) + activeAt.exists(transactionEntry.ledgerEffectiveTime >= _) } + + if (isCasualMonotonicityHeld) + StepContinue(transactionEntry) + else + rejections.buildRejectionStep( + transactionEntry, + RejectionReasonV0.InvalidLedgerTime("Causal monotonicity violated"), + commitContext.recordTime, + ) } } private[transaction] object ModelConformanceValidator { + + private def rejectionForKeyInputError( + transactionEntry: DamlTransactionEntrySummary, + recordTime: Option[Timestamp], + rejections: Rejections, + )( + error: KeyInputError + )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { + val rejectionReason = error match { + case DuplicateKeys(key) => + RejectionReasonV0.Disputed( + s"DuplicateKeys: the transaction contains a duplicate key: ${key.key}" + ) + case InconsistentKeys(key) => + RejectionReasonV0.Disputed( + s"InconsistentKeys: the transaction is internally inconsistent due to a contract with the key: ${key.key}" + ) + } + rejections.buildRejectionStep( + transactionEntry, + rejectionReason, + recordTime, + ) + } + def rejectionReasonForValidationError( validationError: LfError ): RejectionReasonV0 = { diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala similarity index 60% rename from ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidator.scala rename to ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala index a441ac8898a6..7e4d85d3ae8d 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala @@ -6,10 +6,10 @@ package com.daml.ledger.participant.state.kvutils.committer.transaction.validati import com.daml.ledger.participant.state.kvutils.Conversions import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ DamlContractKey, + DamlContractKeyState, + DamlContractState, DamlStateKey, - DamlStateValue, } -import com.daml.ledger.participant.state.kvutils.committer.transaction.validation.KeyMonotonicityValidation.checkContractKeysCausalMonotonicity import com.daml.ledger.participant.state.kvutils.committer.transaction.{ DamlTransactionEntrySummary, Rejections, @@ -17,7 +17,6 @@ import com.daml.ledger.participant.state.kvutils.committer.transaction.{ } import com.daml.ledger.participant.state.kvutils.committer.{CommitContext, StepContinue, StepResult} import com.daml.ledger.participant.state.v1.RejectionReasonV0 -import com.daml.lf.data.Time.Timestamp import com.daml.lf.transaction.Transaction.{ DuplicateKeys, InconsistentKeys, @@ -25,63 +24,56 @@ import com.daml.lf.transaction.Transaction.{ KeyCreate, NegativeKeyLookup, } +import com.daml.lf.value.Value import com.daml.logging.LoggingContext -private[transaction] object ContractKeysValidator extends TransactionValidator { +private[transaction] object TransactionConsistencyValidator extends TransactionValidator { - /** Creates a committer step that validates casual monotonicity and consistency of contract keys - * against the current ledger state. + /** Validates consistency of contracts and contract keys against the current ledger state. + * For contracts, checks whether all contracts used in the transaction are still active. + * + * @param rejections A helper object for creating rejection [[Step]]s. + * @return A committer [[Step]] that performs validation. */ override def createValidationStep(rejections: Rejections): Step = new Step { def apply( commitContext: CommitContext, transactionEntry: DamlTransactionEntrySummary, )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - val damlState = commitContext - .collectInputs[(DamlStateKey, DamlStateValue), Map[DamlStateKey, DamlStateValue]] { - case (key, Some(value)) if key.hasContractKey => key -> value - } - val contractKeyDamlStateKeysToContractIds: Map[DamlStateKey, RawContractId] = - damlState.collect { - case (k, v) if k.hasContractKey && v.getContractKeyState.getContractId.nonEmpty => - k -> v.getContractKeyState.getContractId - } - // State before the transaction - val contractKeyDamlStateKeys: Set[DamlStateKey] = - contractKeyDamlStateKeysToContractIds.keySet - val contractKeysToContractIds: Map[DamlContractKey, RawContractId] = - contractKeyDamlStateKeysToContractIds.map(m => m._1.getContractKey -> m._2) - for { - stateAfterMonotonicityCheck <- checkContractKeysCausalMonotonicity( - commitContext.recordTime, - contractKeyDamlStateKeys, - damlState, + stepResult <- validateConsistencyOfKeys( + commitContext, transactionEntry, rejections, ) - finalState <- performTraversalContractKeysChecks( - commitContext.recordTime, - contractKeysToContractIds, - stateAfterMonotonicityCheck, + finalStepResult <- validateConsistencyOfContracts( + commitContext, + stepResult, rejections, ) - } yield finalState + } yield finalStepResult } } - private def performTraversalContractKeysChecks( - recordTime: Option[Timestamp], - contractKeysToContractIds: Map[DamlContractKey, RawContractId], + private def validateConsistencyOfKeys( + commitContext: CommitContext, transactionEntry: DamlTransactionEntrySummary, rejections: Rejections, )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - import scalaz.std.either._ - import scalaz.std.list._ - import scalaz.syntax.foldable._ + + val contractKeyState: Map[DamlStateKey, DamlContractKeyState] = commitContext.collectInputs { + case (key, Some(value)) if key.hasContractKey => key -> value.getContractKeyState + } + val contractKeysToContractIds: Map[DamlContractKey, RawContractId] = contractKeyState.collect { + case (k, v) if v.getContractId.nonEmpty => + k.getContractKey -> v.getContractId + } val transaction = transactionEntry.transaction + import scalaz.std.either._ + import scalaz.std.list._ + import scalaz.syntax.foldable._ val keysValidationOutcome = for { keyInputs <- transaction.contractKeyInputs.left.map { case DuplicateKeys(_) => Duplicate @@ -116,11 +108,38 @@ private[transaction] object ContractKeysValidator extends TransactionValidator { rejections.buildRejectionStep( transactionEntry, RejectionReasonV0.Inconsistent(message), - recordTime, + commitContext.recordTime, ) } } + def validateConsistencyOfContracts( + commitContext: CommitContext, + transactionEntry: DamlTransactionEntrySummary, + rejections: Rejections, + )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { + val inputContracts: Map[Value.ContractId, DamlContractState] = commitContext + .collectInputs { + case (key, Some(value)) if value.hasContractState => + Conversions.stateKeyToContractId(key) -> value.getContractState + } + + val areContractsConsistent = transactionEntry.transaction.inputContracts.forall(contractId => + !inputContracts(contractId).hasArchivedAt + ) + + if (areContractsConsistent) + StepContinue(transactionEntry) + else + rejections.buildRejectionStep( + transactionEntry, + RejectionReasonV0.Inconsistent( + "InconsistentContracts: at least one contract has been archived since the submission" + ), + commitContext.recordTime, + ) + } + private[validation] type RawContractId = String private[validation] sealed trait KeyValidationError extends Product with Serializable diff --git a/ledger/participant-state/kvutils/src/test/lib/scala/com/daml/ledger/validator/TestHelper.scala b/ledger/participant-state/kvutils/src/test/lib/scala/com/daml/ledger/validator/TestHelper.scala index d4514ff56f3b..7961ada0525b 100644 --- a/ledger/participant-state/kvutils/src/test/lib/scala/com/daml/ledger/validator/TestHelper.scala +++ b/ledger/participant-state/kvutils/src/test/lib/scala/com/daml/ledger/validator/TestHelper.scala @@ -11,7 +11,7 @@ import com.daml.ledger.participant.state.v1.ParticipantId import com.daml.lf.value.ValueOuterClass.Identifier import com.google.protobuf.{ByteString, Empty} -private[validator] object TestHelper { +private[ledger] object TestHelper { lazy val aParticipantId: ParticipantId = ParticipantId.assertFromString("aParticipantId") diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidationSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidationSpec.scala deleted file mode 100644 index 211842b99471..000000000000 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/KeyMonotonicityValidationSpec.scala +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.daml.ledger.participant.state.kvutils.committer.transaction.validation - -import java.time.{Instant, ZoneOffset, ZonedDateTime} - -import com.daml.ledger.participant.state.kvutils.Conversions -import com.daml.ledger.participant.state.kvutils.DamlKvutils._ -import com.daml.ledger.participant.state.kvutils.committer.StepContinue -import com.daml.ledger.participant.state.kvutils.committer.transaction.{ - DamlTransactionEntrySummary, - Rejections, -} -import com.daml.ledger.participant.state.v1.RejectionReasonV0 -import com.daml.logging.LoggingContext -import com.google.protobuf.ByteString -import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AsyncWordSpec - -class KeyMonotonicityValidationSpec - extends AsyncWordSpec - with Matchers - with MockitoSugar - with ArgumentMatchersSugar { - private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting - - private val testKey = DamlStateKey.newBuilder().build() - private val testSubmissionSeed = ByteString.copyFromUtf8("a" * 32) - private val ledgerEffectiveTime = - ZonedDateTime.of(2021, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC).toInstant - private val testTransactionEntry = DamlTransactionEntrySummary( - DamlTransactionEntry.newBuilder - .setSubmissionSeed(testSubmissionSeed) - .setLedgerEffectiveTime(Conversions.buildTimestamp(ledgerEffectiveTime)) - .build - ) - - "checkContractKeysCausalMonotonicity" should { - "create StepContinue in case of correct keys" in { - KeyMonotonicityValidation.checkContractKeysCausalMonotonicity( - None, - Set(testKey), - Map(testKey -> aStateValueActiveAt(ledgerEffectiveTime.minusSeconds(1))), - testTransactionEntry, - mock[Rejections], - ) shouldBe StepContinue(testTransactionEntry) - } - - "reject transaction in case of incorrect keys" in { - val rejections = mock[Rejections] - - KeyMonotonicityValidation - .checkContractKeysCausalMonotonicity( - None, - Set(testKey), - Map(testKey -> aStateValueActiveAt(ledgerEffectiveTime.plusSeconds(1))), - testTransactionEntry, - rejections, - ) - - verify(rejections).buildRejectionStep( - eqTo(testTransactionEntry), - eqTo(RejectionReasonV0.InvalidLedgerTime("Causal monotonicity violated")), - eqTo(None), - )(eqTo(loggingContext)) - - succeed - } - } - - private def aStateValueActiveAt(activeAt: Instant) = - DamlStateValue.newBuilder - .setContractKeyState( - DamlContractKeyState.newBuilder.setActiveAt(Conversions.buildTimestamp(activeAt)) - ) - .build -} diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 505e6ba782da..d8e8f1b400d7 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -3,9 +3,25 @@ package com.daml.ledger.participant.state.kvutils.committer.transaction.validation -import com.daml.ledger.participant.state.kvutils.TestHelpers.lfTuple +import java.time.{Instant, ZoneOffset, ZonedDateTime} + +import com.codahale.metrics.MetricRegistry +import com.daml.ledger.participant.state.kvutils.Conversions +import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ + DamlContractState, + DamlStateKey, + DamlStateValue, + DamlTransactionEntry, +} +import com.daml.ledger.participant.state.kvutils.TestHelpers.{createCommitContext, lfTuple} +import com.daml.ledger.participant.state.kvutils.committer.StepContinue +import com.daml.ledger.participant.state.kvutils.committer.transaction.{ + DamlTransactionEntrySummary, + Rejections, +} import com.daml.ledger.participant.state.v1.{RejectionReason, RejectionReasonV0} -import com.daml.lf.engine.{Error => LfError} +import com.daml.ledger.validator.TestHelper.{makeContractIdStateKey, makeContractIdStateValue} +import com.daml.lf.engine.{Engine, Error => LfError} import com.daml.lf.transaction import com.daml.lf.transaction.test.TransactionBuilder import com.daml.lf.transaction.{ @@ -14,8 +30,13 @@ import com.daml.lf.transaction.{ ReplayNodeMismatch, ReplayedNodeMissing, Transaction, + TransactionVersion, } import com.daml.lf.value.Value +import com.daml.logging.LoggingContext +import com.daml.metrics.Metrics +import com.google.protobuf.ByteString +import org.mockito.ArgumentMatchersSugar.eqTo import org.mockito.MockitoSugar import org.scalatest.Inspectors.forEvery import org.scalatest.matchers.should.Matchers @@ -24,10 +45,18 @@ import org.scalatest.wordspec.AnyWordSpec class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with MockitoSugar { import ModelConformanceValidatorSpec._ - private val txBuilder = TransactionBuilder() + private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting + + private val metrics = new Metrics(new MetricRegistry) + private val modelConformanceValidator = new ModelConformanceValidator(Engine.DevEngine(), metrics) + + private val txBuilder = TransactionBuilder(TransactionVersion.VDev) - private val createInput = create("#inputContractId") - private val create1 = create("#someContractId") + private val inputContractId = "#inputContractId" + private val createInput = create(inputContractId) + private val contractId1 = "#someContractId" + private val contractKey1 = DamlStateKey.newBuilder().setContractId(contractId1).build() + private val create1 = create(contractId1) private val create2 = create("#otherContractId") private val otherKeyCreate = create( contractId = "#contractWithOtherKey", @@ -56,6 +85,56 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki builder.build() -> lookupId } + private val transactionEntry1 = DamlTransactionEntrySummary( + DamlTransactionEntry.newBuilder + .setSubmissionSeed(aSubmissionSeed) + .setLedgerEffectiveTime(Conversions.buildTimestamp(ledgerEffectiveTime)) + .setTransaction(Conversions.encodeTransaction(tx1._1)) + .build + ) + + "validateCausalMonotonicity" should { + "create StepContinue in case of correct input" in { + modelConformanceValidator + .validateCausalMonotonicity( + transactionEntry1, + createCommitContext( + None, + Map( + makeContractIdStateKey(inputContractId) -> Some(makeContractIdStateValue()), + contractKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.minusSeconds(1))), + ), + ), + mock[Rejections], + ) shouldBe StepContinue(transactionEntry1) + } + + "reject transaction in case of incorrect input" in { + val rejections = mock[Rejections] + + modelConformanceValidator + .validateCausalMonotonicity( + transactionEntry1, + createCommitContext( + None, + Map( + makeContractIdStateKey(inputContractId) -> Some(makeContractIdStateValue()), + contractKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.plusSeconds(1))), + ), + ), + rejections, + ) + + verify(rejections).buildRejectionStep( + eqTo(transactionEntry1), + eqTo(RejectionReasonV0.InvalidLedgerTime("Causal monotonicity violated")), + eqTo(None), + )(eqTo(loggingContext)) + + succeed + } + } + "rejectionReasonForValidationError" when { "there is a mismatch in lookupByKey nodes" should { "report an inconsistency if the contracts are not created in the same transaction" in { @@ -149,6 +228,17 @@ object ModelConformanceValidatorSpec { private val aKey = "key" private val aDummyValue = TransactionBuilder.record("field" -> "value") + private val aSubmissionSeed = ByteString.copyFromUtf8("a" * 32) + private val ledgerEffectiveTime = + ZonedDateTime.of(2021, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC).toInstant + + private def aStateValueActiveAt(activeAt: Instant) = + DamlStateValue.newBuilder + .setContractState( + DamlContractState.newBuilder.setActiveAt(Conversions.buildTimestamp(activeAt)) + ) + .build + private def mkMismatch( recorded: (Transaction.Transaction, NodeId), replayed: (Transaction.Transaction, NodeId), diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala similarity index 95% rename from ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidatorSpec.scala rename to ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala index 3be5e4e39ae4..d417e7b47e50 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ContractKeysValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala @@ -29,6 +29,7 @@ import com.daml.ledger.participant.state.kvutils.committer.{ StepResult, StepStop, } +import com.daml.ledger.validator.TestHelper.{makeContractIdStateKey, makeContractIdStateValue} import com.daml.lf.data.ImmArray import com.daml.lf.transaction.SubmittedTransaction import com.daml.lf.transaction.test.TransactionBuilder @@ -40,8 +41,8 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks.{forAll, _} import org.scalatest.wordspec.AnyWordSpec -class ContractKeysValidatorSpec extends AnyWordSpec with Matchers { - import ContractKeysValidatorSpec._ +class TransactionConsistencyValidatorSpec extends AnyWordSpec with Matchers { + import TransactionConsistencyValidatorSpec._ private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting @@ -121,7 +122,13 @@ class ContractKeysValidatorSpec extends AnyWordSpec with Matchers { "succeeds when a global contract gets archived before a local contract gets created" in { val globalCid = s"#$freshContractId" val globalCreate = newCreateNodeWithFixedKey(globalCid) - val context = commitContextWithContractStateKeys(conflictingKey -> Some(globalCid)) + val context = createCommitContext( + recordTime = None, + inputs = Map( + makeContractIdStateKey(globalCid) -> Some(makeContractIdStateValue()), + contractStateKey(conflictingKey) -> Some(contractKeyStateValue(globalCid)), + ), + ) val builder = TransactionBuilder() builder.add(archive(globalCreate, Set("Alice"))) builder.add(newCreateNodeWithFixedKey(s"#$freshContractId")) @@ -274,14 +281,14 @@ class ContractKeysValidatorSpec extends AnyWordSpec with Matchers { ctx: CommitContext, transaction: SubmittedTransaction, )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - ContractKeysValidator.createValidationStep(rejections)( + TransactionConsistencyValidator.createValidationStep(rejections)( ctx, DamlTransactionEntrySummary(createTransactionEntry(List("Alice"), transaction)), ) } } -object ContractKeysValidatorSpec { +object TransactionConsistencyValidatorSpec { private val aKeyMaintainer = "maintainer" private val aKey = "key" private val aDummyValue = TransactionBuilder.record("field" -> "value") From d8e5f4b5cffbd2fe20556f1255da96d8917de91e Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 14 Jul 2021 14:45:19 +0200 Subject: [PATCH 02/18] Add more unit tests --- .../ModelConformanceValidator.scala | 20 +- .../ModelConformanceValidatorSpec.scala | 245 +++++++++++++++--- .../TransactionConsistencyValidatorSpec.scala | 24 ++ 3 files changed, 239 insertions(+), 50 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 805512083424..a32f76e35d3f 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -131,7 +131,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. - private def lookupContract( + private[validation] def lookupContract( commitContext: CommitContext )(coid: Value.ContractId): Option[Value.ContractInst[Value.VersionedValue[Value.ContractId]]] = { val stateKey = contractIdToStateKey(coid) @@ -182,7 +182,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me } yield pkg } - private def lookupKey( + private[validation] def lookupKey( contractKeyInputs: Map[GlobalKey, KeyInput] )(key: GlobalKeyWithMaintainers): Option[Value.ContractId] = { contractKeyInputs.get(key.globalKey) match { @@ -230,19 +230,15 @@ private[transaction] object ModelConformanceValidator { )( error: KeyInputError )(implicit loggingContext: LoggingContext): StepResult[DamlTransactionEntrySummary] = { - val rejectionReason = error match { - case DuplicateKeys(key) => - RejectionReasonV0.Disputed( - s"DuplicateKeys: the transaction contains a duplicate key: ${key.key}" - ) - case InconsistentKeys(key) => - RejectionReasonV0.Disputed( - s"InconsistentKeys: the transaction is internally inconsistent due to a contract with the key: ${key.key}" - ) + val description = error match { + case DuplicateKeys(_) => + "DuplicateKeys: the transaction contains a duplicate key" + case InconsistentKeys(_) => + "InconsistentKeys: the transaction is internally inconsistent" } rejections.buildRejectionStep( transactionEntry, - rejectionReason, + RejectionReasonV0.Disputed(description), recordTime, ) } diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index d8e8f1b400d7..5f71ca5d7c2d 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -9,53 +9,69 @@ import com.codahale.metrics.MetricRegistry import com.daml.ledger.participant.state.kvutils.Conversions import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ DamlContractState, - DamlStateKey, + DamlLogEntry, DamlStateValue, + DamlSubmitterInfo, DamlTransactionEntry, + DamlTransactionRejectionEntry, + InvalidLedgerTime, } import com.daml.ledger.participant.state.kvutils.TestHelpers.{createCommitContext, lfTuple} -import com.daml.ledger.participant.state.kvutils.committer.StepContinue import com.daml.ledger.participant.state.kvutils.committer.transaction.{ DamlTransactionEntrySummary, Rejections, } +import com.daml.ledger.participant.state.kvutils.committer.{StepContinue, StepStop} import com.daml.ledger.participant.state.v1.{RejectionReason, RejectionReasonV0} import com.daml.ledger.validator.TestHelper.{makeContractIdStateKey, makeContractIdStateValue} -import com.daml.lf.engine.{Engine, Error => LfError} +import com.daml.lf.crypto.Hash +import com.daml.lf.data.Time.Timestamp +import com.daml.lf.data.{ImmArray, Ref} +import com.daml.lf.engine.{Engine, Result, ResultError, Error => LfError} +import com.daml.lf.language.Ast import com.daml.lf.transaction +import com.daml.lf.transaction.TransactionOuterClass.ContractInstance import com.daml.lf.transaction.test.TransactionBuilder import com.daml.lf.transaction.{ + GlobalKey, + GlobalKeyWithMaintainers, NodeId, RecordedNodeMissing, ReplayNodeMismatch, ReplayedNodeMissing, + SubmittedTransaction, Transaction, TransactionVersion, } -import com.daml.lf.value.Value +import com.daml.lf.value.Value.{ValueRecord, ValueText} +import com.daml.lf.value.{Value, ValueOuterClass} import com.daml.logging.LoggingContext import com.daml.metrics.Metrics import com.google.protobuf.ByteString -import org.mockito.ArgumentMatchersSugar.eqTo -import org.mockito.MockitoSugar +import org.mockito.{ArgumentMatchersSugar, MockitoSugar} import org.scalatest.Inspectors.forEvery import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with MockitoSugar { +class ModelConformanceValidatorSpec + extends AnyWordSpec + with Matchers + with MockitoSugar + with ArgumentMatchersSugar { import ModelConformanceValidatorSpec._ private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting private val metrics = new Metrics(new MetricRegistry) - private val modelConformanceValidator = new ModelConformanceValidator(Engine.DevEngine(), metrics) - private val txBuilder = TransactionBuilder(TransactionVersion.VDev) + private val mockEngine = mock[Engine] + private val modelConformanceValidator = new ModelConformanceValidator(mockEngine, metrics) + private val rejections = new Rejections(metrics) - private val inputContractId = "#inputContractId" - private val createInput = create(inputContractId) - private val contractId1 = "#someContractId" - private val contractKey1 = DamlStateKey.newBuilder().setContractId(contractId1).build() + private val createInput = create( + inputContractId, + keyAndMaintainer = Some(inputContractKey, inputContractKeyMaintainer), + ) private val create1 = create(contractId1) private val create2 = create("#otherContractId") private val otherKeyCreate = create( @@ -79,7 +95,7 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki } val Seq(tx1, tx2, txNone, txOther) = lookupNodes map { node => - val builder = TransactionBuilder() + val builder = txBuilder val rootId = builder.add(exercise) val lookupId = builder.add(node, rootId) builder.build() -> lookupId @@ -93,45 +109,147 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki .build ) - "validateCausalMonotonicity" should { + "createValidationStep" should { "create StepContinue in case of correct input" in { + val mockValidationResult = mock[Result[Unit]] + when( + mockValidationResult.consume( + any[Value.ContractId => Option[ + Value.ContractInst[Value.VersionedValue[Value.ContractId]] + ]], + any[Ref.PackageId => Option[Ast.Package]], + any[GlobalKeyWithMaintainers => Option[Value.ContractId]], + ) + ).thenReturn(Right(())) + when( + mockEngine.validate( + any[Set[Ref.Party]], + any[SubmittedTransaction], + any[Timestamp], + any[Ref.ParticipantId], + any[Timestamp], + any[Hash], + ) + ).thenReturn(mockValidationResult) + + modelConformanceValidator.createValidationStep(rejections)( + createCommitContext( + None, + Map( + inputContractIdStateKey -> Some(makeContractIdStateValue()), + contractIdStateKey1 -> Some(makeContractIdStateValue()), + ), + ), + transactionEntry1, + ) shouldBe StepContinue(transactionEntry1) + } + + "create StepStop in case of validation error" in { + when( + mockEngine.validate( + any[Set[Ref.Party]], + any[SubmittedTransaction], + any[Timestamp], + any[Ref.ParticipantId], + any[Timestamp], + any[Hash], + ) + ).thenReturn(ResultError(LfError.Validation.ReplayMismatch(mkMismatch(tx1, tx2)))) + + val step = modelConformanceValidator + .createValidationStep(rejections)( + createCommitContext( + None, + Map( + inputContractIdStateKey -> Some(makeContractIdStateValue()), + contractIdStateKey1 -> Some(makeContractIdStateValue()), + ), + ), + transactionEntry1, + ) + step shouldBe a[StepStop] + step + .asInstanceOf[StepStop] + .logEntry + .getTransactionRejectionEntry + .hasInconsistent shouldBe true + } + } + + "lookupContract" should { + "lookup contract" in { + modelConformanceValidator.lookupContract( + createCommitContext( + None, + Map( + inputContractIdStateKey -> Some( + aContractIdStateValue + ) + ), + ) + )(Conversions.decodeContractId(inputContractId)) shouldBe a[Some[_]] + } + } + + "lookupKey" should { + val contractKeyInputs = transactionEntry1.transaction.contractKeyInputs match { + case Left(_) => fail() + case Right(contractKeyInputs) => contractKeyInputs + } + + "return Some when mapping exists" in { + modelConformanceValidator.lookupKey(contractKeyInputs)( + aGlobalKeyWithMaintainers(inputContractKey, inputContractKeyMaintainer) + ) shouldBe Some(Conversions.decodeContractId(inputContractId)) + } + + "return None when mapping does not exist" in { + modelConformanceValidator.lookupKey(contractKeyInputs)( + aGlobalKeyWithMaintainers("nonexistentKey", "nonexistentMaintainer") + ) shouldBe None + } + } + + "validateCausalMonotonicity" should { + "create StepContinue when causal monotonicity is held" in { modelConformanceValidator .validateCausalMonotonicity( transactionEntry1, createCommitContext( None, Map( - makeContractIdStateKey(inputContractId) -> Some(makeContractIdStateValue()), - contractKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.minusSeconds(1))), + inputContractIdStateKey -> Some(makeContractIdStateValue()), + contractIdStateKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.minusSeconds(1))), ), ), - mock[Rejections], + rejections, ) shouldBe StepContinue(transactionEntry1) } - "reject transaction in case of incorrect input" in { - val rejections = mock[Rejections] - - modelConformanceValidator + "reject transaction when causal monotonicity is not held" in { + val step = modelConformanceValidator .validateCausalMonotonicity( transactionEntry1, createCommitContext( None, Map( - makeContractIdStateKey(inputContractId) -> Some(makeContractIdStateValue()), - contractKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.plusSeconds(1))), + inputContractIdStateKey -> Some(makeContractIdStateValue()), + contractIdStateKey1 -> Some(aStateValueActiveAt(ledgerEffectiveTime.plusSeconds(1))), ), ), rejections, ) - verify(rejections).buildRejectionStep( - eqTo(transactionEntry1), - eqTo(RejectionReasonV0.InvalidLedgerTime("Causal monotonicity violated")), - eqTo(None), - )(eqTo(loggingContext)) - - succeed + val expectedEntry = DamlLogEntry.newBuilder + .setTransactionRejectionEntry( + DamlTransactionRejectionEntry.newBuilder + .setSubmitterInfo(DamlSubmitterInfo.getDefaultInstance) + .setInvalidLedgerTime( + InvalidLedgerTime.newBuilder.setDetails("Causal monotonicity violated") + ) + ) + .build() + step shouldBe StepStop(expectedEntry) } } @@ -148,14 +266,14 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki "report Disputed if one of contracts is created in the same transaction" in { val Seq(txC1, txC2, txCNone) = Seq(lookup1, lookup2, lookupNone) map { node => - val builder = TransactionBuilder() + val builder = txBuilder val rootId = builder.add(exercise) builder.add(create1, rootId) val lookupId = builder.add(node, rootId) builder.build() -> lookupId } val Seq(tx1C, txNoneC) = Seq(lookup1, lookupNone) map { node => - val builder = TransactionBuilder() + val builder = txBuilder val rootId = builder.add(exercise) val lookupId = builder.add(node, rootId) builder.add(create1) @@ -178,12 +296,12 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki "the mismatch is not between two lookup nodes" should { "report Disputed" in { val txExerciseOnly = { - val builder = TransactionBuilder() + val builder = txBuilder builder.add(exercise) builder.build() } val txCreate = { - val builder = TransactionBuilder() + val builder = txBuilder val rootId = builder.add(exercise) val createId = builder.add(create1, rootId) builder.build() -> createId @@ -203,15 +321,16 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki signatories: Seq[String] = Seq(aKeyMaintainer), argument: TransactionBuilder.Value = aDummyValue, keyAndMaintainer: Option[(String, String)] = Some(aKey -> aKeyMaintainer), - ): TransactionBuilder.Create = + ): TransactionBuilder.Create = { txBuilder.create( id = contractId, - template = "dummyPackage:DummyModule:DummyTemplate", + template = aTemplateId, argument = argument, signatories = signatories, observers = Seq.empty, key = keyAndMaintainer.map { case (key, maintainer) => lfTuple(maintainer, key) }, ) + } private def checkRejectionReason( mkReason: String => RejectionReason @@ -224,14 +343,64 @@ class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with Mocki } object ModelConformanceValidatorSpec { - private val aKeyMaintainer = "maintainer" + private val inputContractId = "#inputContractId" + private val inputContractIdStateKey = makeContractIdStateKey(inputContractId) + private val contractId1 = "#someContractId" + private val contractIdStateKey1 = makeContractIdStateKey(contractId1) + private val inputContractKey = "inputContractKey" + private val inputContractKeyMaintainer = "inputContractKeyMaintainer" private val aKey = "key" + private val aKeyMaintainer = "maintainer" private val aDummyValue = TransactionBuilder.record("field" -> "value") + private val aTemplateId = "dummyPackage:DummyModule:DummyTemplate" private val aSubmissionSeed = ByteString.copyFromUtf8("a" * 32) private val ledgerEffectiveTime = ZonedDateTime.of(2021, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC).toInstant + private val aContractIdStateValue = { + makeContractIdStateValue().toBuilder + .setContractState( + DamlContractState + .newBuilder() + .setContractInstance( + ContractInstance + .newBuilder() + .setTemplateId( + ValueOuterClass.Identifier + .newBuilder() + .setPackageId("dummyPackage") + .addModuleName("dummyModule") + .addName("dummyName") + ) + .setArgVersioned( + ValueOuterClass.VersionedValue + .newBuilder() + .setVersion(TransactionVersion.VDev.protoValue) + .setValue(ValueOuterClass.Value.newBuilder().setText("dummyValue")) + ) + ) + .build() + ) + .build() + } + + private def txBuilder = TransactionBuilder(TransactionVersion.VDev) + + private def aGlobalKeyWithMaintainers(key: String, maintainer: String) = GlobalKeyWithMaintainers( + GlobalKey.assertBuild( + Ref.TypeConName.assertFromString(aTemplateId), + ValueRecord( + None, + ImmArray( + (None, ValueText(maintainer)), + (None, ValueText(key)), + ), + ), + ), + Set.empty, + ) + private def aStateValueActiveAt(activeAt: Instant) = DamlStateValue.newBuilder .setContractState( diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala index d417e7b47e50..803f9ba07bd5 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala @@ -10,6 +10,7 @@ import com.daml.ledger.participant.state.kvutils.Conversions import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ DamlContractKey, DamlContractKeyState, + DamlContractState, DamlStateKey, DamlStateValue, } @@ -37,6 +38,7 @@ import com.daml.lf.transaction.test.TransactionBuilder.{Create, Exercise} import com.daml.lf.value.Value import com.daml.logging.LoggingContext import com.daml.metrics.Metrics +import com.google.protobuf.Timestamp import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks.{forAll, _} import org.scalatest.wordspec.AnyWordSpec @@ -228,6 +230,28 @@ class TransactionConsistencyValidatorSpec extends AnyWordSpec with Matchers { getTransactionRejectionReason(result).getInconsistent.getDetails rejectionReason should startWith("InconsistentKeys") } + + "fail if a contract is not active anymore" in { + val globalCid = s"#$freshContractId" + val globalCreate = newCreateNodeWithFixedKey(globalCid) + val context = createCommitContext( + recordTime = None, + inputs = Map( + makeContractIdStateKey(globalCid) -> Some( + makeContractIdStateValue().toBuilder + .setContractState( + DamlContractState.newBuilder().setArchivedAt(Timestamp.getDefaultInstance) + ) + .build() + ) + ), + ) + val builder = TransactionBuilder() + builder.add(archive(globalCreate, Set("Alice"))) + val transaction = builder.buildSubmitted() + val result = validate(context, transaction) + result shouldBe a[StepStop] + } } private def newLookupByKeySubmittedTransaction( From ac1c52f38ff6ceb682323a09b29b0d62933ca376 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 14 Jul 2021 15:11:03 +0200 Subject: [PATCH 03/18] Fix a CI build error --- .../transaction/validation/ModelConformanceValidatorSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 5f71ca5d7c2d..2abd343387b8 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -70,7 +70,7 @@ class ModelConformanceValidatorSpec private val createInput = create( inputContractId, - keyAndMaintainer = Some(inputContractKey, inputContractKeyMaintainer), + keyAndMaintainer = Some(inputContractKey -> inputContractKeyMaintainer), ) private val create1 = create(contractId1) private val create2 = create("#otherContractId") From 7c0230869c2e23f2e6418cd5c8b728a434806a40 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Thu, 15 Jul 2021 17:51:33 +0200 Subject: [PATCH 04/18] Remove a comment and simplify a function --- .../validation/ModelConformanceValidator.scala | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index a32f76e35d3f..073d6d907445 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -133,19 +133,14 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // an input to a transaction, we do not need to verify the inputs separately. private[validation] def lookupContract( commitContext: CommitContext - )(coid: Value.ContractId): Option[Value.ContractInst[Value.VersionedValue[Value.ContractId]]] = { - val stateKey = contractIdToStateKey(coid) - // There is the possibility that the reinterpretation of the transaction yields a different - // result in a LookupByKey than the original transaction. This means that the contract state data for the - // contractId pointed to by that contractKey might not have been preloaded into the input state map. - // This is not a problem because after the transaction reinterpretation, we compare the original - // transaction with the reinterpreted one, and the LookupByKey node will not match. + )( + contractId: Value.ContractId + ): Option[Value.ContractInst[Value.VersionedValue[Value.ContractId]]] = commitContext - .read(stateKey) + .read(contractIdToStateKey(contractId)) .map(_.getContractState) .map(_.getContractInstance) .map(Conversions.decodeContractInstance) - } // Helper to lookup package from the state. The package contents // are stored in the [[DamlLogEntry]], which we find by looking up From 9a02f87dd5fe2b3631d8af58c7f581cf07128615 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Thu, 15 Jul 2021 18:33:41 +0200 Subject: [PATCH 05/18] Remove a function, add and simplify tests --- .../ModelConformanceValidator.scala | 70 +------- .../ModelConformanceValidatorSpec.scala | 161 +++++------------- 2 files changed, 42 insertions(+), 189 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 073d6d907445..526cb8c83fcb 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -20,7 +20,7 @@ import com.daml.ledger.participant.state.v1.RejectionReasonV0 import com.daml.lf.archive import com.daml.lf.data.Ref.PackageId import com.daml.lf.data.Time.Timestamp -import com.daml.lf.engine.{Engine, Result, Error => LfError} +import com.daml.lf.engine.{Engine, Result} import com.daml.lf.language.Ast import com.daml.lf.transaction.Transaction.{ DuplicateKeys, @@ -29,17 +29,8 @@ import com.daml.lf.transaction.Transaction.{ KeyInput, KeyInputError, } -import com.daml.lf.transaction.{ - GlobalKey, - GlobalKeyWithMaintainers, - Node, - NodeId, - ReplayNodeMismatch, - SubmittedTransaction, - VersionedTransaction, -} +import com.daml.lf.transaction.{GlobalKey, GlobalKeyWithMaintainers, SubmittedTransaction} import com.daml.lf.value.Value -import com.daml.lf.value.Value.ContractId import com.daml.logging.LoggingContext.withEnrichedLoggingContext import com.daml.logging.{ContextualizedLogger, LoggingContext} import com.daml.metrics.Metrics @@ -110,7 +101,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me .map(error => rejections.buildRejectionStep( transactionEntry, - rejectionReasonForValidationError(error), + RejectionReasonV0.Disputed(error.msg), commitContext.recordTime, ) ) @@ -131,6 +122,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. + @throws[Err.MissingInputState] private[validation] def lookupContract( commitContext: CommitContext )( @@ -237,58 +229,4 @@ private[transaction] object ModelConformanceValidator { recordTime, ) } - - def rejectionReasonForValidationError( - validationError: LfError - ): RejectionReasonV0 = { - def disputed: RejectionReasonV0 = - RejectionReasonV0.Disputed(validationError.msg) - - def resultIsCreatedInTx( - tx: VersionedTransaction[NodeId, ContractId], - result: Option[Value.ContractId], - ): Boolean = - result.exists { contractId => - tx.nodes.exists { - case (_, create: Node.NodeCreate[_]) => create.coid == contractId - case _ => false - } - } - - validationError match { - case LfError.Validation( - LfError.Validation.ReplayMismatch( - ReplayNodeMismatch(recordedTx, recordedNodeId, replayedTx, replayedNodeId) - ) - ) => - // If the problem is that a key lookup has changed and the results do not involve contracts created in this transaction, - // then it's a consistency problem. - - (recordedTx.nodes(recordedNodeId), replayedTx.nodes(replayedNodeId)) match { - case ( - Node.NodeLookupByKey( - recordedTemplateId, - _, - recordedKey, - recordedResult, - recordedVersion, - ), - Node.NodeLookupByKey( - replayedTemplateId, - _, - replayedKey, - replayedResult, - replayedVersion, - ), - ) - if recordedVersion == replayedVersion && - recordedTemplateId == replayedTemplateId && recordedKey == replayedKey - && !resultIsCreatedInTx(recordedTx, recordedResult) - && !resultIsCreatedInTx(replayedTx, replayedResult) => - RejectionReasonV0.Inconsistent(validationError.msg) - case _ => disputed - } - case _ => disputed - } - } } diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 2abd343387b8..354ffe46531d 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -6,7 +6,6 @@ package com.daml.ledger.participant.state.kvutils.committer.transaction.validati import java.time.{Instant, ZoneOffset, ZonedDateTime} import com.codahale.metrics.MetricRegistry -import com.daml.ledger.participant.state.kvutils.Conversions import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ DamlContractState, DamlLogEntry, @@ -22,25 +21,20 @@ import com.daml.ledger.participant.state.kvutils.committer.transaction.{ Rejections, } import com.daml.ledger.participant.state.kvutils.committer.{StepContinue, StepStop} -import com.daml.ledger.participant.state.v1.{RejectionReason, RejectionReasonV0} +import com.daml.ledger.participant.state.kvutils.{Conversions, Err} import com.daml.ledger.validator.TestHelper.{makeContractIdStateKey, makeContractIdStateValue} import com.daml.lf.crypto.Hash import com.daml.lf.data.Time.Timestamp import com.daml.lf.data.{ImmArray, Ref} import com.daml.lf.engine.{Engine, Result, ResultError, Error => LfError} import com.daml.lf.language.Ast -import com.daml.lf.transaction import com.daml.lf.transaction.TransactionOuterClass.ContractInstance import com.daml.lf.transaction.test.TransactionBuilder import com.daml.lf.transaction.{ GlobalKey, GlobalKeyWithMaintainers, - NodeId, - RecordedNodeMissing, - ReplayNodeMismatch, ReplayedNodeMissing, SubmittedTransaction, - Transaction, TransactionVersion, } import com.daml.lf.value.Value.{ValueRecord, ValueText} @@ -49,7 +43,6 @@ import com.daml.logging.LoggingContext import com.daml.metrics.Metrics import com.google.protobuf.ByteString import org.mockito.{ArgumentMatchersSugar, MockitoSugar} -import org.scalatest.Inspectors.forEvery import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -68,20 +61,15 @@ class ModelConformanceValidatorSpec private val modelConformanceValidator = new ModelConformanceValidator(mockEngine, metrics) private val rejections = new Rejections(metrics) - private val createInput = create( + private val inputCreate = create( inputContractId, keyAndMaintainer = Some(inputContractKey -> inputContractKeyMaintainer), ) - private val create1 = create(contractId1) - private val create2 = create("#otherContractId") - private val otherKeyCreate = create( - contractId = "#contractWithOtherKey", - signatories = Seq(aKeyMaintainer), - keyAndMaintainer = Some("otherKey" -> aKeyMaintainer), - ) + private val aCreate = create(aContractId) + private val anotherCreate = create("#anotherContractId") private val exercise = txBuilder.exercise( - contract = createInput, + contract = inputCreate, choice = "DummyChoice", consuming = false, actingParties = Set(aKeyMaintainer), @@ -89,23 +77,23 @@ class ModelConformanceValidatorSpec byKey = false, ) - val lookupNodes @ Seq(lookup1, lookup2, lookupNone, lookupOther @ _) = - Seq(create1 -> true, create2 -> true, create1 -> false, otherKeyCreate -> true) map { - case (create, found) => txBuilder.lookupByKey(create, found) + private val lookupNodes = + Seq(aCreate -> true, anotherCreate -> true) map { case (create, found) => + txBuilder.lookupByKey(create, found) } - val Seq(tx1, tx2, txNone, txOther) = lookupNodes map { node => + val Seq(aTransaction, anotherTransaction) = lookupNodes map { node => val builder = txBuilder val rootId = builder.add(exercise) val lookupId = builder.add(node, rootId) builder.build() -> lookupId } - private val transactionEntry1 = DamlTransactionEntrySummary( + private val aTransactionEntry = DamlTransactionEntrySummary( DamlTransactionEntry.newBuilder .setSubmissionSeed(aSubmissionSeed) .setLedgerEffectiveTime(Conversions.buildTimestamp(ledgerEffectiveTime)) - .setTransaction(Conversions.encodeTransaction(tx1._1)) + .setTransaction(Conversions.encodeTransaction(aTransaction._1)) .build ) @@ -140,8 +128,8 @@ class ModelConformanceValidatorSpec contractIdStateKey1 -> Some(makeContractIdStateValue()), ), ), - transactionEntry1, - ) shouldBe StepContinue(transactionEntry1) + aTransactionEntry, + ) shouldBe StepContinue(aTransactionEntry) } "create StepStop in case of validation error" in { @@ -154,7 +142,13 @@ class ModelConformanceValidatorSpec any[Timestamp], any[Hash], ) - ).thenReturn(ResultError(LfError.Validation.ReplayMismatch(mkMismatch(tx1, tx2)))) + ).thenReturn( + ResultError( + LfError.Validation.ReplayMismatch( + ReplayedNodeMissing(aTransaction._1, aTransaction._2, anotherTransaction._1) + ) + ) + ) val step = modelConformanceValidator .createValidationStep(rejections)( @@ -165,19 +159,19 @@ class ModelConformanceValidatorSpec contractIdStateKey1 -> Some(makeContractIdStateValue()), ), ), - transactionEntry1, + aTransactionEntry, ) step shouldBe a[StepStop] step .asInstanceOf[StepStop] .logEntry .getTransactionRejectionEntry - .hasInconsistent shouldBe true + .hasDisputed shouldBe true } } "lookupContract" should { - "lookup contract" in { + "return Some when a contract is present in the current state" in { modelConformanceValidator.lookupContract( createCommitContext( None, @@ -189,10 +183,19 @@ class ModelConformanceValidatorSpec ) )(Conversions.decodeContractId(inputContractId)) shouldBe a[Some[_]] } + + "throw if a contract does not exist in the current state" in { + a[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupContract( + createCommitContext( + None, + Map.empty, + ) + )(Conversions.decodeContractId(inputContractId)) + } } "lookupKey" should { - val contractKeyInputs = transactionEntry1.transaction.contractKeyInputs match { + val contractKeyInputs = aTransactionEntry.transaction.contractKeyInputs match { case Left(_) => fail() case Right(contractKeyInputs) => contractKeyInputs } @@ -214,7 +217,7 @@ class ModelConformanceValidatorSpec "create StepContinue when causal monotonicity is held" in { modelConformanceValidator .validateCausalMonotonicity( - transactionEntry1, + aTransactionEntry, createCommitContext( None, Map( @@ -223,13 +226,13 @@ class ModelConformanceValidatorSpec ), ), rejections, - ) shouldBe StepContinue(transactionEntry1) + ) shouldBe StepContinue(aTransactionEntry) } "reject transaction when causal monotonicity is not held" in { val step = modelConformanceValidator .validateCausalMonotonicity( - transactionEntry1, + aTransactionEntry, createCommitContext( None, Map( @@ -253,69 +256,6 @@ class ModelConformanceValidatorSpec } } - "rejectionReasonForValidationError" when { - "there is a mismatch in lookupByKey nodes" should { - "report an inconsistency if the contracts are not created in the same transaction" in { - val inconsistentLookups = Seq( - mkMismatch(tx1, tx2), - mkMismatch(tx1, txNone), - mkMismatch(txNone, tx2), - ) - forEvery(inconsistentLookups)(checkRejectionReason(RejectionReasonV0.Inconsistent)) - } - - "report Disputed if one of contracts is created in the same transaction" in { - val Seq(txC1, txC2, txCNone) = Seq(lookup1, lookup2, lookupNone) map { node => - val builder = txBuilder - val rootId = builder.add(exercise) - builder.add(create1, rootId) - val lookupId = builder.add(node, rootId) - builder.build() -> lookupId - } - val Seq(tx1C, txNoneC) = Seq(lookup1, lookupNone) map { node => - val builder = txBuilder - val rootId = builder.add(exercise) - val lookupId = builder.add(node, rootId) - builder.add(create1) - builder.build() -> lookupId - } - val recordedKeyInconsistent = Seq( - mkMismatch(txC2, txC1), - mkMismatch(txCNone, txC1), - mkMismatch(txC1, txCNone), - mkMismatch(tx1C, txNoneC), - ) - forEvery(recordedKeyInconsistent)(checkRejectionReason(RejectionReasonV0.Disputed)) - } - - "report Disputed if the keys are different" in { - checkRejectionReason(RejectionReasonV0.Disputed)(mkMismatch(txOther, tx1)) - } - } - - "the mismatch is not between two lookup nodes" should { - "report Disputed" in { - val txExerciseOnly = { - val builder = txBuilder - builder.add(exercise) - builder.build() - } - val txCreate = { - val builder = txBuilder - val rootId = builder.add(exercise) - val createId = builder.add(create1, rootId) - builder.build() -> createId - } - val miscMismatches = Seq( - mkMismatch(txCreate, tx1), - mkRecordedMissing(txExerciseOnly, tx2), - mkReplayedMissing(tx1, txExerciseOnly), - ) - forEvery(miscMismatches)(checkRejectionReason(RejectionReasonV0.Disputed)) - } - } - } - private def create( contractId: String, signatories: Seq[String] = Seq(aKeyMaintainer), @@ -331,22 +271,13 @@ class ModelConformanceValidatorSpec key = keyAndMaintainer.map { case (key, maintainer) => lfTuple(maintainer, key) }, ) } - - private def checkRejectionReason( - mkReason: String => RejectionReason - )(mismatch: transaction.ReplayMismatch[NodeId, Value.ContractId]) = { - val replayMismatch = LfError.Validation(LfError.Validation.ReplayMismatch(mismatch)) - ModelConformanceValidator.rejectionReasonForValidationError(replayMismatch) shouldBe mkReason( - replayMismatch.msg - ) - } } object ModelConformanceValidatorSpec { private val inputContractId = "#inputContractId" private val inputContractIdStateKey = makeContractIdStateKey(inputContractId) - private val contractId1 = "#someContractId" - private val contractIdStateKey1 = makeContractIdStateKey(contractId1) + private val aContractId = "#someContractId" + private val contractIdStateKey1 = makeContractIdStateKey(aContractId) private val inputContractKey = "inputContractKey" private val inputContractKeyMaintainer = "inputContractKeyMaintainer" private val aKey = "key" @@ -407,20 +338,4 @@ object ModelConformanceValidatorSpec { DamlContractState.newBuilder.setActiveAt(Conversions.buildTimestamp(activeAt)) ) .build - - private def mkMismatch( - recorded: (Transaction.Transaction, NodeId), - replayed: (Transaction.Transaction, NodeId), - ): ReplayNodeMismatch[NodeId, Value.ContractId] = - ReplayNodeMismatch(recorded._1, recorded._2, replayed._1, replayed._2) - private def mkRecordedMissing( - recorded: Transaction.Transaction, - replayed: (Transaction.Transaction, NodeId), - ): RecordedNodeMissing[NodeId, Value.ContractId] = - RecordedNodeMissing(recorded, replayed._1, replayed._2) - private def mkReplayedMissing( - recorded: (Transaction.Transaction, NodeId), - replayed: Transaction.Transaction, - ): ReplayedNodeMissing[NodeId, Value.ContractId] = - ReplayedNodeMissing(recorded._1, recorded._2, replayed) } From 092baf753722d41ded51ec9c77c046b6f877e686 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Thu, 15 Jul 2021 18:39:49 +0200 Subject: [PATCH 06/18] Remove braces --- .../transaction/validation/ModelConformanceValidator.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 526cb8c83fcb..149ac122aab5 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -171,12 +171,11 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me private[validation] def lookupKey( contractKeyInputs: Map[GlobalKey, KeyInput] - )(key: GlobalKeyWithMaintainers): Option[Value.ContractId] = { + )(key: GlobalKeyWithMaintainers): Option[Value.ContractId] = contractKeyInputs.get(key.globalKey) match { case Some(KeyActive(cid)) => Some(cid) case _ => None } - } private[validation] def validateCausalMonotonicity( transactionEntry: DamlTransactionEntrySummary, From 04bbe3e7ba534ac22a6360c9597cd1f4b76086bb Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Thu, 15 Jul 2021 18:59:11 +0200 Subject: [PATCH 07/18] Rename variables ands tests, improve tests --- .../validation/ModelConformanceValidator.scala | 12 ++++++------ .../ModelConformanceValidatorSpec.scala | 16 +++++++++++----- .../TransactionConsistencyValidatorSpec.scala | 5 +++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 149ac122aab5..28eff9db8580 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -177,6 +177,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me case _ => None } + // Checks that input contracts are still active at ledger effective time. private[validation] def validateCausalMonotonicity( transactionEntry: DamlTransactionEntrySummary, commitContext: CommitContext, @@ -189,14 +190,13 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me Conversions.stateKeyToContractId(key) -> value.getContractState } - val isCasualMonotonicityHeld = transactionEntry.transaction.inputContracts.forall { - contractId => - val inputContractState = inputContracts(contractId) - val activeAt = Option(inputContractState.getActiveAt).map(parseTimestamp) - activeAt.exists(transactionEntry.ledgerEffectiveTime >= _) + val isCasuallyMonotonic = transactionEntry.transaction.inputContracts.forall { contractId => + val inputContractState = inputContracts(contractId) + val activeAt = Option(inputContractState.getActiveAt).map(parseTimestamp) + activeAt.exists(transactionEntry.ledgerEffectiveTime >= _) } - if (isCasualMonotonicityHeld) + if (isCasuallyMonotonic) StepContinue(transactionEntry) else rejections.buildRejectionStep( diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 354ffe46531d..3b07ebaf7df0 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -181,7 +181,7 @@ class ModelConformanceValidatorSpec ) ), ) - )(Conversions.decodeContractId(inputContractId)) shouldBe a[Some[_]] + )(Conversions.decodeContractId(inputContractId)) shouldBe Some(aContractInst) } "throw if a contract does not exist in the current state" in { @@ -214,7 +214,7 @@ class ModelConformanceValidatorSpec } "validateCausalMonotonicity" should { - "create StepContinue when causal monotonicity is held" in { + "create StepContinue when causal monotonicity holds" in { modelConformanceValidator .validateCausalMonotonicity( aTransactionEntry, @@ -229,7 +229,7 @@ class ModelConformanceValidatorSpec ) shouldBe StepContinue(aTransactionEntry) } - "reject transaction when causal monotonicity is not held" in { + "reject transaction when causal monotonicity does not hold" in { val step = modelConformanceValidator .validateCausalMonotonicity( aTransactionEntry, @@ -301,8 +301,8 @@ object ModelConformanceValidatorSpec { ValueOuterClass.Identifier .newBuilder() .setPackageId("dummyPackage") - .addModuleName("dummyModule") - .addName("dummyName") + .addModuleName("DummyModule") + .addName("DummyTemplate") ) .setArgVersioned( ValueOuterClass.VersionedValue @@ -316,6 +316,12 @@ object ModelConformanceValidatorSpec { .build() } + private val aContractInst = Value.ContractInst( + Ref.TypeConName.assertFromString(aTemplateId), + Value.VersionedValue(TransactionVersion.VDev, ValueText("dummyValue")), + "", + ) + private def txBuilder = TransactionBuilder(TransactionVersion.VDev) private def aGlobalKeyWithMaintainers(key: String, maintainer: String) = GlobalKeyWithMaintainers( diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala index 803f9ba07bd5..7317450d7647 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala @@ -251,6 +251,11 @@ class TransactionConsistencyValidatorSpec extends AnyWordSpec with Matchers { val transaction = builder.buildSubmitted() val result = validate(context, transaction) result shouldBe a[StepStop] + result + .asInstanceOf[StepStop] + .logEntry + .getTransactionRejectionEntry + .hasInconsistent shouldBe true } } From 15974ba67c87e1ba1cd5c0150320a301f184f810 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Mon, 19 Jul 2021 12:38:31 +0200 Subject: [PATCH 08/18] Change the rejection reason --- .../transaction/validation/ModelConformanceValidator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 28eff9db8580..bd5496c2aab4 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -114,7 +114,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me ) rejections.buildRejectionStep( transactionEntry, - RejectionReasonV0.Disputed(err.getMessage), + RejectionReasonV0.Inconsistent(err.getMessage), commitContext.recordTime, ) } From add643ee33574665787e7f47e43d30c7a2b5f6dd Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Mon, 19 Jul 2021 14:07:48 +0200 Subject: [PATCH 09/18] Add a comment --- .../transaction/validation/ModelConformanceValidator.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index bd5496c2aab4..56eb95bcf872 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -122,6 +122,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. + // A contract may not be in the state only if it's an edge case around committer pruning. @throws[Err.MissingInputState] private[validation] def lookupContract( commitContext: CommitContext From bd21b3fc4b6dda143aa8c1ad93028d06228ade1f Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Mon, 19 Jul 2021 16:25:02 +0200 Subject: [PATCH 10/18] Add more comments --- .../transaction/validation/ModelConformanceValidator.scala | 6 +++++- .../validation/TransactionConsistencyValidator.scala | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 56eb95bcf872..dae05a8dbe82 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -122,7 +122,11 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. - // A contract may not be in the state only if it's an edge case around committer pruning. + // + // Note that a contract may not be in the state only if it was archived and pruned on the committer. + // Then, the participant is able to produce such a transaction only by using a divulged contract that + // appeared as active, because it didn't learn about the archival. On the other hand, using divulged + // contracts for interpretation is deprecated. @throws[Err.MissingInputState] private[validation] def lookupContract( commitContext: CommitContext diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala index 7e4d85d3ae8d..cc5400069da1 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidator.scala @@ -31,6 +31,7 @@ private[transaction] object TransactionConsistencyValidator extends TransactionV /** Validates consistency of contracts and contract keys against the current ledger state. * For contracts, checks whether all contracts used in the transaction are still active. + * For keys, checks whether they are consistent and there are no duplicates. * * @param rejections A helper object for creating rejection [[Step]]s. * @return A committer [[Step]] that performs validation. From 4f3c4d6bbdc08a11507c8283f868184f193e18ed Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Tue, 20 Jul 2021 19:08:26 +0200 Subject: [PATCH 11/18] Change a catch clause and add more tests --- .../ModelConformanceValidator.scala | 11 +- .../ModelConformanceValidatorSpec.scala | 147 +++++++++++++++--- 2 files changed, 135 insertions(+), 23 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index e5baa0aa2d44..c2b7bc9a4b63 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -108,9 +108,10 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me } yield () stepResult.fold(identity, _ => StepContinue(transactionEntry)) } catch { - case err: Err.MissingInputState => - logger.warn( - "Model conformance validation failed due to a missing input state (most likely due to invalid state on the participant)." + case err: Err => + logger.error( + "Model conformance validation failed most likely due to invalid state on the participant.", + err, ) rejections.buildRejectionStep( transactionEntry, @@ -142,7 +143,9 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup package from the state. The package contents // are stored in the [[DamlLogEntry]], which we find by looking up // the Daml state entry at `DamlStateKey(packageId = pkgId)`. - private def lookupPackage( + @throws[Err.MissingInputState] + @throws[Err.DecodeError] + private[validation] def lookupPackage( commitContext: CommitContext )(pkgId: PackageId)(implicit loggingContext: LoggingContext): Option[Ast.Package] = withEnrichedLoggingContext("packageId" -> pkgId) { implicit loggingContext => diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 3b07ebaf7df0..0d89fcf36da9 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -6,9 +6,11 @@ package com.daml.ledger.participant.state.kvutils.committer.transaction.validati import java.time.{Instant, ZoneOffset, ZonedDateTime} import com.codahale.metrics.MetricRegistry +import com.daml.daml_lf_dev.DamlLf import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ DamlContractState, DamlLogEntry, + DamlStateKey, DamlStateValue, DamlSubmitterInfo, DamlTransactionEntry, @@ -23,11 +25,14 @@ import com.daml.ledger.participant.state.kvutils.committer.transaction.{ import com.daml.ledger.participant.state.kvutils.committer.{StepContinue, StepStop} import com.daml.ledger.participant.state.kvutils.{Conversions, Err} import com.daml.ledger.validator.TestHelper.{makeContractIdStateKey, makeContractIdStateValue} +import com.daml.lf.archive.testing.Encode import com.daml.lf.crypto.Hash import com.daml.lf.data.Time.Timestamp import com.daml.lf.data.{ImmArray, Ref} import com.daml.lf.engine.{Engine, Result, ResultError, Error => LfError} -import com.daml.lf.language.Ast +import com.daml.lf.language.Ast.Expr +import com.daml.lf.language.{Ast, LanguageVersion} +import com.daml.lf.testing.parser.Implicits.defaultParserParameters import com.daml.lf.transaction.TransactionOuterClass.ContractInstance import com.daml.lf.transaction.test.TransactionBuilder import com.daml.lf.transaction.{ @@ -43,14 +48,17 @@ import com.daml.logging.LoggingContext import com.daml.metrics.Metrics import com.google.protobuf.ByteString import org.mockito.{ArgumentMatchersSugar, MockitoSugar} +import org.scalatest.Inside.inside import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.wordspec.AnyWordSpec class ModelConformanceValidatorSpec extends AnyWordSpec with Matchers with MockitoSugar - with ArgumentMatchersSugar { + with ArgumentMatchersSugar + with TableDrivenPropertyChecks { import ModelConformanceValidatorSpec._ private implicit val loggingContext: LoggingContext = LoggingContext.ForTesting @@ -161,31 +169,67 @@ class ModelConformanceValidatorSpec ), aTransactionEntry, ) - step shouldBe a[StepStop] - step - .asInstanceOf[StepStop] - .logEntry - .getTransactionRejectionEntry - .hasDisputed shouldBe true + inside(step) { case StepStop(logEntry) => + logEntry.getTransactionRejectionEntry.hasDisputed shouldBe true + } + } + + "create StepStop in case of missing input" in { + val mockValidationResult = mock[Result[Unit]] + when( + mockValidationResult.consume( + any[Value.ContractId => Option[ + Value.ContractInst[Value.VersionedValue[Value.ContractId]] + ]], + any[Ref.PackageId => Option[Ast.Package]], + any[GlobalKeyWithMaintainers => Option[Value.ContractId]], + ) + ).thenThrow(Err.MissingInputState(inputContractIdStateKey)) + when( + mockEngine.validate( + any[Set[Ref.Party]], + any[SubmittedTransaction], + any[Timestamp], + any[Ref.ParticipantId], + any[Timestamp], + any[Hash], + ) + ).thenReturn(mockValidationResult) + + val step = modelConformanceValidator + .createValidationStep(rejections)( + createCommitContext( + None, + Map.empty, + ), + aTransactionEntry, + ) + inside(step) { case StepStop(logEntry) => + logEntry.getTransactionRejectionEntry.hasInconsistent shouldBe true + } } } "lookupContract" should { "return Some when a contract is present in the current state" in { - modelConformanceValidator.lookupContract( - createCommitContext( - None, - Map( - inputContractIdStateKey -> Some( - aContractIdStateValue - ) - ), - ) - )(Conversions.decodeContractId(inputContractId)) shouldBe Some(aContractInst) + val commitContext = createCommitContext( + None, + Map( + inputContractIdStateKey -> Some( + aContractIdStateValue + ) + ), + ) + + val contractInstance = modelConformanceValidator.lookupContract(commitContext)( + Conversions.decodeContractId(inputContractId) + ) + + contractInstance shouldBe Some(aContractInst) } "throw if a contract does not exist in the current state" in { - a[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupContract( + an[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupContract( createCommitContext( None, Map.empty, @@ -213,6 +257,53 @@ class ModelConformanceValidatorSpec } } + "lookupPackage" should { + "return the package" in { + val stateKey = DamlStateKey.newBuilder().setPackageId("aPackage").build() + val stateValue = DamlStateValue + .newBuilder() + .setArchive(anArchive) + .build() + val commitContext = createCommitContext( + None, + Map(stateKey -> Some(stateValue)), + ) + + val maybePackage = modelConformanceValidator + .lookupPackage(commitContext)(Ref.PackageId.assertFromString("aPackage")) + + maybePackage shouldBe a[Some[_]] + } + + "fail when the package is missing" in { + an[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupPackage( + createCommitContext( + None, + Map.empty, + ) + )(Ref.PackageId.assertFromString("nonexistentPackageId")) + } + + "fail when the archive is invalid" in { + val stateKey = DamlStateKey.newBuilder().setPackageId("invalidPackage").build() + + val stateValues = Table( + "state values", + DamlStateValue.newBuilder.build(), + DamlStateValue.newBuilder.setArchive(DamlLf.Archive.newBuilder()).build(), + ) + + forAll(stateValues) { stateValue => + an[Err.DecodeError] should be thrownBy modelConformanceValidator.lookupPackage( + createCommitContext( + None, + Map(stateKey -> Some(stateValue)), + ) + )(Ref.PackageId.assertFromString("invalidPackage")) + } + } + } + "validateCausalMonotonicity" should { "create StepContinue when causal monotonicity holds" in { modelConformanceValidator @@ -322,6 +413,24 @@ object ModelConformanceValidatorSpec { "", ) + private val anArchive: DamlLf.Archive = { + val pkg = Ast.GenPackage[Expr]( + Map.empty, + Set.empty, + LanguageVersion.default, + Some( + Ast.PackageMetadata( + Ref.PackageName.assertFromString("aPackage"), + Ref.PackageVersion.assertFromString("0.0.0"), + ) + ), + ) + Encode.encodeArchive( + defaultParserParameters.defaultPackageId -> pkg, + defaultParserParameters.languageVersion, + ) + } + private def txBuilder = TransactionBuilder(TransactionVersion.VDev) private def aGlobalKeyWithMaintainers(key: String, maintainer: String) = GlobalKeyWithMaintainers( From 7e34a5f6ed6041c6e0ece96ebce497a3dcc958a4 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Tue, 20 Jul 2021 19:15:06 +0200 Subject: [PATCH 12/18] Improve comments --- .../validation/ModelConformanceValidator.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index c2b7bc9a4b63..0644af22b39b 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -124,10 +124,10 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Helper to lookup contract instances. Since we look up every contract that was // an input to a transaction, we do not need to verify the inputs separately. // - // Note that a contract may not be in the state only if it was archived and pruned on the committer. - // Then, the participant is able to produce such a transaction only by using a divulged contract that - // appeared as active, because it didn't learn about the archival. On the other hand, using divulged - // contracts for interpretation is deprecated. + // Note that for an honest participant, a contract may not be in the state only if it was archived and pruned + // on the committer. Then, an honest participant is able to produce such a transaction only by using + // a divulged contract that appeared as active, because it didn't learn about the archival. + // On the other hand, using divulged contracts for interpretation is deprecated so we turn it into Inconsistent. @throws[Err.MissingInputState] private[validation] def lookupContract( commitContext: CommitContext @@ -185,7 +185,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me case _ => None } - // Checks that input contracts are still active at ledger effective time. + // Checks that input contracts have been created before or at the current ledger effective time. private[validation] def validateCausalMonotonicity( transactionEntry: DamlTransactionEntrySummary, commitContext: CommitContext, From 8f8e80f05d16f37002a99b2394a951db60959543 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Tue, 20 Jul 2021 19:33:10 +0200 Subject: [PATCH 13/18] Use `inside` in one more place --- .../TransactionConsistencyValidatorSpec.scala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala index 7317450d7647..67effd43b136 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/TransactionConsistencyValidatorSpec.scala @@ -39,6 +39,7 @@ import com.daml.lf.value.Value import com.daml.logging.LoggingContext import com.daml.metrics.Metrics import com.google.protobuf.Timestamp +import org.scalatest.Inside.inside import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks.{forAll, _} import org.scalatest.wordspec.AnyWordSpec @@ -250,12 +251,9 @@ class TransactionConsistencyValidatorSpec extends AnyWordSpec with Matchers { builder.add(archive(globalCreate, Set("Alice"))) val transaction = builder.buildSubmitted() val result = validate(context, transaction) - result shouldBe a[StepStop] - result - .asInstanceOf[StepStop] - .logEntry - .getTransactionRejectionEntry - .hasInconsistent shouldBe true + inside(result) { case StepStop(logEntry) => + logEntry.getTransactionRejectionEntry.hasInconsistent shouldBe true + } } } From 4891d1fb4323120d5bd5be3b98fdac3fe620caa4 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 21 Jul 2021 12:02:49 +0200 Subject: [PATCH 14/18] Change a rejection reason, add a test --- .../ModelConformanceValidator.scala | 12 ++- .../ModelConformanceValidatorSpec.scala | 78 +++++++++++++------ 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 0644af22b39b..a8745b654763 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -108,6 +108,16 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me } yield () stepResult.fold(identity, _ => StepContinue(transactionEntry)) } catch { + case missingInputErr: Err.MissingInputState => + logger.error( + "Model conformance validation failed due to a missing input state (most likely due to invalid state on the participant).", + missingInputErr, + ) + rejections.buildRejectionStep( + transactionEntry, + RejectionReasonV0.Inconsistent(missingInputErr.getMessage), + commitContext.recordTime, + ) case err: Err => logger.error( "Model conformance validation failed most likely due to invalid state on the participant.", @@ -115,7 +125,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me ) rejections.buildRejectionStep( transactionEntry, - RejectionReasonV0.Inconsistent(err.getMessage), + RejectionReasonV0.Disputed(err.getMessage), commitContext.recordTime, ) } diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index 0d89fcf36da9..fe0ae3c0f6fe 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -65,8 +65,7 @@ class ModelConformanceValidatorSpec private val metrics = new Metrics(new MetricRegistry) - private val mockEngine = mock[Engine] - private val modelConformanceValidator = new ModelConformanceValidator(mockEngine, metrics) + private val defaultValidator = new ModelConformanceValidator(mock[Engine], metrics) private val rejections = new Rejections(metrics) private val inputCreate = create( @@ -107,6 +106,7 @@ class ModelConformanceValidatorSpec "createValidationStep" should { "create StepContinue in case of correct input" in { + val mockEngine = mock[Engine] val mockValidationResult = mock[Result[Unit]] when( mockValidationResult.consume( @@ -128,7 +128,9 @@ class ModelConformanceValidatorSpec ) ).thenReturn(mockValidationResult) - modelConformanceValidator.createValidationStep(rejections)( + val validator = new ModelConformanceValidator(mockEngine, metrics) + + validator.createValidationStep(rejections)( createCommitContext( None, Map( @@ -141,6 +143,7 @@ class ModelConformanceValidatorSpec } "create StepStop in case of validation error" in { + val mockEngine = mock[Engine] when( mockEngine.validate( any[Set[Ref.Party]], @@ -158,7 +161,9 @@ class ModelConformanceValidatorSpec ) ) - val step = modelConformanceValidator + val validator = new ModelConformanceValidator(mockEngine, metrics) + + val step = validator .createValidationStep(rejections)( createCommitContext( None, @@ -175,7 +180,39 @@ class ModelConformanceValidatorSpec } "create StepStop in case of missing input" in { + val validator = createThrowingValidator(Err.MissingInputState(inputContractIdStateKey)) + + val step = validator + .createValidationStep(rejections)( + createCommitContext(None), + aTransactionEntry, + ) + inside(step) { case StepStop(logEntry) => + logEntry.getTransactionRejectionEntry.hasInconsistent shouldBe true + logEntry.getTransactionRejectionEntry.getInconsistent.getDetails should startWith( + "Missing input state for key contract_id: \"#inputContractId\"" + ) + } + } + + "create StepStop in case of decode error" in { + val validator = createThrowingValidator(Err.DecodeError("'test kind'", "'test message'")) + + val step = validator + .createValidationStep(rejections)( + createCommitContext(None), + aTransactionEntry, + ) + inside(step) { case StepStop(logEntry) => + logEntry.getTransactionRejectionEntry.hasDisputed shouldBe true + logEntry.getTransactionRejectionEntry.getDisputed.getDetails shouldBe "Decoding 'test kind' failed: 'test message'" + } + } + + def createThrowingValidator(consumeError: Err): ModelConformanceValidator = { + val mockEngine = mock[Engine] val mockValidationResult = mock[Result[Unit]] + when( mockValidationResult.consume( any[Value.ContractId => Option[ @@ -184,7 +221,8 @@ class ModelConformanceValidatorSpec any[Ref.PackageId => Option[Ast.Package]], any[GlobalKeyWithMaintainers => Option[Value.ContractId]], ) - ).thenThrow(Err.MissingInputState(inputContractIdStateKey)) + ).thenThrow(consumeError) + when( mockEngine.validate( any[Set[Ref.Party]], @@ -196,17 +234,7 @@ class ModelConformanceValidatorSpec ) ).thenReturn(mockValidationResult) - val step = modelConformanceValidator - .createValidationStep(rejections)( - createCommitContext( - None, - Map.empty, - ), - aTransactionEntry, - ) - inside(step) { case StepStop(logEntry) => - logEntry.getTransactionRejectionEntry.hasInconsistent shouldBe true - } + new ModelConformanceValidator(mockEngine, metrics) } } @@ -221,7 +249,7 @@ class ModelConformanceValidatorSpec ), ) - val contractInstance = modelConformanceValidator.lookupContract(commitContext)( + val contractInstance = defaultValidator.lookupContract(commitContext)( Conversions.decodeContractId(inputContractId) ) @@ -229,7 +257,7 @@ class ModelConformanceValidatorSpec } "throw if a contract does not exist in the current state" in { - an[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupContract( + an[Err.MissingInputState] should be thrownBy defaultValidator.lookupContract( createCommitContext( None, Map.empty, @@ -245,13 +273,13 @@ class ModelConformanceValidatorSpec } "return Some when mapping exists" in { - modelConformanceValidator.lookupKey(contractKeyInputs)( + defaultValidator.lookupKey(contractKeyInputs)( aGlobalKeyWithMaintainers(inputContractKey, inputContractKeyMaintainer) ) shouldBe Some(Conversions.decodeContractId(inputContractId)) } "return None when mapping does not exist" in { - modelConformanceValidator.lookupKey(contractKeyInputs)( + defaultValidator.lookupKey(contractKeyInputs)( aGlobalKeyWithMaintainers("nonexistentKey", "nonexistentMaintainer") ) shouldBe None } @@ -269,14 +297,14 @@ class ModelConformanceValidatorSpec Map(stateKey -> Some(stateValue)), ) - val maybePackage = modelConformanceValidator + val maybePackage = defaultValidator .lookupPackage(commitContext)(Ref.PackageId.assertFromString("aPackage")) maybePackage shouldBe a[Some[_]] } "fail when the package is missing" in { - an[Err.MissingInputState] should be thrownBy modelConformanceValidator.lookupPackage( + an[Err.MissingInputState] should be thrownBy defaultValidator.lookupPackage( createCommitContext( None, Map.empty, @@ -294,7 +322,7 @@ class ModelConformanceValidatorSpec ) forAll(stateValues) { stateValue => - an[Err.DecodeError] should be thrownBy modelConformanceValidator.lookupPackage( + an[Err.DecodeError] should be thrownBy defaultValidator.lookupPackage( createCommitContext( None, Map(stateKey -> Some(stateValue)), @@ -306,7 +334,7 @@ class ModelConformanceValidatorSpec "validateCausalMonotonicity" should { "create StepContinue when causal monotonicity holds" in { - modelConformanceValidator + defaultValidator .validateCausalMonotonicity( aTransactionEntry, createCommitContext( @@ -321,7 +349,7 @@ class ModelConformanceValidatorSpec } "reject transaction when causal monotonicity does not hold" in { - val step = modelConformanceValidator + val step = defaultValidator .validateCausalMonotonicity( aTransactionEntry, createCommitContext( From 079a19809bdbb51357e45594259a797af660d895 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 21 Jul 2021 12:07:38 +0200 Subject: [PATCH 15/18] Add a comment --- .../validation/ModelConformanceValidator.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index a8745b654763..28bb7d23a1c5 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -150,9 +150,11 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me .map(_.getContractInstance) .map(Conversions.decodeContractInstance) - // Helper to lookup package from the state. The package contents - // are stored in the [[DamlLogEntry]], which we find by looking up - // the Daml state entry at `DamlStateKey(packageId = pkgId)`. + // Helper to lookup package from the state. The package contents are stored in the [[DamlLogEntry]], + // which we find by looking up the Daml state entry at `DamlStateKey(packageId = pkgId)`. + // + // Note that there is no committer pruning of packages, so MissingInputState can only arise from a malicious + // or buggy participant. @throws[Err.MissingInputState] @throws[Err.DecodeError] private[validation] def lookupPackage( From 54c8f9fad9e550b7a57564e9d9840a57a4e4de8b Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 21 Jul 2021 12:14:30 +0200 Subject: [PATCH 16/18] Change a decoding error --- .../validation/ModelConformanceValidator.scala | 6 +++--- .../validation/ModelConformanceValidatorSpec.scala | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 28bb7d23a1c5..1611aae0d3df 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -156,7 +156,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me // Note that there is no committer pruning of packages, so MissingInputState can only arise from a malicious // or buggy participant. @throws[Err.MissingInputState] - @throws[Err.DecodeError] + @throws[Err.ArchiveDecodingFailed] private[validation] def lookupPackage( commitContext: CommitContext )(pkgId: PackageId)(implicit loggingContext: LoggingContext): Option[Ast.Package] = @@ -178,13 +178,13 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me } catch { case err: archive.Error => logger.warn("Decoding the archive failed.") - throw Err.DecodeError("Archive", err.getMessage) + throw Err.ArchiveDecodingFailed(pkgId, err.getMessage) } case _ => val msg = "value is not a Daml-LF archive" logger.warn(s"Package lookup failed, $msg.") - throw Err.DecodeError("Archive", msg) + throw Err.ArchiveDecodingFailed(pkgId, msg) } } yield pkg } diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala index fe0ae3c0f6fe..ad8125eb9ffc 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidatorSpec.scala @@ -196,7 +196,8 @@ class ModelConformanceValidatorSpec } "create StepStop in case of decode error" in { - val validator = createThrowingValidator(Err.DecodeError("'test kind'", "'test message'")) + val validator = + createThrowingValidator(Err.ArchiveDecodingFailed(aPackageId, "'test message'")) val step = validator .createValidationStep(rejections)( @@ -205,7 +206,9 @@ class ModelConformanceValidatorSpec ) inside(step) { case StepStop(logEntry) => logEntry.getTransactionRejectionEntry.hasDisputed shouldBe true - logEntry.getTransactionRejectionEntry.getDisputed.getDetails shouldBe "Decoding 'test kind' failed: 'test message'" + logEntry.getTransactionRejectionEntry.getDisputed.getDetails should be( + "Decoding of Daml-LF archive aPackage failed: 'test message'" + ) } } @@ -297,8 +300,7 @@ class ModelConformanceValidatorSpec Map(stateKey -> Some(stateValue)), ) - val maybePackage = defaultValidator - .lookupPackage(commitContext)(Ref.PackageId.assertFromString("aPackage")) + val maybePackage = defaultValidator.lookupPackage(commitContext)(aPackageId) maybePackage shouldBe a[Some[_]] } @@ -322,7 +324,7 @@ class ModelConformanceValidatorSpec ) forAll(stateValues) { stateValue => - an[Err.DecodeError] should be thrownBy defaultValidator.lookupPackage( + an[Err.ArchiveDecodingFailed] should be thrownBy defaultValidator.lookupPackage( createCommitContext( None, Map(stateKey -> Some(stateValue)), @@ -403,6 +405,7 @@ object ModelConformanceValidatorSpec { private val aKeyMaintainer = "maintainer" private val aDummyValue = TransactionBuilder.record("field" -> "value") private val aTemplateId = "dummyPackage:DummyModule:DummyTemplate" + private val aPackageId = Ref.PackageId.assertFromString("aPackage") private val aSubmissionSeed = ByteString.copyFromUtf8("a" * 32) private val ledgerEffectiveTime = From fb54e6fdd61875c2ecb8a640f2d6167d8239d908 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 21 Jul 2021 12:19:40 +0200 Subject: [PATCH 17/18] Fix after solving a conflict --- .../transaction/validation/ModelConformanceValidator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index f900868f5ed8..3eef3197b56d 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -180,7 +180,7 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me case _ => val msg = "value is not a Daml-LF archive" logger.warn(s"Package lookup failed, $msg.") - throw Err.ArchiveDecodingFailed(pkgId, err.getMessage) + throw Err.ArchiveDecodingFailed(pkgId, msg) } } From 8e780813ece788b12a04551c368ad05b18ba4031 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 21 Jul 2021 13:07:46 +0200 Subject: [PATCH 18/18] Fix a typo --- .../transaction/validation/ModelConformanceValidator.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala index 3eef3197b56d..141a9ef6dcbe 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/committer/transaction/validation/ModelConformanceValidator.scala @@ -205,13 +205,13 @@ private[transaction] class ModelConformanceValidator(engine: Engine, metrics: Me Conversions.stateKeyToContractId(key) -> value.getContractState } - val isCasuallyMonotonic = transactionEntry.transaction.inputContracts.forall { contractId => + val isCausallyMonotonic = transactionEntry.transaction.inputContracts.forall { contractId => val inputContractState = inputContracts(contractId) val activeAt = Option(inputContractState.getActiveAt).map(parseTimestamp) activeAt.exists(transactionEntry.ledgerEffectiveTime >= _) } - if (isCasuallyMonotonic) + if (isCausallyMonotonic) StepContinue(transactionEntry) else rejections.buildRejectionStep(