[ED] Disclosed contract validation in Sandbox-on-X [DPP-1099] (#14952)

* [ED] Payload validation in conflict-checking Sandbox-on-X

changelog_begin
changelog_end

* Refactor ConflictCheckWithCommittedImpl

* Fix compilation issues

* Addressed Simon's review comments
This commit is contained in:
tudor-da 2022-09-07 22:37:18 +02:00 committed by GitHub
parent 7657ba0342
commit 4e5d908d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 413 additions and 146 deletions

View File

@ -418,6 +418,8 @@ final class Metrics(val registry: MetricRegistry) {
val getTransactionTreeById: Timer = registry.timer(Prefix :+ "get_transaction_tree_by_id")
val getActiveContracts: Timer = registry.timer(Prefix :+ "get_active_contracts")
val lookupActiveContract: Timer = registry.timer(Prefix :+ "lookup_active_contract")
val lookupContractAfterInterpretation: Timer =
registry.timer(Prefix :+ "lookup_contract_after_interpretation")
val lookupContractKey: Timer = registry.timer(Prefix :+ "lookup_contract_key")
val lookupMaximumLedgerTime: Timer = registry.timer(Prefix :+ "lookup_maximum_ledger_time")
val getParticipantId: Timer = registry.timer(Prefix :+ "get_participant_id")

View File

@ -27,6 +27,7 @@ import com.daml.lf.data.Ref.{ApplicationId, Party}
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.value.Value
import com.daml.lf.value.Value.VersionedContractInstance
import com.daml.logging.LoggingContext
import com.daml.metrics.{Metrics, Timed}
@ -223,4 +224,12 @@ private[daml] final class TimedIndexService(delegate: IndexService, metrics: Met
delegate.getMeteringReportData(from, to, applicationId),
)
}
override def lookupContractForValidation(contractId: Value.ContractId)(implicit
loggingContext: LoggingContext
): Future[Option[(VersionedContractInstance, Timestamp)]] =
Timed.future(
metrics.daml.services.index.lookupContractAfterInterpretation,
delegate.lookupContractForValidation(contractId),
)
}

View File

@ -267,6 +267,11 @@ private[index] class IndexServiceImpl(
): Future[Option[VersionedContractInstance]] =
contractStore.lookupActiveContract(forParties, contractId)
override def lookupContractForValidation(contractId: ContractId)(implicit
loggingContext: LoggingContext
): Future[Option[(VersionedContractInstance, Timestamp)]] =
contractStore.lookupContractForValidation(contractId)
override def getTransactionById(
transactionId: TransactionId,
requestingParties: Set[Ref.Party],
@ -463,7 +468,6 @@ private[index] class IndexServiceImpl(
LedgerApiErrors.ServiceNotRunning
.Reject("Index Service")(new DamlContextualizedErrorLogger(logger, loggingContext, None))
.asGrpcError
}
object IndexServiceImpl {

View File

@ -7,6 +7,7 @@ import com.daml.ledger.offset.Offset
import com.daml.ledger.participant.state.index.v2.{ContractStore, MaximumLedgerTime}
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.value.Value.VersionedContractInstance
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.metrics.Metrics
import com.daml.platform.store.cache.ContractKeyStateValue._
@ -73,6 +74,21 @@ private[platform] class MutableCacheBackedContractStore(
readThroughMaximumLedgerTime(toBeFetched.toList, cached.maxOption)
}
override def lookupContractForValidation(
contractId: ContractId
)(implicit
loggingContext: LoggingContext
): Future[Option[(VersionedContractInstance, Timestamp)]] =
contractStateCaches.contractState
.get(contractId)
.map(Future.successful)
.getOrElse(readThroughContractsCache(contractId))
.map {
case NotFound => None
case Active(contract, _, let) => Some(contract -> let)
case Archived(_) => None
}
private def readThroughMaximumLedgerTime(
missing: List[ContractId],
acc: Option[Timestamp],

View File

@ -15,7 +15,7 @@ import com.daml.lf.transaction.test.TransactionBuilder
import com.daml.lf.value.Value.{ContractInstance, ValueRecord, ValueText}
import com.daml.logging.LoggingContext
import com.daml.metrics.Metrics
import com.daml.platform.store.cache.MutableCacheBackedContractStoreSpec._
import com.daml.platform.store.cache.MutableCacheBackedContractStoreSpec.{cId_5, _}
import com.daml.platform.store.dao.events.ContractStateEvent
import com.daml.platform.store.interfaces.LedgerDaoContractsReader
import com.daml.platform.store.interfaces.LedgerDaoContractsReader.{
@ -237,6 +237,43 @@ class MutableCacheBackedContractStoreSpec extends AsyncWordSpec with Matchers wi
}
}
}
"lookupContractAfterInterpretation" should {
"resolve lookup from cache" in {
for {
store <- contractStore(cachesSize = 2L).asFuture
_ = store.contractStateCaches.contractState.putBatch(
offset2,
Map(
// Populate the cache with an active contract
cId_4 -> ContractStateValue.Active(
contract = contract4,
stakeholders = Set.empty,
createLedgerEffectiveTime = t4,
),
// Populate the cache with an archived contract
cId_5 -> ContractStateValue.Archived(Set.empty),
),
)
activeContractLookupResult <- store.lookupContractForValidation(cId_4)
archivedContractLookupResult <- store.lookupContractForValidation(cId_5)
} yield {
activeContractLookupResult shouldBe Some(contract4 -> t4)
archivedContractLookupResult shouldBe None
}
}
"resolve lookup from the ContractsReader when not cached" in {
for {
store <- contractStore(cachesSize = 0L).asFuture
activeContractLookupResult <- store.lookupContractForValidation(cId_4)
archivedContractLookupResult <- store.lookupContractForValidation(cId_5)
} yield {
activeContractLookupResult shouldBe Some(contract4 -> t4)
archivedContractLookupResult shouldBe None
}
}
}
}
object MutableCacheBackedContractStoreSpec {

View File

@ -39,6 +39,12 @@ trait ContractStore {
def lookupMaximumLedgerTimeAfterInterpretation(ids: Set[ContractId])(implicit
loggingContext: LoggingContext
): Future[MaximumLedgerTime]
def lookupContractForValidation(
contractId: ContractId
)(implicit
loggingContext: LoggingContext
): Future[Option[(VersionedContractInstance, Timestamp)]]
}
/** The outcome of determining the maximum ledger time of a set of contracts.

View File

@ -45,7 +45,7 @@ final class TimedWriteService(delegate: WriteService, metrics: Metrics) extends
transaction,
estimatedInterpretationCost,
globalKeyMapping,
ImmArray.empty,
explicitlyDisclosedContracts,
),
)

View File

@ -22,6 +22,7 @@ da_scala_library(
"@maven//:com_github_pureconfig_pureconfig_core",
"@maven//:com_github_pureconfig_pureconfig_generic",
"@maven//:com_chuusai_shapeless",
"@maven//:org_typelevel_cats_core",
],
tags = ["maven_coordinates=com.daml:sandbox-on-x:__VERSION__"],
visibility = [

View File

@ -65,6 +65,7 @@ class BridgeWriteService(
transaction,
estimatedInterpretationCost,
deduplicationDuration,
disclosedContracts,
)
case DeduplicationPeriod.DeduplicationOffset(_) =>
CompletableFuture.completedFuture(
@ -164,6 +165,7 @@ class BridgeWriteService(
transaction: SubmittedTransaction,
estimatedInterpretationCost: Long,
deduplicationDuration: Duration,
disclosedContracts: ImmArray[Versioned[DisclosedContract]],
)(implicit errorLogger: ContextualizedErrorLogger): CompletionStage[SubmissionResult] = {
val maxDeduplicationDuration = submitterInfo.ledgerConfiguration.maxDeduplicationDuration
if (deduplicationDuration.compareTo(maxDeduplicationDuration) > 0)
@ -185,6 +187,7 @@ class BridgeWriteService(
transactionMeta = transactionMeta,
transaction = transaction,
estimatedInterpretationCost = estimatedInterpretationCost,
disclosedContracts = disclosedContracts,
)
)
}

View File

@ -3,18 +3,23 @@
package com.daml.ledger.sandbox.bridge.validate
import cats.data.EitherT
import com.daml.error.ContextualizedErrorLogger
import com.daml.ledger.offset.Offset
import com.daml.ledger.participant.state.index.v2.{IndexService, MaximumLedgerTime}
import ConflictCheckingLedgerBridge._
import com.daml.ledger.participant.state.v2.CompletionInfo
import com.daml.ledger.sandbox.bridge.BridgeMetrics
import com.daml.ledger.sandbox.bridge.validate.ConflictCheckingLedgerBridge._
import com.daml.ledger.sandbox.domain.Rejection
import com.daml.ledger.sandbox.domain.Rejection._
import com.daml.ledger.sandbox.domain.Submission.Transaction
import com.daml.lf.data.Ref
import com.daml.lf.command.DisclosedContract
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.transaction.{Transaction => LfTransaction}
import com.daml.lf.data.{ImmArray, Ref}
import com.daml.lf.transaction.{Versioned, Transaction => LfTransaction}
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.Timed
@ -35,46 +40,53 @@ private[validate] class ConflictCheckWithCommittedImpl(
in: Validation[(Offset, PreparedSubmission)]
): AsyncValidation[(Offset, PreparedSubmission)] = in match {
case Left(rejection) => Future.successful(Left(rejection))
case Right(
validated @ (
_,
PreparedTransactionSubmission(
keyInputs,
inputContracts,
_,
_,
blindingInfo,
transactionInformees,
originalSubmission,
),
)
) =>
withErrorLogger(originalSubmission.submitterInfo.submissionId) { implicit errorLogger =>
Timed
.future(
bridgeMetrics.Stages.ConflictCheckWithCommitted.timer,
validateCausalMonotonicity(
transaction = originalSubmission,
inputContracts = inputContracts,
transactionLedgerEffectiveTime =
originalSubmission.transactionMeta.ledgerEffectiveTime,
divulged = blindingInfo.divulgence.keySet,
).flatMap {
case Right(_) =>
validateKeyUsages(
transactionInformees,
keyInputs,
originalSubmission.loggingContext,
originalSubmission.submitterInfo.toCompletionInfo(),
)
case rejection => Future.successful(rejection)
},
)
.map(_.map(_ => validated))
}(originalSubmission.loggingContext, logger)
case Right(input @ (_, transactionSubmission: PreparedTransactionSubmission)) =>
val submissionId = transactionSubmission.submission.submissionId
withEnrichedLoggingContext("submissionId" -> submissionId) { implicit loggingContext =>
withErrorLogger(Some(submissionId)) { implicit errorLogger =>
Timed
.future(
bridgeMetrics.Stages.ConflictCheckWithCommitted.timer,
validate(transactionSubmission).map(_.map(_ => input)),
)
}
}(transactionSubmission.submission.loggingContext)
case Right(validated) => Future.successful(Right(validated))
}
private def validate(
inputSubmission: PreparedTransactionSubmission
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger,
loggingContext: LoggingContext,
): Future[Either[Rejection, Unit]] = {
import inputSubmission._
val eitherTF: EitherT[Future, Rejection, Unit] =
for {
_ <- validateExplicitDisclosure(
submission.disclosedContracts,
submission.submitterInfo.toCompletionInfo(),
)
_ <- validateCausalMonotonicity(
transaction = submission,
inputContracts = inputContracts,
transactionLedgerEffectiveTime = submission.transactionMeta.ledgerEffectiveTime,
divulged = blindingInfo.divulgence.keySet,
)
_ <- validateKeyUsages(
transactionInformees,
keyInputs,
submission.loggingContext,
submission.submitterInfo.toCompletionInfo(),
)
} yield ()
eitherTF.value
}
private def validateCausalMonotonicity(
transaction: Transaction,
inputContracts: Set[ContractId],
@ -82,35 +94,37 @@ private[validate] class ConflictCheckWithCommittedImpl(
divulged: Set[ContractId],
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): AsyncValidation[Unit] = {
): EitherT[Future, Rejection, Unit] = {
val referredContracts = inputContracts.diff(divulged)
val completionInfo = transaction.submitterInfo.toCompletionInfo()
if (referredContracts.isEmpty)
Future.successful(Right(()))
EitherT[Future, Rejection, Unit](Future.successful(Right(())))
else
indexService
.lookupMaximumLedgerTimeAfterInterpretation(referredContracts)(transaction.loggingContext)
.transform {
case Success(MaximumLedgerTime.Archived(missingContractIds)) =>
Success(Left(UnknownContracts(missingContractIds)(completionInfo)))
EitherT(
indexService
.lookupMaximumLedgerTimeAfterInterpretation(referredContracts)(transaction.loggingContext)
.transform {
case Success(MaximumLedgerTime.Archived(missingContractIds)) =>
Success(Left(UnknownContracts(missingContractIds)(completionInfo)))
case Failure(err) =>
Success(Left(LedgerBridgeInternalError(err, completionInfo)))
case Failure(err) =>
Success(Left(LedgerBridgeInternalError(err, completionInfo)))
case Success(MaximumLedgerTime.Max(maximumLedgerEffectiveTime))
if maximumLedgerEffectiveTime > transactionLedgerEffectiveTime =>
Success(
Left(
CausalMonotonicityViolation(
contractLedgerEffectiveTime = maximumLedgerEffectiveTime,
transactionLedgerEffectiveTime = transactionLedgerEffectiveTime,
)(completionInfo)
case Success(MaximumLedgerTime.Max(maximumLedgerEffectiveTime))
if maximumLedgerEffectiveTime > transactionLedgerEffectiveTime =>
Success(
Left(
CausalMonotonicityViolation(
contractLedgerEffectiveTime = maximumLedgerEffectiveTime,
transactionLedgerEffectiveTime = transactionLedgerEffectiveTime,
)(completionInfo)
)
)
)
case Success(_) =>
Success(Right(()))
}
case Success(_) => Success(Right(()))
}
)
}
private def validateKeyUsages(
@ -120,33 +134,76 @@ private[validate] class ConflictCheckWithCommittedImpl(
completionInfo: CompletionInfo,
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
): AsyncValidation[Unit] = {
keyInputs.foldLeft(Future.successful[Validation[Unit]](Right(()))) {
): EitherT[Future, Rejection, Unit] = {
keyInputs.foldLeft(EitherT(Future.successful[Validation[Unit]](Right(())))) {
case (f, (key, inputState)) =>
f.flatMap {
case Right(_) =>
indexService
.lookupContractKey(transactionInformees, key)(loggingContext)
.map { lookupResult =>
(inputState, lookupResult) match {
case (LfTransaction.NegativeKeyLookup, Some(actual)) =>
Left(
InconsistentContractKey(None, Some(actual))(completionInfo)
)
case (LfTransaction.KeyCreate, Some(_)) =>
Left(DuplicateKey(key)(completionInfo))
case (LfTransaction.KeyActive(expected), actual) if !actual.contains(expected) =>
Left(
InconsistentContractKey(Some(expected), actual)(
completionInfo
)
)
case _ => Right(())
}
f.flatMapF { _ =>
indexService
.lookupContractKey(transactionInformees, key)(loggingContext)
.map { lookupResult =>
(inputState, lookupResult) match {
case (LfTransaction.NegativeKeyLookup, Some(actual)) =>
Left(InconsistentContractKey(None, Some(actual))(completionInfo))
case (LfTransaction.KeyCreate, Some(_)) =>
Left(DuplicateKey(key)(completionInfo))
case (LfTransaction.KeyActive(expected), actual) if !actual.contains(expected) =>
Left(InconsistentContractKey(Some(expected), actual)(completionInfo))
case _ => Right(())
}
case left => Future.successful(left)
}
}
}
}
private def validateExplicitDisclosure(
disclosedContracts: ImmArray[Versioned[DisclosedContract]],
completionInfo: CompletionInfo,
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger,
loggingContext: LoggingContext,
): EitherT[Future, Rejection, Unit] = {
// Note that the validation fails fast on the first unknown/invalid contract.
disclosedContracts.foldLeft(EitherT(Future.successful[Validation[Unit]](Right(())))) {
case (f, provided) =>
f.flatMapF { _ =>
indexService
.lookupContractForValidation(provided.unversioned.contractId)
.map {
case None =>
// Disclosed contract was archived or never existed
Left(UnknownContracts(Set(provided.unversioned.contractId))(completionInfo))
case Some(actual) =>
sameContractData(actual, provided).left.map { errMessage =>
logger.info(errMessage)
DisclosedContractInvalid(provided.unversioned.contractId, completionInfo)
}
}
}
}
}
private def sameContractData(
actual: (Value.VersionedContractInstance, Timestamp),
provided: Versioned[DisclosedContract],
): Either[String, Unit] = {
val providedContractId = provided.unversioned.contractId
val actualTemplate = actual._1.unversioned.template
val providedTemplate = provided.unversioned.templateId
val actualArgument = actual._1.unversioned.arg
val providedArgument = provided.unversioned.argument
val actualLet = actual._2
val providedLet = provided.unversioned.metadata.createdAt
if (actualTemplate != providedTemplate)
Left(s"Disclosed contract $providedContractId has invalid template id")
else if (actualArgument != providedArgument)
Left(s"Disclosed contract $providedContractId has invalid argument")
else if (actualLet != providedLet)
Left(s"Disclosed contract $providedContractId has invalid ledgerEffectiveTime")
else
Right(())
}
}

View File

@ -26,7 +26,7 @@ private[validate] class PrepareSubmissionImpl(bridgeMetrics: BridgeMetrics)(impl
override def apply(submission: Submission): AsyncValidation[PreparedSubmission] =
submission match {
case transactionSubmission @ Submission.Transaction(submitterInfo, _, transaction, _) =>
case transactionSubmission @ Submission.Transaction(submitterInfo, _, transaction, _, _) =>
Timed.future(
bridgeMetrics.Stages.PrepareSubmission.timer,
Future {

View File

@ -198,4 +198,16 @@ private[sandbox] object Rejection {
.rpcStatus()
}
}
final case class DisclosedContractInvalid(
contractId: ContractId,
completionInfo: CompletionInfo,
)(implicit
contextualizedErrorLogger: ContextualizedErrorLogger
) extends Rejection {
override def toStatus: Status =
LedgerApiErrors.ConsistencyErrors.DisclosedContractInvalid
.Reject(contractId)
.rpcStatus()
}
}

View File

@ -6,9 +6,10 @@ package com.daml.ledger.sandbox.domain
import com.daml.daml_lf_dev.DamlLf.Archive
import com.daml.ledger.configuration.Configuration
import com.daml.ledger.participant.state.v2.{SubmitterInfo, TransactionMeta}
import com.daml.lf.command.DisclosedContract
import com.daml.lf.data.Ref.SubmissionId
import com.daml.lf.data.{Ref, Time}
import com.daml.lf.transaction.SubmittedTransaction
import com.daml.lf.data.{ImmArray, Ref, Time}
import com.daml.lf.transaction.{SubmittedTransaction, Versioned}
import com.daml.logging.LoggingContext
private[sandbox] sealed trait Submission extends Product with Serializable {
@ -22,6 +23,7 @@ private[sandbox] object Submission {
transactionMeta: TransactionMeta,
transaction: SubmittedTransaction,
estimatedInterpretationCost: Long,
disclosedContracts: ImmArray[Versioned[DisclosedContract]],
)(implicit val loggingContext: LoggingContext)
extends Submission {
val submissionId: SubmissionId = {

View File

@ -68,6 +68,7 @@ class BridgeWriteServiceTest extends AnyFlatSpec with MockitoSugar with Matchers
transactionMeta,
transaction = tx,
estimatedInterpretationCost = 0,
disclosedContracts = ImmArray.empty,
)(LoggingContext.ForTesting)
val expected = TransactionNodeStatistics(tx)

View File

@ -14,50 +14,49 @@ import com.daml.ledger.sandbox.bridge.BridgeMetrics
import com.daml.ledger.sandbox.bridge.validate.ConflictCheckWithCommittedSpec._
import com.daml.ledger.sandbox.domain.Rejection._
import com.daml.ledger.sandbox.domain.Submission
import com.daml.lf.command.{ContractMetadata, DisclosedContract}
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.data.{Ref, Time}
import com.daml.lf.data.{ImmArray, Ref, Time}
import com.daml.lf.transaction._
import com.daml.lf.transaction.test.TransactionBuilder
import com.daml.lf.transaction.{BlindingInfo, GlobalKey, Transaction}
import com.daml.lf.value.Value
import com.daml.lf.value.Value.ContractId
import com.daml.lf.value.Value.{ContractId, ValueTrue, VersionedContractInstance}
import com.daml.logging.LoggingContext
import com.daml.metrics.Metrics
import org.mockito.{ArgumentMatchersSugar, MockitoSugar}
import org.scalatest.FixtureContext
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import java.time.Duration
import scala.concurrent.Future
class ConflictCheckWithCommittedSpec
extends AsyncFlatSpec
extends AnyFlatSpec
with Matchers
with MockitoSugar
with ScalaFutures
with ArgumentMatchersSugar {
behavior of classOf[ConflictCheckWithCommittedImpl].getSimpleName
it should "validate causal monotonicity and key usages" in new TestContext {
conflictCheckWithCommitted(input).map(_ shouldBe input)
conflictCheckWithCommitted(input).futureValue shouldBe input
}
it should "pass causal monotonicity check if referred contracts is empty" in new TestContext {
val submissionWithEmptyReferredContracts: PreparedTransactionSubmission =
preparedTransactionSubmission.copy(inputContracts = inputContracts -- referredContracts)
for {
validationResult <- conflictCheckWithCommitted(
Right(offset -> submissionWithEmptyReferredContracts)
)
} yield {
verify(indexServiceMock, never).lookupMaximumLedgerTimeAfterInterpretation(
any[Set[ContractId]]
)(
any[LoggingContext]
)
validationResult shouldBe Right(offset -> submissionWithEmptyReferredContracts)
}
private val validationResult = conflictCheckWithCommitted(
Right(offset -> submissionWithEmptyReferredContracts)
).futureValue
verify(indexServiceMock, never)
.lookupMaximumLedgerTimeAfterInterpretation(any[Set[ContractId]])(any[LoggingContext])
validationResult shouldBe Right(offset -> submissionWithEmptyReferredContracts)
}
it should "handle causal monotonicity violation" in new TestContext {
@ -67,13 +66,15 @@ class ConflictCheckWithCommittedSpec
txSubmission.copy(transactionMeta = transactionMeta.copy(ledgerEffectiveTime = lateTxLet))
)
conflictCheckWithCommitted(Right(offset -> nonCausalTxSubmission))
.map {
case Left(CausalMonotonicityViolation(actualContractMaxLedgerTime, actualTxLet)) =>
actualContractMaxLedgerTime shouldBe contractMaxLedgerTime
actualTxLet shouldBe lateTxLet
case failure => fail(s"Expectation mismatch: got $failure")
}
private val validationResult =
conflictCheckWithCommitted(Right(offset -> nonCausalTxSubmission)).futureValue
validationResult match {
case Left(CausalMonotonicityViolation(actualContractMaxLedgerTime, actualTxLet)) =>
actualContractMaxLedgerTime shouldBe contractMaxLedgerTime
actualTxLet shouldBe lateTxLet
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "handle missing contracts" in new TestContext {
@ -82,13 +83,14 @@ class ConflictCheckWithCommittedSpec
indexServiceMock.lookupMaximumLedgerTimeAfterInterpretation(referredContracts)(loggingContext)
)
.thenReturn(Future.successful(MaximumLedgerTime.Archived(missingContracts)))
private val validationResult =
conflictCheckWithCommitted(input).futureValue
conflictCheckWithCommitted(input)
.map {
case Left(UnknownContracts(actualMissingContracts)) =>
actualMissingContracts shouldBe missingContracts
case failure => fail(s"Expectation mismatch: got $failure")
}
validationResult match {
case Left(UnknownContracts(actualMissingContracts)) =>
actualMissingContracts shouldBe missingContracts
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "handle a generic lookupMaximumLedgerTime error" in new TestContext {
@ -98,25 +100,26 @@ class ConflictCheckWithCommittedSpec
indexServiceMock.lookupMaximumLedgerTimeAfterInterpretation(referredContracts)(loggingContext)
)
.thenReturn(Future.failed(someInternalError))
private val validationResult = conflictCheckWithCommitted(input).futureValue
conflictCheckWithCommitted(input)
.map {
case Left(LedgerBridgeInternalError(actualInternalError, _)) =>
actualInternalError shouldBe someInternalError
case failure => fail(s"Expectation mismatch: got $failure")
}
validationResult match {
case Left(LedgerBridgeInternalError(actualInternalError, _)) =>
actualInternalError shouldBe someInternalError
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "handle an inconsistent contract key (on key active input)" in new TestContext {
when(indexServiceMock.lookupContractKey(informeesSet, activeKey)(loggingContext))
.thenReturn(Future.successful(None))
private val validationResult =
conflictCheckWithCommitted(input).futureValue
conflictCheckWithCommitted(input)
.map {
case Left(InconsistentContractKey(Some(actualInputContract), None)) =>
actualInputContract shouldBe inputContract
case failure => fail(s"Expectation mismatch: got $failure")
}
validationResult match {
case Left(InconsistentContractKey(Some(actualInputContract), None)) =>
actualInputContract shouldBe inputContract
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "handle an inconsistent contract key (on negative lookup input)" in new TestContext {
@ -124,13 +127,14 @@ class ConflictCheckWithCommittedSpec
when(indexServiceMock.lookupContractKey(informeesSet, nonExistingKey)(loggingContext))
.thenReturn(Future.successful(Some(existingContractForKey)))
private val validationResult =
conflictCheckWithCommitted(input).futureValue
conflictCheckWithCommitted(input)
.map {
case Left(InconsistentContractKey(None, Some(actualExistingContractForKey))) =>
actualExistingContractForKey shouldBe existingContractForKey
case failure => fail(s"Expectation mismatch: got $failure")
}
validationResult match {
case Left(InconsistentContractKey(None, Some(actualExistingContractForKey))) =>
actualExistingContractForKey shouldBe existingContractForKey
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "handle a duplicate contract key" in new TestContext {
@ -138,12 +142,93 @@ class ConflictCheckWithCommittedSpec
when(indexServiceMock.lookupContractKey(informeesSet, keyCreated)(loggingContext))
.thenReturn(Future.successful(Some(existingContractForKey)))
private val validationResult = conflictCheckWithCommitted(input).futureValue
conflictCheckWithCommitted(input)
.map {
case Left(DuplicateKey(actualDuplicateKey)) => actualDuplicateKey shouldBe keyCreated
case failure => fail(s"Expectation mismatch: got $failure")
}
validationResult match {
case Left(DuplicateKey(actualDuplicateKey)) => actualDuplicateKey shouldBe keyCreated
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "fail validation mismatching let in disclosed contract" in new TestContext {
when(
indexServiceMock.lookupContractForValidation(eqTo(disclosedContract.contractId))(
any[LoggingContext]
)
)
.thenReturn(
Future.successful(
Some(
VersionedContractInstance(
templateId,
Versioned(TransactionVersion.VDev, disclosedContract.argument),
"",
) -> disclosedContract.metadata.createdAt.add(Duration.ofSeconds(1000L))
)
)
)
private val validationResult = conflictCheckWithCommitted(input).futureValue
validationResult match {
case Left(DisclosedContractInvalid(contractId, _)) =>
contractId shouldBe disclosedContract.contractId
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "fail validation mismatching contract argument in disclosed contract" in new TestContext {
when(
indexServiceMock.lookupContractForValidation(eqTo(disclosedContract.contractId))(
any[LoggingContext]
)
)
.thenReturn(
Future.successful(
Some(
VersionedContractInstance(
templateId,
Versioned(TransactionVersion.VDev, ValueTrue),
"",
) -> disclosedContract.metadata.createdAt
)
)
)
private val validationResult = conflictCheckWithCommitted(input).futureValue
validationResult match {
case Left(DisclosedContractInvalid(contractId, _)) =>
contractId shouldBe disclosedContract.contractId
case failure => fail(s"Expectation mismatch: got $failure")
}
}
it should "fail validation mismatching template id in disclosed contract" in new TestContext {
when(
indexServiceMock.lookupContractForValidation(eqTo(disclosedContract.contractId))(
any[LoggingContext]
)
)
.thenReturn(
Future.successful(
Some(
VersionedContractInstance(
templateId.copy(packageId = Ref.PackageId.assertFromString("anotherPackageId")),
Versioned(TransactionVersion.VDev, disclosedContract.argument),
"",
) -> disclosedContract.metadata.createdAt
)
)
)
private val validationResult = conflictCheckWithCommitted(input).futureValue
validationResult match {
case Left(DisclosedContractInvalid(contractId, _)) =>
contractId shouldBe disclosedContract.contractId
case failure => fail(s"Expectation mismatch: got $failure")
}
}
private class TestContext extends FixtureContext {
@ -187,11 +272,23 @@ class ConflictCheckWithCommittedSpec
val informeesSet: Set[Ref.Party] = transactionInformees.toSet
val blindingInfo: BlindingInfo = BlindingInfo(Map(), Map(divulgedContract -> Set(informee1)))
val disclosedContract: DisclosedContract = DisclosedContract(
templateId = templateId,
contractId = cid(1),
argument = Value.ValueText("Some contract value"),
metadata = ContractMetadata(
createdAt = Time.Timestamp.now(),
keyHash = None, // Not affected by this validation
driverMetadata = ImmArray.empty, // Not affected by this validation
),
)
val txSubmission: Submission.Transaction = Submission.Transaction(
submitterInfo = submitterInfo,
transactionMeta = transactionMeta,
transaction = TransactionBuilder.EmptySubmitted,
estimatedInterpretationCost = 0L,
disclosedContracts = ImmArray(Versioned(TransactionVersion.VDev, disclosedContract)),
)(loggingContext)
val preparedTransactionSubmission: PreparedTransactionSubmission =
@ -220,6 +317,23 @@ class ConflictCheckWithCommittedSpec
.thenReturn(Future.successful(Some(inputContract)))
when(indexServiceMock.lookupContractKey(informeesSet, nonExistingKey))
.thenReturn(Future.successful(None))
when(
indexServiceMock.lookupContractForValidation(eqTo(disclosedContract.contractId))(
any[LoggingContext]
)
)
.thenReturn(
Future.successful(
Some(
VersionedContractInstance(
templateId,
Versioned(TransactionVersion.VDev, disclosedContract.argument),
"",
) -> disclosedContract.metadata.createdAt
)
)
)
}
}

View File

@ -16,7 +16,7 @@ import com.daml.ledger.sandbox.domain.Rejection.{
}
import com.daml.ledger.sandbox.domain.Submission
import com.daml.lf.crypto.Hash
import com.daml.lf.data.{Ref, Time}
import com.daml.lf.data.{ImmArray, Ref, Time}
import com.daml.lf.transaction.test.TransactionBuilder
import com.daml.lf.transaction.{GlobalKey, SubmittedTransaction}
import com.daml.lf.value.Value
@ -85,6 +85,7 @@ class PrepareSubmissionSpec extends AsyncFlatSpec with Matchers {
transactionMeta = txMeta,
transaction = SubmittedTransaction(txBuilder.build()),
estimatedInterpretationCost = 0L,
disclosedContracts = ImmArray.empty,
)
)
validationResult.map(
@ -120,6 +121,7 @@ class PrepareSubmissionSpec extends AsyncFlatSpec with Matchers {
transactionMeta = txMeta,
transaction = SubmittedTransaction(txBuilder.build()),
estimatedInterpretationCost = 0L,
disclosedContracts = ImmArray.empty,
)
)
validationResult.map(

View File

@ -21,7 +21,7 @@ import com.daml.ledger.sandbox.domain.{Rejection, Submission}
import com.daml.lf.crypto.Hash
import com.daml.lf.data.Ref.IdString
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.data.{Ref, Time}
import com.daml.lf.data.{ImmArray, Ref, Time}
import com.daml.lf.transaction.Transaction.{KeyActive, KeyCreate}
import com.daml.lf.transaction._
import com.daml.lf.transaction.test.TransactionBuilder
@ -384,6 +384,7 @@ class SequenceSpec
transactionMeta = transactionMeta,
transaction = txMock,
estimatedInterpretationCost = 0L,
disclosedContracts = ImmArray.empty,
)(loggingContext)
val txInformees: Set[IdString.Party] = allocatedInformees.take(2)