mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
[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:
parent
7657ba0342
commit
4e5d908d85
@ -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")
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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],
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -45,7 +45,7 @@ final class TimedWriteService(delegate: WriteService, metrics: Metrics) extends
|
||||
transaction,
|
||||
estimatedInterpretationCost,
|
||||
globalKeyMapping,
|
||||
ImmArray.empty,
|
||||
explicitlyDisclosedContracts,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user