From ddcfe32a71b70938d1e3a65ca43ab199d0d0ecbb Mon Sep 17 00:00:00 2001 From: Fabio Tudone Date: Wed, 13 Oct 2021 15:54:49 +0200 Subject: [PATCH] Introduce v2 (self-service) KV error codes behind the CLI switch --- .../state/kvutils/Conversions.scala | 211 ++------ .../state/kvutils/KeyValueConsumption.scala | 74 +-- .../updates/TransactionRejections.scala | 468 ++++++++++++++++++ .../state/kvutils/ConversionsSpec.scala | 13 +- .../kvutils/KeyValueConsumptionSpec.scala | 8 +- 5 files changed, 523 insertions(+), 251 deletions(-) create mode 100644 ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/updates/TransactionRejections.scala diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala index 4f8c35ff277e..7084367f611c 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/Conversions.scala @@ -3,11 +3,8 @@ package com.daml.ledger.participant.state.kvutils -import java.io.StringWriter -import java.time.{Duration, Instant} - +import com.daml.error.ValueSwitch import com.daml.ledger.api.DeduplicationPeriod -import com.daml.ledger.grpc.GrpcStatuses import com.daml.ledger.offset.Offset import com.daml.ledger.participant.state.kvutils.DamlKvutils._ import com.daml.ledger.participant.state.kvutils.committer.transaction.Rejection @@ -15,6 +12,7 @@ import com.daml.ledger.participant.state.kvutils.committer.transaction.Rejection ExternallyInconsistentTransaction, InternallyInconsistentTransaction, } +import com.daml.ledger.participant.state.kvutils.updates.TransactionRejections._ import com.daml.ledger.participant.state.kvutils.store.events.DamlSubmitterInfo.DeduplicationPeriodCase import com.daml.ledger.participant.state.kvutils.store.events.DamlTransactionBlindingInfo.{ DisclosureEntry, @@ -39,13 +37,10 @@ import com.daml.lf.transaction._ import com.daml.lf.value.Value.{ContractId, VersionedValue} import com.daml.lf.value.{Value, ValueCoder, ValueOuterClass} import com.daml.lf.{crypto, data} -import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.Empty -import com.google.protobuf.any.{Any => AnyProto} -import com.google.rpc.code.Code -import com.google.rpc.error_details.ErrorInfo import com.google.rpc.status.Status +import java.time.{Duration, Instant} import scala.annotation.nowarn import scala.collection.mutable import scala.jdk.CollectionConverters._ @@ -208,8 +203,8 @@ private[state] object Conversions { Ref.SubmissionId.assertFromString ), ) - } + def buildTimestamp(ts: Time.Timestamp): com.google.protobuf.Timestamp = buildTimestamp(ts.toInstant) @@ -461,216 +456,66 @@ private[state] object Conversions { @nowarn("msg=deprecated") def decodeTransactionRejectionEntry( - entry: DamlTransactionRejectionEntry + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], ): Option[FinalReason] = { - def buildStatus( - code: Code, - message: String, - additionalMetadata: Map[String, String] = Map.empty, - ) = Status.of( - code.value, - message, - Seq( - AnyProto.pack[ErrorInfo]( - ErrorInfo(metadata = - additionalMetadata + (GrpcStatuses.DefiniteAnswerKey -> entry.getDefiniteAnswer.toString) - ) - ) - ), - ) - - val status = entry.getReasonCase match { + val status = Some(entry.getReasonCase match { case DamlTransactionRejectionEntry.ReasonCase.INVALID_LEDGER_TIME => val rejection = entry.getInvalidLedgerTime - Some( - buildStatus( - Code.ABORTED, - s"Invalid ledger time: ${rejection.getDetails}", - Map( - "ledger_time" -> rejection.getLedgerTime.toString, - "lower_bound" -> rejection.getLowerBound.toString, - "upper_bound" -> rejection.getUpperBound.toString, - ), - ) - ) + invalidLedgerTimeStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.DISPUTED => val rejection = entry.getDisputed - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Disputed: ${rejection.getDetails}", - ) - ) + disputedStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.SUBMITTER_CANNOT_ACT_VIA_PARTICIPANT => val rejection = entry.getSubmitterCannotActViaParticipant - Some( - buildStatus( - Code.PERMISSION_DENIED, - s"Submitter cannot act via participant: ${rejection.getDetails}", - Map( - "submitter_party" -> rejection.getSubmitterParty, - "participant_id" -> rejection.getParticipantId, - ), - ) - ) + submitterCannotActViaParticipantStatus(entry, rejection) case DamlTransactionRejectionEntry.ReasonCase.INCONSISTENT => val rejection = entry.getInconsistent - Some( - buildStatus( - Code.ABORTED, - s"Inconsistent: ${rejection.getDetails}", - ) - ) + inconsistentStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.RESOURCES_EXHAUSTED => val rejection = entry.getResourcesExhausted - Some( - buildStatus( - Code.ABORTED, - s"Resources exhausted: ${rejection.getDetails}", - ) - ) + resourceExhaustedStatus(entry, rejection) case DamlTransactionRejectionEntry.ReasonCase.DUPLICATE_COMMAND => - Some( - buildStatus( - Code.ALREADY_EXISTS, - "Duplicate commands", - ) - ) + duplicateCommandStatus(entry) case DamlTransactionRejectionEntry.ReasonCase.PARTY_NOT_KNOWN_ON_LEDGER => val rejection = entry.getPartyNotKnownOnLedger - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Party not known on ledger: ${rejection.getDetails}", - ) - ) + partyNotKnownOnLedgerStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.VALIDATION_FAILURE => val rejection = entry.getValidationFailure - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Disputed: ${rejection.getDetails}", - ) - ) + validationFailureStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.INTERNALLY_DUPLICATE_KEYS => - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Disputed: ${InternallyInconsistentTransaction.DuplicateKeys.description}", - ) - ) + internallyDuplicateKeysStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.INTERNALLY_INCONSISTENT_KEYS => - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Disputed: ${InternallyInconsistentTransaction.InconsistentKeys.description}", - ) - ) + internallyInconsistentKeysStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.EXTERNALLY_INCONSISTENT_CONTRACTS => - Some( - buildStatus( - Code.ABORTED, - s"Inconsistent: ${ExternallyInconsistentTransaction.InconsistentContracts.description}", - ) - ) + externallyInconsistentContractsStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.EXTERNALLY_DUPLICATE_KEYS => - Some( - buildStatus( - Code.ABORTED, - s"Inconsistent: ${ExternallyInconsistentTransaction.DuplicateKeys.description}", - ) - ) + externallyDuplicateKeysStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.EXTERNALLY_INCONSISTENT_KEYS => - Some( - buildStatus( - Code.ABORTED, - s"Inconsistent: ${ExternallyInconsistentTransaction.InconsistentKeys.description}", - ) - ) + externallyInconsistentKeysStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.MISSING_INPUT_STATE => val rejection = entry.getMissingInputState - Some( - buildStatus( - Code.ABORTED, - s"Inconsistent: Missing input state for key ${rejection.getKey.toString}", - Map("key" -> rejection.getKey.toString), - ) - ) + missingInputStateStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.RECORD_TIME_OUT_OF_RANGE => val rejection = entry.getRecordTimeOutOfRange - Some( - buildStatus( - Code.ABORTED, - s"Invalid ledger time: Record time is outside of valid range [${rejection.getMinimumRecordTime}, ${rejection.getMaximumRecordTime}]", - Map( - "minimum_record_time" -> Instant - .ofEpochSecond( - rejection.getMinimumRecordTime.getSeconds, - rejection.getMinimumRecordTime.getNanos.toLong, - ) - .toString, - "maximum_record_time" -> Instant - .ofEpochSecond( - rejection.getMaximumRecordTime.getSeconds, - rejection.getMaximumRecordTime.getNanos.toLong, - ) - .toString, - ), - ) - ) + recordTimeOutOfRangeStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.CAUSAL_MONOTONICITY_VIOLATED => - Some( - buildStatus( - Code.ABORTED, - "Invalid ledger time: Causal monotonicity violated", - ) - ) + causalMonotonicityViolatedStatus(entry, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.SUBMITTING_PARTY_NOT_KNOWN_ON_LEDGER => val rejection = entry.getSubmittingPartyNotKnownOnLedger - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Party not known on ledger: Submitting party '${rejection.getSubmitterParty}' not known", - Map("submitter_party" -> rejection.getSubmitterParty), - ) - ) + submittingPartyNotKnownOnLedgerStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.PARTIES_NOT_KNOWN_ON_LEDGER => val rejection = entry.getPartiesNotKnownOnLedger - val parties = rejection.getPartiesList - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Party not known on ledger: Parties not known on ledger ${parties.asScala.mkString("[", ",", "]")}", - Map("parties" -> objectToJsonString(parties)), - ) - ) + partiesNotKnownOnLedgerStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.INVALID_PARTICIPANT_STATE => val rejection = entry.getInvalidParticipantState - Some( - buildStatus( - Code.INVALID_ARGUMENT, - s"Disputed: ${rejection.getDetails}", - rejection.getMetadataMap.asScala.toMap, - ) - ) + invalidParticipantStateStatus(entry, rejection, errorVersionSwitch) case DamlTransactionRejectionEntry.ReasonCase.REASON_NOT_SET => - Some( - buildStatus( - Code.UNKNOWN, - "No reason set for rejection", - ) - ) - } + reasonNotSetStatus(entry, errorVersionSwitch) + }) status.map(FinalReason) } - private def objectToJsonString(obj: Object): String = { - val stringWriter = new StringWriter - val objectMapper = new ObjectMapper - objectMapper.writeValue(stringWriter, obj) - stringWriter.toString - } - private def encodeParties(parties: Set[Ref.Party]): List[String] = (parties.toList: List[String]).sorted diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala index 8c49c1ab4793..276c948b2f39 100644 --- a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumption.scala @@ -5,9 +5,9 @@ package com.daml.ledger.participant.state.kvutils import com.daml.error.ValueSwitch import com.daml.ledger.configuration.Configuration -import com.daml.ledger.grpc.GrpcStatuses import com.daml.ledger.participant.state.kvutils.Conversions._ import com.daml.ledger.participant.state.kvutils.DamlKvutils._ +import com.daml.ledger.participant.state.kvutils.updates.TransactionRejections._ import com.daml.ledger.participant.state.kvutils.store.events.PackageUpload.DamlPackageUploadRejectionEntry import com.daml.ledger.participant.state.kvutils.store.events.{ DamlTransactionBlindingInfo, @@ -20,7 +20,6 @@ import com.daml.ledger.participant.state.kvutils.store.{ DamlOutOfTimeBoundsEntry, DamlStateKey, } -import com.daml.ledger.participant.state.v2.Update.CommandRejected.FinalReason import com.daml.ledger.participant.state.v2.{DivulgedContract, TransactionMeta, Update} import com.daml.lf.data.Ref import com.daml.lf.data.Ref.LedgerString @@ -28,13 +27,9 @@ import com.daml.lf.data.Time.Timestamp import com.daml.lf.transaction.CommittedTransaction import com.google.common.io.BaseEncoding import com.google.protobuf.ByteString -import com.google.protobuf.any.{Any => AnyProto} -import com.google.rpc.code.Code -import com.google.rpc.error_details.ErrorInfo import com.google.rpc.status.Status import org.slf4j.LoggerFactory -import scala.annotation.nowarn import scala.jdk.CollectionConverters._ /** Utilities for producing [[Update]] events from [[DamlLogEntry]]'s committed to a @@ -61,9 +56,7 @@ object KeyValueConsumption { def logEntryToUpdate( entryId: DamlLogEntryId, entry: DamlLogEntry, - @nowarn( - "msg=parameter value errorVersionSwitch.* is never used" - ) errorVersionSwitch: ValueSwitch[Status], + errorVersionSwitch: ValueSwitch[Status], recordTimeForUpdate: Option[Timestamp] = None, ): List[Update] = { val recordTimeFromLogEntry = PartialFunction.condOpt(entry.hasRecordTime) { case true => @@ -217,10 +210,15 @@ object KeyValueConsumption { transactionRejectionEntryToUpdate( recordTime, entry.getTransactionRejectionEntry, + errorVersionSwitch, ).toList case DamlLogEntry.PayloadCase.OUT_OF_TIME_BOUNDS_ENTRY => - outOfTimeBoundsEntryToUpdate(recordTime, entry.getOutOfTimeBoundsEntry).toList + outOfTimeBoundsEntryToUpdate( + recordTime, + entry.getOutOfTimeBoundsEntry, + errorVersionSwitch, + ).toList case DamlLogEntry.PayloadCase.TIME_UPDATE_ENTRY => List.empty @@ -244,8 +242,9 @@ object KeyValueConsumption { private def transactionRejectionEntryToUpdate( recordTime: Timestamp, rejEntry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], ): Option[Update] = Conversions - .decodeTransactionRejectionEntry(rejEntry) + .decodeTransactionRejectionEntry(rejEntry, errorVersionSwitch) .map { reason => Update.CommandRejected( recordTime = recordTime, @@ -337,6 +336,7 @@ object KeyValueConsumption { private[kvutils] def outOfTimeBoundsEntryToUpdate( recordTime: Timestamp, outOfTimeBoundsEntry: DamlOutOfTimeBoundsEntry, + errorVersionSwitch: ValueSwitch[Status], ): Option[Update] = { val timeBounds = parseTimeBounds(outOfTimeBoundsEntry) val deduplicated = timeBounds.deduplicateUntil.exists(recordTime <= _) @@ -348,31 +348,7 @@ object KeyValueConsumption { wrappedLogEntry.getPayloadCase match { case DamlLogEntry.PayloadCase.TRANSACTION_REJECTION_ENTRY if deduplicated => val rejectionEntry = wrappedLogEntry.getTransactionRejectionEntry - Some( - Update.CommandRejected( - recordTime = recordTime, - completionInfo = parseCompletionInfo( - Conversions.parseInstant(recordTime), - rejectionEntry.getSubmitterInfo, - ), - reasonTemplate = FinalReason( - Status.of( - Code.ALREADY_EXISTS.value, - "Duplicate commands", - Seq( - AnyProto.pack[ErrorInfo]( - // the definite answer is false, as the rank-based deduplication is not yet implemented - ErrorInfo(metadata = - Map( - GrpcStatuses.DefiniteAnswerKey -> rejectionEntry.getDefiniteAnswer.toString - ) - ) - ) - ), - ) - ), - ) - ) + duplicateCommandsRejectionUpdate(recordTime, rejectionEntry, errorVersionSwitch) case _ if deduplicated => // We only emit updates for duplicate transaction submissions. @@ -390,30 +366,7 @@ object KeyValueConsumption { case _ => "Record time outside of valid range" } - Some( - Update.CommandRejected( - recordTime = recordTime, - completionInfo = parseCompletionInfo( - Conversions.parseInstant(recordTime), - rejectionEntry.getSubmitterInfo, - ), - reasonTemplate = FinalReason( - Status.of( - Code.ABORTED.value, - reason, - Seq( - AnyProto.pack[ErrorInfo]( - ErrorInfo(metadata = - Map( - GrpcStatuses.DefiniteAnswerKey -> rejectionEntry.getDefiniteAnswer.toString - ) - ) - ) - ), - ) - ), - ) - ) + invalidRecordTimeRejectionUpdate(recordTime, rejectionEntry, reason, errorVersionSwitch) case DamlLogEntry.PayloadCase.CONFIGURATION_REJECTION_ENTRY if invalidRecordTime => val configurationRejectionEntry = wrappedLogEntry.getConfigurationRejectionEntry @@ -452,7 +405,6 @@ object KeyValueConsumption { ) } } - private def parseTimeBounds(outOfTimeBoundsEntry: DamlOutOfTimeBoundsEntry): TimeBounds = { val duplicateUntilMaybe = parseOptionalTimestamp( outOfTimeBoundsEntry.hasDuplicateUntil, diff --git a/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/updates/TransactionRejections.scala b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/updates/TransactionRejections.scala new file mode 100644 index 000000000000..349eef864c4d --- /dev/null +++ b/ledger/participant-state/kvutils/src/main/scala/com/daml/ledger/participant/state/kvutils/updates/TransactionRejections.scala @@ -0,0 +1,468 @@ +// 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.updates + +import com.daml.error.ValueSwitch +import com.daml.ledger.grpc.GrpcStatuses +import com.daml.ledger.participant.state.kvutils.Conversions.parseCompletionInfo +import com.daml.ledger.participant.state.kvutils.DamlKvutils.{ + Disputed, + Inconsistent, + InvalidLedgerTime, + InvalidParticipantState, + MissingInputState, + PartiesNotKnownOnLedger, + PartyNotKnownOnLedger, + RecordTimeOutOfRange, + ResourcesExhausted, + SubmitterCannotActViaParticipant, + SubmittingPartyNotKnownOnLedger, + ValidationFailure, +} +import com.daml.ledger.participant.state.kvutils.committer.transaction.Rejection.{ + ExternallyInconsistentTransaction, + InternallyInconsistentTransaction, +} +import com.daml.ledger.participant.state.kvutils.{Conversions, CorrelationId} +import com.daml.ledger.participant.state.kvutils.store.events.DamlTransactionRejectionEntry +import com.daml.ledger.participant.state.v2.Update +import com.daml.ledger.participant.state.v2.Update.CommandRejected.FinalReason +import com.daml.lf.data.Time.Timestamp +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.rpc.code.Code +import com.google.rpc.error_details.ErrorInfo +import com.google.rpc.status.Status +import com.google.protobuf.any.{Any => AnyProto} + +import java.io.StringWriter +import java.time.Instant +import scala.jdk.CollectionConverters._ + +/** Utilities for converting between rejection log entries and updates and/or gRPC statuses. + */ +private[kvutils] object TransactionRejections { + + def invalidRecordTimeRejectionUpdate( + recordTime: Timestamp, + rejectionEntry: DamlTransactionRejectionEntry, + reason: CorrelationId, + errorVersionSwitch: ValueSwitch[Status], + ): Some[Update.CommandRejected] = { + val statusBuilder = invalidRecordTimeRejectionStatus(rejectionEntry, reason, _) + Some( + Update.CommandRejected( + recordTime = recordTime, + completionInfo = parseCompletionInfo( + Conversions.parseInstant(recordTime), + rejectionEntry.getSubmitterInfo, + ), + reasonTemplate = FinalReason( + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), + ) + ), + ) + ) + } + + def duplicateCommandsRejectionUpdate( + recordTime: Timestamp, + rejectionEntry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Some[Update.CommandRejected] = { + val statusBuilder = duplicateCommandsRejectionStatus(rejectionEntry, _) + Some( + Update.CommandRejected( + recordTime = recordTime, + completionInfo = parseCompletionInfo( + Conversions.parseInstant(recordTime), + rejectionEntry.getSubmitterInfo, + ), + reasonTemplate = FinalReason( + errorVersionSwitch.choose( + statusBuilder(Code.ALREADY_EXISTS), + statusBuilder(Code.FAILED_PRECONDITION), // Unexpired dedup key + ) + ), + ) + ) + } + + def reasonNotSetStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus(entry, _, "No reason set for rejection") + errorVersionSwitch.choose( + statusBuilder(Code.UNKNOWN), + statusBuilder(Code.INTERNAL), // We should always set a reason + ) + } + + def invalidParticipantStateStatus( + entry: DamlTransactionRejectionEntry, + rejection: InvalidParticipantState, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Disputed: ${rejection.getDetails}", + rejection.getMetadataMap.asScala.toMap, + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.INTERNAL), + ) + } + + def partiesNotKnownOnLedgerStatus( + entry: DamlTransactionRejectionEntry, + rejection: PartiesNotKnownOnLedger, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + val parties = rejection.getPartiesList + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Party not known on ledger: Parties not known on ledger ${parties.asScala.mkString("[", ",", "]")}", + Map("parties" -> objectToJsonString(parties)), + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.FAILED_PRECONDITION), // The party may become known at a later time + ) + } + + def submittingPartyNotKnownOnLedgerStatus( + entry: DamlTransactionRejectionEntry, + rejection: SubmittingPartyNotKnownOnLedger, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Party not known on ledger: Submitting party '${rejection.getSubmitterParty}' not known", + Map("submitter_party" -> rejection.getSubmitterParty), + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.FAILED_PRECONDITION), // The party may become known at a later time + ) + } + + def partyNotKnownOnLedgerStatus( + entry: DamlTransactionRejectionEntry, + rejection: PartyNotKnownOnLedger, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Party not known on ledger: ${rejection.getDetails}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.FAILED_PRECONDITION), // The party may become known at a later time + ) + } + + def causalMonotonicityViolatedStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + "Invalid ledger time: Causal monotonicity violated", + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def recordTimeOutOfRangeStatus( + entry: DamlTransactionRejectionEntry, + rejection: RecordTimeOutOfRange, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Invalid ledger time: Record time is outside of valid range [${rejection.getMinimumRecordTime}, ${rejection.getMaximumRecordTime}]", + Map( + "minimum_record_time" -> Instant + .ofEpochSecond( + rejection.getMinimumRecordTime.getSeconds, + rejection.getMinimumRecordTime.getNanos.toLong, + ) + .toString, + "maximum_record_time" -> Instant + .ofEpochSecond( + rejection.getMaximumRecordTime.getSeconds, + rejection.getMaximumRecordTime.getNanos.toLong, + ) + .toString, + ), + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def missingInputStateStatus( + entry: DamlTransactionRejectionEntry, + rejection: MissingInputState, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Inconsistent: Missing input state for key ${rejection.getKey.toString}", + Map("key" -> rejection.getKey.toString), + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.INTERNAL), // The inputs should have been provided by the participant + ) + } + + def externallyInconsistentKeysStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Inconsistent: ${ExternallyInconsistentTransaction.InconsistentKeys.description}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def externallyDuplicateKeysStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Inconsistent: ${ExternallyInconsistentTransaction.DuplicateKeys.description}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def externallyInconsistentContractsStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Inconsistent: ${ExternallyInconsistentTransaction.InconsistentContracts.description}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def internallyInconsistentKeysStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Disputed: ${InternallyInconsistentTransaction.InconsistentKeys.description}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.INTERNAL), // Should have been caught by the participant + ) + } + + def internallyDuplicateKeysStatus( + entry: DamlTransactionRejectionEntry, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Disputed: ${InternallyInconsistentTransaction.DuplicateKeys.description}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.INTERNAL), // Should have been caught by the participant + ) + } + + def validationFailureStatus( + entry: DamlTransactionRejectionEntry, + rejection: ValidationFailure, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + val statusBuilder = disputedStatusBuilder(entry, rejection.getDetails) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.INTERNAL), // Should have been caught by the participant + ) + } + + def duplicateCommandStatus( + entry: DamlTransactionRejectionEntry + ): Status = buildStatus( + entry, + Code.ALREADY_EXISTS, + "Duplicate commands", + ) + + def resourceExhaustedStatus( + entry: DamlTransactionRejectionEntry, + rejection: ResourcesExhausted, + ): Status = buildStatus( + entry, + Code.RESOURCE_EXHAUSTED, + s"Resources exhausted: ${rejection.getDetails}", + ) + + def inconsistentStatus( + entry: DamlTransactionRejectionEntry, + rejection: Inconsistent, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + def statusBuilder: Code => Status = buildStatus( + entry, + _, + s"Inconsistent: ${rejection.getDetails}", + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + def submitterCannotActViaParticipantStatus( + entry: DamlTransactionRejectionEntry, + rejection: SubmitterCannotActViaParticipant, + ): Status = buildStatus( + entry, + Code.PERMISSION_DENIED, + s"Submitter cannot act via participant: ${rejection.getDetails}", + Map( + "submitter_party" -> rejection.getSubmitterParty, + "participant_id" -> rejection.getParticipantId, + ), + ) + + def disputedStatus( + entry: DamlTransactionRejectionEntry, + rejection: Disputed, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + val statusBuilder = disputedStatusBuilder(entry, rejection.getDetails) + errorVersionSwitch.choose( + statusBuilder(Code.INVALID_ARGUMENT), + statusBuilder(Code.INTERNAL), // Should have been caught by the participant + ) + } + + def invalidLedgerTimeStatus( + entry: DamlTransactionRejectionEntry, + rejection: InvalidLedgerTime, + errorVersionSwitch: ValueSwitch[Status], + ): Status = { + val statusBuilder = buildStatus( + entry, + _, + s"Invalid ledger time: ${rejection.getDetails}", + Map( + "ledger_time" -> rejection.getLedgerTime.toString, + "lower_bound" -> rejection.getLowerBound.toString, + "upper_bound" -> rejection.getUpperBound.toString, + ), + ) + errorVersionSwitch.choose( + statusBuilder(Code.ABORTED), + statusBuilder(Code.FAILED_PRECONDITION), // May succeed at a later time + ) + } + + private def buildStatus( + entry: DamlTransactionRejectionEntry, + code: Code, + message: String, + additionalMetadata: Map[String, String] = Map.empty, + ) = Status.of( + code.value, + message, + Seq( + AnyProto.pack[ErrorInfo]( + ErrorInfo(metadata = + additionalMetadata + (GrpcStatuses.DefiniteAnswerKey -> entry.getDefiniteAnswer.toString) + ) + ) + ), + ) + + private def invalidRecordTimeRejectionStatus( + rejectionEntry: DamlTransactionRejectionEntry, + reason: CorrelationId, + errorCode: Code, + ) = Status.of( + errorCode.value, + reason, + Seq( + AnyProto.pack[ErrorInfo]( + ErrorInfo(metadata = + Map( + GrpcStatuses.DefiniteAnswerKey -> rejectionEntry.getDefiniteAnswer.toString + ) + ) + ) + ), + ) + + private def duplicateCommandsRejectionStatus( + rejectionEntry: DamlTransactionRejectionEntry, + errorCode: Code, + ) = Status.of( + errorCode.value, + "Duplicate commands", + Seq( + AnyProto.pack[ErrorInfo]( + // the definite answer is false, as the rank-based deduplication is not yet implemented + ErrorInfo(metadata = + Map( + GrpcStatuses.DefiniteAnswerKey -> rejectionEntry.getDefiniteAnswer.toString + ) + ) + ) + ), + ) + + private def objectToJsonString(obj: Object): String = { + val stringWriter = new StringWriter + val objectMapper = new ObjectMapper + objectMapper.writeValue(stringWriter, obj) + stringWriter.toString + } + + private def disputedStatusBuilder( + entry: DamlTransactionRejectionEntry, + rejectionString: String, + ): Code => Status = buildStatus( + entry, + _, + s"Disputed: $rejectionString", + ) +} diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/ConversionsSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/ConversionsSpec.scala index dc87612491da..95600457dc3a 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/ConversionsSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/ConversionsSpec.scala @@ -3,8 +3,9 @@ package com.daml.ledger.participant.state.kvutils -import java.time.{Duration, Instant} +import com.daml.error.ValueSwitch +import java.time.{Duration, Instant} import com.daml.ledger.api.DeduplicationPeriod import com.daml.ledger.configuration.LedgerTimeModel import com.daml.ledger.participant.state.kvutils.Conversions._ @@ -34,6 +35,7 @@ import com.daml.lf.value.ValueOuterClass import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.{TextFormat, Timestamp} import com.google.rpc.error_details.ErrorInfo +import com.google.rpc.status.Status import io.grpc.Status.Code import org.scalatest.OptionValues import org.scalatest.matchers.should.Matchers @@ -226,7 +228,7 @@ class ConversionsSpec extends AnyWordSpec with Matchers with OptionValues { ) .build() val finalReason = Conversions - .decodeTransactionRejectionEntry(encodedEntry) + .decodeTransactionRejectionEntry(encodedEntry, v1ErrorSwitch) .value finalReason.code shouldBe expectedCode.value() finalReason.definiteAnswer shouldBe false @@ -275,7 +277,7 @@ class ConversionsSpec extends AnyWordSpec with Matchers with OptionValues { ) .build() val finalReason = Conversions - .decodeTransactionRejectionEntry(encodedEntry) + .decodeTransactionRejectionEntry(encodedEntry, v1ErrorSwitch) .value finalReason.definiteAnswer shouldBe false val actualDetails = finalReasonToDetails(finalReason).toMap @@ -352,7 +354,8 @@ class ConversionsSpec extends AnyWordSpec with Matchers with OptionValues { val finalReason = Conversions .decodeTransactionRejectionEntry( rejectionBuilder(DamlTransactionRejectionEntry.newBuilder()) - .build() + .build(), + v1ErrorSwitch, ) .value finalReason.code shouldBe code.value() @@ -480,6 +483,8 @@ class ConversionsSpec extends AnyWordSpec with Matchers with OptionValues { private[this] val txVersion = TransactionVersion.StableVersions.max + private[this] val v1ErrorSwitch = new ValueSwitch[Status](enableSelfServiceErrorCodes = false) + private def deduplicationKeyBytesFor(parties: List[String]): Array[Byte] = { val submitterInfo = DamlSubmitterInfo.newBuilder .setApplicationId("test") diff --git a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumptionSpec.scala b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumptionSpec.scala index 96b115e92cb9..d92db60916cb 100644 --- a/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumptionSpec.scala +++ b/ledger/participant-state/kvutils/src/test/suite/scala/com/daml/ledger/participant/state/kvutils/KeyValueConsumptionSpec.scala @@ -180,7 +180,7 @@ class KeyValueConsumptionSpec extends AnyWordSpec with Matchers { TRANSACTION_REJECTION_ENTRY, definiteAnswer = Some(true), ) - val actual = outOfTimeBoundsEntryToUpdate(aRecordTime, inputEntry) + val actual = outOfTimeBoundsEntryToUpdate(aRecordTime, inputEntry, v1ErrorSwitch) inside(actual) { case Some(CommandRejected(_, _, FinalReason(status))) => status.code shouldBe Code.ALREADY_EXISTS.value status.details shouldBe Seq( @@ -360,10 +360,10 @@ class KeyValueConsumptionSpec extends AnyWordSpec with Matchers { val inputEntry = buildOutOfTimeBoundsEntry(timeBounds, logEntryType) if (assertions.throwsInternalError) { assertThrows[Err.InternalError]( - outOfTimeBoundsEntryToUpdate(recordTime, inputEntry) + outOfTimeBoundsEntryToUpdate(recordTime, inputEntry, v1ErrorSwitch) ) } else { - val actual = outOfTimeBoundsEntryToUpdate(recordTime, inputEntry) + val actual = outOfTimeBoundsEntryToUpdate(recordTime, inputEntry, v1ErrorSwitch) assertions.verify(actual) () } @@ -439,4 +439,6 @@ class KeyValueConsumptionSpec extends AnyWordSpec with Matchers { } builder.build } + + private[this] val v1ErrorSwitch = new ValueSwitch[Status](enableSelfServiceErrorCodes = false) }