From 26c412c7381a655ca0d4a658d9929863ef3a5044 Mon Sep 17 00:00:00 2001 From: Hubert Slojewski Date: Wed, 14 Jul 2021 10:05:26 +0200 Subject: [PATCH] Refine transaction validation --- .../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")