update canton to 3.0.0-snapshot.100000000.20240301.12744.0.v046ec13c (#18624)

* update canton to 3.0.0-snapshot.100000000.20240301.12744.0.v046ec13c

tell-slack: canton

* fix

---------

Co-authored-by: Azure Pipelines Daml Build <support@digitalasset.com>
Co-authored-by: Remy Haemmerle <Remy.Haemmerle@daml.com>
This commit is contained in:
azure-pipelines[bot] 2024-03-04 17:52:32 +01:00 committed by GitHub
parent 6e74d0d4bc
commit ee73911caf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
130 changed files with 1737 additions and 2031 deletions

View File

@ -428,6 +428,7 @@ scala_library(
"@maven//:com_github_blemale_scaffeine_2_13", "@maven//:com_github_blemale_scaffeine_2_13",
"@maven//:com_github_pathikrit_better_files_2_13", "@maven//:com_github_pathikrit_better_files_2_13",
"@maven//:com_github_pureconfig_pureconfig_core_2_13", "@maven//:com_github_pureconfig_pureconfig_core_2_13",
"@maven//:com_google_crypto_tink_tink",
"@maven//:com_google_guava_guava", "@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_protobuf_protobuf_java",
"@maven//:com_lihaoyi_fansi_2_13", "@maven//:com_lihaoyi_fansi_2_13",

View File

@ -27,7 +27,9 @@ message MemberTrafficStatus {
// Total extra traffic consumed // Total extra traffic consumed
uint64 total_extra_traffic_consumed = 3; uint64 total_extra_traffic_consumed = 3;
// Current and future top up events that have been registered but are not necessarily active yet // Current and future top up events that have been registered but are not necessarily active yet
repeated TopUpEvent top_up_events = 4; repeated TopUpEvent top_up_events = 4; // TODO(i17477): Was never used, remove when we're done with the rework
// Timestamp at which the status is valid // Timestamp at which the status is valid
google.protobuf.Timestamp ts = 5; google.protobuf.Timestamp ts = 5;
// Serial number of the balance (total_extra_traffic_limit) of this status
google.protobuf.UInt32Value balance_serial = 6;
} }

View File

@ -5,7 +5,7 @@ package com.digitalasset.canton.admin.api.client.commands
import cats.syntax.either.* import cats.syntax.either.*
import cats.syntax.traverse.* import cats.syntax.traverse.*
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.domain.admin import com.digitalasset.canton.domain.admin
import com.digitalasset.canton.domain.sequencing.sequencer.SequencerPruningStatus import com.digitalasset.canton.domain.sequencing.sequencer.SequencerPruningStatus
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerTrafficStatus import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerTrafficStatus
@ -73,7 +73,7 @@ object SequencerAdminCommands {
final case class SetTrafficControlBalance( final case class SetTrafficControlBalance(
member: Member, member: Member,
newTrafficBalance: NonNegativeLong, newTrafficBalance: NonNegativeLong,
serial: NonNegativeLong, serial: PositiveInt,
) extends BaseSequencerAdministrationCommands[ ) extends BaseSequencerAdministrationCommands[
admin.v30.SetTrafficBalanceRequest, admin.v30.SetTrafficBalanceRequest,
admin.v30.SetTrafficBalanceResponse, admin.v30.SetTrafficBalanceResponse,

View File

@ -17,7 +17,7 @@ import com.digitalasset.canton.topology.store.{
StoredTopologyTransactionX, StoredTopologyTransactionX,
StoredTopologyTransactionsX, StoredTopologyTransactionsX,
} }
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.PositiveSignedTopologyTransactionX import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
import com.digitalasset.canton.topology.transaction.{ import com.digitalasset.canton.topology.transaction.{
SignedTopologyTransactionX, SignedTopologyTransactionX,
TopologyChangeOpX, TopologyChangeOpX,
@ -43,13 +43,13 @@ class SequencerXSetupGroup(parent: ConsoleCommandGroup) extends ConsoleCommandGr
"This is called as part of the domain.setup.bootstrap command, so you are unlikely to need to call this directly." "This is called as part of the domain.setup.bootstrap command, so you are unlikely to need to call this directly."
) )
def assign_from_beginning( def assign_from_beginning(
genesisState: Seq[PositiveSignedTopologyTransactionX], genesisState: Seq[GenericSignedTopologyTransactionX],
domainParameters: StaticDomainParameters, domainParameters: StaticDomainParameters,
): InitializeSequencerResponseX = ): InitializeSequencerResponseX =
consoleEnvironment.run { consoleEnvironment.run {
runner.adminCommand( runner.adminCommand(
InitializeX( InitializeX(
StoredTopologyTransactionsX[TopologyChangeOpX.Replace, TopologyMappingX]( StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX](
genesisState.map(signed => genesisState.map(signed =>
StoredTopologyTransactionX( StoredTopologyTransactionX(
SequencedTime(SignedTopologyTransactionX.InitialTopologySequencingTime), SequencedTime(SignedTopologyTransactionX.InitialTopologySequencingTime),
@ -67,7 +67,7 @@ class SequencerXSetupGroup(parent: ConsoleCommandGroup) extends ConsoleCommandGr
@Help.Summary( @Help.Summary(
"Dynamically initialize a sequencer from a point later than the beginning of the event stream." + "Dynamically initialize a sequencer from a point later than the beginning of the event stream." +
"This is called as part of the domain.setup.onboard_new_sequencer command, so you are unlikely to need to call this directly." "This is called as part of the sequencer.setup.onboard_new_sequencer command, so you are unlikely to need to call this directly."
) )
def assign_from_snapshot( def assign_from_snapshot(
topologySnapshot: GenericStoredTopologyTransactionsX, topologySnapshot: GenericStoredTopologyTransactionsX,

View File

@ -42,6 +42,7 @@ import com.digitalasset.canton.topology.store.{
StoredTopologyTransactionX, StoredTopologyTransactionX,
StoredTopologyTransactionsX, StoredTopologyTransactionsX,
TimeQuery, TimeQuery,
TopologyStoreId,
} }
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
@ -1055,12 +1056,13 @@ class TopologyAdministrationGroup(
store: String = AuthorizedStore.filterName, store: String = AuthorizedStore.filterName,
): SignedTopologyTransactionX[TopologyChangeOpX, PartyToParticipantX] = { ): SignedTopologyTransactionX[TopologyChangeOpX, PartyToParticipantX] = {
val currentO = expectAtMostOneResult( val currentO = TopologyStoreId(store) match {
list( case TopologyStoreId.DomainStore(domainId, _) =>
filterStore = store, expectAtMostOneResult(list(domainId, filterParty = party.filterString))
filterParty = party.filterString,
).filter(_.item.domainId == domainId) case TopologyStoreId.AuthorizedStore =>
) expectAtMostOneResult(list_from_authorized(filterParty = party.filterString))
}
val (existingPermissions, newSerial) = currentO match { val (existingPermissions, newSerial) = currentO match {
case Some(current) if current.context.operation == TopologyChangeOpX.Remove => case Some(current) if current.context.operation == TopologyChangeOpX.Remove =>
@ -1158,14 +1160,13 @@ class TopologyAdministrationGroup(
synchronisation.runAdminCommand(synchronize)(command) synchronisation.runAdminCommand(synchronize)(command)
} }
@Help.Summary("List party to participant mapping transactions") @Help.Summary("List party to participant mapping transactions from domain store")
@Help.Description( @Help.Description(
"""List the party to participant mapping transactions present in the stores. Party to participant mappings """List the party to participant mapping transactions present in the stores. Party to participant mappings
|are topology transactions used to allocate a party to certain participants. The same party can be allocated |are topology transactions used to allocate a party to certain participants. The same party can be allocated
|on several participants with different privileges. |on several participants with different privileges.
filterStore: - "Authorized": Look in the node's authorized store. domainId: Domain to be considered
- "<domain-id>": Look in the specified domain store.
proposals: Whether to query proposals instead of authorized transactions. proposals: Whether to query proposals instead of authorized transactions.
timeQuery: The time query allows to customize the query by time. The following options are supported: timeQuery: The time query allows to customize the query by time. The following options are supported:
TimeQuery.HeadState (default): The most recent known state. TimeQuery.HeadState (default): The most recent known state.
@ -1179,7 +1180,7 @@ class TopologyAdministrationGroup(
|""" |"""
) )
def list( def list(
filterStore: String = "", domain: DomainId,
proposals: Boolean = false, proposals: Boolean = false,
timeQuery: TimeQuery = TimeQuery.HeadState, timeQuery: TimeQuery = TimeQuery.HeadState,
operation: Option[TopologyChangeOpX] = None, operation: Option[TopologyChangeOpX] = None,
@ -1191,7 +1192,93 @@ class TopologyAdministrationGroup(
adminCommand( adminCommand(
TopologyAdminCommandsX.Read.ListPartyToParticipant( TopologyAdminCommandsX.Read.ListPartyToParticipant(
BaseQueryX( BaseQueryX(
filterStore, filterStore = domain.filterString,
proposals,
timeQuery,
operation,
filterSigningKey,
protocolVersion.map(ProtocolVersion.tryCreate),
),
filterParty,
filterParticipant,
)
)
}
@Help.Summary("List party to participant mapping transactions from the authorized store")
@Help.Description(
"""List the party to participant mapping transactions present in the stores. Party to participant mappings
|are topology transactions used to allocate a party to certain participants. The same party can be allocated
|on several participants with different privileges.
proposals: Whether to query proposals instead of authorized transactions.
timeQuery: The time query allows to customize the query by time. The following options are supported:
TimeQuery.HeadState (default): The most recent known state.
TimeQuery.Snapshot(ts): The state at a certain point in time.
TimeQuery.Range(fromO, toO): Time-range of when the transaction was added to the store
operation: Optionally, what type of operation the transaction should have.
filterParty: Filter for parties starting with the given filter string.
filterParticipant: Filter for participants starting with the given filter string.
filterSigningKey: Filter for transactions that are authorized with a key that starts with the given filter string.
protocolVersion: Export the topology transactions in the optional protocol version.
|"""
)
def list_from_authorized(
proposals: Boolean = false,
timeQuery: TimeQuery = TimeQuery.HeadState,
operation: Option[TopologyChangeOpX] = None,
filterParty: String = "",
filterParticipant: String = "",
filterSigningKey: String = "",
protocolVersion: Option[String] = None,
): Seq[ListPartyToParticipantResult] = consoleEnvironment.run {
adminCommand(
TopologyAdminCommandsX.Read.ListPartyToParticipant(
BaseQueryX(
filterStore = AuthorizedStore.filterName,
proposals,
timeQuery,
operation,
filterSigningKey,
protocolVersion.map(ProtocolVersion.tryCreate),
),
filterParty,
filterParticipant,
)
)
}
@Help.Summary("List party to participant mapping transactions from all stores")
@Help.Description(
"""List the party to participant mapping transactions present in the stores. Party to participant mappings
|are topology transactions used to allocate a party to certain participants. The same party can be allocated
|on several participants with different privileges.
proposals: Whether to query proposals instead of authorized transactions.
timeQuery: The time query allows to customize the query by time. The following options are supported:
TimeQuery.HeadState (default): The most recent known state.
TimeQuery.Snapshot(ts): The state at a certain point in time.
TimeQuery.Range(fromO, toO): Time-range of when the transaction was added to the store
operation: Optionally, what type of operation the transaction should have.
filterParty: Filter for parties starting with the given filter string.
filterParticipant: Filter for participants starting with the given filter string.
filterSigningKey: Filter for transactions that are authorized with a key that starts with the given filter string.
protocolVersion: Export the topology transactions in the optional protocol version.
|"""
)
def list_from_all(
proposals: Boolean = false,
timeQuery: TimeQuery = TimeQuery.HeadState,
operation: Option[TopologyChangeOpX] = None,
filterParty: String = "",
filterParticipant: String = "",
filterSigningKey: String = "",
protocolVersion: Option[String] = None,
): Seq[ListPartyToParticipantResult] = consoleEnvironment.run {
adminCommand(
TopologyAdminCommandsX.Read.ListPartyToParticipant(
BaseQueryX(
filterStore = "",
proposals, proposals,
timeQuery, timeQuery,
operation, operation,

View File

@ -5,55 +5,27 @@ syntax = "proto3";
package com.digitalasset.canton.protocol.v30; package com.digitalasset.canton.protocol.v30;
import "google/protobuf/empty.proto"; import "google/rpc/status.proto";
import "scalapb/scalapb.proto"; import "scalapb/scalapb.proto";
// Definition of the ConfirmationResponse message which is shared between the transaction and transfer protocol // Definition of the ConfirmationResponse message which is shared between the transaction and transfer protocol
message LocalVerdict { message LocalVerdict {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.StableProtoVersion"; option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion";
oneof some_local_verdict { VerdictCode code = 1;
google.protobuf.Empty local_approve = 1; google.rpc.Status reason = 2; // ok iff code is approve
LocalReject local_reject = 2;
enum VerdictCode {
VERDICT_CODE_UNSPECIFIED = 0;
VERDICT_CODE_LOCAL_APPROVE = 1;
VERDICT_CODE_LOCAL_REJECT = 2;
VERDICT_CODE_LOCAL_MALFORMED = 3;
} }
} }
message LocalReject {
enum Code {
CODE_UNSPECIFIED = 0;
CODE_LOCKED_CONTRACTS = 1;
CODE_LOCKED_KEYS = 2;
CODE_INACTIVE_CONTRACTS = 3;
CODE_DUPLICATE_KEY = 4;
CODE_CREATES_EXISTING_CONTRACT = 5;
CODE_LEDGER_TIME = 6;
CODE_SUBMISSION_TIME = 7;
CODE_LOCAL_TIMEOUT = 8;
CODE_MALFORMED_PAYLOADS = 9;
CODE_MALFORMED_MODEL = 10;
CODE_MALFORMED_CONFIRMATION_POLICY = 11;
CODE_BAD_ROOT_HASH_MESSAGE = 12;
CODE_TRANSFER_OUT_ACTIVENESS_CHECK = 13;
CODE_TRANSFER_IN_ALREADY_COMPLETED = 14;
CODE_TRANSFER_IN_ALREADY_ACTIVE = 15;
CODE_TRANSFER_IN_ALREADY_ARCHIVED = 16;
CODE_TRANSFER_IN_LOCKED = 17;
CODE_INCONSISTENT_KEY = 18;
}
// cause_prefix + details constitute the cause of the rejection.
string cause_prefix = 1;
string details = 2;
repeated string resource = 3; // affected resources
string error_code = 5;
uint32 error_category = 6;
}
message ConfirmationResponse { message ConfirmationResponse {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.StableProtoVersion"; option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion";
int64 request_id = 1; // in microseconds of UTC time since Unix epoch int64 request_id = 1; // in microseconds of UTC time since Unix epoch
string sender = 2; string sender = 2;

View File

@ -14,19 +14,6 @@ import "scalapb/scalapb.proto";
// Messages related to the transaction or transfer protocol sent by a mediator // Messages related to the transaction or transfer protocol sent by a mediator
message MediatorRejection {
enum Code {
CODE_UNSPECIFIED = 0;
CODE_INFORMEES_NOT_HOSTED_ON_ACTIVE_PARTICIPANT = 1;
CODE_NOT_ENOUGH_CONFIRMING_PARTIES = 2;
CODE_VIEW_THRESHOLD_BELOW_MINIMUM_THRESHOLD = 3;
CODE_VIEW_INVALID_ROOT_HASH_MESSAGE = 4;
CODE_TIMEOUT = 5;
CODE_WRONG_DECLARED_MEDIATOR = 6;
CODE_NON_UNIQUE_REQUEST_UUID = 7;
}
}
message InformeeTree { message InformeeTree {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.StableProtoVersion"; option (scalapb.message).companion_extends = "com.digitalasset.canton.version.StableProtoVersion";
@ -39,20 +26,12 @@ message ParticipantReject {
message RejectionReason { message RejectionReason {
repeated string parties = 1; repeated string parties = 1;
com.digitalasset.canton.protocol.v30.LocalReject reject = 2; com.digitalasset.canton.protocol.v30.LocalVerdict reject = 2;
} }
message MediatorReject { message MediatorReject {
google.rpc.Status reason = 1; // Must not be OK google.rpc.Status reason = 1; // Must not be OK
} bool is_malformed = 2; // True if the request has been recognized as malformed.
message MalformedMediatorConfirmationRequestResult {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion";
int64 request_id = 1; // in microseconds of UTC time since Unix epoch
string domain_id = 2;
com.digitalasset.canton.protocol.v30.ViewType view_type = 3;
MediatorReject rejection = 4;
} }
message Verdict { message Verdict {
@ -65,23 +44,14 @@ message Verdict {
} }
} }
message TransactionResultMessage { // This covers transactions and transfers
message ConfirmationResultMessage {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion"; option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion";
int64 request_id = 1; // in microseconds of UTC time since Unix epoch string domain_id = 1;
Verdict verdict = 2; com.digitalasset.canton.protocol.v30.ViewType view_type = 2;
bytes root_hash = 3; int64 request_id = 3;
string domain_id = 4; bytes root_hash = 4;
}
message TransferResult {
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.UnstableProtoVersion";
int64 request_id = 1; // in microseconds of UTC time since Unix epoch
oneof domain {
string source_domain = 2; // result for transfer-out request
string target_domain = 3; // result for transfer-in request
}
repeated string informees = 4;
Verdict verdict = 5; Verdict verdict = 5;
repeated string informees = 6; // empty for transactions
} }

View File

@ -18,11 +18,9 @@ message TypedSignedProtocolMessageContent {
oneof some_signed_protocol_message { oneof some_signed_protocol_message {
bytes confirmation_response = 2; bytes confirmation_response = 2;
bytes transaction_result = 3; bytes confirmation_result = 3;
bytes malformed_mediator_confirmation_request_result = 4; bytes acs_commitment = 4;
bytes transfer_result = 5; bytes set_traffic_balance = 5;
bytes acs_commitment = 6;
bytes set_traffic_balance = 7;
} }
} }

View File

@ -33,7 +33,7 @@ message SetTrafficBalanceMessage {
// Member to update the balance for // Member to update the balance for
string member = 1; string member = 1;
// Serial number - must be unique and monotonically increasing for each new balance update // Serial number - must be unique and monotonically increasing for each new balance update
int64 serial = 2; uint32 serial = 2;
// New total traffic balance // New total traffic balance
uint64 total_traffic_balance = 4; uint64 total_traffic_balance = 4;
// Domain Id // Domain Id

View File

@ -80,6 +80,15 @@ trait CryptoPureApi
with RandomOps with RandomOps
with PasswordBasedEncryptionOps with PasswordBasedEncryptionOps
sealed trait CryptoPureApiError extends Product with Serializable with PrettyPrinting
object CryptoPureApiError {
final case class KeyParseAndValidateError(error: String) extends CryptoPureApiError {
override def pretty: Pretty[KeyParseAndValidateError] = prettyOfClass(
unnamedParam(_.error.unquoted)
)
}
}
trait CryptoPrivateApi extends EncryptionPrivateOps with SigningPrivateOps trait CryptoPrivateApi extends EncryptionPrivateOps with SigningPrivateOps
trait CryptoPrivateStoreApi trait CryptoPrivateStoreApi
extends CryptoPrivateApi extends CryptoPrivateApi

View File

@ -0,0 +1,119 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.crypto
import cats.syntax.either.*
import com.digitalasset.canton.crypto.CryptoPureApiError.KeyParseAndValidateError
import com.digitalasset.canton.crypto.provider.jce.JceJavaConverter
import com.digitalasset.canton.crypto.provider.tink.TinkKeyFormat
import com.google.crypto.tink.KeysetHandle
import com.google.crypto.tink.proto.OutputPrefixType
import java.security.PublicKey as JPublicKey
import scala.collection.concurrent.TrieMap
object CryptoKeyValidation {
// TODO(#15632): Make this a real cache with an eviction rule
// keeps track of the public keys that have been validated
private lazy val validatedPublicKeys: TrieMap[PublicKey, Either[KeyParseAndValidateError, Unit]] =
TrieMap.empty
private[crypto] def parseAndValidateTinkKey(
publicKey: PublicKey
): Either[KeyParseAndValidateError, KeysetHandle] =
for {
/* deserialize the public key using Tink and only then regenerate fingerprint. If the
* deserialization fails, the format is not correct and we fail validation.
*/
handle <- TinkKeyFormat
.deserializeHandle(publicKey.key)
.leftMap(err => KeyParseAndValidateError(err.show))
fingerprint <- TinkKeyFormat
.fingerprint(handle, HashAlgorithm.Sha256)
.leftMap(KeyParseAndValidateError)
_ <- Either.cond(
fingerprint == publicKey.id,
(),
KeyParseAndValidateError(
s"The regenerated fingerprint $fingerprint does not match the fingerprint of the object: ${publicKey.id}"
),
)
outputPrefixType = handle.getKeysetInfo.getKeyInfo(0).getOutputPrefixType
_ <- Either.cond(
outputPrefixType == OutputPrefixType.RAW,
(),
KeyParseAndValidateError(
s"Wrong output prefix type: expected RAW got $outputPrefixType"
),
)
} yield handle
private[crypto] def parseAndValidateDerOrRawKey(
publicKey: PublicKey
): Either[KeyParseAndValidateError, JPublicKey] = {
val fingerprint = Fingerprint.create(publicKey.key)
for {
// the fingerprint must be regenerated before we convert the key
_ <- Either.cond(
fingerprint == publicKey.id,
(),
KeyParseAndValidateError(
s"The regenerated fingerprint $fingerprint does not match the fingerprint of the object: ${publicKey.id}"
),
)
// we try to convert the key to a Java key to ensure the format is correct
jceJavaConverter = new JceJavaConverter(
SigningKeyScheme.allSchemes,
EncryptionKeyScheme.allSchemes,
)
javaPublicKeyAndAlgorithm <- jceJavaConverter
.toJava(publicKey)
.leftMap(err => KeyParseAndValidateError(err.show))
(_, javaPublicKey) = javaPublicKeyAndAlgorithm
} yield javaPublicKey
}
// TODO(#15634): Verify crypto scheme as part of key validation
/** Parses and validates a public key. This includes recomputing the fingerprint and verifying that it matches the
* id of the key, as well as validating its format.
* We store the validation results in a cache.
*/
private[crypto] def parseAndValidatePublicKey[E](
publicKey: PublicKey,
errFn: String => E,
): Either[E, Unit] = {
val parseRes = publicKey.format match {
case CryptoKeyFormat.Tink =>
/* We check the cache first and if it's not there we:
* 1. deserialize handle (and consequently check the key format); 2. check fingerprint
*/
parseAndValidateTinkKey(publicKey).map(_ => ())
case CryptoKeyFormat.Der | CryptoKeyFormat.Raw =>
/* We check the cache first and if it's not there we:
* 1. check fingerprint; 2. convert to Java Key (and consequently check the key format)
*/
parseAndValidateDerOrRawKey(publicKey).map(_ => ())
case CryptoKeyFormat.Symbolic =>
Right(())
}
// If the result is already in the cache it means the key has already been validated.
validatedPublicKeys
.getOrElseUpdate(publicKey, parseRes)
.leftMap(err => errFn(s"Failed to deserialize ${publicKey.format} public key: $err"))
}
private[crypto] def ensureFormat[E](
actual: CryptoKeyFormat,
acceptedFormats: Set[CryptoKeyFormat],
errFn: String => E,
): Either[E, Unit] =
Either.cond(
acceptedFormats.contains(actual),
(),
errFn(s"Expected key formats $acceptedFormats, but got $actual"),
)
}

View File

@ -18,7 +18,11 @@ import com.digitalasset.canton.crypto.store.{
import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups} import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups}
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.serialization.{DeserializationError, ProtoConverter} import com.digitalasset.canton.serialization.{
DefaultDeserializationError,
DeserializationError,
ProtoConverter,
}
import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.* import com.digitalasset.canton.util.*
import com.digitalasset.canton.version.{ import com.digitalasset.canton.version.{
@ -29,7 +33,10 @@ import com.digitalasset.canton.version.{
ProtoVersion, ProtoVersion,
ProtocolVersion, ProtocolVersion,
} }
import com.google.common.annotations.VisibleForTesting
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import monocle.Lens
import monocle.macros.GenLens
import slick.jdbc.GetResult import slick.jdbc.GetResult
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -349,6 +356,15 @@ object EncryptionKeyPair {
new EncryptionKeyPair(publicKey, privateKey) new EncryptionKeyPair(publicKey, privateKey)
} }
@VisibleForTesting
def wrongEncryptionKeyPairWithPublicKeyUnsafe(
publicKey: EncryptionPublicKey
): EncryptionKeyPair = {
val privateKey =
new EncryptionPrivateKey(publicKey.id, publicKey.format, publicKey.key, publicKey.scheme)
new EncryptionKeyPair(publicKey, privateKey)
}
def fromProtoV30( def fromProtoV30(
encryptionKeyPairP: v30.EncryptionKeyPair encryptionKeyPairP: v30.EncryptionKeyPair
): ParsingResult[EncryptionKeyPair] = ): ParsingResult[EncryptionKeyPair] =
@ -373,11 +389,20 @@ final case class EncryptionPublicKey private[crypto] (
scheme: EncryptionKeyScheme, scheme: EncryptionKeyScheme,
) extends PublicKey ) extends PublicKey
with PrettyPrinting with PrettyPrinting
with HasVersionedWrapper[EncryptionPublicKey] with HasVersionedWrapper[EncryptionPublicKey] {
with NoCopy {
override protected def companionObj = EncryptionPublicKey override protected def companionObj = EncryptionPublicKey
// TODO(#15649): Make EncryptionPublicKey object invariant
protected def validated: Either[ProtoDeserializationError.CryptoDeserializationError, this.type] =
CryptoKeyValidation
.parseAndValidatePublicKey(
this,
errMsg =>
ProtoDeserializationError.CryptoDeserializationError(DefaultDeserializationError(errMsg)),
)
.map(_ => this)
val purpose: KeyPurpose = KeyPurpose.Encryption val purpose: KeyPurpose = KeyPurpose.Encryption
def toProtoV30: v30.EncryptionPublicKey = def toProtoV30: v30.EncryptionPublicKey =
@ -398,6 +423,7 @@ final case class EncryptionPublicKey private[crypto] (
object EncryptionPublicKey object EncryptionPublicKey
extends HasVersionedMessageCompanion[EncryptionPublicKey] extends HasVersionedMessageCompanion[EncryptionPublicKey]
with HasVersionedMessageCompanionDbHelpers[EncryptionPublicKey] { with HasVersionedMessageCompanionDbHelpers[EncryptionPublicKey] {
override def name: String = "encryption public key"
val supportedProtoVersions: SupportedProtoVersions = SupportedProtoVersions( val supportedProtoVersions: SupportedProtoVersions = SupportedProtoVersions(
ProtoVersion(30) -> ProtoCodec( ProtoVersion(30) -> ProtoCodec(
ProtocolVersion.v30, ProtocolVersion.v30,
@ -406,15 +432,17 @@ object EncryptionPublicKey
) )
) )
override def name: String = "encryption public key" private[crypto] def create(
private[this] def apply(
id: Fingerprint, id: Fingerprint,
format: CryptoKeyFormat, format: CryptoKeyFormat,
key: ByteString, key: ByteString,
scheme: EncryptionKeyScheme, scheme: EncryptionKeyScheme,
): EncryptionPrivateKey = ): Either[ProtoDeserializationError.CryptoDeserializationError, EncryptionPublicKey] =
throw new UnsupportedOperationException("Use generate or deserialization methods") new EncryptionPublicKey(id, format, key, scheme).validated
@VisibleForTesting
val idUnsafe: Lens[EncryptionPublicKey, Fingerprint] =
GenLens[EncryptionPublicKey](_.id)
def fromProtoV30( def fromProtoV30(
publicKeyP: v30.EncryptionPublicKey publicKeyP: v30.EncryptionPublicKey
@ -423,7 +451,13 @@ object EncryptionPublicKey
id <- Fingerprint.fromProtoPrimitive(publicKeyP.id) id <- Fingerprint.fromProtoPrimitive(publicKeyP.id)
format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format) format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format)
scheme <- EncryptionKeyScheme.fromProtoEnum("scheme", publicKeyP.scheme) scheme <- EncryptionKeyScheme.fromProtoEnum("scheme", publicKeyP.scheme)
} yield new EncryptionPublicKey(id, format, publicKeyP.publicKey, scheme) encryptionPublicKey <- EncryptionPublicKey.create(
id,
format,
publicKeyP.publicKey,
scheme,
)
} yield encryptionPublicKey
} }
final case class EncryptionPublicKeyWithName( final case class EncryptionPublicKeyWithName(
@ -479,14 +513,6 @@ object EncryptionPrivateKey extends HasVersionedMessageCompanion[EncryptionPriva
override def name: String = "encryption private key" override def name: String = "encryption private key"
private[this] def apply(
id: Fingerprint,
format: CryptoKeyFormat,
key: ByteString,
scheme: EncryptionKeyScheme,
): EncryptionPrivateKey =
throw new UnsupportedOperationException("Use generate or deserialization methods")
def fromProtoV30( def fromProtoV30(
privateKeyP: v30.EncryptionPrivateKey privateKeyP: v30.EncryptionPrivateKey
): ParsingResult[EncryptionPrivateKey] = ): ParsingResult[EncryptionPrivateKey] =

View File

@ -15,8 +15,8 @@ import com.digitalasset.canton.crypto.store.{
} }
import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups} import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups}
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.serialization.{DefaultDeserializationError, ProtoConverter}
import com.digitalasset.canton.topology.Member import com.digitalasset.canton.topology.Member
import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.NoCopy import com.digitalasset.canton.util.NoCopy
@ -27,7 +27,10 @@ import com.digitalasset.canton.version.{
ProtoVersion, ProtoVersion,
ProtocolVersion, ProtocolVersion,
} }
import com.google.common.annotations.VisibleForTesting
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import monocle.Lens
import monocle.macros.GenLens
import slick.jdbc.GetResult import slick.jdbc.GetResult
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -290,6 +293,15 @@ object SigningKeyPair {
new SigningKeyPair(publicKey, privateKey) new SigningKeyPair(publicKey, privateKey)
} }
@VisibleForTesting
def wrongSigningKeyPairWithPublicKeyUnsafe(
publicKey: SigningPublicKey
): SigningKeyPair = {
val privateKey =
new SigningPrivateKey(publicKey.id, publicKey.format, publicKey.key, publicKey.scheme)
new SigningKeyPair(publicKey, privateKey)
}
def fromProtoV30( def fromProtoV30(
signingKeyPairP: v30.SigningKeyPair signingKeyPairP: v30.SigningKeyPair
): ParsingResult[SigningKeyPair] = ): ParsingResult[SigningKeyPair] =
@ -315,12 +327,21 @@ case class SigningPublicKey private[crypto] (
scheme: SigningKeyScheme, scheme: SigningKeyScheme,
) extends PublicKey ) extends PublicKey
with PrettyPrinting with PrettyPrinting
with NoCopy
with HasVersionedWrapper[SigningPublicKey] { with HasVersionedWrapper[SigningPublicKey] {
override val purpose: KeyPurpose = KeyPurpose.Signing override val purpose: KeyPurpose = KeyPurpose.Signing
override protected def companionObj = SigningPublicKey override protected def companionObj = SigningPublicKey
// TODO(#15649): Make SigningPublicKey object invariant
protected def validated: Either[ProtoDeserializationError.CryptoDeserializationError, this.type] =
CryptoKeyValidation
.parseAndValidatePublicKey(
this,
errMsg =>
ProtoDeserializationError.CryptoDeserializationError(DefaultDeserializationError(errMsg)),
)
.map(_ => this)
def toProtoV30: v30.SigningPublicKey = def toProtoV30: v30.SigningPublicKey =
v30.SigningPublicKey( v30.SigningPublicKey(
id = id.toProtoPrimitive, id = id.toProtoPrimitive,
@ -349,13 +370,17 @@ object SigningPublicKey
) )
) )
private[this] def apply( private[crypto] def create(
id: Fingerprint, id: Fingerprint,
format: CryptoKeyFormat, format: CryptoKeyFormat,
key: ByteString, key: ByteString,
scheme: SigningKeyScheme, scheme: SigningKeyScheme,
): SigningPrivateKey = ): Either[ProtoDeserializationError.CryptoDeserializationError, SigningPublicKey] =
throw new UnsupportedOperationException("Use keypair generate or deserialization methods") new SigningPublicKey(id, format, key, scheme).validated
@VisibleForTesting
val idUnsafe: Lens[SigningPublicKey, Fingerprint] =
GenLens[SigningPublicKey](_.id)
def fromProtoV30( def fromProtoV30(
publicKeyP: v30.SigningPublicKey publicKeyP: v30.SigningPublicKey
@ -364,7 +389,13 @@ object SigningPublicKey
id <- Fingerprint.fromProtoPrimitive(publicKeyP.id) id <- Fingerprint.fromProtoPrimitive(publicKeyP.id)
format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format) format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format)
scheme <- SigningKeyScheme.fromProtoEnum("scheme", publicKeyP.scheme) scheme <- SigningKeyScheme.fromProtoEnum("scheme", publicKeyP.scheme)
} yield new SigningPublicKey(id, format, publicKeyP.publicKey, scheme) signingPublicKey <- SigningPublicKey.create(
id,
format,
publicKeyP.publicKey,
scheme,
)
} yield signingPublicKey
def collect(initialKeys: Map[Member, Seq[PublicKey]]): Map[Member, Seq[SigningPublicKey]] = def collect(initialKeys: Map[Member, Seq[PublicKey]]): Map[Member, Seq[SigningPublicKey]] =
initialKeys.map { case (k, v) => initialKeys.map { case (k, v) =>
@ -425,14 +456,6 @@ object SigningPrivateKey extends HasVersionedMessageCompanion[SigningPrivateKey]
override def name: String = "signing private key" override def name: String = "signing private key"
private[this] def apply(
id: Fingerprint,
format: CryptoKeyFormat,
key: ByteString,
scheme: SigningKeyScheme,
): SigningPrivateKey =
throw new UnsupportedOperationException("Use keypair generate or deserialization methods")
def fromProtoV30( def fromProtoV30(
privateKeyP: v30.SigningPrivateKey privateKeyP: v30.SigningPrivateKey
): ParsingResult[SigningPrivateKey] = ): ParsingResult[SigningPrivateKey] =

View File

@ -32,22 +32,16 @@ class JceJavaConverter(
import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.ShowUtil.*
private def ensureFormat(
key: CryptoKey,
format: CryptoKeyFormat,
): Either[JavaKeyConversionError, Unit] =
Either.cond(
key.format == format,
(),
JavaKeyConversionError.UnsupportedKeyFormat(key.format, format),
)
private def toJavaEcDsa( private def toJavaEcDsa(
publicKey: PublicKey, publicKey: PublicKey,
curveType: CurveType, curveType: CurveType,
): Either[JavaKeyConversionError, (AlgorithmIdentifier, JPublicKey)] = ): Either[JavaKeyConversionError, (AlgorithmIdentifier, JPublicKey)] =
for { for {
_ <- ensureFormat(publicKey, CryptoKeyFormat.Der) _ <- CryptoKeyValidation.ensureFormat(
publicKey.format,
Set(CryptoKeyFormat.Der),
_ => JavaKeyConversionError.UnsupportedKeyFormat(publicKey.format, CryptoKeyFormat.Der),
)
// We are using the tink-subtle API here, thus using the TinkJavaConverter to have a consistent mapping of curve // We are using the tink-subtle API here, thus using the TinkJavaConverter to have a consistent mapping of curve
// type to algo id. // type to algo id.
algoId <- TinkJavaConverter algoId <- TinkJavaConverter
@ -70,7 +64,11 @@ class JceJavaConverter(
keyInstance: String, keyInstance: String,
): Either[JavaKeyConversionError, JPublicKey] = ): Either[JavaKeyConversionError, JPublicKey] =
for { for {
_ <- ensureFormat(publicKey, format) _ <- CryptoKeyValidation.ensureFormat(
publicKey.format,
Set(format),
_ => JavaKeyConversionError.UnsupportedKeyFormat(publicKey.format, format),
)
x509KeySpec = new X509EncodedKeySpec(x509PublicKey) x509KeySpec = new X509EncodedKeySpec(x509PublicKey)
keyFactory <- Either keyFactory <- Either
.catchOnly[NoSuchAlgorithmException]( .catchOnly[NoSuchAlgorithmException](

View File

@ -6,7 +6,7 @@ package com.digitalasset.canton.crypto.provider.tink
import cats.syntax.either.* import cats.syntax.either.*
import com.digitalasset.canton.crypto.{Fingerprint, HashAlgorithm} import com.digitalasset.canton.crypto.{Fingerprint, HashAlgorithm}
import com.digitalasset.canton.serialization.{DefaultDeserializationError, DeserializationError} import com.digitalasset.canton.serialization.{DefaultDeserializationError, DeserializationError}
import com.google.crypto.tink.{proto, *} import com.google.crypto.tink.*
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
class TinkKeyFingerprintException(message: String) extends RuntimeException(message) class TinkKeyFingerprintException(message: String) extends RuntimeException(message)

View File

@ -6,10 +6,11 @@ package com.digitalasset.canton.error
import com.daml.error.* import com.daml.error.*
import com.digitalasset.canton.error.CantonErrorGroups.MediatorErrorGroup import com.digitalasset.canton.error.CantonErrorGroups.MediatorErrorGroup
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.v30
import org.slf4j.event.Level import org.slf4j.event.Level
sealed trait MediatorError extends Product with Serializable with PrettyPrinting sealed trait MediatorError extends Product with Serializable with PrettyPrinting {
def isMalformed: Boolean
}
object MediatorError extends MediatorErrorGroup { object MediatorError extends MediatorErrorGroup {
@ -30,6 +31,9 @@ object MediatorError extends MediatorErrorGroup {
unresponsiveParties: String = "", unresponsiveParties: String = "",
) extends BaseCantonError.Impl(cause) ) extends BaseCantonError.Impl(cause)
with MediatorError { with MediatorError {
override def isMalformed: Boolean = false
override def pretty: Pretty[Reject] = prettyOfClass( override def pretty: Pretty[Reject] = prettyOfClass(
param("code", _.code.id.unquoted), param("code", _.code.id.unquoted),
param("cause", _.cause.unquoted), param("cause", _.cause.unquoted),
@ -62,10 +66,12 @@ object MediatorError extends MediatorErrorGroup {
override def logLevel: Level = Level.WARN override def logLevel: Level = Level.WARN
final case class Reject( final case class Reject(
override val cause: String, override val cause: String
_v0CodeP: v30.MediatorRejection.Code = v30.MediatorRejection.Code.CODE_TIMEOUT,
) extends BaseCantonError.Impl(cause) ) extends BaseCantonError.Impl(cause)
with MediatorError { with MediatorError {
override def isMalformed: Boolean = false
override def pretty: Pretty[Reject] = prettyOfClass( override def pretty: Pretty[Reject] = prettyOfClass(
param("code", _.code.id.unquoted), param("code", _.code.id.unquoted),
param("cause", _.cause.unquoted), param("cause", _.cause.unquoted),
@ -82,11 +88,13 @@ object MediatorError extends MediatorErrorGroup {
object MalformedMessage extends AlarmErrorCode("MEDIATOR_RECEIVED_MALFORMED_MESSAGE") { object MalformedMessage extends AlarmErrorCode("MEDIATOR_RECEIVED_MALFORMED_MESSAGE") {
final case class Reject( final case class Reject(
override val cause: String, override val cause: String
_v0CodeP: v30.MediatorRejection.Code = v30.MediatorRejection.Code.CODE_TIMEOUT,
) extends Alarm(cause) ) extends Alarm(cause)
with MediatorError with MediatorError
with BaseCantonError { with BaseCantonError {
override def isMalformed: Boolean = true
override def pretty: Pretty[Reject] = prettyOfClass( override def pretty: Pretty[Reject] = prettyOfClass(
param("code", _.code.id.unquoted), param("code", _.code.id.unquoted),
param("cause", _.cause.unquoted), param("cause", _.cause.unquoted),

View File

@ -6,43 +6,41 @@ package com.digitalasset.canton.protocol
import com.daml.error.* import com.daml.error.*
import com.digitalasset.canton.error.CantonErrorGroups.ParticipantErrorGroup.TransactionErrorGroup.LocalRejectionGroup import com.digitalasset.canton.error.CantonErrorGroups.ParticipantErrorGroup.TransactionErrorGroup.LocalRejectionGroup
import com.digitalasset.canton.error.{AlarmErrorCode, BaseAlarm, TransactionError} import com.digitalasset.canton.error.{AlarmErrorCode, BaseAlarm, TransactionError}
import com.digitalasset.canton.logging.pretty.Pretty import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.messages.{LocalReject, LocalVerdict} import com.digitalasset.canton.protocol.messages.{LocalReject, TransactionRejection}
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion} import com.digitalasset.canton.version.ProtocolVersion
import com.google.rpc.status.Status
import org.slf4j.event.Level import org.slf4j.event.Level
/** Base type for error codes related to local reject.
*/
trait BaseLocalRejectErrorCode {
/** The code of a LocalReject
* This is used to serialize rejections to LocalReject.
*/
def v30CodeP: v30.LocalReject.Code
}
/** Base type for ErrorCodes related to LocalReject, if the rejection does not (necessarily) occur due to malicious behavior. /** Base type for ErrorCodes related to LocalReject, if the rejection does not (necessarily) occur due to malicious behavior.
*/ */
abstract class LocalRejectErrorCode( abstract class LocalRejectErrorCode(
id: String, id: String,
category: ErrorCategory, category: ErrorCategory,
override val v30CodeP: v30.LocalReject.Code,
)(implicit parent: ErrorClass) )(implicit parent: ErrorClass)
extends ErrorCode(id, category) extends ErrorCode(id, category) {
with BaseLocalRejectErrorCode {
override implicit val code: LocalRejectErrorCode = this override implicit val code: LocalRejectErrorCode = this
} }
/** Base type for ErrorCodes related to LocalReject, if the rejection is due to malicious behavior. /** Base type for ErrorCodes related to LocalRejectError, if the rejection is due to malicious behavior.
*/ */
abstract class MalformedErrorCode(id: String, override val v30CodeP: v30.LocalReject.Code)(implicit abstract class MalformedErrorCode(id: String)(implicit
parent: ErrorClass parent: ErrorClass
) extends AlarmErrorCode(id) ) extends AlarmErrorCode(id) {
with BaseLocalRejectErrorCode {
implicit override val code: MalformedErrorCode = this implicit override val code: MalformedErrorCode = this
} }
sealed trait LocalRejectError extends LocalReject with TransactionError { sealed trait LocalRejectError
extends TransactionError
with TransactionRejection
with PrettyPrinting
with Product
with Serializable {
override def reason(): Status = rpcStatusWithoutLoggingContext()
def toLocalReject(protocolVersion: ProtocolVersion): LocalReject =
LocalReject.create(reason(), isMalformed = false, protocolVersion)
/** The first part of the cause. Typically the same for all instances of the particular type. /** The first part of the cause. Typically the same for all instances of the particular type.
*/ */
@ -55,8 +53,7 @@ sealed trait LocalRejectError extends LocalReject with TransactionError {
override def cause: String = _causePrefix + _details override def cause: String = _causePrefix + _details
// Make sure the ErrorCode has a v0CodeP. override def code: ErrorCode
override def code: ErrorCode & BaseLocalRejectErrorCode
/** Make sure to define this, if _resources is non-empty. /** Make sure to define this, if _resources is non-empty.
*/ */
@ -71,23 +68,10 @@ sealed trait LocalRejectError extends LocalReject with TransactionError {
override def resources: Seq[(ErrorResource, String)] = override def resources: Seq[(ErrorResource, String)] =
_resourcesType.fold(Seq.empty[(ErrorResource, String)])(rt => _resources.map(rs => (rt, rs))) _resourcesType.fold(Seq.empty[(ErrorResource, String)])(rt => _resources.map(rs => (rt, rs)))
protected[protocol] def toProtoV30: v30.LocalVerdict =
v30.LocalVerdict(v30.LocalVerdict.SomeLocalVerdict.LocalReject(toLocalRejectProtoV30))
protected[protocol] def toLocalRejectProtoV30: v30.LocalReject =
v30.LocalReject(
causePrefix = _causePrefix,
details = _details,
resource = _resources,
errorCode = code.id,
errorCategory = code.category.asInt,
)
override def pretty: Pretty[LocalRejectError] = override def pretty: Pretty[LocalRejectError] =
prettyOfClass( prettyOfClass(
param("code", _.code.id.unquoted), param("code", _.code.id.unquoted),
param("causePrefix", _._causePrefix.doubleQuoted), param("cause", _.cause.doubleQuoted),
param("details", _._details.doubleQuoted, _._details.nonEmpty),
param("resources", _._resources.map(_.singleQuoted)), param("resources", _._resources.map(_.singleQuoted)),
paramIfDefined("throwable", _.throwableO), paramIfDefined("throwable", _.throwableO),
) )
@ -115,7 +99,11 @@ sealed abstract class Malformed(
)(implicit )(implicit
override val code: MalformedErrorCode override val code: MalformedErrorCode
) extends BaseAlarm ) extends BaseAlarm
with LocalRejectError with LocalRejectError {
override def toLocalReject(protocolVersion: ProtocolVersion): LocalReject =
LocalReject.create(rpcStatusWithoutLoggingContext(), isMalformed = true, protocolVersion)
}
object LocalRejectError extends LocalRejectionGroup { object LocalRejectError extends LocalRejectionGroup {
@ -129,22 +117,13 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "LOCAL_VERDICT_LOCKED_CONTRACTS", id = "LOCAL_VERDICT_LOCKED_CONTRACTS",
ErrorCategory.ContentionOnSharedResources, ErrorCategory.ContentionOnSharedResources,
v30.LocalReject.Code.CODE_LOCKED_CONTRACTS,
) { ) {
final case class Reject(override val _resources: Seq[String])( final case class Reject(override val _resources: Seq[String])
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = s"Rejected transaction is referring to locked contracts ", _causePrefix = s"Rejected transaction is referring to locked contracts ",
_resourcesType = Some(ErrorResource.ContractId), _resourcesType = Some(ErrorResource.ContractId),
) )
object Reject {
def apply(resources: Seq[String], protocolVersion: ProtocolVersion): Reject =
Reject(resources)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -156,21 +135,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "LOCAL_VERDICT_INACTIVE_CONTRACTS", id = "LOCAL_VERDICT_INACTIVE_CONTRACTS",
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
v30.LocalReject.Code.CODE_INACTIVE_CONTRACTS,
) { ) {
final case class Reject(override val _resources: Seq[String])( final case class Reject(override val _resources: Seq[String])
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = "Rejected transaction is referring to inactive contracts ", _causePrefix = "Rejected transaction is referring to inactive contracts ",
_resourcesType = Some(ErrorResource.ContractId), _resourcesType = Some(ErrorResource.ContractId),
) )
object Reject {
def apply(resources: Seq[String], protocolVersion: ProtocolVersion): Reject =
Reject(resources)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
} }
@ -187,21 +157,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "LOCAL_VERDICT_LEDGER_TIME_OUT_OF_BOUND", id = "LOCAL_VERDICT_LEDGER_TIME_OUT_OF_BOUND",
ErrorCategory.ContentionOnSharedResources, ErrorCategory.ContentionOnSharedResources,
v30.LocalReject.Code.CODE_LEDGER_TIME,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = _causePrefix =
"Rejected transaction as delta of the ledger time and the record time exceed the time tolerance " "Rejected transaction as delta of the ledger time and the record time exceed the time tolerance "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -215,21 +176,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "LOCAL_VERDICT_SUBMISSION_TIME_OUT_OF_BOUND", id = "LOCAL_VERDICT_SUBMISSION_TIME_OUT_OF_BOUND",
ErrorCategory.ContentionOnSharedResources, ErrorCategory.ContentionOnSharedResources,
v30.LocalReject.Code.CODE_SUBMISSION_TIME,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = _causePrefix =
"Rejected transaction as delta of the submission time and the record time exceed the time tolerance " "Rejected transaction as delta of the submission time and the record time exceed the time tolerance "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -245,21 +197,14 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "LOCAL_VERDICT_TIMEOUT", id = "LOCAL_VERDICT_TIMEOUT",
ErrorCategory.ContentionOnSharedResources, ErrorCategory.ContentionOnSharedResources,
v30.LocalReject.Code.CODE_LEDGER_TIME,
) { ) {
override def logLevel: Level = Level.WARN override def logLevel: Level = Level.WARN
final case class Reject()( final case class Reject()
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = "Rejected transaction due to a participant determined timeout " _causePrefix = "Rejected transaction due to a participant determined timeout "
) )
object Reject { val status: Status = Reject().rpcStatusWithoutLoggingContext()
def apply(protocolVersion: ProtocolVersion): Reject =
Reject()(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
} }
@ -272,19 +217,9 @@ object LocalRejectError extends LocalRejectionGroup {
@Resolution("Please contact support.") @Resolution("Please contact support.")
object MalformedRequest object MalformedRequest
extends MalformedErrorCode( extends MalformedErrorCode(
id = "LOCAL_VERDICT_MALFORMED_REQUEST", id = "LOCAL_VERDICT_MALFORMED_REQUEST"
v30.LocalReject.Code.CODE_MALFORMED_PAYLOADS,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String) extends Malformed(_causePrefix = "")
override val representativeProtocolVersion: RepresentativeProtocolVersion[
LocalVerdict.type
]
) extends Malformed(_causePrefix = "")
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -293,21 +228,12 @@ object LocalRejectError extends LocalRejectionGroup {
@Resolution("This indicates either malicious or faulty behaviour.") @Resolution("This indicates either malicious or faulty behaviour.")
object Payloads object Payloads
extends MalformedErrorCode( extends MalformedErrorCode(
id = "LOCAL_VERDICT_MALFORMED_PAYLOAD", id = "LOCAL_VERDICT_MALFORMED_PAYLOAD"
v30.LocalReject.Code.CODE_MALFORMED_PAYLOADS,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends Malformed(
LocalVerdict.type
]
) extends Malformed(
_causePrefix = "Rejected transaction due to malformed payload within views " _causePrefix = "Rejected transaction due to malformed payload within views "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -316,21 +242,12 @@ object LocalRejectError extends LocalRejectionGroup {
@Resolution("This indicates either malicious or faulty behaviour.") @Resolution("This indicates either malicious or faulty behaviour.")
object ModelConformance object ModelConformance
extends MalformedErrorCode( extends MalformedErrorCode(
id = "LOCAL_VERDICT_FAILED_MODEL_CONFORMANCE_CHECK", id = "LOCAL_VERDICT_FAILED_MODEL_CONFORMANCE_CHECK"
v30.LocalReject.Code.CODE_MALFORMED_MODEL,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends Malformed(
LocalVerdict.type
]
) extends Malformed(
_causePrefix = "Rejected transaction due to a failed model conformance check: " _causePrefix = "Rejected transaction due to a failed model conformance check: "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -341,21 +258,12 @@ object LocalRejectError extends LocalRejectionGroup {
) )
object BadRootHashMessages object BadRootHashMessages
extends MalformedErrorCode( extends MalformedErrorCode(
id = "LOCAL_VERDICT_BAD_ROOT_HASH_MESSAGES", id = "LOCAL_VERDICT_BAD_ROOT_HASH_MESSAGES"
v30.LocalReject.Code.CODE_BAD_ROOT_HASH_MESSAGE,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends Malformed(
LocalVerdict.type
]
) extends Malformed(
_causePrefix = "Rejected transaction due to bad root hash error messages. " _causePrefix = "Rejected transaction due to bad root hash error messages. "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -364,22 +272,13 @@ object LocalRejectError extends LocalRejectionGroup {
@Resolution("This error indicates either faulty or malicious behaviour.") @Resolution("This error indicates either faulty or malicious behaviour.")
object CreatesExistingContracts object CreatesExistingContracts
extends MalformedErrorCode( extends MalformedErrorCode(
id = "LOCAL_VERDICT_CREATES_EXISTING_CONTRACTS", id = "LOCAL_VERDICT_CREATES_EXISTING_CONTRACTS"
v30.LocalReject.Code.CODE_CREATES_EXISTING_CONTRACT,
) { ) {
final case class Reject(override val _resources: Seq[String])( final case class Reject(override val _resources: Seq[String])
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends Malformed(
LocalVerdict.type
]
) extends Malformed(
_causePrefix = "Rejected transaction would create contract(s) that already exist ", _causePrefix = "Rejected transaction would create contract(s) that already exist ",
_resourcesType = Some(ErrorResource.ContractId), _resourcesType = Some(ErrorResource.ContractId),
) )
object Reject {
def apply(resources: Seq[String], protocolVersion: ProtocolVersion): Reject =
Reject(resources)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
} }
@ -397,19 +296,10 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "TRANSFER_OUT_ACTIVENESS_CHECK_FAILED", id = "TRANSFER_OUT_ACTIVENESS_CHECK_FAILED",
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
v30.LocalReject.Code.CODE_TRANSFER_OUT_ACTIVENESS_CHECK,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(_causePrefix = "Activeness check failed.")
LocalVerdict.type
]
) extends LocalRejectErrorImpl(_causePrefix = "Activeness check failed.")
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
} }
@ -422,21 +312,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "TRANSFER_IN_CONTRACT_ALREADY_ARCHIVED", id = "TRANSFER_IN_CONTRACT_ALREADY_ARCHIVED",
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing, ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_ARCHIVED,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = "Rejected transfer as transferred contract is already archived. " _causePrefix = "Rejected transfer as transferred contract is already archived. "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -446,22 +327,13 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "TRANSFER_IN_CONTRACT_ALREADY_ACTIVE", id = "TRANSFER_IN_CONTRACT_ALREADY_ACTIVE",
ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, ErrorCategory.InvalidGivenCurrentSystemStateResourceExists,
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_ACTIVE,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = _causePrefix =
"Rejected transfer as the contract is already active on the target domain. " "Rejected transfer as the contract is already active on the target domain. "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -471,21 +343,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "TRANSFER_IN_CONTRACT_IS_LOCKED", id = "TRANSFER_IN_CONTRACT_IS_LOCKED",
ErrorCategory.ContentionOnSharedResources, ErrorCategory.ContentionOnSharedResources,
v30.LocalReject.Code.CODE_TRANSFER_IN_LOCKED,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = "Rejected transfer as the transferred contract is locked." _causePrefix = "Rejected transfer as the transferred contract is locked."
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
@Explanation( @Explanation(
@ -495,58 +358,12 @@ object LocalRejectError extends LocalRejectionGroup {
extends LocalRejectErrorCode( extends LocalRejectErrorCode(
id = "TRANSFER_IN_ALREADY_COMPLETED", id = "TRANSFER_IN_ALREADY_COMPLETED",
ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, ErrorCategory.InvalidGivenCurrentSystemStateResourceExists,
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_COMPLETED,
) { ) {
final case class Reject(override val _details: String)( final case class Reject(override val _details: String)
override val representativeProtocolVersion: RepresentativeProtocolVersion[ extends LocalRejectErrorImpl(
LocalVerdict.type
]
) extends LocalRejectErrorImpl(
_causePrefix = "Rejected transfer as the transfer has already completed " _causePrefix = "Rejected transfer as the transfer has already completed "
) )
object Reject {
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
}
} }
} }
/** Fallback for deserializing local rejects that are not known to the current Canton version.
* Should not be serialized.
*/
final case class GenericReject(
override val _causePrefix: String,
override val _details: String,
override val _resources: Seq[String],
id: String,
category: ErrorCategory,
)(
override val representativeProtocolVersion: RepresentativeProtocolVersion[LocalVerdict.type]
) extends LocalRejectErrorImpl(
_causePrefix = _causePrefix,
// Append _resources to details, because we don't know _resourcesType and the _resources field is ignored if _resourcesType is None.
_details = _details + _resources.mkString(", "),
)(
new LocalRejectErrorCode(
id,
category,
v30.LocalReject.Code.CODE_LOCAL_TIMEOUT, // Using a dummy value, as this will not we used.
) {}
)
private[protocol] object GenericReject {
def apply(
causePrefix: String,
details: String,
resources: Seq[String],
id: String,
category: ErrorCategory,
protocolVersion: ProtocolVersion,
): GenericReject =
GenericReject(causePrefix, details, resources, id, category)(
LocalVerdict.protocolVersionRepresentativeFor(protocolVersion)
)
}
} }

View File

@ -36,12 +36,6 @@ trait Phase37Processor[RequestBatch] {
traceContext: TraceContext traceContext: TraceContext
): HandlerResult ): HandlerResult
def processMalformedMediatorConfirmationRequestResult(
timestamp: CantonTimestamp,
sequencerCounter: SequencerCounter,
signedResultBatch: WithOpeningErrors[SignedContent[Deliver[DefaultOpenEnvelope]]],
)(implicit traceContext: TraceContext): HandlerResult
/** Processes a result message, commits the changes or rolls them back and emits events via the /** Processes a result message, commits the changes or rolls them back and emits events via the
* [[com.digitalasset.canton.participant.event.RecordOrderPublisher]]. * [[com.digitalasset.canton.participant.event.RecordOrderPublisher]].
* *

View File

@ -26,7 +26,7 @@ import monocle.macros.GenLens
* @param requestId The unique identifier of the request. * @param requestId The unique identifier of the request.
* @param sender The identity of the sender. * @param sender The identity of the sender.
* @param viewPositionO the view position of the underlying view. * @param viewPositionO the view position of the underlying view.
* May be empty if the [[localVerdict]] is [[protocol.LocalRejectError.Malformed]]. * May be empty if the [[localVerdict]] is [[com.digitalasset.canton.protocol.LocalRejectError.Malformed]].
* Must be empty if the protoVersion is strictly lower than 2. * Must be empty if the protoVersion is strictly lower than 2.
* @param localVerdict The participant's verdict on the request's view. * @param localVerdict The participant's verdict on the request's view.
* @param rootHash The root hash of the request if the local verdict is [[com.digitalasset.canton.protocol.messages.LocalApprove]] * @param rootHash The root hash of the request if the local verdict is [[com.digitalasset.canton.protocol.messages.LocalApprove]]
@ -89,23 +89,22 @@ case class ConfirmationResponse private (
// If an object invariant is violated, throw an exception specific to the class. // If an object invariant is violated, throw an exception specific to the class.
// Thus, the exception can be caught during deserialization and translated to a human readable error message. // Thus, the exception can be caught during deserialization and translated to a human readable error message.
localVerdict match { if (localVerdict.isMalformed) {
case _: Malformed => if (confirmingParties.nonEmpty)
if (confirmingParties.nonEmpty) throw InvalidConfirmationResponse("Confirming parties must be empty for verdict Malformed.")
throw InvalidConfirmationResponse("Confirming parties must be empty for verdict Malformed.") } else {
case _: LocalApprove | _: LocalReject => if (confirmingParties.isEmpty)
if (confirmingParties.isEmpty) throw InvalidConfirmationResponse(
throw InvalidConfirmationResponse( show"Confirming parties must not be empty for verdict $localVerdict"
show"Confirming parties must not be empty for verdict $localVerdict" )
) if (rootHash.isEmpty)
if (rootHash.isEmpty) throw InvalidConfirmationResponse(
throw InvalidConfirmationResponse( show"Root hash must not be empty for verdict $localVerdict"
show"Root hash must not be empty for verdict $localVerdict" )
) if (viewPositionO.isEmpty)
if (viewPositionO.isEmpty) throw InvalidConfirmationResponse(
throw InvalidConfirmationResponse( show"View position must not be empty for verdict $localVerdict"
show"View position must not be empty for verdict $localVerdict" )
)
} }
override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap) override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap)

View File

@ -1,35 +0,0 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.data.{CantonTimestamp, ViewType}
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
import com.digitalasset.canton.serialization.ProtocolVersionedMemoizedEvidence
trait ConfirmationResult
extends ProtocolVersionedMemoizedEvidence
with HasDomainId
with HasRequestId
with SignedProtocolMessageContent {
def verdict: Verdict
override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap)
def viewType: ViewType
}
/** The mediator issues a regular confirmation result for well-formed mediator confirmation requests.
* Malformed confirmation requests lead to a [[MalformedConfirmationRequestResult]].
*/
trait RegularConfirmationResult extends ConfirmationResult
object RegularConfirmationResult {
implicit val regularMediatorResultMessageCast
: SignedMessageContentCast[RegularConfirmationResult] =
SignedMessageContentCast.create[RegularConfirmationResult]("RegularConfirmationResult") {
case m: RegularConfirmationResult => Some(m)
case _ => None
}
}

View File

@ -3,81 +3,92 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.data.ViewType.TransactionViewType import cats.syntax.traverse.*
import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.data.{CantonTimestamp, ViewType}
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
import com.digitalasset.canton.protocol.{RequestId, RootHash, v30} import com.digitalasset.canton.protocol.{RequestId, RootHash, v30}
import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.serialization.{ProtoConverter, ProtocolVersionedMemoizedEvidence}
import com.digitalasset.canton.topology.DomainId import com.digitalasset.canton.topology.DomainId
import com.digitalasset.canton.version.* import com.digitalasset.canton.version.*
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
/** Transaction result message that the mediator sends to all stakeholders of a transaction confirmation request with its verdict. /** Result message that the mediator sends to all stakeholders of a request with its verdict.
* https://engineering.da-int.net/docs/platform-architecture-handbook/arch/canton/transactions.html#phase-6-broadcast-of-result
* *
* @param requestId identifier of the confirmation request * @param domainId the domain on which the request is running
* @param verdict the finalized verdict on the request * @param viewType determines which processor (transaction / transfer) must process this message
* @param requestId unique identifier of the confirmation request
* @param rootHashO hash over the contents of the request
* @param verdict the finalized verdict on the request
* @param informees of the request - empty for transactions
*/ */
@SuppressWarnings(Array("org.wartremover.warts.FinalCaseClass")) // This class is mocked in tests @SuppressWarnings(Array("org.wartremover.warts.FinalCaseClass")) // This class is mocked in tests
case class ConfirmationResultMessage private ( case class ConfirmationResultMessage private (
override val requestId: RequestId,
override val verdict: Verdict,
rootHash: RootHash,
override val domainId: DomainId, override val domainId: DomainId,
viewType: ViewType,
override val requestId: RequestId,
rootHashO: Option[RootHash],
verdict: Verdict,
informees: Set[LfPartyId],
)( )(
override val representativeProtocolVersion: RepresentativeProtocolVersion[ override val representativeProtocolVersion: RepresentativeProtocolVersion[
ConfirmationResultMessage.type ConfirmationResultMessage.type
], ],
override val deserializedFrom: Option[ByteString], override val deserializedFrom: Option[ByteString],
) extends RegularConfirmationResult ) extends ProtocolVersionedMemoizedEvidence
with HasDomainId
with HasRequestId
with SignedProtocolMessageContent
with HasProtocolVersionedWrapper[ConfirmationResultMessage] with HasProtocolVersionedWrapper[ConfirmationResultMessage]
with PrettyPrinting { with PrettyPrinting {
override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap)
def copy( def copy(
requestId: RequestId = this.requestId,
verdict: Verdict = this.verdict,
rootHash: RootHash = this.rootHash,
domainId: DomainId = this.domainId, domainId: DomainId = this.domainId,
viewType: ViewType = this.viewType,
requestId: RequestId = this.requestId,
rootHashO: Option[RootHash] = this.rootHashO,
verdict: Verdict = this.verdict,
informees: Set[LfPartyId] = this.informees,
): ConfirmationResultMessage = ): ConfirmationResultMessage =
ConfirmationResultMessage(requestId, verdict, rootHash, domainId)( ConfirmationResultMessage(domainId, viewType, requestId, rootHashO, verdict, informees)(
representativeProtocolVersion, representativeProtocolVersion,
None, None,
) )
override def viewType: TransactionViewType = TransactionViewType
/** Computes the serialization of the object as a [[com.google.protobuf.ByteString]].
*
* Must meet the contract of [[com.digitalasset.canton.serialization.HasCryptographicEvidence.getCryptographicEvidence]]
* except that when called several times, different [[com.google.protobuf.ByteString]]s may be returned.
*/
override protected[this] def toByteStringUnmemoized: ByteString = override protected[this] def toByteStringUnmemoized: ByteString =
super[HasProtocolVersionedWrapper].toByteString super[HasProtocolVersionedWrapper].toByteString
@transient override protected lazy val companionObj: ConfirmationResultMessage.type = @transient override protected lazy val companionObj: ConfirmationResultMessage.type =
ConfirmationResultMessage ConfirmationResultMessage
protected def toProtoV30: v30.TransactionResultMessage = protected def toProtoV30: v30.ConfirmationResultMessage =
v30.TransactionResultMessage( v30.ConfirmationResultMessage(
requestId = requestId.toProtoPrimitive,
verdict = Some(verdict.toProtoV30),
rootHash = rootHash.toProtoPrimitive,
domainId = domainId.toProtoPrimitive, domainId = domainId.toProtoPrimitive,
viewType = viewType.toProtoEnum,
requestId = requestId.toProtoPrimitive,
rootHash = rootHashO.fold(ByteString.EMPTY)(_.toProtoPrimitive),
verdict = Some(verdict.toProtoV30),
informees = informees.toSeq,
) )
override protected[messages] def toProtoTypedSomeSignedProtocolMessage override protected[messages] def toProtoTypedSomeSignedProtocolMessage
: v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage = : v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage =
v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage.TransactionResult( v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage.ConfirmationResult(
getCryptographicEvidence getCryptographicEvidence
) )
override def pretty: Pretty[ConfirmationResultMessage] = override def pretty: Pretty[ConfirmationResultMessage] =
prettyOfClass( prettyOfClass(
param("requestId", _.requestId.unwrap),
param("verdict", _.verdict),
param("rootHash", _.rootHash),
param("domainId", _.domainId), param("domainId", _.domainId),
param("viewType", _.viewType),
param("requestId", _.requestId.unwrap),
param("rootHash", _.rootHashO),
param("verdict", _.verdict),
paramIfNonEmpty("informees", _.informees),
) )
} }
@ -85,50 +96,63 @@ object ConfirmationResultMessage
extends HasMemoizedProtocolVersionedWrapperCompanion[ extends HasMemoizedProtocolVersionedWrapperCompanion[
ConfirmationResultMessage, ConfirmationResultMessage,
] { ] {
override val name: String = "TransactionResultMessage" override val name: String = "ConfirmationResultMessage"
val supportedProtoVersions = SupportedProtoVersions( val supportedProtoVersions = SupportedProtoVersions(
ProtoVersion(30) -> VersionedProtoConverter(ProtocolVersion.v30)( ProtoVersion(30) -> VersionedProtoConverter(ProtocolVersion.v30)(
v30.TransactionResultMessage v30.ConfirmationResultMessage
)( )(
supportedProtoVersionMemoized(_)(fromProtoV30), supportedProtoVersionMemoized(_)(fromProtoV30),
_.toProtoV30.toByteString, _.toProtoV30.toByteString,
) )
) )
def apply( def create(
requestId: RequestId,
verdict: Verdict,
rootHash: RootHash,
domainId: DomainId, domainId: DomainId,
viewType: ViewType,
requestId: RequestId,
rootHashO: Option[RootHash],
verdict: Verdict,
informees: Set[LfPartyId],
protocolVersion: ProtocolVersion, protocolVersion: ProtocolVersion,
): ConfirmationResultMessage = ): ConfirmationResultMessage =
ConfirmationResultMessage(requestId, verdict, rootHash, domainId)( ConfirmationResultMessage(domainId, viewType, requestId, rootHashO, verdict, informees)(
protocolVersionRepresentativeFor(protocolVersion), protocolVersionRepresentativeFor(protocolVersion),
None, None,
) )
private def fromProtoV30(protoResultMessage: v30.TransactionResultMessage)( private def fromProtoV30(protoResultMessage: v30.ConfirmationResultMessage)(
bytes: ByteString bytes: ByteString
): ParsingResult[ConfirmationResultMessage] = { ): ParsingResult[ConfirmationResultMessage] = {
val v30.TransactionResultMessage(requestIdP, verdictPO, rootHashP, domainIdP) = val v30.ConfirmationResultMessage(
protoResultMessage domainIdP,
viewTypeP,
requestIdP,
rootHashP,
verdictPO,
informeesP,
) = protoResultMessage
for { for {
requestId <- RequestId.fromProtoPrimitive(requestIdP)
transactionResult <- ProtoConverter
.required("verdict", verdictPO)
.flatMap(Verdict.fromProtoV30)
rootHash <- RootHash.fromProtoPrimitive(rootHashP)
domainId <- DomainId.fromProtoPrimitive(domainIdP, "domain_id") domainId <- DomainId.fromProtoPrimitive(domainIdP, "domain_id")
viewType <- ViewType.fromProtoEnum(viewTypeP)
requestId <- RequestId.fromProtoPrimitive(requestIdP)
rootHashO <- RootHash.fromProtoPrimitiveOption(rootHashP)
verdict <- ProtoConverter.parseRequired(Verdict.fromProtoV30, "verdict", verdictPO)
informees <- informeesP.traverse(ProtoConverter.parseLfPartyId)
rpv <- protocolVersionRepresentativeFor(ProtoVersion(30)) rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
} yield ConfirmationResultMessage(requestId, transactionResult, rootHash, domainId)( } yield ConfirmationResultMessage(
rpv, domainId,
Some(bytes), viewType,
) requestId,
rootHashO,
verdict,
informees.toSet,
)(rpv, Some(bytes))
} }
implicit val transactionResultMessageCast: SignedMessageContentCast[ConfirmationResultMessage] = implicit val confirmationResultMessageCast: SignedMessageContentCast[ConfirmationResultMessage] =
SignedMessageContentCast.create[ConfirmationResultMessage]("TransactionResultMessage") { SignedMessageContentCast.create[ConfirmationResultMessage]("ConfirmationResultMessage") {
case m: ConfirmationResultMessage => Some(m) case m: ConfirmationResultMessage => Some(m)
case _ => None case _ => None
} }

View File

@ -5,9 +5,10 @@ package com.digitalasset.canton.protocol.messages
import cats.syntax.either.* import cats.syntax.either.*
import cats.syntax.functorFilter.* import cats.syntax.functorFilter.*
import com.digitalasset.canton.data.ViewType
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.TransferId
import com.digitalasset.canton.protocol.messages.DeliveredTransferOutResult.InvalidTransferOutResult import com.digitalasset.canton.protocol.messages.DeliveredTransferOutResult.InvalidTransferOutResult
import com.digitalasset.canton.protocol.{SourceDomainId, TransferId}
import com.digitalasset.canton.sequencing.RawProtocolEvent import com.digitalasset.canton.sequencing.RawProtocolEvent
import com.digitalasset.canton.sequencing.protocol.{ import com.digitalasset.canton.sequencing.protocol.{
Batch, Batch,
@ -19,10 +20,16 @@ import com.digitalasset.canton.sequencing.protocol.{
final case class DeliveredTransferOutResult(result: SignedContent[Deliver[DefaultOpenEnvelope]]) final case class DeliveredTransferOutResult(result: SignedContent[Deliver[DefaultOpenEnvelope]])
extends PrettyPrinting { extends PrettyPrinting {
val unwrap: TransferOutResult = result.content match { val unwrap: ConfirmationResultMessage = result.content match {
case Deliver(_, _, _, _, Batch(envelopes), _) => case Deliver(_, _, _, _, Batch(envelopes), _) =>
val transferOutResults = val transferOutResults =
envelopes.mapFilter(ProtocolMessage.select[SignedProtocolMessage[TransferOutResult]]) envelopes
.mapFilter(env =>
ProtocolMessage.select[SignedProtocolMessage[ConfirmationResultMessage]](env)
)
.filter { env =>
env.protocolMessage.message.viewType == ViewType.TransferOutViewType
}
val size = transferOutResults.size val size = transferOutResults.size
if (size != 1) if (size != 1)
throw InvalidTransferOutResult( throw InvalidTransferOutResult(
@ -38,7 +45,7 @@ final case class DeliveredTransferOutResult(result: SignedContent[Deliver[Defaul
throw InvalidTransferOutResult(result.content, "The transfer-out result must be approving.") throw InvalidTransferOutResult(result.content, "The transfer-out result must be approving.")
} }
def transferId: TransferId = TransferId(unwrap.domain, unwrap.requestId.unwrap) def transferId: TransferId = TransferId(SourceDomainId(unwrap.domainId), unwrap.requestId.unwrap)
override def pretty: Pretty[DeliveredTransferOutResult] = prettyOfParam(_.unwrap) override def pretty: Pretty[DeliveredTransferOutResult] = prettyOfParam(_.unwrap)
} }

View File

@ -3,13 +3,12 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
import com.digitalasset.canton.crypto.{HashOps, Signature} import com.digitalasset.canton.crypto.{HashOps, Signature}
import com.digitalasset.canton.data.{FullInformeeTree, Informee, ViewPosition, ViewType} import com.digitalasset.canton.data.{FullInformeeTree, Informee, ViewPosition, ViewType}
import com.digitalasset.canton.logging.pretty.Pretty import com.digitalasset.canton.logging.pretty.Pretty
import com.digitalasset.canton.protocol.messages.ProtocolMessage.ProtocolMessageContentCast import com.digitalasset.canton.protocol.messages.ProtocolMessage.ProtocolMessageContentCast
import com.digitalasset.canton.protocol.{RequestId, RootHash, v30} import com.digitalasset.canton.protocol.{RootHash, v30}
import com.digitalasset.canton.sequencing.protocol.MediatorsOfDomain import com.digitalasset.canton.sequencing.protocol.MediatorsOfDomain
import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
@ -61,19 +60,6 @@ case class InformeeMessage(
: Map[ViewPosition, (Set[Informee], NonNegativeInt)] = : Map[ViewPosition, (Set[Informee], NonNegativeInt)] =
fullInformeeTree.informeesAndThresholdByViewPosition fullInformeeTree.informeesAndThresholdByViewPosition
override def createConfirmationResult(
requestId: RequestId,
verdict: Verdict,
recipientParties: Set[LfPartyId],
): ConfirmationResultMessage =
ConfirmationResultMessage(
requestId,
verdict,
fullInformeeTree.tree.rootHash,
domainId,
protocolVersion,
)
// Implementing a `toProto<version>` method allows us to compose serializable classes. // Implementing a `toProto<version>` method allows us to compose serializable classes.
// You should define the toProtoV30 method on the serializable class, because then it is easiest to find and use. // You should define the toProtoV30 method on the serializable class, because then it is easiest to find and use.
// (Conversely, you should not define a separate proto converter class.) // (Conversely, you should not define a separate proto converter class.)
@ -97,6 +83,8 @@ case class InformeeMessage(
override def pretty: Pretty[InformeeMessage] = prettyOfClass(unnamedParam(_.fullInformeeTree)) override def pretty: Pretty[InformeeMessage] = prettyOfClass(unnamedParam(_.fullInformeeTree))
@transient override protected lazy val companionObj: InformeeMessage.type = InformeeMessage @transient override protected lazy val companionObj: InformeeMessage.type = InformeeMessage
override def informeesArePublic: Boolean = false
} }
object InformeeMessage object InformeeMessage

View File

@ -3,22 +3,20 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.daml.error.{ContextualizedErrorLogger, ErrorCategory, ErrorCode, NoLogging} import com.daml.error.*
import com.digitalasset.canton.ProtoDeserializationError.OtherError import com.digitalasset.canton.ProtoDeserializationError.{FieldNotSet, OtherError}
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.LocalRejectError.MalformedRejects.CreatesExistingContracts import com.digitalasset.canton.protocol.v30.LocalVerdict.VerdictCode.{
import com.digitalasset.canton.protocol.LocalRejectError.{ VERDICT_CODE_LOCAL_APPROVE,
ConsistencyRejections, VERDICT_CODE_LOCAL_MALFORMED,
MalformedRejects, VERDICT_CODE_LOCAL_REJECT,
TimeRejects, VERDICT_CODE_UNSPECIFIED,
TransferInRejects,
TransferOutRejects,
} }
import com.digitalasset.canton.protocol.messages.LocalVerdict.protocolVersionRepresentativeFor import com.digitalasset.canton.protocol.{messages, v30}
import com.digitalasset.canton.protocol.{LocalRejectError, messages, v30} import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.version.* import com.digitalasset.canton.version.*
import com.google.protobuf.empty import com.google.rpc.status.Status
/** Possible verdicts on a transaction view from the participant's perspective. /** Possible verdicts on a transaction view from the participant's perspective.
* The verdict can be `LocalApprove`, `LocalReject` or `Malformed`. * The verdict can be `LocalApprove`, `LocalReject` or `Malformed`.
@ -29,6 +27,9 @@ sealed trait LocalVerdict
with Serializable with Serializable
with PrettyPrinting with PrettyPrinting
with HasProtocolVersionedWrapper[LocalVerdict] { with HasProtocolVersionedWrapper[LocalVerdict] {
def isMalformed: Boolean
private[messages] def toProtoV30: v30.LocalVerdict private[messages] def toProtoV30: v30.LocalVerdict
@transient override protected lazy val companionObj: LocalVerdict.type = LocalVerdict @transient override protected lazy val companionObj: LocalVerdict.type = LocalVerdict
@ -51,25 +52,39 @@ object LocalVerdict extends HasProtocolVersionedCompanion[LocalVerdict] {
private[messages] def fromProtoV30( private[messages] def fromProtoV30(
localVerdictP: v30.LocalVerdict localVerdictP: v30.LocalVerdict
): ParsingResult[LocalVerdict] = { ): ParsingResult[LocalVerdict] = {
import v30.LocalVerdict.SomeLocalVerdict as Lv val v30.LocalVerdict(codeP, reasonPO) = localVerdictP
for {
val protocolVersion = protocolVersionRepresentativeFor(ProtoVersion(30)) rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
val v30.LocalVerdict(someLocalVerdictP) = localVerdictP reason <- ProtoConverter.required("reason", reasonPO)
localVerdict <- codeP match {
someLocalVerdictP match { case VERDICT_CODE_LOCAL_APPROVE => Right(LocalApprove()(rpv))
case Lv.LocalApprove(empty.Empty(_)) => protocolVersion.map(LocalApprove()(_)) case VERDICT_CODE_LOCAL_REJECT =>
case Lv.LocalReject(localRejectP) => LocalReject.fromProtoV30(localRejectP) Right(LocalReject(reason, isMalformed = false)(rpv))
case Lv.Empty => case VERDICT_CODE_LOCAL_MALFORMED =>
Left(OtherError("Unable to deserialize LocalVerdict, as the content is empty")) Right(LocalReject(reason, isMalformed = true)(rpv))
} case VERDICT_CODE_UNSPECIFIED => Left(FieldNotSet("LocalVerdict.code"))
case v30.LocalVerdict.VerdictCode.Unrecognized(_) =>
Left(
OtherError(
s"Unable to deserialize LocalVerdict due to invalid code $codeP."
)
)
}
} yield localVerdict
} }
} }
final case class LocalApprove()( final case class LocalApprove()(
override val representativeProtocolVersion: RepresentativeProtocolVersion[LocalVerdict.type] override val representativeProtocolVersion: RepresentativeProtocolVersion[LocalVerdict.type]
) extends LocalVerdict { ) extends LocalVerdict {
override def isMalformed: Boolean = false
private[messages] def toProtoV30: v30.LocalVerdict = private[messages] def toProtoV30: v30.LocalVerdict =
v30.LocalVerdict(v30.LocalVerdict.SomeLocalVerdict.LocalApprove(empty.Empty())) v30.LocalVerdict(
code = VERDICT_CODE_LOCAL_APPROVE,
reason = Some(Status(code = 0)), // 0 means OK
)
override def pretty: Pretty[this.type] = prettyOfClass() override def pretty: Pretty[this.type] = prettyOfClass()
} }
@ -79,70 +94,37 @@ object LocalApprove {
LocalApprove()(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion)) LocalApprove()(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
} }
// TODO(i16856): temporary intermediate type, until the verdict LocalReject has been separated from the error LocalReject. final case class LocalReject(reason: com.google.rpc.status.Status, isMalformed: Boolean)(
trait LocalReject extends LocalVerdict with TransactionRejection { override val representativeProtocolVersion: RepresentativeProtocolVersion[LocalVerdict.type]
) extends LocalVerdict
with TransactionRejection
with PrettyPrinting {
def code: ErrorCode override def logWithContext(
extra: Map[String, String]
)(implicit contextualizedErrorLogger: ContextualizedErrorLogger): Unit = {
// Log with level INFO, leave it to LocalRejectError to log the details.
lazy val action = if (isMalformed) "malformed" else "rejected"
contextualizedErrorLogger.info(show"Request is $action. $reason")
}
def rpcStatus( override private[messages] def toProtoV30: v30.LocalVerdict = {
overrideCode: Option[io.grpc.Status.Code] = None val codeP =
)(implicit loggingContext: ContextualizedErrorLogger): com.google.rpc.status.Status if (isMalformed) VERDICT_CODE_LOCAL_MALFORMED else VERDICT_CODE_LOCAL_REJECT
v30.LocalVerdict(code = codeP, reason = Some(reason))
override lazy val reason: com.google.rpc.status.Status = rpcStatus()(NoLogging) }
protected[messages] def toLocalRejectProtoV30: v30.LocalReject
protected[messages] def toProtoV30: v30.LocalVerdict
override def pretty: Pretty[LocalReject] = prettyOfClass(
param("reason", _.reason),
param("isMalformed", _.isMalformed),
)
} }
object LocalReject { object LocalReject {
def create(
// list of local errors, used to map them during transport reason: com.google.rpc.status.Status,
// if you add a new error below, you must add it to this list here as well isMalformed: Boolean,
private[messages] def fromProtoV30( protocolVersion: ProtocolVersion,
localRejectP: v30.LocalReject ): LocalReject =
): ParsingResult[LocalReject] = { LocalReject(reason, isMalformed)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
import ConsistencyRejections.*
val v30.LocalReject(causePrefix, details, resource, errorCodeP, errorCategoryP) = localRejectP
protocolVersionRepresentativeFor(ProtoVersion(30)).map { protocolVersion =>
errorCodeP match {
case LockedContracts.id => LockedContracts.Reject(resource)(protocolVersion)
case InactiveContracts.id => InactiveContracts.Reject(resource)(protocolVersion)
case CreatesExistingContracts.id =>
CreatesExistingContracts.Reject(resource)(protocolVersion)
case TimeRejects.LedgerTime.id =>
TimeRejects.LedgerTime.Reject(details)(protocolVersion)
case TimeRejects.SubmissionTime.id =>
TimeRejects.SubmissionTime.Reject(details)(protocolVersion)
case TimeRejects.LocalTimeout.id => TimeRejects.LocalTimeout.Reject()(protocolVersion)
case MalformedRejects.MalformedRequest.id =>
MalformedRejects.MalformedRequest.Reject(details)(protocolVersion)
case MalformedRejects.Payloads.id =>
MalformedRejects.Payloads.Reject(details)(protocolVersion)
case MalformedRejects.ModelConformance.id =>
MalformedRejects.ModelConformance.Reject(details)(protocolVersion)
case MalformedRejects.BadRootHashMessages.id =>
MalformedRejects.BadRootHashMessages.Reject(details)(protocolVersion)
case TransferOutRejects.ActivenessCheckFailed.id =>
TransferOutRejects.ActivenessCheckFailed.Reject(details)(protocolVersion)
case TransferInRejects.AlreadyCompleted.id =>
TransferInRejects.AlreadyCompleted.Reject(details)(protocolVersion)
case TransferInRejects.ContractAlreadyActive.id =>
TransferInRejects.ContractAlreadyActive.Reject(details)(protocolVersion)
case TransferInRejects.ContractAlreadyArchived.id =>
TransferInRejects.ContractAlreadyArchived.Reject(details)(protocolVersion)
case TransferInRejects.ContractIsLocked.id =>
TransferInRejects.ContractIsLocked.Reject(details)(protocolVersion)
case id =>
val category = ErrorCategory
.fromInt(errorCategoryP)
.getOrElse(ErrorCategory.SystemInternalAssumptionViolated)
LocalRejectError.GenericReject(causePrefix, details, resource, id, category)(
protocolVersion
)
}
}
}
} }

View File

@ -1,149 +0,0 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.protocol.messages
import cats.syntax.either.*
import com.digitalasset.canton.data.ViewType
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
import com.digitalasset.canton.protocol.messages.Verdict.MediatorReject
import com.digitalasset.canton.protocol.{RequestId, v30}
import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.topology.DomainId
import com.digitalasset.canton.version.{
HasMemoizedProtocolVersionedWrapperCompanion,
HasProtocolVersionedWrapper,
ProtoVersion,
ProtocolVersion,
RepresentativeProtocolVersion,
}
import com.google.protobuf.ByteString
/** Sent by the mediator to indicate that a mediator confirmation request was malformed.
* The request counts as being rejected and the request UUID will not be deduplicated.
*
* @param requestId The ID of the malformed request
* @param domainId The domain ID of the mediator
* @param verdict The rejection reason as a verdict
*/
final case class MalformedConfirmationRequestResult private (
override val requestId: RequestId,
override val domainId: DomainId,
override val viewType: ViewType,
override val verdict: Verdict.MediatorReject,
)(
override val representativeProtocolVersion: RepresentativeProtocolVersion[
MalformedConfirmationRequestResult.type
],
override val deserializedFrom: Option[ByteString],
) extends ConfirmationResult
with SignedProtocolMessageContent
with HasProtocolVersionedWrapper[MalformedConfirmationRequestResult]
with PrettyPrinting {
override protected[messages] def toProtoTypedSomeSignedProtocolMessage
: v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage =
v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage
.MalformedMediatorConfirmationRequestResult(
getCryptographicEvidence
)
@transient override protected lazy val companionObj: MalformedConfirmationRequestResult.type =
MalformedConfirmationRequestResult
protected def toProtoV30: v30.MalformedMediatorConfirmationRequestResult =
v30.MalformedMediatorConfirmationRequestResult(
requestId = requestId.toProtoPrimitive,
domainId = domainId.toProtoPrimitive,
viewType = viewType.toProtoEnum,
rejection = Some(verdict.toProtoMediatorRejectV30),
)
override protected[this] def toByteStringUnmemoized: ByteString =
super[HasProtocolVersionedWrapper].toByteString
override def pretty: Pretty[MalformedConfirmationRequestResult] = prettyOfClass(
param("request id", _.requestId),
param("reject", _.verdict),
param("view type", _.viewType),
param("domain id", _.domainId),
)
}
object MalformedConfirmationRequestResult
extends HasMemoizedProtocolVersionedWrapperCompanion[MalformedConfirmationRequestResult] {
override val name: String = "MalformedMediatorConfirmationRequestResult"
val supportedProtoVersions = SupportedProtoVersions(
ProtoVersion(30) -> VersionedProtoConverter(ProtocolVersion.v30)(
v30.MalformedMediatorConfirmationRequestResult
)(
supportedProtoVersionMemoized(_)(fromProtoV30),
_.toProtoV30.toByteString,
)
)
def tryCreate(
requestId: RequestId,
domainId: DomainId,
viewType: ViewType,
verdict: Verdict.MediatorReject,
protocolVersion: ProtocolVersion,
): MalformedConfirmationRequestResult =
MalformedConfirmationRequestResult(requestId, domainId, viewType, verdict)(
protocolVersionRepresentativeFor(protocolVersion),
None,
)
def create(
requestId: RequestId,
domainId: DomainId,
viewType: ViewType,
verdict: Verdict.MediatorReject,
protocolVersion: ProtocolVersion,
): Either[String, MalformedConfirmationRequestResult] =
Either
.catchOnly[IllegalArgumentException](
MalformedConfirmationRequestResult
.tryCreate(requestId, domainId, viewType, verdict, protocolVersion)
)
.leftMap(_.getMessage)
private def fromProtoV30(
MalformedMediatorConfirmationRequestResultP: v30.MalformedMediatorConfirmationRequestResult
)(
bytes: ByteString
): ParsingResult[MalformedConfirmationRequestResult] = {
val v30.MalformedMediatorConfirmationRequestResult(
requestIdP,
domainIdP,
viewTypeP,
rejectionPO,
) =
MalformedMediatorConfirmationRequestResultP
for {
requestId <- RequestId.fromProtoPrimitive(requestIdP)
domainId <- DomainId.fromProtoPrimitive(domainIdP, "domain_id")
viewType <- ViewType.fromProtoEnum(viewTypeP)
reject <- ProtoConverter.parseRequired(
MediatorReject.fromProtoV30,
"rejection",
rejectionPO,
)
rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
} yield MalformedConfirmationRequestResult(requestId, domainId, viewType, reject)(
rpv,
Some(bytes),
)
}
implicit val MalformedMediatorConfirmationRequestResultCast
: SignedMessageContentCast[MalformedConfirmationRequestResult] = SignedMessageContentCast
.create[MalformedConfirmationRequestResult]("MalformedMediatorConfirmationRequestResult") {
case m: MalformedConfirmationRequestResult => Some(m)
case _ => None
}
}

View File

@ -7,8 +7,8 @@ import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
import com.digitalasset.canton.crypto.Signature import com.digitalasset.canton.crypto.Signature
import com.digitalasset.canton.data.{Informee, ViewPosition, ViewType} import com.digitalasset.canton.data.{Informee, ViewPosition, ViewType}
import com.digitalasset.canton.protocol.RootHash
import com.digitalasset.canton.protocol.messages.ProtocolMessage.ProtocolMessageContentCast import com.digitalasset.canton.protocol.messages.ProtocolMessage.ProtocolMessageContentCast
import com.digitalasset.canton.protocol.{RequestId, RootHash}
import com.digitalasset.canton.sequencing.protocol.MediatorsOfDomain import com.digitalasset.canton.sequencing.protocol.MediatorsOfDomain
import com.digitalasset.canton.topology.ParticipantId import com.digitalasset.canton.topology.ParticipantId
@ -29,11 +29,8 @@ trait MediatorConfirmationRequest extends UnsignedProtocolMessage {
.map(_.party) .map(_.party)
.toSet .toSet
def createConfirmationResult( /** Determines whether the mediator may disclose informees as part of its result message. */
requestId: RequestId, def informeesArePublic: Boolean
verdict: Verdict,
recipientParties: Set[LfPartyId],
): ConfirmationResult with SignedProtocolMessageContent
def minimumThreshold(informees: Set[Informee]): NonNegativeInt def minimumThreshold(informees: Set[Informee]): NonNegativeInt

View File

@ -3,7 +3,7 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
@ -17,7 +17,7 @@ import com.google.protobuf.ByteString
final case class SetTrafficBalanceMessage private ( final case class SetTrafficBalanceMessage private (
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
domainId: DomainId, domainId: DomainId,
)( )(
@ -79,7 +79,7 @@ object SetTrafficBalanceMessage
def apply( def apply(
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
domainId: DomainId, domainId: DomainId,
protocolVersion: ProtocolVersion, protocolVersion: ProtocolVersion,
@ -94,7 +94,7 @@ object SetTrafficBalanceMessage
)(bytes: ByteString): ParsingResult[SetTrafficBalanceMessage] = { )(bytes: ByteString): ParsingResult[SetTrafficBalanceMessage] = {
for { for {
member <- Member.fromProtoPrimitive(proto.member, "member") member <- Member.fromProtoPrimitive(proto.member, "member")
serial <- ProtoConverter.parseNonNegativeLong(proto.serial) serial <- ProtoConverter.parsePositiveInt(proto.serial)
totalTrafficBalance <- ProtoConverter.parseNonNegativeLong(proto.totalTrafficBalance) totalTrafficBalance <- ProtoConverter.parseNonNegativeLong(proto.totalTrafficBalance)
domainId <- DomainId.fromProtoPrimitive(proto.domainId, "domain_id") domainId <- DomainId.fromProtoPrimitive(proto.domainId, "domain_id")
rpv <- protocolVersionRepresentativeFor(ProtoVersion(1)) rpv <- protocolVersionRepresentativeFor(ProtoVersion(1))

View File

@ -3,7 +3,6 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.ProtoDeserializationError.OtherError import com.digitalasset.canton.ProtoDeserializationError.OtherError
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
import com.digitalasset.canton.crypto.{HashOps, Signature} import com.digitalasset.canton.crypto.{HashOps, Signature}
@ -64,25 +63,6 @@ final case class TransferInMediatorMessage(
override def minimumThreshold(informees: Set[Informee]): NonNegativeInt = NonNegativeInt.one override def minimumThreshold(informees: Set[Informee]): NonNegativeInt = NonNegativeInt.one
override def createConfirmationResult(
requestId: RequestId,
verdict: Verdict,
recipientParties: Set[LfPartyId],
): ConfirmationResult with SignedProtocolMessageContent = {
val informees = commonData.stakeholders
require(
recipientParties.subsetOf(informees),
"Recipient parties of the transfer-in result must be stakeholders.",
)
TransferResult.create(
requestId,
informees,
commonData.targetDomain,
verdict,
protocolVersion.v,
)
}
override def toProtoSomeEnvelopeContentV30: v30.EnvelopeContent.SomeEnvelopeContent = override def toProtoSomeEnvelopeContentV30: v30.EnvelopeContent.SomeEnvelopeContent =
v30.EnvelopeContent.SomeEnvelopeContent.TransferInMediatorMessage(toProtoV30) v30.EnvelopeContent.SomeEnvelopeContent.TransferInMediatorMessage(toProtoV30)
@ -100,6 +80,8 @@ final case class TransferInMediatorMessage(
@transient override protected lazy val companionObj: TransferInMediatorMessage.type = @transient override protected lazy val companionObj: TransferInMediatorMessage.type =
TransferInMediatorMessage TransferInMediatorMessage
override def informeesArePublic: Boolean = true
} }
object TransferInMediatorMessage object TransferInMediatorMessage

View File

@ -3,7 +3,6 @@
package com.digitalasset.canton.protocol.messages package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.ProtoDeserializationError.OtherError import com.digitalasset.canton.ProtoDeserializationError.OtherError
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
import com.digitalasset.canton.crypto.{HashOps, Signature} import com.digitalasset.canton.crypto.{HashOps, Signature}
@ -62,25 +61,6 @@ final case class TransferOutMediatorMessage(
override def minimumThreshold(informees: Set[Informee]): NonNegativeInt = NonNegativeInt.one override def minimumThreshold(informees: Set[Informee]): NonNegativeInt = NonNegativeInt.one
override def createConfirmationResult(
requestId: RequestId,
verdict: Verdict,
recipientParties: Set[LfPartyId],
): ConfirmationResult with SignedProtocolMessageContent = {
val informees = commonData.stakeholders ++ commonData.adminParties
require(
recipientParties.subsetOf(informees),
"Recipient parties of the transfer-out result are neither stakeholders nor admin parties",
)
TransferResult.create(
requestId,
informees,
commonData.sourceDomain,
verdict,
protocolVersion.v,
)
}
def toProtoV30: v30.TransferOutMediatorMessage = def toProtoV30: v30.TransferOutMediatorMessage =
v30.TransferOutMediatorMessage( v30.TransferOutMediatorMessage(
tree = Some(tree.toProtoV30), tree = Some(tree.toProtoV30),
@ -98,6 +78,8 @@ final case class TransferOutMediatorMessage(
@transient override protected lazy val companionObj: TransferOutMediatorMessage.type = @transient override protected lazy val companionObj: TransferOutMediatorMessage.type =
TransferOutMediatorMessage TransferOutMediatorMessage
override def informeesArePublic: Boolean = true
} }
object TransferOutMediatorMessage object TransferOutMediatorMessage

View File

@ -1,150 +0,0 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.protocol.messages
import cats.Functor
import cats.syntax.traverse.*
import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.ProtoDeserializationError.FieldNotSet
import com.digitalasset.canton.data.ViewType
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.protocol.TransferDomainId.TransferDomainIdCast
import com.digitalasset.canton.protocol.*
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.topology.DomainId
import com.digitalasset.canton.version.*
import com.google.protobuf.ByteString
/** Confirmation request result for a transfer request
*
* @param requestId timestamp of the corresponding [[TransferOutRequest]] on the source domain
*/
final case class TransferResult[+Domain <: TransferDomainId] private (
override val requestId: RequestId,
informees: Set[LfPartyId],
domain: Domain, // For transfer-out, this is the source domain. For transfer-in, this is the target domain.
override val verdict: Verdict,
)(
override val representativeProtocolVersion: RepresentativeProtocolVersion[TransferResult.type],
override val deserializedFrom: Option[ByteString],
) extends RegularConfirmationResult
with HasProtocolVersionedWrapper[TransferResult[TransferDomainId]]
with PrettyPrinting {
override def domainId: DomainId = domain.unwrap
override def viewType: ViewType = domain.toViewType
override protected[messages] def toProtoTypedSomeSignedProtocolMessage
: v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage =
v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage.TransferResult(
getCryptographicEvidence
)
@transient override protected lazy val companionObj: TransferResult.type = TransferResult
private def toProtoV30: v30.TransferResult = {
val domainP = (domain: @unchecked) match {
case SourceDomainId(domainId) =>
v30.TransferResult.Domain.SourceDomain(domainId.toProtoPrimitive)
case TargetDomainId(domainId) =>
v30.TransferResult.Domain.TargetDomain(domainId.toProtoPrimitive)
}
v30.TransferResult(
requestId = requestId.toProtoPrimitive,
domain = domainP,
informees = informees.toSeq,
verdict = Some(verdict.toProtoV30),
)
}
override protected[this] def toByteStringUnmemoized: ByteString =
super[HasProtocolVersionedWrapper].toByteString
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
private[TransferResult] def traverse[F[_], Domain2 <: TransferDomainId](
f: Domain => F[Domain2]
)(implicit F: Functor[F]): F[TransferResult[Domain2]] =
F.map(f(domain)) { newDomain =>
if (newDomain eq domain) this.asInstanceOf[TransferResult[Domain2]]
else if (newDomain == domain)
TransferResult(requestId, informees, newDomain, verdict)(
representativeProtocolVersion,
deserializedFrom,
)
else
TransferResult(requestId, informees, newDomain, verdict)(
representativeProtocolVersion,
None,
)
}
override def pretty: Pretty[TransferResult[_ <: TransferDomainId]] =
prettyOfClass(
param("requestId", _.requestId.unwrap),
param("verdict", _.verdict),
param("informees", _.informees),
param("domain", _.domain),
)
}
object TransferResult
extends HasMemoizedProtocolVersionedWrapperCompanion[TransferResult[TransferDomainId]] {
override val name: String = "TransferResult"
val supportedProtoVersions = SupportedProtoVersions(
ProtoVersion(30) -> VersionedProtoConverter(ProtocolVersion.v30)(v30.TransferResult)(
supportedProtoVersionMemoized(_)(fromProtoV30),
_.toProtoV30.toByteString,
)
)
def create[Domain <: TransferDomainId](
requestId: RequestId,
informees: Set[LfPartyId],
domain: Domain,
verdict: Verdict,
protocolVersion: ProtocolVersion,
): TransferResult[Domain] =
TransferResult[Domain](requestId, informees, domain, verdict)(
protocolVersionRepresentativeFor(protocolVersion),
None,
)
private def fromProtoV30(transferResultP: v30.TransferResult)(
bytes: ByteString
): ParsingResult[TransferResult[TransferDomainId]] = {
val v30.TransferResult(requestIdP, domainP, informeesP, verdictPO) = transferResultP
import v30.TransferResult.Domain
for {
requestId <- RequestId.fromProtoPrimitive(requestIdP)
domain <- domainP match {
case Domain.SourceDomain(sourceDomain) =>
DomainId
.fromProtoPrimitive(sourceDomain, "TransferResult.sourceDomain")
.map(SourceDomainId(_))
case Domain.TargetDomain(targetDomain) =>
DomainId
.fromProtoPrimitive(targetDomain, "TransferResult.targetDomain")
.map(TargetDomainId(_))
case Domain.Empty => Left(FieldNotSet("TransferResponse.domain"))
}
informees <- informeesP.traverse(ProtoConverter.parseLfPartyId)
verdict <- ProtoConverter
.required("TransferResult.verdict", verdictPO)
.flatMap(Verdict.fromProtoV30)
rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
} yield TransferResult(requestId, informees.toSet, domain, verdict)(rpv, Some(bytes))
}
implicit def transferResultCast[Kind <: TransferDomainId](implicit
cast: TransferDomainIdCast[Kind]
): SignedMessageContentCast[TransferResult[Kind]] =
SignedMessageContentCast.create[TransferResult[Kind]]("TransferResult") {
case result: TransferResult[TransferDomainId] => result.traverse(cast.toKind)
case _ => None
}
}

View File

@ -101,22 +101,14 @@ object TypedSignedProtocolMessageContent
message <- (messageBytes match { message <- (messageBytes match {
case Sm.ConfirmationResponse(confirmationResponseBytes) => case Sm.ConfirmationResponse(confirmationResponseBytes) =>
ConfirmationResponse.fromByteString(expectedProtocolVersion)(confirmationResponseBytes) ConfirmationResponse.fromByteString(expectedProtocolVersion)(confirmationResponseBytes)
case Sm.TransactionResult(transactionResultMessageBytes) => case Sm.ConfirmationResult(confirmationResultMessageBytes) =>
ConfirmationResultMessage.fromByteString(expectedProtocolVersion)( ConfirmationResultMessage.fromByteString(expectedProtocolVersion)(
transactionResultMessageBytes confirmationResultMessageBytes
) )
case Sm.TransferResult(transferResultBytes) =>
TransferResult.fromByteString(expectedProtocolVersion)(transferResultBytes)
case Sm.AcsCommitment(acsCommitmentBytes) => case Sm.AcsCommitment(acsCommitmentBytes) =>
AcsCommitment.fromByteString(expectedProtocolVersion)(acsCommitmentBytes) AcsCommitment.fromByteString(expectedProtocolVersion)(acsCommitmentBytes)
case Sm.SetTrafficBalance(setTrafficBalanceBytes) => case Sm.SetTrafficBalance(setTrafficBalanceBytes) =>
SetTrafficBalanceMessage.fromByteString(expectedProtocolVersion)(setTrafficBalanceBytes) SetTrafficBalanceMessage.fromByteString(expectedProtocolVersion)(setTrafficBalanceBytes)
case Sm.MalformedMediatorConfirmationRequestResult(
malformedMediatorConfirmationRequestResultBytes
) =>
MalformedConfirmationRequestResult.fromByteString(expectedProtocolVersion)(
malformedMediatorConfirmationRequestResultBytes
)
case Sm.Empty => case Sm.Empty =>
Left(OtherError("Deserialization of a SignedMessage failed due to a missing message")) Left(OtherError("Deserialization of a SignedMessage failed due to a missing message"))
}): ParsingResult[SignedProtocolMessageContent] }): ParsingResult[SignedProtocolMessageContent]

View File

@ -27,13 +27,15 @@ trait TransactionRejection {
def reason(): com.google.rpc.status.Status def reason(): com.google.rpc.status.Status
} }
/** Verdicts sent from the mediator to the participants inside the [[ConfirmationResult]] message */ /** Verdicts sent from the mediator to the participants inside the [[ConfirmationResultMessage]] */
sealed trait Verdict sealed trait Verdict
extends Product extends Product
with Serializable with Serializable
with PrettyPrinting with PrettyPrinting
with HasProtocolVersionedWrapper[Verdict] { with HasProtocolVersionedWrapper[Verdict] {
def isApprove: Boolean = false
/** Whether the verdict represents a timeout that the mediator has determined. */ /** Whether the verdict represents a timeout that the mediator has determined. */
def isTimeoutDeterminedByMediator: Boolean def isTimeoutDeterminedByMediator: Boolean
@ -57,6 +59,9 @@ object Verdict
final case class Approve()( final case class Approve()(
override val representativeProtocolVersion: RepresentativeProtocolVersion[Verdict.type] override val representativeProtocolVersion: RepresentativeProtocolVersion[Verdict.type]
) extends Verdict { ) extends Verdict {
override def isApprove: Boolean = true
override def isTimeoutDeterminedByMediator: Boolean = false override def isTimeoutDeterminedByMediator: Boolean = false
private[messages] override def toProtoV30: v30.Verdict = private[messages] override def toProtoV30: v30.Verdict =
@ -71,7 +76,10 @@ object Verdict
) )
} }
final case class MediatorReject private (override val reason: com.google.rpc.status.Status)( final case class MediatorReject private (
override val reason: com.google.rpc.status.Status,
isMalformed: Boolean,
)(
override val representativeProtocolVersion: RepresentativeProtocolVersion[Verdict.type] override val representativeProtocolVersion: RepresentativeProtocolVersion[Verdict.type]
) extends Verdict ) extends Verdict
with TransactionRejection { with TransactionRejection {
@ -80,10 +88,12 @@ object Verdict
private[messages] override def toProtoV30: v30.Verdict = private[messages] override def toProtoV30: v30.Verdict =
v30.Verdict(v30.Verdict.SomeVerdict.MediatorReject(toProtoMediatorRejectV30)) v30.Verdict(v30.Verdict.SomeVerdict.MediatorReject(toProtoMediatorRejectV30))
def toProtoMediatorRejectV30: v30.MediatorReject = v30.MediatorReject(Some(reason)) def toProtoMediatorRejectV30: v30.MediatorReject =
v30.MediatorReject(reason = Some(reason), isMalformed = isMalformed)
override def pretty: Pretty[MediatorReject.this.type] = prettyOfClass( override def pretty: Pretty[MediatorReject.this.type] = prettyOfClass(
unnamedParam(_.reason) unnamedParam(_.reason),
param("isMalformed", _.isMalformed),
) )
override def logWithContext(extra: Map[String, String])(implicit override def logWithContext(extra: Map[String, String])(implicit
@ -103,18 +113,19 @@ object Verdict
// TODO(#15628) Make it safe (intercept the exception and return an either) // TODO(#15628) Make it safe (intercept the exception and return an either)
def tryCreate( def tryCreate(
status: com.google.rpc.status.Status, status: com.google.rpc.status.Status,
isMalformed: Boolean,
protocolVersion: ProtocolVersion, protocolVersion: ProtocolVersion,
): MediatorReject = ): MediatorReject =
MediatorReject(status)(Verdict.protocolVersionRepresentativeFor(protocolVersion)) MediatorReject(status, isMalformed)(Verdict.protocolVersionRepresentativeFor(protocolVersion))
private[messages] def fromProtoV30( private[messages] def fromProtoV30(
mediatorRejectP: v30.MediatorReject mediatorRejectP: v30.MediatorReject
): ParsingResult[MediatorReject] = { ): ParsingResult[MediatorReject] = {
val v30.MediatorReject(statusO) = mediatorRejectP val v30.MediatorReject(statusO, isMalformed) = mediatorRejectP
for { for {
status <- ProtoConverter.required("rejection_reason", statusO) status <- ProtoConverter.required("rejection_reason", statusO)
rpv <- protocolVersionRepresentativeFor(ProtoVersion(30)) rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
} yield MediatorReject(status)(rpv) } yield MediatorReject(status, isMalformed)(rpv)
} }
} }
@ -127,7 +138,7 @@ object Verdict
private[messages] override def toProtoV30: v30.Verdict = { private[messages] override def toProtoV30: v30.Verdict = {
val reasonsP = v30.ParticipantReject(reasons.map { case (parties, message) => val reasonsP = v30.ParticipantReject(reasons.map { case (parties, message) =>
v30.RejectionReason(parties.toSeq, Some(message.toLocalRejectProtoV30)) v30.RejectionReason(parties.toSeq, Some(message.toProtoV30))
}) })
v30.Verdict(someVerdict = v30.Verdict.SomeVerdict.ParticipantReject(reasonsP)) v30.Verdict(someVerdict = v30.Verdict.SomeVerdict.ParticipantReject(reasonsP))
} }
@ -150,7 +161,7 @@ object Verdict
val message = show"Request was rejected with multiple reasons. $reasons" val message = show"Request was rejected with multiple reasons. $reasons"
loggingContext.logger.info(message)(loggingContext.traceContext) loggingContext.logger.info(message)(loggingContext.traceContext)
} }
reasons.map { case (_, localReject) => localReject }.maxBy1(_.code.category) reasons.map { case (_, localReject) => localReject }.head1
} }
override def isTimeoutDeterminedByMediator: Boolean = false override def isTimeoutDeterminedByMediator: Boolean = false
@ -204,10 +215,15 @@ object Verdict
private def fromProtoReasonV30( private def fromProtoReasonV30(
protoReason: v30.RejectionReason protoReason: v30.RejectionReason
): ParsingResult[(Set[LfPartyId], LocalReject)] = { ): ParsingResult[(Set[LfPartyId], LocalReject)] = {
val v30.RejectionReason(partiesP, messageP) = protoReason val v30.RejectionReason(partiesP, rejectP) = protoReason
for { for {
parties <- partiesP.traverse(ProtoConverter.parseLfPartyId).map(_.toSet) parties <- partiesP.traverse(ProtoConverter.parseLfPartyId).map(_.toSet)
message <- ProtoConverter.parseRequired(LocalReject.fromProtoV30, "reject", messageP) localVerdict <- ProtoConverter.parseRequired(LocalVerdict.fromProtoV30, "reject", rejectP)
} yield (parties, message) localReject <- localVerdict match {
case localReject: LocalReject => Right(localReject)
case LocalApprove() =>
Left(InvariantViolation("RejectionReason.reject must not be approve."))
}
} yield (parties, localReject)
} }
} }

View File

@ -12,12 +12,6 @@ import com.digitalasset.canton.sequencing.protocol.{AllMembersOfDomain, OpenEnve
*/ */
package object messages { package object messages {
type TransferOutResult = TransferResult[SourceDomainId]
val TransferOutResult: TransferResult.type = TransferResult
type TransferInResult = TransferResult[TargetDomainId]
val TransferInResult: TransferResult.type = TransferResult
type TransactionViewMessage = EncryptedViewMessage[TransactionViewType] type TransactionViewMessage = EncryptedViewMessage[TransactionViewType]
type DefaultOpenEnvelope = OpenEnvelope[ProtocolMessage] type DefaultOpenEnvelope = OpenEnvelope[ProtocolMessage]

View File

@ -169,12 +169,6 @@ class SequencerAggregator(
val messagesToCombine = nonEmptyMessages.map(_._2).toList val messagesToCombine = nonEmptyMessages.map(_._2).toList
val (sequencerIdToNotify, _) = nonEmptyMessages.head1 val (sequencerIdToNotify, _) = nonEmptyMessages.head1
// TODO(#17462) Consider to remove excessive debug logging
implicit val traceContext: TraceContext = nonEmptyMessages.head1._2.traceContext
logger.debug(
s"Aggregation threshold reached for timestamp $nextMinimumTimestamp. Chosen sequencer loop to notify: $sequencerIdToNotify"
)
nextData.promise nextData.promise
.outcome( .outcome(
addEventToQueue(messagesToCombine).map(_ => sequencerIdToNotify) addEventToQueue(messagesToCombine).map(_ => sequencerIdToNotify)

View File

@ -758,6 +758,7 @@ class RichSequencerClientImpl(
private val handlerIdle: AtomicReference[Promise[Unit]] = new AtomicReference( private val handlerIdle: AtomicReference[Promise[Unit]] = new AtomicReference(
Promise.successful(()) Promise.successful(())
) )
private val handlerIdleLock: Object = new Object
override protected def subscribeAfterInternal( override protected def subscribeAfterInternal(
priorTimestamp: CantonTimestamp, priorTimestamp: CantonTimestamp,
@ -1024,10 +1025,6 @@ class RichSequencerClientImpl(
.leftMap[SequencerClientSubscriptionError](EventAggregationError) .leftMap[SequencerClientSubscriptionError](EventAggregationError)
} yield } yield
if (toSignalHandler) { if (toSignalHandler) {
// TODO(#17462) Consider to remove excessive debug logging
logger.debug(
s"Signalling event with counter ${serializedEvent.counter} to the application handler from loop for $sequencerId (alias=$sequencerAlias)"
)
signalHandler(applicationHandler) signalHandler(applicationHandler)
}).value }).value
} }
@ -1054,11 +1051,13 @@ class RichSequencerClientImpl(
// futures have been added in the meantime as the synchronous flush future finished. // futures have been added in the meantime as the synchronous flush future finished.
// c. I (rv) think that waiting on the `handlerIdle` is a unnecessary for shutdown as it does the // c. I (rv) think that waiting on the `handlerIdle` is a unnecessary for shutdown as it does the
// same as the flush future. We only need it to ensure we don't start the sequential processing in parallel. // same as the flush future. We only need it to ensure we don't start the sequential processing in parallel.
// TODO(#13789) This code should really not live in the `SubscriptionHandler` class of which we have multiple
// instances with equivalent parameters in case of BFT subscriptions.
private def signalHandler( private def signalHandler(
eventHandler: OrdinaryApplicationHandler[ClosedEnvelope] eventHandler: OrdinaryApplicationHandler[ClosedEnvelope]
)(implicit traceContext: TraceContext): Unit = performUnlessClosing(functionFullName) { )(implicit traceContext: TraceContext): Unit = performUnlessClosing(functionFullName) {
val isIdle = blocking { val isIdle = blocking {
synchronized { handlerIdleLock.synchronized {
val oldPromise = handlerIdle.getAndUpdate(p => if (p.isCompleted) Promise() else p) val oldPromise = handlerIdle.getAndUpdate(p => if (p.isCompleted) Promise() else p)
oldPromise.isCompleted oldPromise.isCompleted
} }
@ -1074,22 +1073,12 @@ class RichSequencerClientImpl(
): Future[Unit] = { ): Future[Unit] = {
val inboxSize = config.eventInboxSize.unwrap val inboxSize = config.eventInboxSize.unwrap
val javaEventList = new java.util.ArrayList[OrdinarySerializedEvent](inboxSize) val javaEventList = new java.util.ArrayList[OrdinarySerializedEvent](inboxSize)
// TODO(#17462) Consider to remove excessive debug logging
noTracingLogger.debug(
s"Draining events in handler loop for $sequencerId (alias=$sequencerAlias)"
)
if (sequencerAggregator.eventQueue.drainTo(javaEventList, inboxSize) > 0) { if (sequencerAggregator.eventQueue.drainTo(javaEventList, inboxSize) > 0) {
import scala.jdk.CollectionConverters.* import scala.jdk.CollectionConverters.*
val handlerEvents = javaEventList.asScala.toSeq val handlerEvents = javaEventList.asScala.toSeq
// TODO(#17462) Consider to remove excessive debug logging
noTracingLogger.debug(
s"Drained ${handlerEvents.size} events in handler loop for $sequencerId (alias=$sequencerAlias): sequencerCounters ${handlerEvents
.map(_.counter)
.minOption} to ${handlerEvents.map(_.counter).maxOption}"
)
def stopHandler(): Unit = blocking { def stopHandler(): Unit = blocking {
this.synchronized { val _ = handlerIdle.get().success(()) } handlerIdleLock.synchronized { val _ = handlerIdle.get().success(()) }
} }
sendTracker sendTracker
@ -1104,7 +1093,7 @@ class RichSequencerClientImpl(
} }
} else { } else {
val stillBusy = blocking { val stillBusy = blocking {
this.synchronized { handlerIdleLock.synchronized {
val idlePromise = handlerIdle.get() val idlePromise = handlerIdle.get()
if (sequencerAggregator.eventQueue.isEmpty) { if (sequencerAggregator.eventQueue.isEmpty) {
// signalHandler must not be executed here, because that would lead to lost signals. // signalHandler must not be executed here, because that would lead to lost signals.
@ -1118,10 +1107,6 @@ class RichSequencerClientImpl(
if (stillBusy) { if (stillBusy) {
handleReceivedEventsUntilEmpty(eventHandler) handleReceivedEventsUntilEmpty(eventHandler)
} else { } else {
// TODO(#17462) Consider to remove excessive debug logging
noTracingLogger.debug(
s"Stopping handler loop for $sequencerId (alias=$sequencerAlias) because we ran out of queued events"
)
Future.unit Future.unit
} }
} }

View File

@ -3,13 +3,18 @@
package com.digitalasset.canton.topology.client package com.digitalasset.canton.topology.client
import com.digitalasset.canton.config.RequireTypes.PositiveLong import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong}
import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.topology.Member import com.digitalasset.canton.topology.Member
import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext
import scala.concurrent.Future import scala.concurrent.Future
final case class MemberTrafficControlState(totalExtraTrafficLimit: PositiveLong) final case class MemberTrafficControlState(
totalExtraTrafficLimit: PositiveLong,
serial: PositiveInt,
effectiveTimestamp: CantonTimestamp,
)
/** The subset of the topology client providing traffic state information */ /** The subset of the topology client providing traffic state information */
trait DomainTrafficControlStateClient { trait DomainTrafficControlStateClient {

View File

@ -403,12 +403,19 @@ class StoreBasedTopologySnapshotX(
.result .result
.groupBy(_.mapping.member) .groupBy(_.mapping.member)
.flatMap { case (member, mappings) => .flatMap { case (member, mappings) =>
collectLatestMapping( collectLatestTransaction(
TopologyMappingX.Code.TrafficControlStateX, TopologyMappingX.Code.TrafficControlStateX,
mappings.sortBy(_.validFrom), mappings.sortBy(_.validFrom),
).map(mapping => ).map { tx =>
Some(MemberTrafficControlState(totalExtraTrafficLimit = mapping.totalExtraTrafficLimit)) val mapping = tx.mapping
).map(member -> _) Some(
MemberTrafficControlState(
totalExtraTrafficLimit = mapping.totalExtraTrafficLimit,
tx.serial,
tx.validFrom.value,
)
)
}.map(member -> _)
} }
val membersWithoutState = members.toSet.diff(membersWithState.keySet).map(_ -> None).toMap val membersWithoutState = members.toSet.diff(membersWithState.keySet).map(_ -> None).toMap

View File

@ -160,8 +160,8 @@ object TopologyStoreId {
override def isDomainStore: Boolean = false override def isDomainStore: Boolean = false
} }
def apply(fName: String): TopologyStoreId = fName match { def apply(fName: String): TopologyStoreId = fName.toLowerCase match {
case "Authorized" => AuthorizedStore case "authorized" => AuthorizedStore
case domain => DomainStore(DomainId(UniqueIdentifier.tryFromProtoPrimitive(domain))) case domain => DomainStore(DomainId(UniqueIdentifier.tryFromProtoPrimitive(domain)))
} }

View File

@ -7,7 +7,7 @@ import cats.data.EitherT
import cats.syntax.bifunctor.* import cats.syntax.bifunctor.*
import cats.syntax.parallel.* import cats.syntax.parallel.*
import com.daml.nonempty.NonEmpty import com.daml.nonempty.NonEmpty
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.crypto.{DomainSnapshotSyncCryptoApi, DomainSyncCryptoClient} import com.digitalasset.canton.crypto.{DomainSnapshotSyncCryptoApi, DomainSyncCryptoClient}
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
@ -51,7 +51,7 @@ class TrafficBalanceSubmissionHandler(
member: Member, member: Member,
domainId: DomainId, domainId: DomainId,
protocolVersion: ProtocolVersion, protocolVersion: ProtocolVersion,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
sequencerClient: SequencerClientSend, sequencerClient: SequencerClientSend,
cryptoApi: DomainSyncCryptoClient, cryptoApi: DomainSyncCryptoClient,

View File

@ -1,10 +1,10 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: CantonExamples name: CantonExamples
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -5,11 +5,12 @@ package com.digitalasset.canton.crypto.provider.jce
import cats.syntax.either.* import cats.syntax.either.*
import com.digitalasset.canton.DiscardOps import com.digitalasset.canton.DiscardOps
import com.digitalasset.canton.config.CommunityCryptoProvider.Jce
import com.digitalasset.canton.crypto.CryptoPureApiError.KeyParseAndValidateError
import com.digitalasset.canton.crypto.HkdfError.HkdfInternalError import com.digitalasset.canton.crypto.HkdfError.HkdfInternalError
import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.*
import com.digitalasset.canton.crypto.deterministic.encryption.DeterministicRandom import com.digitalasset.canton.crypto.deterministic.encryption.DeterministicRandom
import com.digitalasset.canton.crypto.provider.CryptoKeyConverter import com.digitalasset.canton.crypto.provider.CryptoKeyConverter
import com.digitalasset.canton.crypto.provider.jce.JceSecurityProvider.bouncyCastleProvider
import com.digitalasset.canton.crypto.provider.tink.TinkJavaConverter import com.digitalasset.canton.crypto.provider.tink.TinkJavaConverter
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.serialization.{ import com.digitalasset.canton.serialization.{
@ -33,6 +34,7 @@ import org.bouncycastle.crypto.DataLengthException
import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.generators.{Argon2BytesGenerator, HKDFBytesGenerator} import org.bouncycastle.crypto.generators.{Argon2BytesGenerator, HKDFBytesGenerator}
import org.bouncycastle.crypto.params.{Argon2Parameters, HKDFParameters} import org.bouncycastle.crypto.params.{Argon2Parameters, HKDFParameters}
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
import org.bouncycastle.jce.spec.IESParameterSpec import org.bouncycastle.jce.spec.IESParameterSpec
import sun.security.ec.ECPrivateKeyImpl import sun.security.ec.ECPrivateKeyImpl
@ -80,11 +82,14 @@ class JcePureCrypto(
with ShowUtil with ShowUtil
with NamedLogging { with NamedLogging {
// TODO(#15632): Make these real caches with an eviction rule
// Cache for the java key conversion results // Cache for the java key conversion results
private val javaPublicKeyCache: TrieMap[Fingerprint, Either[JavaKeyConversionError, JPublicKey]] = private val javaPublicKeyCache
: TrieMap[Fingerprint, Either[KeyParseAndValidateError, JPublicKey]] =
TrieMap.empty TrieMap.empty
private val javaPrivateKeyCache private val javaPrivateKeyCache
: TrieMap[Fingerprint, Either[JavaKeyConversionError, JPrivateKey]] = TrieMap.empty : TrieMap[Fingerprint, Either[KeyParseAndValidateError, JPrivateKey]] =
TrieMap.empty
private lazy val tinkJavaConverter = new TinkJavaConverter private lazy val tinkJavaConverter = new TinkJavaConverter
private lazy val keyConverter = new CryptoKeyConverter(tinkJavaConverter, javaKeyConverter) private lazy val keyConverter = new CryptoKeyConverter(tinkJavaConverter, javaKeyConverter)
@ -119,19 +124,80 @@ class JcePureCrypto(
val keySizeInBits = 2048 val keySizeInBits = 2048
} }
private def checkKeyFormat[E]( /** Parses and converts a public key to a java public key.
expected: CryptoKeyFormat, * We store the deserialization result in a cache.
actual: CryptoKeyFormat, *
errFn: String => E, * @return Either an error or the converted java private key
): Either[E, Unit] = */
Either.cond(expected == actual, (), errFn(s"Expected key format $expected, but got $actual")) private def parseAndGetPublicKey[E](
private def convertKey[E](
publicKey: PublicKey, publicKey: PublicKey,
expected: CryptoKeyFormat,
errFn: String => E, errFn: String => E,
): Either[E, PublicKey] = ): Either[E, JPublicKey] = {
keyConverter.convert(publicKey, expected).leftMap(errFn) val keyFormat = publicKey.format
def convertToFormatAndGenerateJavaPublicKey: Either[KeyParseAndValidateError, JPublicKey] = {
for {
formatToConvertTo <- publicKey match {
case _: EncryptionPublicKey => Right(CryptoKeyFormat.Der)
case SigningPublicKey(_, _, _, scheme) =>
scheme match {
case SigningKeyScheme.Ed25519 => Right(CryptoKeyFormat.Raw)
case SigningKeyScheme.EcDsaP256 | SigningKeyScheme.EcDsaP384 =>
Right(CryptoKeyFormat.Der)
}
case _ => Left(KeyParseAndValidateError(s"Unsupported key type"))
}
/* Convert key to the expected format that can be interpreted by this crypto provider (i.e. JCE): DER or RAW.
* Finally convert the key to a java key, which can be cached and used for future crypto operations.
*/
pubKey <- keyConverter
.convert(publicKey, formatToConvertTo)
.leftMap(KeyParseAndValidateError)
// convert key to java key
jPublicKey <- javaKeyConverter
.toJava(pubKey)
.map(_._2)
.leftMap(err =>
KeyParseAndValidateError(s"Failed to convert public key to java key: $err")
)
} yield jPublicKey
}
def getFromCacheOrDeserializeKey: Either[E, JPublicKey] =
javaPublicKeyCache
.getOrElseUpdate(
publicKey.id,
convertToFormatAndGenerateJavaPublicKey,
)
.leftMap(err => errFn(s"Failed to deserialize ${publicKey.format} public key: $err"))
if (Jce.supportedCryptoKeyFormats.contains(keyFormat))
getFromCacheOrDeserializeKey
else Left(errFn(s"$keyFormat key format not supported"))
}
/** Parses and converts an asymmetric private key to a java private key.
* We store the deserialization result in a cache.
*
* @return Either an error or the converted java private key
*/
private def parseAndGetPrivateKey[E, T <: JPrivateKey](
privateKey: PrivateKey,
checker: PartialFunction[JPrivateKey, Either[E, T]],
errFn: String => E,
): Either[E, T] =
for {
privateKey <- javaPrivateKeyCache
.getOrElseUpdate(
privateKey.id,
toJava(privateKey)
.leftMap(err =>
KeyParseAndValidateError(s"Failed to convert private key to java key: ${err.show}")
),
)
.leftMap(err => errFn(s"Failed to deserialize ${privateKey.format} private key: $err"))
checkedPrivateKey <- checker(privateKey)
} yield checkedPrivateKey
private def encryptAes128Gcm( private def encryptAes128Gcm(
plaintext: ByteString, plaintext: ByteString,
@ -181,22 +247,16 @@ class JcePureCrypto(
} }
} }
private def ensureFormat(
key: CryptoKey,
format: CryptoKeyFormat,
): Either[JavaKeyConversionError, Unit] =
Either.cond(
key.format == format,
(),
JavaKeyConversionError.UnsupportedKeyFormat(key.format, format),
)
private def toJavaEcDsa( private def toJavaEcDsa(
privateKey: PrivateKey, privateKey: PrivateKey,
curveType: CurveType, curveType: CurveType,
): Either[JavaKeyConversionError, JPrivateKey] = ): Either[JavaKeyConversionError, JPrivateKey] =
for { for {
_ <- ensureFormat(privateKey, CryptoKeyFormat.Der) _ <- CryptoKeyValidation.ensureFormat(
privateKey.format,
Set(CryptoKeyFormat.Der),
_ => JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, CryptoKeyFormat.Der),
)
ecPrivateKey <- Either ecPrivateKey <- Either
.catchOnly[GeneralSecurityException]( .catchOnly[GeneralSecurityException](
EllipticCurves.getEcPrivateKey(curveType, privateKey.key.toByteArray) EllipticCurves.getEcPrivateKey(curveType, privateKey.key.toByteArray)
@ -224,10 +284,10 @@ class JcePureCrypto(
keyInstance: String, keyInstance: String,
): Either[JavaKeyConversionError, JPrivateKey] = ): Either[JavaKeyConversionError, JPrivateKey] =
for { for {
_ <- Either.cond( _ <- CryptoKeyValidation.ensureFormat(
privateKey.format == format, privateKey.format,
(), Set(format),
JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, format), _ => JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, format),
) )
pkcs8KeySpec = new PKCS8EncodedKeySpec(pkcs8PrivateKey) pkcs8KeySpec = new PKCS8EncodedKeySpec(pkcs8PrivateKey)
keyFactory <- Either keyFactory <- Either
@ -268,22 +328,11 @@ class JcePureCrypto(
hashType: HashType, hashType: HashType,
): Either[SigningError, PublicKeySign] = ): Either[SigningError, PublicKeySign] =
for { for {
_ <- checkKeyFormat(CryptoKeyFormat.Der, signingKey.format, SigningError.InvalidSigningKey) ecPrivateKey <- parseAndGetPrivateKey(
javaPrivateKey <- javaPrivateKeyCache signingKey,
.getOrElseUpdate( { case k: ECPrivateKey => Right(k) },
signingKey.id, SigningError.InvalidSigningKey,
toJava(signingKey), )
)
.leftMap(err =>
SigningError.InvalidSigningKey(s"Failed to convert signing private key: $err")
)
ecPrivateKey <- javaPrivateKey match {
case k: ECPrivateKey => Right(k)
case _ =>
Left(SigningError.InvalidSigningKey(s"Signing private key is not an EC private key"))
}
signer <- Either signer <- Either
.catchOnly[GeneralSecurityException]( .catchOnly[GeneralSecurityException](
new EcdsaSignJce(ecPrivateKey, hashType, EcdsaEncoding.DER) new EcdsaSignJce(ecPrivateKey, hashType, EcdsaEncoding.DER)
@ -296,22 +345,7 @@ class JcePureCrypto(
hashType: HashType, hashType: HashType,
): Either[SignatureCheckError, PublicKeyVerify] = ): Either[SignatureCheckError, PublicKeyVerify] =
for { for {
pubKey <- convertKey( javaPublicKey <- parseAndGetPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
publicKey,
CryptoKeyFormat.Der,
SignatureCheckError.InvalidKeyError,
)
javaPublicKey <- javaPublicKeyCache
.getOrElseUpdate(
pubKey.id,
javaKeyConverter
.toJava(pubKey)
.map(_._2),
)
.leftMap(err =>
SignatureCheckError.InvalidKeyError(s"Failed to convert signing public key: $err")
)
ecPublicKey <- javaPublicKey match { ecPublicKey <- javaPublicKey match {
case k: ECPublicKey => Right(k) case k: ECPublicKey => Right(k)
case _ => case _ =>
@ -373,9 +407,9 @@ class JcePureCrypto(
signingKey.scheme match { signingKey.scheme match {
case SigningKeyScheme.Ed25519 => case SigningKeyScheme.Ed25519 =>
for { for {
_ <- checkKeyFormat( _ <- CryptoKeyValidation.ensureFormat(
CryptoKeyFormat.Raw,
signingKey.format, signingKey.format,
Set(CryptoKeyFormat.Raw),
SigningError.InvalidSigningKey, SigningError.InvalidSigningKey,
) )
signer <- Either signer <- Either
@ -421,13 +455,17 @@ class JcePureCrypto(
_ <- publicKey.scheme match { _ <- publicKey.scheme match {
case SigningKeyScheme.Ed25519 => case SigningKeyScheme.Ed25519 =>
for { for {
pubKey <- convertKey( javaPublicKey <- parseAndGetPublicKey(
publicKey, publicKey,
CryptoKeyFormat.Raw,
SignatureCheckError.InvalidKeyError, SignatureCheckError.InvalidKeyError,
) )
ed25519PublicKey <- javaPublicKey match {
case k: BCEdDSAPublicKey =>
Right(k.getPointEncoding)
case _ => Left(SignatureCheckError.InvalidKeyError("Not an Ed25519 public key"))
}
verifier <- Either verifier <- Either
.catchOnly[GeneralSecurityException](new Ed25519Verify(pubKey.key.toByteArray)) .catchOnly[GeneralSecurityException](new Ed25519Verify(ed25519PublicKey))
.leftMap(err => .leftMap(err =>
SignatureCheckError.InvalidKeyError(show"Failed to get signer for Ed25519: $err") SignatureCheckError.InvalidKeyError(show"Failed to get signer for Ed25519: $err")
) )
@ -459,42 +497,20 @@ class JcePureCrypto(
) )
} }
private def checkDerFormatAndGetPublicKey[T <: JPublicKey](
encPubKey: EncryptionPublicKey,
checker: PartialFunction[JPublicKey, Either[EncryptionError, T]],
): Either[EncryptionError, T] = {
for {
pubKey <- convertKey(
encPubKey,
CryptoKeyFormat.Der,
EncryptionError.InvalidEncryptionKey,
)
javaPublicKey <- javaPublicKeyCache
.getOrElseUpdate(
pubKey.id,
javaKeyConverter
.toJava(pubKey)
.map(_._2),
)
.leftMap(err => EncryptionError.InvalidEncryptionKey(err.toString))
publicKey <- checker(javaPublicKey)
} yield publicKey
}
private def encryptWithEciesP256HmacSha256Aes128Cbc[M <: HasVersionedToByteString]( private def encryptWithEciesP256HmacSha256Aes128Cbc[M <: HasVersionedToByteString](
message: ByteString, message: ByteString,
publicKey: EncryptionPublicKey, publicKey: EncryptionPublicKey,
random: SecureRandom, random: SecureRandom,
): Either[EncryptionError, AsymmetricEncrypted[M]] = ): Either[EncryptionError, AsymmetricEncrypted[M]] =
for { for {
ecPublicKey <- checkDerFormatAndGetPublicKey[ECPublicKey]( javaPublicKey <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
publicKey, ecPublicKey <- javaPublicKey match {
{ case k: ECPublicKey => case k: ECPublicKey =>
checkEcKeyInCurve(k, publicKey.id).leftMap(err => checkEcKeyInCurve(k, publicKey.id).leftMap(err =>
EncryptionError.InvalidEncryptionKey(err) EncryptionError.InvalidEncryptionKey(err)
) )
}, case _ => Left(EncryptionError.InvalidEncryptionKey("Not an EC public key"))
) }
/* this encryption scheme makes use of AES-128-CBC as a DEM (Data Encapsulation Method) /* this encryption scheme makes use of AES-128-CBC as a DEM (Data Encapsulation Method)
* and therefore we need to generate a IV/nonce of 16bytes as the IV for CBC mode. * and therefore we need to generate a IV/nonce of 16bytes as the IV for CBC mode.
*/ */
@ -503,7 +519,10 @@ class JcePureCrypto(
encrypter <- Either encrypter <- Either
.catchOnly[GeneralSecurityException] { .catchOnly[GeneralSecurityException] {
val cipher = Cipher val cipher = Cipher
.getInstance(EciesP256HmacSha256Aes128CbcParams.jceInternalName, bouncyCastleProvider) .getInstance(
EciesP256HmacSha256Aes128CbcParams.jceInternalName,
JceSecurityProvider.bouncyCastleProvider,
)
cipher.init( cipher.init(
Cipher.ENCRYPT_MODE, Cipher.ENCRYPT_MODE,
ecPublicKey, ecPublicKey,
@ -533,12 +552,12 @@ class JcePureCrypto(
random: SecureRandom, random: SecureRandom,
): Either[EncryptionError, AsymmetricEncrypted[M]] = ): Either[EncryptionError, AsymmetricEncrypted[M]] =
for { for {
rsaPublicKey <- checkDerFormatAndGetPublicKey[RSAPublicKey]( javaPublicKey <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
publicKey, rsaPublicKey <- javaPublicKey match {
{ case k: RSAPublicKey => case k: RSAPublicKey =>
checkRsaKeySize(k, publicKey.id).leftMap(err => EncryptionError.InvalidEncryptionKey(err)) checkRsaKeySize(k, publicKey.id).leftMap(err => EncryptionError.InvalidEncryptionKey(err))
}, case _ => Left(EncryptionError.InvalidEncryptionKey("Not a RSA public key"))
) }
encrypter <- Either encrypter <- Either
.catchOnly[GeneralSecurityException] { .catchOnly[GeneralSecurityException] {
val cipher = Cipher val cipher = Cipher
@ -565,14 +584,17 @@ class JcePureCrypto(
publicKey.scheme match { publicKey.scheme match {
case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm => case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm =>
for { for {
ecPublicKey <- checkDerFormatAndGetPublicKey[ECPublicKey]( javaPublicKey <- parseAndGetPublicKey(
publicKey, publicKey,
{ case k: ECPublicKey => EncryptionError.InvalidEncryptionKey,
)
ecPublicKey <- javaPublicKey match {
case k: ECPublicKey =>
checkEcKeyInCurve(k, publicKey.id).leftMap(err => checkEcKeyInCurve(k, publicKey.id).leftMap(err =>
EncryptionError.InvalidEncryptionKey(err) EncryptionError.InvalidEncryptionKey(err)
) )
}, case _ => Left(EncryptionError.InvalidEncryptionKey("Not an EC public key"))
) }
encrypter <- Either encrypter <- Either
.catchOnly[GeneralSecurityException]( .catchOnly[GeneralSecurityException](
new EciesAeadHkdfHybridEncrypt( new EciesAeadHkdfHybridEncrypt(
@ -660,33 +682,17 @@ class JcePureCrypto(
deserialize: ByteString => Either[DeserializationError, M] deserialize: ByteString => Either[DeserializationError, M]
): Either[DecryptionError, M] = { ): Either[DecryptionError, M] = {
def checkDerFormatAndGetPrivateKey[T <: JPrivateKey](
checker: PartialFunction[JPrivateKey, Either[DecryptionError, T]]
): Either[DecryptionError, T] =
for {
_ <- checkKeyFormat(
CryptoKeyFormat.Der,
privateKey.format,
DecryptionError.InvalidEncryptionKey,
)
javaPrivateKey <- javaPrivateKeyCache
.getOrElseUpdate(
privateKey.id,
toJava(privateKey),
)
.leftMap(err => DecryptionError.InvalidEncryptionKey(err.toString))
privateKey <- checker(javaPrivateKey)
} yield privateKey
privateKey.scheme match { privateKey.scheme match {
case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm => case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm =>
for { for {
ecPrivateKey <- checkDerFormatAndGetPrivateKey( ecPrivateKey <- parseAndGetPrivateKey(
privateKey,
{ case k: ECPrivateKey => { case k: ECPrivateKey =>
checkEcKeyInCurve(k, privateKey.id).leftMap(err => checkEcKeyInCurve(k, privateKey.id).leftMap(err =>
DecryptionError.InvalidEncryptionKey(err) DecryptionError.InvalidEncryptionKey(err)
) )
} },
DecryptionError.InvalidEncryptionKey,
) )
decrypter <- Either decrypter <- Either
.catchOnly[GeneralSecurityException]( .catchOnly[GeneralSecurityException](
@ -709,12 +715,14 @@ class JcePureCrypto(
} yield message } yield message
case EncryptionKeyScheme.EciesP256HmacSha256Aes128Cbc => case EncryptionKeyScheme.EciesP256HmacSha256Aes128Cbc =>
for { for {
ecPrivateKey <- checkDerFormatAndGetPrivateKey[ECPrivateKey]( ecPrivateKey <- parseAndGetPrivateKey(
privateKey,
{ case k: ECPrivateKey => { case k: ECPrivateKey =>
checkEcKeyInCurve(k, privateKey.id).leftMap(err => checkEcKeyInCurve(k, privateKey.id).leftMap(err =>
DecryptionError.InvalidEncryptionKey(err) DecryptionError.InvalidEncryptionKey(err)
) )
} },
DecryptionError.InvalidEncryptionKey,
) )
/* we split at 'ivSizeForAesCbc' (=16) because that is the size of our iv (for AES-128-CBC) /* we split at 'ivSizeForAesCbc' (=16) because that is the size of our iv (for AES-128-CBC)
* that gets pre-appended to the ciphertext. * that gets pre-appended to the ciphertext.
@ -733,7 +741,7 @@ class JcePureCrypto(
val cipher = Cipher val cipher = Cipher
.getInstance( .getInstance(
EciesP256HmacSha256Aes128CbcParams.jceInternalName, EciesP256HmacSha256Aes128CbcParams.jceInternalName,
bouncyCastleProvider, JceSecurityProvider.bouncyCastleProvider,
) )
cipher.init( cipher.init(
Cipher.DECRYPT_MODE, Cipher.DECRYPT_MODE,
@ -753,12 +761,14 @@ class JcePureCrypto(
} yield message } yield message
case EncryptionKeyScheme.Rsa2048OaepSha256 => case EncryptionKeyScheme.Rsa2048OaepSha256 =>
for { for {
rsaPrivateKey <- checkDerFormatAndGetPrivateKey[RSAPrivateKey]( rsaPrivateKey <- parseAndGetPrivateKey(
privateKey,
{ case k: RSAPrivateKey => { case k: RSAPrivateKey =>
checkRsaKeySize(k, privateKey.id).leftMap(err => checkRsaKeySize(k, privateKey.id).leftMap(err =>
DecryptionError.InvalidEncryptionKey(err) DecryptionError.InvalidEncryptionKey(err)
) )
} },
DecryptionError.InvalidEncryptionKey,
) )
decrypter <- Either decrypter <- Either
.catchOnly[GeneralSecurityException] { .catchOnly[GeneralSecurityException] {
@ -799,9 +809,9 @@ class JcePureCrypto(
symmetricKey.scheme match { symmetricKey.scheme match {
case SymmetricKeyScheme.Aes128Gcm => case SymmetricKeyScheme.Aes128Gcm =>
for { for {
_ <- checkKeyFormat( _ <- CryptoKeyValidation.ensureFormat(
CryptoKeyFormat.Raw,
symmetricKey.format, symmetricKey.format,
Set(CryptoKeyFormat.Raw),
EncryptionError.InvalidSymmetricKey, EncryptionError.InvalidSymmetricKey,
) )
encryptedBytes <- encryptAes128Gcm( encryptedBytes <- encryptAes128Gcm(
@ -818,9 +828,9 @@ class JcePureCrypto(
symmetricKey.scheme match { symmetricKey.scheme match {
case SymmetricKeyScheme.Aes128Gcm => case SymmetricKeyScheme.Aes128Gcm =>
for { for {
_ <- checkKeyFormat( _ <- CryptoKeyValidation.ensureFormat(
CryptoKeyFormat.Raw,
symmetricKey.format, symmetricKey.format,
Set(CryptoKeyFormat.Raw),
DecryptionError.InvalidSymmetricKey, DecryptionError.InvalidSymmetricKey,
) )
plaintext <- decryptAes128Gcm(encrypted.ciphertext, symmetricKey.key) plaintext <- decryptAes128Gcm(encrypted.ciphertext, symmetricKey.key)

View File

@ -5,6 +5,8 @@ package com.digitalasset.canton.crypto.provider.tink
import cats.syntax.either.* import cats.syntax.either.*
import cats.syntax.foldable.* import cats.syntax.foldable.*
import com.digitalasset.canton.config.CommunityCryptoProvider.Tink
import com.digitalasset.canton.crypto.CryptoPureApiError.KeyParseAndValidateError
import com.digitalasset.canton.crypto.HkdfError.{HkdfHmacError, HkdfInternalError} import com.digitalasset.canton.crypto.HkdfError.{HkdfHmacError, HkdfInternalError}
import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.*
import com.digitalasset.canton.crypto.provider.CryptoKeyConverter import com.digitalasset.canton.crypto.provider.CryptoKeyConverter
@ -30,13 +32,95 @@ class TinkPureCrypto private (
override val defaultHashAlgorithm: HashAlgorithm, override val defaultHashAlgorithm: HashAlgorithm,
) extends CryptoPureApi { ) extends CryptoPureApi {
// Cache for the public and private key keyset deserialization result // TODO(#15632): Make these real caches with an eviction rule
// Cache for the public and private keyset deserialization result.
// Note: We do not cache symmetric keys as we generate random keys for each new view. // Note: We do not cache symmetric keys as we generate random keys for each new view.
private val publicKeysetCache: TrieMap[Fingerprint, Either[DeserializationError, KeysetHandle]] = private val publicKeysetCache
: TrieMap[Fingerprint, Either[KeyParseAndValidateError, KeysetHandle]] =
TrieMap.empty TrieMap.empty
private val privateKeysetCache: TrieMap[Fingerprint, Either[DeserializationError, KeysetHandle]] = private val privateKeysetCache
: TrieMap[Fingerprint, Either[KeyParseAndValidateError, KeysetHandle]] =
TrieMap.empty TrieMap.empty
/** Parses and converts a public key to a tink public key handle.
* We store the deserialization result in a cache.
*
* @return Either an error or the converted public keyset handle
*/
private def parseAndGetPublicKey[E](
publicKey: PublicKey,
errFn: String => E,
): Either[E, KeysetHandle] = {
val keyFormat = publicKey.format
def getFromCacheOrDeserializeKey: Either[E, KeysetHandle] =
// if the public key is already in cache it already has been deserialized and validated
publicKeysetCache
.getOrElseUpdate(
publicKey.id, {
for {
// convert key to Tink format so that we can deserialize it
tinkPublicKey <- keyConverter
.convert(publicKey, CryptoKeyFormat.Tink)
.leftMap(KeyParseAndValidateError)
handle <- TinkKeyFormat
.deserializeHandle(tinkPublicKey.key)
.leftMap(err => KeyParseAndValidateError(s"Deserialization error: $err"))
} yield handle
},
)
.leftMap(err => errFn(s"Failed to deserialize ${publicKey.format} public key: $err"))
if (Tink.supportedCryptoKeyFormats.contains(keyFormat))
getFromCacheOrDeserializeKey
else Left(errFn(s"$keyFormat key format not supported"))
}
/** Parses and converts a private or symmetric key to a tink keyset handle.
* We store the deserialization result in a cache.
*
* @return Either an error or the converted tink keyset handle
*/
private def parseAndGetPrivateKey[E](
privateKey: CryptoKey,
errFn: String => E,
): Either[E, KeysetHandle] =
(for {
// we are using Tink as the provider so we expect all private keys to be in Tink format
_ <- CryptoKeyValidation.ensureFormat(
privateKey.format,
Set(CryptoKeyFormat.Tink),
KeyParseAndValidateError,
)
keysetHandle <- privateKey match {
case key: PrivateKey =>
privateKeysetCache
.getOrElseUpdate(
key.id,
TinkKeyFormat
.deserializeHandle(privateKey.key)
.leftMap(err => KeyParseAndValidateError(s"Deserialization error: $err")),
)
case _: SymmetricKey =>
TinkKeyFormat
.deserializeHandle(privateKey.key)
.leftMap(err => KeyParseAndValidateError(s"Deserialization error: $err"))
case _ => Left(KeyParseAndValidateError("Key is not a private or symmetric key"))
}
} yield keysetHandle)
.leftMap(err => errFn(s"Failed to deserialize ${privateKey.format} private key: $err"))
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
private def getPrimitive[P: ClassTag, E](
keysetHandle: KeysetHandle,
errFn: String => E,
): Either[E, P] =
Either
.catchOnly[GeneralSecurityException](
keysetHandle.getPrimitive(classTag[P].runtimeClass.asInstanceOf[Class[P]])
)
.leftMap(err => errFn(show"Failed to get primitive: $err"))
private def encryptWith[M <: HasVersionedToByteString]( private def encryptWith[M <: HasVersionedToByteString](
message: M, message: M,
encrypt: Array[Byte] => Array[Byte], encrypt: Array[Byte] => Array[Byte],
@ -80,40 +164,6 @@ class TinkPureCrypto private (
deserialize(ByteString.copyFrom(plain)).leftMap(DecryptionError.FailedToDeserialize) deserialize(ByteString.copyFrom(plain)).leftMap(DecryptionError.FailedToDeserialize)
) )
private def ensureTinkFormat[E](format: CryptoKeyFormat, errFn: String => E): Either[E, Unit] =
Either.cond(format == CryptoKeyFormat.Tink, (), errFn(s"Key format must be Tink"))
private def convertPublicKey[E](publicKey: PublicKey, errFn: String => E): Either[E, PublicKey] =
keyConverter.convert(publicKey, CryptoKeyFormat.Tink).leftMap(errFn)
private def keysetNonCached[E](key: CryptoKey, errFn: String => E): Either[E, KeysetHandle] =
for {
_ <- ensureTinkFormat(key.format, errFn)
keysetHandle <- TinkKeyFormat
.deserializeHandle(key.key)
.leftMap(err => errFn(s"Failed to deserialize keyset: $err"))
} yield keysetHandle
private def keysetCached[E](key: CryptoKeyPairKey, errFn: String => E): Either[E, KeysetHandle] =
for {
_ <- ensureTinkFormat(key.format, errFn)
keysetCache = if (key.isPublicKey) publicKeysetCache else privateKeysetCache
keysetHandle <- keysetCache
.getOrElseUpdate(key.id, TinkKeyFormat.deserializeHandle(key.key))
.leftMap(err => errFn(s"Failed to deserialize keyset: $err"))
} yield keysetHandle
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
private def getPrimitive[P: ClassTag, E](
keysetHandle: KeysetHandle,
errFn: String => E,
): Either[E, P] =
Either
.catchOnly[GeneralSecurityException](
keysetHandle.getPrimitive(classTag[P].runtimeClass.asInstanceOf[Class[P]])
)
.leftMap(err => errFn(show"Failed to get primitive: $err"))
/** Generates a random symmetric key */ /** Generates a random symmetric key */
override def generateSymmetricKey( override def generateSymmetricKey(
scheme: SymmetricKeyScheme = defaultSymmetricKeyScheme scheme: SymmetricKeyScheme = defaultSymmetricKeyScheme
@ -184,7 +234,10 @@ class TinkPureCrypto private (
version: ProtocolVersion, version: ProtocolVersion,
): Either[EncryptionError, Encrypted[M]] = ): Either[EncryptionError, Encrypted[M]] =
for { for {
keysetHandle <- keysetNonCached(symmetricKey, EncryptionError.InvalidSymmetricKey) keysetHandle <- parseAndGetPrivateKey(
symmetricKey,
EncryptionError.InvalidSymmetricKey,
)
aead <- getPrimitive[tink.Aead, EncryptionError]( aead <- getPrimitive[tink.Aead, EncryptionError](
keysetHandle, keysetHandle,
EncryptionError.InvalidSymmetricKey, EncryptionError.InvalidSymmetricKey,
@ -197,7 +250,10 @@ class TinkPureCrypto private (
deserialize: ByteString => Either[DeserializationError, M] deserialize: ByteString => Either[DeserializationError, M]
): Either[DecryptionError, M] = ): Either[DecryptionError, M] =
for { for {
keysetHandle <- keysetNonCached(symmetricKey, DecryptionError.InvalidSymmetricKey) keysetHandle <- parseAndGetPrivateKey(
symmetricKey,
DecryptionError.InvalidSymmetricKey,
)
aead <- getPrimitive[tink.Aead, DecryptionError]( aead <- getPrimitive[tink.Aead, DecryptionError](
keysetHandle, keysetHandle,
DecryptionError.InvalidSymmetricKey, DecryptionError.InvalidSymmetricKey,
@ -212,8 +268,8 @@ class TinkPureCrypto private (
version: ProtocolVersion, version: ProtocolVersion,
): Either[EncryptionError, AsymmetricEncrypted[M]] = ): Either[EncryptionError, AsymmetricEncrypted[M]] =
for { for {
tinkPublicKey <- convertPublicKey(publicKey, EncryptionError.InvalidEncryptionKey) keysetHandle <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
keysetHandle <- keysetCached(tinkPublicKey, EncryptionError.InvalidEncryptionKey) .leftMap(err => EncryptionError.InvalidEncryptionKey(err.show))
hybrid <- getPrimitive[tink.HybridEncrypt, EncryptionError]( hybrid <- getPrimitive[tink.HybridEncrypt, EncryptionError](
keysetHandle, keysetHandle,
EncryptionError.InvalidEncryptionKey, EncryptionError.InvalidEncryptionKey,
@ -228,7 +284,10 @@ class TinkPureCrypto private (
deserialize: ByteString => Either[DeserializationError, M] deserialize: ByteString => Either[DeserializationError, M]
): Either[DecryptionError, M] = ): Either[DecryptionError, M] =
for { for {
keysetHandle <- keysetCached(privateKey, DecryptionError.InvalidEncryptionKey) keysetHandle <- parseAndGetPrivateKey(
privateKey,
DecryptionError.InvalidEncryptionKey,
)
hybrid <- getPrimitive[tink.HybridDecrypt, DecryptionError]( hybrid <- getPrimitive[tink.HybridDecrypt, DecryptionError](
keysetHandle, keysetHandle,
DecryptionError.InvalidEncryptionKey, DecryptionError.InvalidEncryptionKey,
@ -241,7 +300,10 @@ class TinkPureCrypto private (
signingKey: SigningPrivateKey, signingKey: SigningPrivateKey,
): Either[SigningError, Signature] = ): Either[SigningError, Signature] =
for { for {
keysetHandle <- keysetCached(signingKey, SigningError.InvalidSigningKey) keysetHandle <- parseAndGetPrivateKey(
signingKey,
SigningError.InvalidSigningKey,
)
verify <- getPrimitive[tink.PublicKeySign, SigningError]( verify <- getPrimitive[tink.PublicKeySign, SigningError](
keysetHandle, keysetHandle,
SigningError.InvalidSigningKey, SigningError.InvalidSigningKey,
@ -265,8 +327,6 @@ class TinkPureCrypto private (
signature: Signature, signature: Signature,
): Either[SignatureCheckError, Unit] = ): Either[SignatureCheckError, Unit] =
for { for {
tinkPublicKey <- convertPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
keysetHandle <- keysetCached(tinkPublicKey, SignatureCheckError.InvalidKeyError)
_ <- Either.cond( _ <- Either.cond(
signature.signedBy == publicKey.id, signature.signedBy == publicKey.id,
(), (),
@ -274,6 +334,7 @@ class TinkPureCrypto private (
s"Signature signed by ${signature.signedBy} instead of ${publicKey.id}" s"Signature signed by ${signature.signedBy} instead of ${publicKey.id}"
), ),
) )
keysetHandle <- parseAndGetPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
verify <- getPrimitive[tink.PublicKeyVerify, SignatureCheckError]( verify <- getPrimitive[tink.PublicKeyVerify, SignatureCheckError](
keysetHandle, keysetHandle,
SignatureCheckError.InvalidKeyError, SignatureCheckError.InvalidKeyError,
@ -331,7 +392,7 @@ class TinkPureCrypto private (
hmacWithSecret(prk, last.concat(info.bytes).concat(chunkByte), algorithm) hmacWithSecret(prk, last.concat(info.bytes).concat(chunkByte), algorithm)
.bimap(HkdfHmacError, hmac => out.concat(hmac.unwrap) -> hmac.unwrap) .bimap(HkdfHmacError, hmac => out.concat(hmac.unwrap) -> hmac.unwrap)
} }
(out, _last) = outputAndLast (out, _) = outputAndLast
} yield SecureRandomness(out.substring(0, outputBytes)) } yield SecureRandomness(out.substring(0, outputBytes))
} }

View File

@ -5,12 +5,15 @@ package com.digitalasset.canton.sequencing.handlers
import cats.instances.either.* import cats.instances.either.*
import cats.syntax.either.* import cats.syntax.either.*
import com.daml.error.{ContextualizedErrorLogger, Explanation, Resolution}
import com.digitalasset.canton.ProtoDeserializationError import com.digitalasset.canton.ProtoDeserializationError
import com.digitalasset.canton.crypto.HashOps import com.digitalasset.canton.crypto.HashOps
import com.digitalasset.canton.error.CantonErrorGroups.SequencerErrorGroup
import com.digitalasset.canton.error.{Alarm, AlarmErrorCode}
import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope
import com.digitalasset.canton.sequencing.handlers.EnvelopeOpener.EventDeserializationError import com.digitalasset.canton.sequencing.handlers.EnvelopeOpenerError.EventDeserializationError
import com.digitalasset.canton.sequencing.protocol.{ClosedEnvelope, Envelope} import com.digitalasset.canton.sequencing.protocol.{ClosedEnvelope, Envelope}
import com.digitalasset.canton.sequencing.{ApplicationHandler, EnvelopeBox} import com.digitalasset.canton.sequencing.{ApplicationHandler, EnvelopeBox, HandlerResult}
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.version.ProtocolVersion
@ -33,14 +36,33 @@ class EnvelopeOpener[Box[+_ <: Envelope[_]]](protocolVersion: ProtocolVersion, h
object EnvelopeOpener { object EnvelopeOpener {
/** Opens the envelopes inside the [[EnvelopeBox]] before handing them to the given application handler. */ /** Opens the envelopes inside the [[EnvelopeBox]] before handing them to the given application handler. */
def apply[Box[+_ <: Envelope[_]]](protocolVersion: ProtocolVersion, hashOps: HashOps)( def apply[Box[+_ <: Envelope[_]]](
protocolVersion: ProtocolVersion,
hashOps: HashOps,
)(
handler: ApplicationHandler[Box, DefaultOpenEnvelope] handler: ApplicationHandler[Box, DefaultOpenEnvelope]
)(implicit Box: EnvelopeBox[Box]): ApplicationHandler[Box, ClosedEnvelope] = handler.replace { )(implicit
Box: EnvelopeBox[Box],
logger: ContextualizedErrorLogger,
): ApplicationHandler[Box, ClosedEnvelope] = handler.replace {
val opener = new EnvelopeOpener[Box](protocolVersion, hashOps) val opener = new EnvelopeOpener[Box](protocolVersion, hashOps)
closedEvent =>
opener.open(closedEvent) match {
case Right(openEnvelope) =>
handler(openEnvelope)
case Left(err) =>
val alarm =
EnvelopeOpenerError.EnvelopeOpenerDeserializationError.Error(err, protocolVersion)
alarm.report()
HandlerResult.done
closedEvent => handler(opener.tryOpen(closedEvent)) }
} }
}
object EnvelopeOpenerError extends SequencerErrorGroup {
@SuppressWarnings(Array("org.wartremover.warts.Null")) @SuppressWarnings(Array("org.wartremover.warts.Null"))
final case class EventDeserializationError( final case class EventDeserializationError(
error: ProtoDeserializationError, error: ProtoDeserializationError,
@ -50,4 +72,21 @@ object EnvelopeOpener {
s"Failed to deserialize event with protocol version $protocolVersion: $error", s"Failed to deserialize event with protocol version $protocolVersion: $error",
cause, cause,
) )
@Explanation("""
|This error indicates that the sequencer client was unable to parse a message.
|This indicates that the message has been created by a bogus or malicious sender.
|The message will be dropped.
|""")
@Resolution(
"If no other errors are reported, Canton has recovered automatically. " +
"You should still consider to start an investigation to understand why the sender has sent an invalid message."
)
object EnvelopeOpenerDeserializationError extends AlarmErrorCode("EVENT_DESERIALIZATION_ERROR") {
final case class Error(
error: ProtoDeserializationError,
protocolVersion: ProtocolVersion,
) extends Alarm(s"Failed to deserialize event with protocol version $protocolVersion: $error")
}
} }

View File

@ -6,7 +6,9 @@ package com.digitalasset.canton.traffic
import cats.syntax.traverse.* import cats.syntax.traverse.*
import com.digitalasset.canton.ProtoDeserializationError import com.digitalasset.canton.ProtoDeserializationError
import com.digitalasset.canton.admin.traffic.v30.MemberTrafficStatus as MemberTrafficStatusP import com.digitalasset.canton.admin.traffic.v30.MemberTrafficStatus as MemberTrafficStatusP
import com.digitalasset.canton.config.RequireTypes.PositiveInt
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.sequencing.protocol.SequencedEventTrafficState import com.digitalasset.canton.sequencing.protocol.SequencedEventTrafficState
import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.topology.Member import com.digitalasset.canton.topology.Member
@ -15,17 +17,20 @@ final case class MemberTrafficStatus(
member: Member, member: Member,
timestamp: CantonTimestamp, timestamp: CantonTimestamp,
trafficState: SequencedEventTrafficState, trafficState: SequencedEventTrafficState,
currentAndFutureTopUps: List[TopUpEvent], balanceSerial: Option[PositiveInt],
) { ) extends Product
with PrettyPrinting {
def toProtoV30: MemberTrafficStatusP = { def toProtoV30: MemberTrafficStatusP = {
MemberTrafficStatusP( MemberTrafficStatusP(
member.toProtoPrimitive, member.toProtoPrimitive,
trafficState.extraTrafficLimit.map(_.value), trafficState.extraTrafficLimit.map(_.value),
trafficState.extraTrafficConsumed.value, trafficState.extraTrafficConsumed.value,
currentAndFutureTopUps.map(_.toProtoV30), List.empty,
Some(timestamp.toProtoTimestamp), Some(timestamp.toProtoTimestamp),
balanceSerial.map(_.value),
) )
} }
override def pretty: Pretty[this.type] = adHocPrettyInstance
} }
object MemberTrafficStatus { object MemberTrafficStatus {
@ -40,13 +45,15 @@ object MemberTrafficStatus {
totalExtraTrafficLimitOpt <- trafficStatusP.totalExtraTrafficLimit.traverse( totalExtraTrafficLimitOpt <- trafficStatusP.totalExtraTrafficLimit.traverse(
ProtoConverter.parseNonNegativeLong ProtoConverter.parseNonNegativeLong
) )
balanceSerialOpt <- trafficStatusP.balanceSerial.traverse(
ProtoConverter.parsePositiveInt
)
totalExtraTrafficConsumed <- ProtoConverter.parseNonNegativeLong( totalExtraTrafficConsumed <- ProtoConverter.parseNonNegativeLong(
trafficStatusP.totalExtraTrafficConsumed trafficStatusP.totalExtraTrafficConsumed
) )
totalExtraTrafficRemainder <- ProtoConverter.parseNonNegativeLong( totalExtraTrafficRemainder <- ProtoConverter.parseNonNegativeLong(
totalExtraTrafficLimitOpt.map(_.value - totalExtraTrafficConsumed.value).getOrElse(0L) totalExtraTrafficLimitOpt.map(_.value - totalExtraTrafficConsumed.value).getOrElse(0L)
) )
topUps <- trafficStatusP.topUpEvents.toList.traverse(TopUpEvent.fromProtoV30)
ts <- ProtoConverter.parseRequired( ts <- ProtoConverter.parseRequired(
CantonTimestamp.fromProtoTimestamp, CantonTimestamp.fromProtoTimestamp,
"ts", "ts",
@ -59,7 +66,7 @@ object MemberTrafficStatus {
totalExtraTrafficRemainder, totalExtraTrafficRemainder,
totalExtraTrafficConsumed, totalExtraTrafficConsumed,
), ),
topUps, balanceSerialOpt,
) )
} }
} }

View File

@ -76,16 +76,18 @@ object GeneratorsCrypto {
private lazy val privateCrypto = cryptoFactory.crypto.privateCrypto private lazy val privateCrypto = cryptoFactory.crypto.privateCrypto
private lazy val pureCryptoApi: CryptoPureApi = cryptoFactory.pureCrypto private lazy val pureCryptoApi: CryptoPureApi = cryptoFactory.pureCrypto
// TODO(#15813): Change arbitrary signing keys to match real keys
implicit val signingPublicKeyArb: Arbitrary[SigningPublicKey] = Arbitrary(for { implicit val signingPublicKeyArb: Arbitrary[SigningPublicKey] = Arbitrary(for {
id <- Arbitrary.arbitrary[Fingerprint] id <- Arbitrary.arbitrary[Fingerprint]
format <- Arbitrary.arbitrary[CryptoKeyFormat] format = CryptoKeyFormat.Symbolic
key <- Arbitrary.arbitrary[ByteString] key <- Arbitrary.arbitrary[ByteString]
scheme <- Arbitrary.arbitrary[SigningKeyScheme] scheme <- Arbitrary.arbitrary[SigningKeyScheme]
} yield new SigningPublicKey(id, format, key, scheme)) } yield new SigningPublicKey(id, format, key, scheme))
// TODO(#15813): Change arbitrary encryption keys to match real keys
implicit val encryptionPublicKeyArb: Arbitrary[EncryptionPublicKey] = Arbitrary(for { implicit val encryptionPublicKeyArb: Arbitrary[EncryptionPublicKey] = Arbitrary(for {
id <- Arbitrary.arbitrary[Fingerprint] id <- Arbitrary.arbitrary[Fingerprint]
format <- Arbitrary.arbitrary[CryptoKeyFormat] format = CryptoKeyFormat.Symbolic
key <- Arbitrary.arbitrary[ByteString] key <- Arbitrary.arbitrary[ByteString]
scheme <- Arbitrary.arbitrary[EncryptionKeyScheme] scheme <- Arbitrary.arbitrary[EncryptionKeyScheme]
} yield new EncryptionPublicKey(id, format, key, scheme)) } yield new EncryptionPublicKey(id, format, key, scheme))

View File

@ -0,0 +1,113 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.crypto
import com.digitalasset.canton.BaseTest
import com.google.protobuf.ByteString
import org.scalatest.wordspec.AsyncWordSpec
import scala.concurrent.Future
trait PublicKeyValidationTest extends BaseTest with CryptoTestHelper { this: AsyncWordSpec =>
private def modifyPublicKey(
publicKey: PublicKey,
newFormat: Option[CryptoKeyFormat],
newId: Option[Fingerprint],
): PublicKey =
publicKey match {
case EncryptionPublicKey(id, format, key, scheme) =>
new EncryptionPublicKey(newId.getOrElse(id), newFormat.getOrElse(format), key, scheme)
case SigningPublicKey(id, format, key, scheme) =>
new SigningPublicKey(newId.getOrElse(id), newFormat.getOrElse(format), key, scheme)
case _ => fail(s"unsupported key type")
}
private def keyValidationTest[K <: PublicKey](
supportedCryptoKeyFormats: Set[CryptoKeyFormat],
name: String,
newCrypto: => Future[Crypto],
newPublicKey: Crypto => Future[PublicKey],
): Unit = {
// change format
forAll(supportedCryptoKeyFormats) { format =>
s"Validate $name public key with $format" in {
for {
crypto <- newCrypto
publicKey <- newPublicKey(crypto)
newPublicKeyWithTargetFormat = modifyPublicKey(publicKey, Some(format), None)
validationRes = CryptoKeyValidation.parseAndValidatePublicKey(
newPublicKeyWithTargetFormat,
errString => errString,
)
} yield
if (format == publicKey.format || format == CryptoKeyFormat.Symbolic)
validationRes shouldEqual Right(())
else
validationRes.left.value should include(
s"Failed to deserialize $format public key: KeyParseAndValidateError"
)
}
}
// with wrong fingerprint
s"Validate $name public key with wrong fingerprint" in {
val hash = Hash.digest(
HashPurpose.PublicKeyFingerprint,
ByteString.copyFrom("mock".getBytes),
HashAlgorithm.Sha256,
)
val invalidFingerprint = new Fingerprint(hash.toLengthLimitedHexString)
for {
crypto <- newCrypto
publicKey <- newPublicKey(crypto)
newPublicKeyWithWrongFingerprint = modifyPublicKey(
publicKey,
None,
Some(invalidFingerprint),
)
validationRes = CryptoKeyValidation.parseAndValidatePublicKey(
newPublicKeyWithWrongFingerprint,
errString => errString,
)
} yield validationRes.left.value should fullyMatch regex
raw"Failed to deserialize ${publicKey.format} public key: " +
raw"KeyParseAndValidateError\(The regenerated fingerprint ${publicKey.fingerprint} does not match the fingerprint of the object: $invalidFingerprint\)"
}
}
/** Test public key validation
*/
def publicKeyValidationProvider(
supportedSigningKeySchemes: Set[SigningKeyScheme],
supportedEncryptionKeySchemes: Set[EncryptionKeyScheme],
supportedCryptoKeyFormats: Set[CryptoKeyFormat],
newCrypto: => Future[Crypto],
): Unit = {
"Validate public keys" should {
forAll(supportedSigningKeySchemes) { signingKeyScheme =>
keyValidationTest[SigningPublicKey](
supportedCryptoKeyFormats,
signingKeyScheme.toString,
newCrypto,
crypto => getSigningPublicKey(crypto, signingKeyScheme),
)
}
forAll(supportedEncryptionKeySchemes) { encryptionKeyScheme =>
keyValidationTest[EncryptionPublicKey](
supportedCryptoKeyFormats,
encryptionKeyScheme.toString,
newCrypto,
crypto => getEncryptionPublicKey(crypto, encryptionKeyScheme),
)
}
}
}
}

View File

@ -24,11 +24,12 @@ class JceCryptoTest
with HkdfTest with HkdfTest
with PasswordBasedEncryptionTest with PasswordBasedEncryptionTest
with RandomTest with RandomTest
with JavaPublicKeyConverterTest { with JavaPublicKeyConverterTest
with PublicKeyValidationTest {
"JceCrypto" can { "JceCrypto" can {
def jceCrypto(): Future[Crypto] = { def jceCrypto(): Future[Crypto] =
new CommunityCryptoFactory() new CommunityCryptoFactory()
.create( .create(
CommunityCryptoConfig(provider = Jce), CommunityCryptoConfig(provider = Jce),
@ -40,7 +41,6 @@ class JceCryptoTest
NoReportingTracerProvider, NoReportingTracerProvider,
) )
.valueOr(err => throw new RuntimeException(s"failed to create crypto: $err")) .valueOr(err => throw new RuntimeException(s"failed to create crypto: $err"))
}
behave like signingProvider(Jce.signing.supported, jceCrypto()) behave like signingProvider(Jce.signing.supported, jceCrypto())
behave like encryptionProvider( behave like encryptionProvider(
@ -111,5 +111,12 @@ class JceCryptoTest
Jce.symmetric.supported, Jce.symmetric.supported,
jceCrypto().map(_.pureCrypto), jceCrypto().map(_.pureCrypto),
) )
behave like publicKeyValidationProvider(
Jce.signing.supported,
Jce.encryption.supported,
Jce.supportedCryptoKeyFormats,
jceCrypto(),
)
} }
} }

View File

@ -55,6 +55,8 @@ class SymbolicCryptoTest
// Symbolic crypto does not satisfy golden tests for HKDF // Symbolic crypto does not satisfy golden tests for HKDF
// Symbolic crypto does not support Java key conversion, thus not tested // Symbolic crypto does not support Java key conversion, thus not tested
// Symbolic crypto does not support public key validation, thus not tested
} }
} }

View File

@ -21,11 +21,12 @@ class TinkCryptoTest
with PrivateKeySerializationTest with PrivateKeySerializationTest
with HkdfTest with HkdfTest
with RandomTest with RandomTest
with JavaPublicKeyConverterTest { with JavaPublicKeyConverterTest
with PublicKeyValidationTest {
"TinkCrypto" can { "TinkCrypto" can {
def tinkCrypto(): Future[Crypto] = { def tinkCrypto(): Future[Crypto] =
new CommunityCryptoFactory() new CommunityCryptoFactory()
.create( .create(
CommunityCryptoConfig(provider = Tink), CommunityCryptoConfig(provider = Tink),
@ -37,7 +38,6 @@ class TinkCryptoTest
NoReportingTracerProvider, NoReportingTracerProvider,
) )
.valueOrFail("create crypto") .valueOrFail("create crypto")
}
behave like signingProvider(Tink.signing.supported, tinkCrypto()) behave like signingProvider(Tink.signing.supported, tinkCrypto())
behave like encryptionProvider( behave like encryptionProvider(
@ -67,6 +67,14 @@ class TinkCryptoTest
"JCE", "JCE",
new JceJavaConverter(Jce.signing.supported, Jce.encryption.supported), new JceJavaConverter(Jce.signing.supported, Jce.encryption.supported),
) )
behave like publicKeyValidationProvider(
Tink.signing.supported,
Tink.encryption.supported,
Tink.supportedCryptoKeyFormats,
tinkCrypto(),
)
} }
} }

View File

@ -0,0 +1,32 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.data
import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.protocol.messages.SetTrafficBalanceMessage
import com.digitalasset.canton.topology.{DomainId, Member}
import com.digitalasset.canton.version.ProtocolVersion
import org.scalacheck.Arbitrary
final class GeneratorsTrafficData(
protocolVersion: ProtocolVersion
) {
import com.digitalasset.canton.config.GeneratorsConfig.*
import com.digitalasset.canton.topology.GeneratorsTopology.*
implicit val setTrafficBalanceArb: Arbitrary[SetTrafficBalanceMessage] = Arbitrary(
for {
member <- Arbitrary.arbitrary[Member]
serial <- Arbitrary.arbitrary[PositiveInt]
trafficBalance <- Arbitrary.arbitrary[NonNegativeLong]
domainId <- Arbitrary.arbitrary[DomainId]
} yield SetTrafficBalanceMessage.apply(
member,
serial,
trafficBalance,
domainId,
protocolVersion,
)
)
}

View File

@ -4,14 +4,12 @@
package com.digitalasset.canton.data package com.digitalasset.canton.data
import com.digitalasset.canton.LfPartyId import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
import com.digitalasset.canton.crypto.{GeneratorsCrypto, Salt, TestHash} import com.digitalasset.canton.crypto.{GeneratorsCrypto, Salt, TestHash}
import com.digitalasset.canton.protocol.* import com.digitalasset.canton.protocol.*
import com.digitalasset.canton.protocol.messages.{ import com.digitalasset.canton.protocol.messages.{
ConfirmationResultMessage,
DeliveredTransferOutResult, DeliveredTransferOutResult,
SetTrafficBalanceMessage,
SignedProtocolMessage, SignedProtocolMessage,
TransferResult,
Verdict, Verdict,
} }
import com.digitalasset.canton.sequencing.protocol.{ import com.digitalasset.canton.sequencing.protocol.{
@ -21,7 +19,7 @@ import com.digitalasset.canton.sequencing.protocol.{
SignedContent, SignedContent,
TimeProof, TimeProof,
} }
import com.digitalasset.canton.topology.{DomainId, Member, ParticipantId} import com.digitalasset.canton.topology.ParticipantId
import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.version.ProtocolVersion
import com.digitalasset.canton.version.Transfer.{SourceProtocolVersion, TargetProtocolVersion} import com.digitalasset.canton.version.Transfer.{SourceProtocolVersion, TargetProtocolVersion}
import magnolify.scalacheck.auto.* import magnolify.scalacheck.auto.*
@ -37,7 +35,6 @@ final class GeneratorsTransferData(
import com.digitalasset.canton.Generators.* import com.digitalasset.canton.Generators.*
import com.digitalasset.canton.GeneratorsLf.* import com.digitalasset.canton.GeneratorsLf.*
import com.digitalasset.canton.crypto.GeneratorsCrypto.* import com.digitalasset.canton.crypto.GeneratorsCrypto.*
import com.digitalasset.canton.config.GeneratorsConfig.*
import com.digitalasset.canton.data.GeneratorsDataTime.* import com.digitalasset.canton.data.GeneratorsDataTime.*
import com.digitalasset.canton.topology.GeneratorsTopology.* import com.digitalasset.canton.topology.GeneratorsTopology.*
import org.scalatest.EitherValues.* import org.scalatest.EitherValues.*
@ -136,14 +133,18 @@ final class GeneratorsTransferData(
sourceDomain <- Arbitrary.arbitrary[SourceDomainId] sourceDomain <- Arbitrary.arbitrary[SourceDomainId]
requestId <- Arbitrary.arbitrary[RequestId] requestId <- Arbitrary.arbitrary[RequestId]
rootHash <- Arbitrary.arbitrary[RootHash]
protocolVersion = sourceProtocolVersion.v protocolVersion = sourceProtocolVersion.v
verdict = Verdict.Approve(protocolVersion) verdict = Verdict.Approve(protocolVersion)
result = TransferResult.create(
result = ConfirmationResultMessage.create(
sourceDomain.id,
ViewType.TransferOutViewType,
requestId, requestId,
contract.metadata.stakeholders, Some(rootHash),
sourceDomain,
verdict, verdict,
contract.metadata.stakeholders,
protocolVersion, protocolVersion,
) )
@ -220,20 +221,4 @@ final class GeneratorsTransferData(
transferCounter, transferCounter,
) )
) )
implicit val setTrafficBalanceArb: Arbitrary[SetTrafficBalanceMessage] = Arbitrary(
for {
member <- Arbitrary.arbitrary[Member]
serial <- Arbitrary.arbitrary[NonNegativeLong]
trafficBalance <- Arbitrary.arbitrary[NonNegativeLong]
domainId <- Arbitrary.arbitrary[DomainId]
} yield SetTrafficBalanceMessage.apply(
member,
serial,
trafficBalance,
domainId,
protocolVersion,
)
)
} }

View File

@ -31,7 +31,9 @@ class ConfirmationResponseTest extends AnyWordSpec with BaseTest with HasCryptog
RequestId(CantonTimestamp.now()), RequestId(CantonTimestamp.now()),
topology.ParticipantId(UniqueIdentifier.tryFromProtoPrimitive("da::p1")), topology.ParticipantId(UniqueIdentifier.tryFromProtoPrimitive("da::p1")),
None, None,
LocalRejectError.MalformedRejects.Payloads.Reject("test message")(localVerdictProtocolVersion), LocalRejectError.MalformedRejects.Payloads
.Reject("test message")
.toLocalReject(testedProtocolVersion),
Some(RootHash(TestHash.digest("txid3"))), Some(RootHash(TestHash.digest("txid3"))),
Set.empty, Set.empty,
DomainId(UniqueIdentifier.tryFromProtoPrimitive("da::default")), DomainId(UniqueIdentifier.tryFromProtoPrimitive("da::default")),

View File

@ -11,6 +11,7 @@ import com.digitalasset.canton.protocol.LocalRejectError.ConsistencyRejections.{
import com.digitalasset.canton.protocol.LocalRejectError.MalformedRejects.{ import com.digitalasset.canton.protocol.LocalRejectError.MalformedRejects.{
BadRootHashMessages, BadRootHashMessages,
CreatesExistingContracts, CreatesExistingContracts,
MalformedRequest,
ModelConformance, ModelConformance,
Payloads, Payloads,
} }
@ -27,7 +28,7 @@ import com.digitalasset.canton.protocol.LocalRejectError.TransferInRejects.{
} }
import com.digitalasset.canton.protocol.LocalRejectError.TransferOutRejects.ActivenessCheckFailed import com.digitalasset.canton.protocol.LocalRejectError.TransferOutRejects.ActivenessCheckFailed
import com.digitalasset.canton.protocol.{LocalRejectErrorImpl, Malformed} import com.digitalasset.canton.protocol.{LocalRejectErrorImpl, Malformed}
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion} import com.digitalasset.canton.version.ProtocolVersion
import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.{Arbitrary, Gen}
final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) { final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) {
@ -35,56 +36,49 @@ final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) {
import com.digitalasset.canton.GeneratorsLf.lfPartyIdArb import com.digitalasset.canton.GeneratorsLf.lfPartyIdArb
// TODO(#14515) Check that the generator is exhaustive // TODO(#14515) Check that the generator is exhaustive
private def localRejectErrorImplGen: Gen[LocalRejectErrorImpl] = { private def localVerdictRejectGen: Gen[LocalReject] = {
val resources = List("resource1", "resource2") val resources = List("resource1", "resource2")
val details = "details" val details = "details"
val builders: Seq[RepresentativeProtocolVersion[LocalVerdict.type] => LocalRejectErrorImpl] = val builders = Seq[LocalRejectErrorImpl](
Seq( LockedContracts.Reject(resources),
LockedContracts.Reject(resources), InactiveContracts.Reject(resources),
InactiveContracts.Reject(resources), LedgerTime.Reject(details),
LedgerTime.Reject(details), SubmissionTime.Reject(details),
SubmissionTime.Reject(details), LocalTimeout.Reject(),
LocalTimeout.Reject(), ActivenessCheckFailed.Reject(details),
ActivenessCheckFailed.Reject(details), ContractAlreadyArchived.Reject(details),
ContractAlreadyArchived.Reject(details), ContractAlreadyActive.Reject(details),
ContractAlreadyActive.Reject(details), ContractIsLocked.Reject(details),
ContractIsLocked.Reject(details), AlreadyCompleted.Reject(details),
AlreadyCompleted.Reject(details), )
/*
GenericReject is intentionally excluded
Reason: it should not be serialized.
*/
// GenericReject("cause", details, resources, "SOME_ID", ErrorCategory.TransientServerFailure),
)
Gen.oneOf(builders).map(_(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))) Gen
.oneOf(builders)
.map(_.toLocalReject(protocolVersion))
} }
// TODO(#14515) Check that the generator is exhaustive // TODO(#14515) Check that the generator is exhaustive
private def localVerdictMalformedGen: Gen[Malformed] = { private def localVerdictMalformedGen: Gen[LocalReject] = {
val resources = List("resource1", "resource2") val resources = List("resource1", "resource2")
val details = "details" val details = "details"
val builders: Seq[RepresentativeProtocolVersion[LocalVerdict.type] => Malformed] = Seq( val builders = Seq[Malformed](
/* MalformedRequest.Reject(details),
MalformedRequest.Reject is intentionally excluded
The reason is for backward compatibility reason, its `v0.LocalReject.Code` does not correspond to the id
(`v0.LocalReject.Code.MalformedPayloads` vs "LOCAL_VERDICT_MALFORMED_REQUEST")
*/
// MalformedRequest.Reject(details),
Payloads.Reject(details), Payloads.Reject(details),
ModelConformance.Reject(details), ModelConformance.Reject(details),
BadRootHashMessages.Reject(details), BadRootHashMessages.Reject(details),
CreatesExistingContracts.Reject(resources), CreatesExistingContracts.Reject(resources),
) )
Gen.oneOf(builders).map(_(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))) Gen
.oneOf(builders)
.map(_.toLocalReject(protocolVersion))
} }
// TODO(#14515) Check that the generator is exhaustive // TODO(#14515) Check that the generator is exhaustive
private def localRejectGen: Gen[LocalReject] = private def localRejectGen: Gen[LocalReject] =
Gen.oneOf(localRejectErrorImplGen, localVerdictMalformedGen) Gen.oneOf(localVerdictRejectGen, localVerdictMalformedGen)
private def localApproveGen: Gen[LocalApprove] = private def localApproveGen: Gen[LocalApprove] =
Gen.const(LocalApprove(protocolVersion)) Gen.const(LocalApprove(protocolVersion))

View File

@ -6,13 +6,7 @@ package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.LfPartyId import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.crypto.Signature import com.digitalasset.canton.crypto.Signature
import com.digitalasset.canton.data.{CantonTimestampSecond, GeneratorsData, ViewPosition, ViewType} import com.digitalasset.canton.data.{CantonTimestampSecond, GeneratorsData, ViewPosition, ViewType}
import com.digitalasset.canton.protocol.{ import com.digitalasset.canton.protocol.{GeneratorsProtocol, RequestId, RootHash}
GeneratorsProtocol,
Malformed,
RequestId,
RootHash,
TransferDomainId,
}
import com.digitalasset.canton.time.PositiveSeconds import com.digitalasset.canton.time.PositiveSeconds
import com.digitalasset.canton.topology.{DomainId, ParticipantId} import com.digitalasset.canton.topology.{DomainId, ParticipantId}
import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.version.ProtocolVersion
@ -66,38 +60,26 @@ final class GeneratorsMessages(
) )
) )
implicit val transferResultArb: Arbitrary[TransferResult[TransferDomainId]] = Arbitrary(for { implicit val confirmationResultMessageArb: Arbitrary[ConfirmationResultMessage] = Arbitrary(
requestId <- Arbitrary.arbitrary[RequestId] for {
informees <- Gen.containerOf[Set, LfPartyId](Arbitrary.arbitrary[LfPartyId]) domainId <- Arbitrary.arbitrary[DomainId]
domain <- Arbitrary.arbitrary[TransferDomainId] viewType <- Arbitrary.arbitrary[ViewType]
verdict <- verdictArb.arbitrary requestId <- Arbitrary.arbitrary[RequestId]
} yield TransferResult.create(requestId, informees, domain, verdict, protocolVersion)) rootHash <- Arbitrary.arbitrary[RootHash]
verdict <- verdictArb.arbitrary
informees <- Arbitrary.arbitrary[Set[LfPartyId]]
implicit val MalformedMediatorConfirmationRequestResultArb // TODO(#14241) Also generate instance that makes pv above cover all the values
: Arbitrary[MalformedConfirmationRequestResult] = } yield ConfirmationResultMessage.create(
Arbitrary( domainId,
for { viewType,
requestId <- Arbitrary.arbitrary[RequestId] requestId,
domainId <- Arbitrary.arbitrary[DomainId] Some(rootHash),
viewType <- Arbitrary.arbitrary[ViewType] verdict,
mediatorReject <- mediatorRejectArb.arbitrary informees,
} yield MalformedConfirmationRequestResult.tryCreate( protocolVersion,
requestId,
domainId,
viewType,
mediatorReject,
protocolVersion,
)
) )
)
implicit val transactionResultMessageArb: Arbitrary[ConfirmationResultMessage] = Arbitrary(for {
verdict <- verdictArb.arbitrary
rootHash <- Arbitrary.arbitrary[RootHash]
requestId <- Arbitrary.arbitrary[RequestId]
domainId <- Arbitrary.arbitrary[DomainId]
// TODO(#14241) Also generate instance that contains InformeeTree + make pv above cover all the values
} yield ConfirmationResultMessage(requestId, verdict, rootHash, domainId, protocolVersion))
implicit val confirmationResponseArb: Arbitrary[ConfirmationResponse] = Arbitrary( implicit val confirmationResponseArb: Arbitrary[ConfirmationResponse] = Arbitrary(
for { for {
@ -107,18 +89,11 @@ final class GeneratorsMessages(
domainId <- Arbitrary.arbitrary[DomainId] domainId <- Arbitrary.arbitrary[DomainId]
confirmingParties <- localVerdict match { confirmingParties <-
case _: Malformed => if (localVerdict.isMalformed) Gen.const(Set.empty[LfPartyId])
Gen.const(Set.empty[LfPartyId]) else nonEmptySet(implicitly[Arbitrary[LfPartyId]]).arbitrary.map(_.forgetNE)
case _: LocalApprove | _: LocalReject =>
nonEmptySet(implicitly[Arbitrary[LfPartyId]]).arbitrary.map(_.forgetNE)
case _ => Gen.containerOf[Set, LfPartyId](Arbitrary.arbitrary[LfPartyId])
}
rootHash <- localVerdict match { rootHash <- Arbitrary.arbitrary[RootHash]
case _: LocalApprove | _: LocalReject => Gen.some(Arbitrary.arbitrary[RootHash])
case _ => Gen.option(Arbitrary.arbitrary[RootHash])
}
viewPositionO <- localVerdict match { viewPositionO <- localVerdict match {
case _: LocalApprove | _: LocalReject => case _: LocalApprove | _: LocalReject =>
@ -131,29 +106,19 @@ final class GeneratorsMessages(
sender, sender,
viewPositionO, viewPositionO,
localVerdict, localVerdict,
rootHash, Some(rootHash),
confirmingParties, confirmingParties,
domainId, domainId,
protocolVersion, protocolVersion,
) )
) )
// TODO(#14515) Check that the generator is exhaustive
implicit val mediatorResultArb: Arbitrary[ConfirmationResult] = Arbitrary(
Gen.oneOf[ConfirmationResult](
Arbitrary.arbitrary[MalformedConfirmationRequestResult],
Arbitrary.arbitrary[ConfirmationResultMessage],
Arbitrary.arbitrary[TransferResult[TransferDomainId]],
)
)
// TODO(#14515) Check that the generator is exhaustive // TODO(#14515) Check that the generator is exhaustive
implicit val signedProtocolMessageContentArb: Arbitrary[SignedProtocolMessageContent] = Arbitrary( implicit val signedProtocolMessageContentArb: Arbitrary[SignedProtocolMessageContent] = Arbitrary(
Gen.oneOf( Gen.oneOf[SignedProtocolMessageContent](
Arbitrary.arbitrary[AcsCommitment], Arbitrary.arbitrary[AcsCommitment],
Arbitrary.arbitrary[MalformedConfirmationRequestResult],
Arbitrary.arbitrary[ConfirmationResponse], Arbitrary.arbitrary[ConfirmationResponse],
Arbitrary.arbitrary[ConfirmationResult], Arbitrary.arbitrary[ConfirmationResultMessage],
) )
) )

View File

@ -6,7 +6,6 @@ package com.digitalasset.canton.protocol.messages
import com.digitalasset.canton.Generators.nonEmptyListGen import com.digitalasset.canton.Generators.nonEmptyListGen
import com.digitalasset.canton.LfPartyId import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.version.ProtocolVersion
import magnolify.scalacheck.auto.*
import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.{Arbitrary, Gen}
final case class GeneratorsVerdict( final case class GeneratorsVerdict(
@ -15,17 +14,13 @@ final case class GeneratorsVerdict(
) { ) {
import generatorsLocalVerdict.* import generatorsLocalVerdict.*
// TODO(#14515): move elsewhere?
implicit val protoMediatorRejectionCodeArb
: Arbitrary[com.digitalasset.canton.protocol.v30.MediatorRejection.Code] = genArbitrary
// TODO(#14515) Check that the generator is exhaustive // TODO(#14515) Check that the generator is exhaustive
implicit val mediatorRejectArb: Arbitrary[Verdict.MediatorReject] = implicit val mediatorRejectArb: Arbitrary[Verdict.MediatorReject] =
Arbitrary( Arbitrary(
// TODO(#14515): do we want randomness here? // TODO(#14515): do we want randomness here?
Gen.const { Gen.const {
val status = com.google.rpc.status.Status(com.google.rpc.Code.CANCELLED_VALUE) val status = com.google.rpc.status.Status(com.google.rpc.Code.CANCELLED_VALUE)
Verdict.MediatorReject.tryCreate(status, protocolVersion) Verdict.MediatorReject.tryCreate(status, isMalformed = false, protocolVersion)
} }
) )

View File

@ -3,11 +3,11 @@
package com.digitalasset.canton.sequencing.protocol package com.digitalasset.canton.sequencing.protocol
import com.digitalasset.canton.crypto.CryptoPureApi
import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.crypto.{CryptoPureApi, TestHash}
import com.digitalasset.canton.data.{CantonTimestamp, ViewType}
import com.digitalasset.canton.protocol.RequestId
import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.protocol.messages.*
import com.digitalasset.canton.protocol.{RequestId, TargetDomainId}
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
import com.digitalasset.canton.topology.DefaultTestIdentities import com.digitalasset.canton.topology.DefaultTestIdentities
import com.digitalasset.canton.topology.DefaultTestIdentities.domainId import com.digitalasset.canton.topology.DefaultTestIdentities.domainId
@ -22,11 +22,13 @@ class SequencedEventTest extends BaseTestWordSpec {
// there's no significance to this choice of message beyond it being easy to construct // there's no significance to this choice of message beyond it being easy to construct
val message = val message =
SignedProtocolMessage.from( SignedProtocolMessage.from(
TransferResult.create( ConfirmationResultMessage.create(
domainId,
ViewType.TransferOutViewType,
RequestId(CantonTimestamp.now()), RequestId(CantonTimestamp.now()),
Set.empty, Some(TestHash.dummyRootHash),
TargetDomainId(domainId),
Verdict.Approve(testedProtocolVersion), Verdict.Approve(testedProtocolVersion),
Set.empty,
testedProtocolVersion, testedProtocolVersion,
), ),
testedProtocolVersion, testedProtocolVersion,

View File

@ -5,7 +5,7 @@ package com.digitalasset.canton.traffic
import cats.data.EitherT import cats.data.EitherT
import com.daml.nonempty.NonEmpty import com.daml.nonempty.NonEmpty
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.lifecycle.UnlessShutdown import com.digitalasset.canton.lifecycle.UnlessShutdown
import com.digitalasset.canton.protocol.messages.{ import com.digitalasset.canton.protocol.messages.{
@ -100,7 +100,7 @@ class TrafficBalanceSubmissionHandlerTest
recipient1, recipient1,
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
NonNegativeLong.tryCreate(5), PositiveInt.tryCreate(5),
NonNegativeLong.tryCreate(1000), NonNegativeLong.tryCreate(1000),
sequencerClient, sequencerClient,
crypto, crypto,
@ -182,7 +182,7 @@ class TrafficBalanceSubmissionHandlerTest
recipient1, recipient1,
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
NonNegativeLong.tryCreate(5), PositiveInt.tryCreate(5),
NonNegativeLong.tryCreate(1000), NonNegativeLong.tryCreate(1000),
sequencerClient, sequencerClient,
crypto, crypto,
@ -236,7 +236,7 @@ class TrafficBalanceSubmissionHandlerTest
recipient1, recipient1,
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
NonNegativeLong.tryCreate(5), PositiveInt.tryCreate(5),
NonNegativeLong.tryCreate(1000), NonNegativeLong.tryCreate(1000),
sequencerClient, sequencerClient,
crypto, crypto,
@ -272,7 +272,7 @@ class TrafficBalanceSubmissionHandlerTest
recipient1, recipient1,
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
NonNegativeLong.tryCreate(5), PositiveInt.tryCreate(5),
NonNegativeLong.tryCreate(1000), NonNegativeLong.tryCreate(1000),
sequencerClient, sequencerClient,
crypto, crypto,
@ -324,7 +324,7 @@ class TrafficBalanceSubmissionHandlerTest
recipient1, recipient1,
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
NonNegativeLong.tryCreate(5), PositiveInt.tryCreate(5),
NonNegativeLong.tryCreate(1000), NonNegativeLong.tryCreate(1000),
sequencerClient, sequencerClient,
crypto, crypto,

View File

@ -5,7 +5,7 @@ package com.digitalasset.canton.traffic
import com.daml.nonempty.NonEmpty import com.daml.nonempty.NonEmpty
import com.digitalasset.canton.config.CantonRequireTypes.String255 import com.digitalasset.canton.config.CantonRequireTypes.String255
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.crypto.Signature import com.digitalasset.canton.crypto.Signature
import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
@ -83,7 +83,7 @@ class TrafficControlProcessorTest extends AnyWordSpec with BaseTest with HasExec
): SignedProtocolMessage[SetTrafficBalanceMessage] = { ): SignedProtocolMessage[SetTrafficBalanceMessage] = {
val setBalance = SetTrafficBalanceMessage( val setBalance = SetTrafficBalanceMessage(
participantId, participantId,
NonNegativeLong.one, PositiveInt.one,
NonNegativeLong.tryCreate(100), NonNegativeLong.tryCreate(100),
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,

View File

@ -55,10 +55,14 @@ class SerializationDeserializationTest
generatorsProtocol, generatorsProtocol,
generatorsProtocolSeq, generatorsProtocolSeq,
) )
val generatorsTrafficData = new GeneratorsTrafficData(
version
)
import generatorsData.* import generatorsData.*
import generatorsMessages.* import generatorsMessages.*
import generatorsTransferData.* import generatorsTransferData.*
import generatorsTrafficData.*
import generatorsVerdict.* import generatorsVerdict.*
import generatorsLocalVerdict.* import generatorsLocalVerdict.*
import generatorsProtocol.* import generatorsProtocol.*
@ -77,8 +81,6 @@ class SerializationDeserializationTest
testProtocolVersionedWithCtx(SignedProtocolMessage, version) testProtocolVersionedWithCtx(SignedProtocolMessage, version)
testProtocolVersioned(LocalVerdict) testProtocolVersioned(LocalVerdict)
testProtocolVersioned(TransferResult)
testProtocolVersioned(MalformedConfirmationRequestResult)
testProtocolVersionedWithCtx(EnvelopeContent, (TestHash, version)) testProtocolVersionedWithCtx(EnvelopeContent, (TestHash, version))
testMemoizedProtocolVersioned(ConfirmationResultMessage) testMemoizedProtocolVersioned(ConfirmationResultMessage)

View File

@ -1,11 +1,11 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: ai-analysis name: ai-analysis
source: AIAnalysis.daml source: AIAnalysis.daml
init-script: AIAnalysis:setup init-script: AIAnalysis:setup
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -1,11 +1,11 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: bank name: bank
source: Bank.daml source: Bank.daml
init-script: Bank:setup init-script: Bank:setup
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -1,11 +1,11 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: doctor name: doctor
source: Doctor.daml source: Doctor.daml
init-script: Doctor:setup init-script: Doctor:setup
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -1,11 +1,11 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: health-insurance name: health-insurance
source: HealthInsurance.daml source: HealthInsurance.daml
init-script: HealthInsurance:setup init-script: HealthInsurance:setup
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -1,11 +1,11 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
build-options: build-options:
- --target=2.1 - --target=2.1
name: medical-records name: medical-records
source: MedicalRecord.daml source: MedicalRecord.daml
init-script: MedicalRecord:setup init-script: MedicalRecord:setup
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib
- daml3-script - daml3-script

View File

@ -36,7 +36,7 @@ message TrafficControlStateResponse {
message SetTrafficBalanceRequest { message SetTrafficBalanceRequest {
string member = 1; // Member to top up string member = 1; // Member to top up
int64 serial = 2; // Serial number of the request, will be used for idempotency uint32 serial = 2; // Serial number of the request, will be used for idempotency
int64 total_traffic_balance = 3; // Amount of traffic to top up int64 total_traffic_balance = 3; // Amount of traffic to top up
} }

View File

@ -1506,7 +1506,9 @@ class BlockUpdateGenerator(
) )
.map { trafficStateUpdates => .map { trafficStateUpdates =>
ephemeralState ephemeralState
.copy(trafficState = ephemeralState.trafficState ++ trafficStateUpdates) .copy(trafficState =
ephemeralState.trafficState ++ trafficStateUpdates.view.mapValues(_.state).toMap
)
} }
} }
} }

View File

@ -18,8 +18,8 @@ import com.digitalasset.canton.error.MediatorError
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, HasCloseContext} import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, HasCloseContext}
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.protocol.*
import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.protocol.messages.*
import com.digitalasset.canton.protocol.{v30, *}
import com.digitalasset.canton.sequencing.HandlerResult import com.digitalasset.canton.sequencing.HandlerResult
import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.sequencing.protocol.*
import com.digitalasset.canton.time.DomainTimeTracker import com.digitalasset.canton.time.DomainTimeTracker
@ -333,8 +333,7 @@ private[mediator] class ConfirmationResponseProcessor(
.allHaveActiveParticipants(request.allInformees) .allHaveActiveParticipants(request.allInformees)
.leftMap { informeesNoParticipant => .leftMap { informeesNoParticipant =>
val reject = MediatorError.InvalidMessage.Reject( val reject = MediatorError.InvalidMessage.Reject(
show"Received a mediator confirmation request with id $requestId with some informees not being hosted by an active participant: $informeesNoParticipant. Rejecting request...", show"Received a mediator confirmation request with id $requestId with some informees not being hosted by an active participant: $informeesNoParticipant. Rejecting request..."
v30.MediatorRejection.Code.CODE_INFORMEES_NOT_HOSTED_ON_ACTIVE_PARTICIPANT,
) )
reject.log() reject.log()
Option(MediatorVerdict.MediatorReject(reject)) Option(MediatorVerdict.MediatorReject(reject))
@ -369,8 +368,7 @@ private[mediator] class ConfirmationResponseProcessor(
!batchAlsoContainsTopologyXTransaction, { !batchAlsoContainsTopologyXTransaction, {
val rejection = MediatorError.MalformedMessage val rejection = MediatorError.MalformedMessage
.Reject( .Reject(
s"Received a mediator confirmation request with id $requestId also containing a topology transaction.", s"Received a mediator confirmation request with id $requestId also containing a topology transaction."
v30.MediatorRejection.Code.CODE_UNSPECIFIED,
) )
.reported() .reported()
MediatorVerdict.MediatorReject(rejection) MediatorVerdict.MediatorReject(rejection)
@ -393,8 +391,7 @@ private[mediator] class ConfirmationResponseProcessor(
MediatorVerdict.MediatorReject( MediatorVerdict.MediatorReject(
MediatorError.MalformedMessage MediatorError.MalformedMessage
.Reject( .Reject(
show"Rejecting mediator confirmation request with $requestId, mediator ${request.mediator}, topology at ${topologySnapshot.timestamp} due to $hint", show"Rejecting mediator confirmation request with $requestId, mediator ${request.mediator}, topology at ${topologySnapshot.timestamp} due to $hint"
v30.MediatorRejection.Code.CODE_WRONG_DECLARED_MEDIATOR,
) )
.reported() .reported()
) )
@ -514,8 +511,7 @@ private[mediator] class ConfirmationResponseProcessor(
unitOrRejectionReason.leftMap { rejectionReason => unitOrRejectionReason.leftMap { rejectionReason =>
val rejection = MediatorError.MalformedMessage val rejection = MediatorError.MalformedMessage
.Reject( .Reject(
s"Received a mediator confirmation request with id $requestId with invalid root hash messages. Rejecting... Reason: $rejectionReason", s"Received a mediator confirmation request with id $requestId with invalid root hash messages. Rejecting... Reason: $rejectionReason"
v30.MediatorRejection.Code.CODE_VIEW_INVALID_ROOT_HASH_MESSAGE,
) )
.reported()(loggingContext) .reported()(loggingContext)
MediatorVerdict.MediatorReject(rejection) MediatorVerdict.MediatorReject(rejection)
@ -535,8 +531,7 @@ private[mediator] class ConfirmationResponseProcessor(
MediatorVerdict.MediatorReject( MediatorVerdict.MediatorReject(
MediatorError.MalformedMessage MediatorError.MalformedMessage
.Reject( .Reject(
s"Received a mediator confirmation request with id $requestId having threshold $threshold for transaction view at $viewPosition, which is below the confirmation policy's minimum threshold of $minimumThreshold. Rejecting request...", s"Received a mediator confirmation request with id $requestId having threshold $threshold for transaction view at $viewPosition, which is below the confirmation policy's minimum threshold of $minimumThreshold. Rejecting request..."
v30.MediatorRejection.Code.CODE_VIEW_THRESHOLD_BELOW_MINIMUM_THRESHOLD,
) )
.reported() .reported()
), ),
@ -589,8 +584,7 @@ private[mediator] class ConfirmationResponseProcessor(
s"Received a mediator confirmation request with id $requestId with insufficient authorized confirming parties for transaction view at $viewPosition. " + s"Received a mediator confirmation request with id $requestId with insufficient authorized confirming parties for transaction view at $viewPosition. " +
s"Rejecting request. Threshold: $threshold." + s"Rejecting request. Threshold: $threshold." +
insufficientPermissionHint + insufficientPermissionHint +
authorizedPartiesHint, authorizedPartiesHint
v30.MediatorRejection.Code.CODE_NOT_ENOUGH_CONFIRMING_PARTIES,
) )
.reported() .reported()
MediatorVerdict.MediatorReject(rejection) MediatorVerdict.MediatorReject(rejection)

View File

@ -12,14 +12,8 @@ import com.digitalasset.canton.domain.mediator.store.MediatorDeduplicationStore
import com.digitalasset.canton.error.MediatorError import com.digitalasset.canton.error.MediatorError
import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.lifecycle.CloseContext
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.protocol.messages.{ import com.digitalasset.canton.protocol.messages.*
DefaultOpenEnvelope, import com.digitalasset.canton.protocol.{DynamicDomainParametersWithValidity, RequestId}
MediatorConfirmationRequest,
ProtocolMessage,
RootHashMessage,
SerializedRootHashMessagePayload,
}
import com.digitalasset.canton.protocol.{DynamicDomainParametersWithValidity, RequestId, v30}
import com.digitalasset.canton.sequencing.TracedProtocolEvent import com.digitalasset.canton.sequencing.TracedProtocolEvent
import com.digitalasset.canton.topology.client.DomainTopologyClient import com.digitalasset.canton.topology.client.DomainTopologyClient
import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.tracing.{TraceContext, Traced}
@ -176,8 +170,7 @@ class DefaultMediatorEventDeduplicator(
case Some(previousUsagesNE) => case Some(previousUsagesNE) =>
val expireAfter = previousUsagesNE.map(_.expireAfter).max1 val expireAfter = previousUsagesNE.map(_.expireAfter).max1
val rejection = MediatorError.MalformedMessage.Reject( val rejection = MediatorError.MalformedMessage.Reject(
s"The request uuid ($uuid) must not be used until $expireAfter.", s"The request uuid ($uuid) must not be used until $expireAfter."
v30.MediatorRejection.Code.CODE_NON_UNIQUE_REQUEST_UUID,
) )
rejection.report() rejection.report()

View File

@ -25,8 +25,9 @@ object MediatorVerdict {
} }
type MediatorApprove = MediatorApprove.type type MediatorApprove = MediatorApprove.type
final case class ParticipantReject(reasons: NonEmpty[List[(Set[LfPartyId], LocalReject)]]) final case class ParticipantReject(
extends MediatorVerdict { reasons: NonEmpty[List[(Set[LfPartyId], LocalReject)]]
) extends MediatorVerdict {
override def toVerdict(protocolVersion: ProtocolVersion): Verdict = override def toVerdict(protocolVersion: ProtocolVersion): Verdict =
Verdict.ParticipantReject(reasons, protocolVersion) Verdict.ParticipantReject(reasons, protocolVersion)
@ -50,7 +51,11 @@ object MediatorVerdict {
case invalid: InvalidMessage.Reject => invalid case invalid: InvalidMessage.Reject => invalid
case malformed: MalformedMessage.Reject => malformed case malformed: MalformedMessage.Reject => malformed
} }
Verdict.MediatorReject.tryCreate(error.rpcStatusWithoutLoggingContext(), protocolVersion) Verdict.MediatorReject.tryCreate(
error.rpcStatusWithoutLoggingContext(),
reason.isMalformed,
protocolVersion,
)
} }
override def pretty: Pretty[MediatorReject] = prettyOfClass( override def pretty: Pretty[MediatorReject] = prettyOfClass(

View File

@ -13,9 +13,10 @@ import com.digitalasset.canton.error.MediatorError
import com.digitalasset.canton.logging.pretty.Pretty import com.digitalasset.canton.logging.pretty.Pretty
import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggingContext} import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggingContext}
import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.protocol.messages.*
import com.digitalasset.canton.protocol.{Malformed, RequestId, RootHash} import com.digitalasset.canton.protocol.{RequestId, RootHash}
import com.digitalasset.canton.topology.ParticipantId import com.digitalasset.canton.topology.ParticipantId
import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.client.TopologySnapshot
import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.ErrorUtil import com.digitalasset.canton.util.ErrorUtil
import com.digitalasset.canton.util.FutureInstances.* import com.digitalasset.canton.util.FutureInstances.*
import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.ShowUtil.*
@ -64,13 +65,13 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
ec: ExecutionContext, ec: ExecutionContext,
loggingContext: NamedLoggingContext, loggingContext: NamedLoggingContext,
): OptionT[Future, List[(VKEY, Set[LfPartyId])]] = { ): OptionT[Future, List[(VKEY, Set[LfPartyId])]] = {
implicit val tc = loggingContext.traceContext implicit val tc: TraceContext = loggingContext.traceContext
def authorizedPartiesOfSender( def authorizedPartiesOfSender(
viewKey: VKEY, viewKey: VKEY,
declaredConfirmingParties: Set[ConfirmingParty], declaredConfirmingParties: Set[ConfirmingParty],
): OptionT[Future, Set[LfPartyId]] = ): OptionT[Future, Set[LfPartyId]] = {
localVerdict match { localVerdict match {
case malformed: Malformed => case malformed: LocalReject if malformed.isMalformed =>
malformed.logWithContext( malformed.logWithContext(
Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender") Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender")
) )
@ -86,8 +87,7 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
Some(hostedConfirmingParties): Option[Set[LfPartyId]] Some(hostedConfirmingParties): Option[Set[LfPartyId]]
} }
OptionT(res) OptionT(res)
case _: LocalVerdict =>
case _: LocalApprove | _: LocalReject =>
val unexpectedConfirmingParties = val unexpectedConfirmingParties =
confirmingParties -- declaredConfirmingParties.map(_.party) confirmingParties -- declaredConfirmingParties.map(_.party)
for { for {
@ -126,6 +126,7 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
} }
} yield confirmingParties } yield confirmingParties
} }
}
for { for {
_ <- OptionT.fromOption[Future](rootHashO.traverse_ { rootHash => _ <- OptionT.fromOption[Future](rootHashO.traverse_ { rootHash =>
@ -146,7 +147,7 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
// If no view key is given, the local verdict is Malformed and confirming parties is empty by the invariants of ConfirmationResponse. // If no view key is given, the local verdict is Malformed and confirming parties is empty by the invariants of ConfirmationResponse.
// We treat this as a rejection for all parties hosted by the participant. // We treat this as a rejection for all parties hosted by the participant.
localVerdict match { localVerdict match {
case malformed: Malformed => case malformed @ LocalReject(_, true) =>
malformed.logWithContext( malformed.logWithContext(
Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender") Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender")
) )

View File

@ -11,28 +11,18 @@ import cats.syntax.parallel.*
import com.daml.nonempty.NonEmpty import com.daml.nonempty.NonEmpty
import com.digitalasset.canton.LfPartyId import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, SyncCryptoError} import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, SyncCryptoError}
import com.digitalasset.canton.data.{CantonTimestamp, ViewType} import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.lifecycle.UnlessShutdown import com.digitalasset.canton.lifecycle.UnlessShutdown
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.protocol.RequestId
import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.protocol.messages.*
import com.digitalasset.canton.protocol.{RequestId, SourceDomainId, TargetDomainId}
import com.digitalasset.canton.sequencing.client.{ import com.digitalasset.canton.sequencing.client.{
SendCallback, SendCallback,
SendResult, SendResult,
SendType, SendType,
SequencerClientSend, SequencerClientSend,
} }
import com.digitalasset.canton.sequencing.protocol.{ import com.digitalasset.canton.sequencing.protocol.*
AggregationRule,
Batch,
MediatorsOfDomain,
MemberRecipient,
OpenEnvelope,
ParticipantsOfParty,
Recipient,
Recipients,
SequencerErrors,
}
import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.client.TopologySnapshot
import com.digitalasset.canton.topology.{MediatorId, ParticipantId, PartyId} import com.digitalasset.canton.topology.{MediatorId, ParticipantId, PartyId}
import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext
@ -203,7 +193,15 @@ private[mediator] class DefaultVerdictSender(
) )
(informeesMap, informeesWithGroupAddressing) = result (informeesMap, informeesWithGroupAddressing) = result
envelopes <- { envelopes <- {
val result = request.createConfirmationResult(requestId, verdict, request.allInformees) val result = ConfirmationResultMessage.create(
crypto.domainId,
request.viewType,
requestId,
Some(request.rootHash),
verdict,
if (request.informeesArePublic) request.allInformees else Set.empty,
protocolVersion,
)
val recipientSeq = val recipientSeq =
informeesMap.keys.toSeq.map(MemberRecipient) ++ informeesWithGroupAddressing.toSeq informeesMap.keys.toSeq.map(MemberRecipient) ++ informeesWithGroupAddressing.toSeq
.map(p => ParticipantsOfParty(PartyId.tryFromLfParty(p))) .map(p => ParticipantsOfParty(PartyId.tryFromLfParty(p)))
@ -324,55 +322,15 @@ private[mediator] class DefaultVerdictSender(
snapshot <- crypto.awaitSnapshot(requestId.unwrap) snapshot <- crypto.awaitSnapshot(requestId.unwrap)
envs <- recipientsByViewType.toSeq envs <- recipientsByViewType.toSeq
.parTraverse { case (viewType, flatRecipients) => .parTraverse { case (viewType, flatRecipients) =>
// This is currently a bit messy. We need to a TransactionResultMessage or TransferXResult whenever possible, val rejection = ConfirmationResultMessage.create(
// because that allows us to easily intercept and change the verdict in tests. crypto.domainId,
// However, in some cases, the required information is not available, so we fall back to MalformedMediatorConfirmationRequestResult. viewType,
// TODO(i11326): Remove unnecessary fields from the result message types, so we can get rid of MalformedMediatorConfirmationRequestResult and simplify this code. requestId,
val rejection = (viewType match { rootHashO = None,
case ViewType.TransactionViewType => rejectionReason,
requestO match { Set.empty,
case Some(request @ InformeeMessage(_, _)) => protocolVersion,
request.createConfirmationResult( )
requestId,
rejectionReason,
Set.empty,
)
// For other kinds of request, or if the request is unknown, we send a generic result
case _ =>
MalformedConfirmationRequestResult.tryCreate(
requestId,
crypto.domainId,
viewType,
rejectionReason,
protocolVersion,
)
}
case ViewType.TransferInViewType =>
TransferInResult.create(
requestId,
Set.empty,
TargetDomainId(crypto.domainId),
rejectionReason,
protocolVersion,
)
case ViewType.TransferOutViewType =>
TransferOutResult.create(
requestId,
Set.empty,
SourceDomainId(crypto.domainId),
rejectionReason,
protocolVersion,
)
case _: ViewType =>
MalformedConfirmationRequestResult.tryCreate(
requestId,
crypto.domainId,
viewType,
rejectionReason,
protocolVersion,
)
}): ConfirmationResult
val recipients = Recipients.recipientGroups(flatRecipients.map(r => NonEmpty(Set, r))) val recipients = Recipients.recipientGroups(flatRecipients.map(r => NonEmpty(Set, r)))

View File

@ -392,7 +392,7 @@ class DatabaseSequencer(
FutureUnlessShutdown.pure(SequencerTrafficStatus(Seq.empty)) FutureUnlessShutdown.pure(SequencerTrafficStatus(Seq.empty))
override def setTrafficBalance( override def setTrafficBalance(
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
sequencerClient: SequencerClient, sequencerClient: SequencerClient,
)(implicit )(implicit

View File

@ -136,7 +136,7 @@ trait Sequencer
def setTrafficBalance( def setTrafficBalance(
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
sequencerClient: SequencerClient, sequencerClient: SequencerClient,
)(implicit )(implicit

View File

@ -31,6 +31,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
SequencerRateLimitManager, SequencerRateLimitManager,
SequencerTrafficStatus, SequencerTrafficStatus,
} }
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.TrafficStateUpdateResult
import com.digitalasset.canton.domain.sequencing.traffic.store.TrafficBalanceStore import com.digitalasset.canton.domain.sequencing.traffic.store.TrafficBalanceStore
import com.digitalasset.canton.health.admin.data.SequencerHealthStatus import com.digitalasset.canton.health.admin.data.SequencerHealthStatus
import com.digitalasset.canton.lifecycle.* import com.digitalasset.canton.lifecycle.*
@ -465,7 +466,7 @@ class BlockSequencer(
upToDateTrafficStatesForMembers( upToDateTrafficStatesForMembers(
stateManager.getHeadState.chunk.ephemeral.status.members.map(_.member), stateManager.getHeadState.chunk.ephemeral.status.members.map(_.member),
Some(clock.now), Some(clock.now),
) ).map(_.view.mapValues(_.state).toMap)
} }
/** Compute traffic states for the specified members at the provided timestamp, /** Compute traffic states for the specified members at the provided timestamp,
@ -478,14 +479,14 @@ class BlockSequencer(
updateTimestamp: Option[CantonTimestamp] = None, updateTimestamp: Option[CantonTimestamp] = None,
)(implicit )(implicit
traceContext: TraceContext traceContext: TraceContext
): FutureUnlessShutdown[Map[Member, TrafficState]] = { ): FutureUnlessShutdown[Map[Member, TrafficStateUpdateResult]] = {
// Get the parameters for the traffic control // Get the parameters for the traffic control
OptionUtil.zipWithFDefaultValue( OptionUtil.zipWithFDefaultValue(
rateLimitManager, rateLimitManager,
FutureUnlessShutdown.outcomeF( FutureUnlessShutdown.outcomeF(
cryptoApi.headSnapshot.ipsSnapshot.trafficControlParameters(protocolVersion) cryptoApi.headSnapshot.ipsSnapshot.trafficControlParameters(protocolVersion)
), ),
Map.empty[Member, TrafficState], Map.empty[Member, TrafficStateUpdateResult],
) { case (rlm, parameters) => ) { case (rlm, parameters) =>
// Use the head ephemeral state to get the known traffic states // Use the head ephemeral state to get the known traffic states
val headEphemeral = stateManager.getHeadState.chunk.ephemeral val headEphemeral = stateManager.getHeadState.chunk.ephemeral
@ -521,7 +522,7 @@ class BlockSequencer(
override def setTrafficBalance( override def setTrafficBalance(
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
sequencerClient: SequencerClient, sequencerClient: SequencerClient,
)(implicit )(implicit
@ -543,12 +544,12 @@ class BlockSequencer(
): FutureUnlessShutdown[SequencerTrafficStatus] = { ): FutureUnlessShutdown[SequencerTrafficStatus] = {
upToDateTrafficStatesForMembers(requestedMembers) upToDateTrafficStatesForMembers(requestedMembers)
.map { updated => .map { updated =>
updated.map { case (member, state) => updated.map { case (member, TrafficStateUpdateResult(state, balanceUpdateSerial)) =>
MemberTrafficStatus( MemberTrafficStatus(
member, member,
state.timestamp, state.timestamp,
state.toSequencedEventTrafficState, state.toSequencedEventTrafficState,
List.empty, // TODO(i17477): Was never used, set to empty for now and remove when we're done with the rework balanceUpdateSerial,
) )
}.toList }.toList
} }

View File

@ -6,6 +6,7 @@ package com.digitalasset.canton.domain.sequencing.sequencer.traffic
import cats.data.EitherT import cats.data.EitherT
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.TrafficStateUpdateResult
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
import com.digitalasset.canton.sequencing.TrafficControlParameters import com.digitalasset.canton.sequencing.TrafficControlParameters
import com.digitalasset.canton.sequencing.protocol.{ import com.digitalasset.canton.sequencing.protocol.{
@ -76,7 +77,7 @@ trait SequencerRateLimitManager {
)(implicit )(implicit
ec: ExecutionContext, ec: ExecutionContext,
tc: TraceContext, tc: TraceContext,
): FutureUnlessShutdown[Map[Member, TrafficState]] ): FutureUnlessShutdown[Map[Member, TrafficStateUpdateResult]]
} }
sealed trait SequencerRateLimitError sealed trait SequencerRateLimitError

View File

@ -121,7 +121,7 @@ class GrpcSequencerAdministrationService(
val result = { val result = {
for { for {
member <- wrapErrUS(Member.fromProtoPrimitive(requestP.member, "member")) member <- wrapErrUS(Member.fromProtoPrimitive(requestP.member, "member"))
serial <- wrapErrUS(ProtoConverter.parseNonNegativeLong(requestP.serial)) serial <- wrapErrUS(ProtoConverter.parsePositiveInt(requestP.serial))
totalTrafficBalance <- wrapErrUS( totalTrafficBalance <- wrapErrUS(
ProtoConverter.parseNonNegativeLong(requestP.totalTrafficBalance) ProtoConverter.parseNonNegativeLong(requestP.totalTrafficBalance)
) )

View File

@ -4,7 +4,6 @@
package com.digitalasset.canton.domain.sequencing.traffic package com.digitalasset.canton.domain.sequencing.traffic
import cats.data.EitherT import cats.data.EitherT
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.BalanceUpdateClient import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.BalanceUpdateClient
@ -30,10 +29,9 @@ class BalanceUpdateClientImpl(
warnIfApproximate: Boolean, warnIfApproximate: Boolean,
)(implicit )(implicit
traceContext: TraceContext traceContext: TraceContext
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong] = { ): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]] = {
manager manager
.getTrafficBalanceAt(member, timestamp, lastSeen, warnIfApproximate) .getTrafficBalanceAt(member, timestamp, lastSeen, warnIfApproximate)
.map(_.map(_.balance).getOrElse(NonNegativeLong.zero))
.leftMap { case TrafficBalanceManager.TrafficBalanceAlreadyPruned(member, timestamp) => .leftMap { case TrafficBalanceManager.TrafficBalanceAlreadyPruned(member, timestamp) =>
SequencerRateLimitError.UnknownBalance(member, timestamp) SequencerRateLimitError.UnknownBalance(member, timestamp)
} }

View File

@ -4,7 +4,6 @@
package com.digitalasset.canton.domain.sequencing.traffic package com.digitalasset.canton.domain.sequencing.traffic
import cats.data.EitherT import cats.data.EitherT
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, SyncCryptoClient} import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, SyncCryptoClient}
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError
@ -34,7 +33,7 @@ class BalanceUpdateClientTopologyImpl(
warnIfApproximate: Boolean, warnIfApproximate: Boolean,
)(implicit )(implicit
traceContext: TraceContext traceContext: TraceContext
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong] = { ): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]] = {
val balanceFUS = for { val balanceFUS = for {
topology <- SyncCryptoClient.getSnapshotForTimestampUS( topology <- SyncCryptoClient.getSnapshotForTimestampUS(
syncCrypto, syncCrypto,
@ -46,11 +45,26 @@ class BalanceUpdateClientTopologyImpl(
trafficBalance <- FutureUnlessShutdown.outcomeF( trafficBalance <- FutureUnlessShutdown.outcomeF(
topology.ipsSnapshot topology.ipsSnapshot
.trafficControlStatus(Seq(member)) .trafficControlStatus(Seq(member))
.map( .map { statusMap =>
_.get(member).flatten.map(_.totalExtraTrafficLimit.toNonNegative) statusMap
) .get(member)
.flatten
.map { status =>
// Craft a `TrafficBalance` from the `MemberTrafficControlState` coming from topology
// This is temporary while we have both implementations in place
// Note that we use the topology "effectiveTimestamp" as the "sequencingTimestamp"
// In the new balance implementation they are the same, but semantically for the topology one it makes more sense
// to use the effective timestamp.
TrafficBalance(
member,
status.serial,
status.totalExtraTrafficLimit.toNonNegative,
status.effectiveTimestamp,
)
}
}
) )
} yield trafficBalance.getOrElse(NonNegativeLong.zero) } yield trafficBalance
EitherT.liftF(balanceFUS) EitherT.liftF(balanceFUS)
} }

View File

@ -9,14 +9,17 @@ import cats.syntax.bifunctor.*
import cats.syntax.parallel.* import cats.syntax.parallel.*
import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.concurrent.FutureSupervisor
import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.ProcessingTimeout
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt}
import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.domain.metrics.SequencerMetrics import com.digitalasset.canton.domain.metrics.SequencerMetrics
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{ import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
SequencerRateLimitError, SequencerRateLimitError,
SequencerRateLimitManager, SequencerRateLimitManager,
} }
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.BalanceUpdateClient import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.{
BalanceUpdateClient,
TrafficStateUpdateResult,
}
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle} import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle}
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.sequencing.TrafficControlParameters import com.digitalasset.canton.sequencing.TrafficControlParameters
@ -118,7 +121,7 @@ class EnterpriseSequencerRateLimitManager(
trafficControlParameters, trafficControlParameters,
trafficState, trafficState,
groupToMembers, groupToMembers,
currentBalance, currentBalance.map(_.balance).getOrElse(NonNegativeLong.zero),
) )
) )
.leftWiden[SequencerRateLimitError] .leftWiden[SequencerRateLimitError]
@ -133,34 +136,47 @@ class EnterpriseSequencerRateLimitManager(
)(implicit )(implicit
ec: ExecutionContext, ec: ExecutionContext,
tc: TraceContext, tc: TraceContext,
): FutureUnlessShutdown[Map[Member, TrafficState]] = { ): FutureUnlessShutdown[Map[Member, TrafficStateUpdateResult]] = {
def getBalanceOrNone(member: Member, timestamp: CantonTimestamp) = {
balanceUpdateClient(member, timestamp, lastBalanceUpdateTimestamp, warnIfApproximate)
.valueOr { err =>
logger.warn(s"Failed to obtain the traffic balance for $member at $timestamp", err)
None
}
}
// Use the provided timestamp or the latest known balance otherwise // Use the provided timestamp or the latest known balance otherwise
val timestampO = updateTimestamp.orElse(balanceUpdateClient.lastKnownTimestamp) val timestampO = updateTimestamp.orElse(balanceUpdateClient.lastKnownTimestamp)
for { for {
updated <- partialTrafficStates.toList updated <- partialTrafficStates.toList
.parTraverse { case (member, state) => .parTraverse { case (member, originalState) =>
timestampO match { timestampO match {
// Only update if the provided timestamp is in the future compared to the latest known state // Only update if the provided timestamp is in the future compared to the latest known state
// We don't provide updates in the past // We don't provide updates in the past
case Some(ts) if ts > state.timestamp => case Some(ts) if ts > originalState.timestamp =>
balanceUpdateClient(member, ts, lastBalanceUpdateTimestamp, warnIfApproximate) getBalanceOrNone(member, ts)
.valueOr { err =>
logger.warn(s"Failed to obtain the traffic balance for $member at $ts", err)
state.extraTrafficLimit.map(_.toNonNegative).getOrElse(NonNegativeLong.zero)
}
.map { balance => .map { balance =>
getOrCreateMemberRateLimiter(member) val state = getOrCreateMemberRateLimiter(member)
.updateTrafficState( .updateTrafficState(
ts, ts,
trafficControlParameters, trafficControlParameters,
NonNegativeLong.zero, NonNegativeLong.zero,
state, originalState,
balance, balance
.map(_.balance)
// If we don't have a balance, we use the original limit if there's one
.orElse(originalState.extraTrafficLimit.map(_.toNonNegative))
// Otherwise it means no traffic was bought
.getOrElse(NonNegativeLong.zero),
) )
._1 ._1
TrafficStateUpdateResult(state, balance.map(_.serial))
} }
.map(member -> _) .map(member -> _)
case _ => FutureUnlessShutdown.pure(member -> state) case _ =>
getBalanceOrNone(member, originalState.timestamp)
.map(balance =>
member -> TrafficStateUpdateResult(originalState, balance.map(_.serial))
)
} }
} }
.map(_.toMap) .map(_.toMap)
@ -173,6 +189,15 @@ class EnterpriseSequencerRateLimitManager(
} }
object EnterpriseSequencerRateLimitManager { object EnterpriseSequencerRateLimitManager {
/** Wrapper class returned when updating the traffic state of members
* Optionally contains the serial of the balance update that corresponds to the "balance" of the traffic state
*/
final case class TrafficStateUpdateResult(
state: TrafficState,
balanceUpdateSerial: Option[PositiveInt],
)
trait BalanceUpdateClient extends AutoCloseable { trait BalanceUpdateClient extends AutoCloseable {
def apply( def apply(
member: Member, member: Member,
@ -181,7 +206,7 @@ object EnterpriseSequencerRateLimitManager {
warnIfApproximate: Boolean = true, warnIfApproximate: Boolean = true,
)(implicit )(implicit
traceContext: TraceContext traceContext: TraceContext
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong] ): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]]
def lastKnownTimestamp: Option[CantonTimestamp] def lastKnownTimestamp: Option[CantonTimestamp]
} }
} }

View File

@ -43,7 +43,7 @@ import scala.jdk.CollectionConverters.*
import scala.language.reflectiveCalls import scala.language.reflectiveCalls
@nowarn("msg=match may not be exhaustive") @nowarn("msg=match may not be exhaustive")
class ConfirmationResponseProcessorTestV5 class ConfirmationResponseProcessorTest
extends AsyncWordSpec extends AsyncWordSpec
with BaseTest with BaseTest
with HasTestCloseContext with HasTestCloseContext
@ -106,9 +106,6 @@ class ConfirmationResponseProcessorTestV5
protected val initialDomainParameters: DynamicDomainParameters = protected val initialDomainParameters: DynamicDomainParameters =
TestDomainParameters.defaultDynamic TestDomainParameters.defaultDynamic
private lazy val localVerdictProtocolVersion =
LocalVerdict.protocolVersionRepresentativeFor(testedProtocolVersion)
protected val confirmationResponseTimeout: NonNegativeFiniteDuration = protected val confirmationResponseTimeout: NonNegativeFiniteDuration =
NonNegativeFiniteDuration.tryOfMillis(100L) NonNegativeFiniteDuration.tryOfMillis(100L)
@ -347,9 +344,10 @@ class ConfirmationResponseProcessorTestV5
) )
} yield { } yield {
val sentResult = sut.verdictSender.sentResults.loneElement val sentResult = sut.verdictSender.sentResults.loneElement
inside(sentResult.verdict.value) { case MediatorReject(status) => inside(sentResult.verdict.value) { case MediatorReject(status, isMalformed) =>
status.code shouldBe Code.INVALID_ARGUMENT.value() status.code shouldBe Code.INVALID_ARGUMENT.value()
status.message shouldBe s"An error occurred. Please contact the operator and inquire about the request <no-correlation-id> with tid <no-tid>" status.message shouldBe s"An error occurred. Please contact the operator and inquire about the request <no-correlation-id> with tid <no-tid>"
isMalformed shouldBe true
} }
} }
} }
@ -652,7 +650,7 @@ class ConfirmationResponseProcessorTestV5
val results = resultBatch.envelopes.map { envelope => val results = resultBatch.envelopes.map { envelope =>
envelope.recipients -> Some( envelope.recipients -> Some(
envelope.protocolMessage envelope.protocolMessage
.asInstanceOf[SignedProtocolMessage[ConfirmationResult]] .asInstanceOf[SignedProtocolMessage[ConfirmationResultMessage]]
.message .message
.viewType .viewType
) )
@ -972,14 +970,6 @@ class ConfirmationResponseProcessorTestV5
val malformedMsg = "this is a test malformed response" val malformedMsg = "this is a test malformed response"
def isMalformedWarn(participant: ParticipantId)(logEntry: LogEntry): Assertion = {
logEntry.shouldBeCantonError(
LocalRejectError.MalformedRejects.Payloads,
_ shouldBe s"Rejected transaction due to malformed payload within views $malformedMsg",
_ should contain("reportedBy" -> s"$participant"),
)
}
def malformedResponse( def malformedResponse(
participant: ParticipantId participant: ParticipantId
): Future[SignedProtocolMessage[ConfirmationResponse]] = { ): Future[SignedProtocolMessage[ConfirmationResponse]] = {
@ -988,7 +978,8 @@ class ConfirmationResponseProcessorTestV5
participant, participant,
None, None,
LocalRejectError.MalformedRejects.Payloads LocalRejectError.MalformedRejects.Payloads
.Reject(malformedMsg)(localVerdictProtocolVersion), .Reject(malformedMsg)
.toLocalReject(testedProtocolVersion),
Some(fullInformeeTree.transactionId.toRootHash), Some(fullInformeeTree.transactionId.toRootHash),
Set.empty, Set.empty,
factory.domainId, factory.domainId,
@ -1040,23 +1031,16 @@ class ConfirmationResponseProcessorTestV5
)(Predef.identity) )(Predef.identity)
// records the request // records the request
_ <- loggerFactory.assertLogs( _ <- sequentialTraverse_(malformed)(
sequentialTraverse_(malformed)( sut.processor.processResponse(
sut.processor.processResponse( ts1,
ts1, notSignificantCounter,
notSignificantCounter, ts1.plusSeconds(60),
ts1.plusSeconds(60), ts1.plusSeconds(120),
ts1.plusSeconds(120), _,
_, Some(requestId.unwrap),
Some(requestId.unwrap), Recipients.cc(mediatorGroupRecipient),
Recipients.cc(mediatorGroupRecipient), )
)
),
isMalformedWarn(participant1),
isMalformedWarn(participant3),
isMalformedWarn(participant3),
isMalformedWarn(participant2),
isMalformedWarn(participant3),
) )
finalState <- sut.mediatorState.fetch(requestId).value finalState <- sut.mediatorState.fetch(requestId).value
@ -1072,9 +1056,11 @@ class ConfirmationResponseProcessorTestV5
// TODO(#5337) These are only the rejections for the first view because this view happens to be finalized first. // TODO(#5337) These are only the rejections for the first view because this view happens to be finalized first.
reasons.length shouldEqual 2 reasons.length shouldEqual 2
reasons.foreach { case (party, reject) => reasons.foreach { case (party, reject) =>
reject shouldBe LocalRejectError.MalformedRejects.Payloads.Reject(malformedMsg)( reject shouldBe LocalRejectError.MalformedRejects.Payloads
localVerdictProtocolVersion .Reject(malformedMsg)
) .toLocalReject(
testedProtocolVersion
)
party should (contain(submitter) or contain(signatory)) party should (contain(submitter) or contain(signatory))
} }
} }

View File

@ -17,8 +17,8 @@ import com.digitalasset.canton.error.MediatorError
import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.lifecycle.CloseContext
import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.logging.NamedLoggerFactory
import com.digitalasset.canton.logging.pretty.Pretty import com.digitalasset.canton.logging.pretty.Pretty
import com.digitalasset.canton.protocol.RequestId
import com.digitalasset.canton.protocol.messages.* import com.digitalasset.canton.protocol.messages.*
import com.digitalasset.canton.protocol.{RequestId, v30}
import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.sequencing.protocol.*
import com.digitalasset.canton.topology.DefaultTestIdentities.* import com.digitalasset.canton.topology.DefaultTestIdentities.*
import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.tracing.TraceContext
@ -123,8 +123,7 @@ class MediatorEventDeduplicatorTest
val request = envelope.protocolMessage val request = envelope.protocolMessage
val reject = MediatorVerdict.MediatorReject( val reject = MediatorVerdict.MediatorReject(
MediatorError.MalformedMessage.Reject( MediatorError.MalformedMessage.Reject(
s"The request uuid (${request.requestUuid}) must not be used until $expireAfter.", s"The request uuid (${request.requestUuid}) must not be used until $expireAfter."
v30.MediatorRejection.Code.CODE_NON_UNIQUE_REQUEST_UUID,
) )
) )

View File

@ -33,11 +33,9 @@ import java.util.UUID
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.language.existentials import scala.language.existentials
class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest { class ResponseAggregationTest extends PathAnyFunSpec with BaseTest {
private implicit val ec: ExecutionContext = directExecutionContext private implicit val ec: ExecutionContext = directExecutionContext
private lazy val localVerdictProtocolVersion =
LocalVerdict.protocolVersionRepresentativeFor(testedProtocolVersion)
describe(classOf[ResponseAggregation[?]].getSimpleName) { describe(classOf[ResponseAggregation[?]].getSimpleName) {
def b[A](i: Int): BlindedNode[A] = BlindedNode(RootHash(TestHash.digest(i))) def b[A](i: Int): BlindedNode[A] = BlindedNode(RootHash(TestHash.digest(i)))
@ -146,9 +144,9 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
describe("under the Signatory policy") { describe("under the Signatory policy") {
def testReject() = def testReject() =
LocalRejectError.ConsistencyRejections.LockedContracts.Reject(Seq())( LocalRejectError.ConsistencyRejections.LockedContracts
localVerdictProtocolVersion .Reject(Seq())
) .toLocalReject(testedProtocolVersion)
val fullInformeeTree = val fullInformeeTree =
FullInformeeTree.tryCreate( FullInformeeTree.tryCreate(
@ -430,16 +428,15 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
mkResponse( mkResponse(
view1Position, view1Position,
LocalRejectError.MalformedRejects.Payloads LocalRejectError.MalformedRejects.Payloads
.Reject("test4")(localVerdictProtocolVersion), .Reject("test4")
.toLocalReject(testedProtocolVersion),
Set.empty, Set.empty,
rootHash, rootHash,
) )
lazy val result = loggerFactory.assertLogs( lazy val result =
step3 step3
.validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot) .validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot)
.futureValue, .futureValue
_.shouldBeCantonErrorCode(LocalRejectError.MalformedRejects.Payloads),
)
it("should not allow repeated rejection") { it("should not allow repeated rejection") {
result shouldBe None result shouldBe None
} }
@ -541,7 +538,9 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
lazy val changeTs = requestId.unwrap.plusSeconds(1) lazy val changeTs = requestId.unwrap.plusSeconds(1)
def testReject(reason: String) = def testReject(reason: String) =
LocalRejectError.MalformedRejects.Payloads.Reject(reason)(localVerdictProtocolVersion) LocalRejectError.MalformedRejects.Payloads
.Reject(reason)
.toLocalReject(testedProtocolVersion)
describe("for a single view") { describe("for a single view") {
it("should update the pending confirming parties set for all hosted parties") { it("should update the pending confirming parties set for all hosted parties") {
@ -555,17 +554,12 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
) )
val result = loggerFactory.assertLogs( val result =
valueOrFail( valueOrFail(
sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue
)( )(
"Malformed response for a view hash" "Malformed response for a view hash"
), )
_.shouldBeCantonError(
LocalRejectError.MalformedRejects.Payloads,
_ shouldBe "Rejected transaction due to malformed payload within views malformed view",
),
)
result.version shouldBe changeTs result.version shouldBe changeTs
result.state shouldBe Right( result.state shouldBe Right(
@ -608,20 +602,12 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
domainId, domainId,
testedProtocolVersion, testedProtocolVersion,
) )
val result = loggerFactory.assertLogs( val result =
valueOrFail( valueOrFail(
sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue
)( )(
"Malformed response without view hash" "Malformed response without view hash"
), )
_.shouldBeCantonError(
LocalRejectError.MalformedRejects.Payloads,
_ shouldBe s"Rejected transaction due to malformed payload within views $rejectMsg",
_ should (contain("reportedBy" -> s"$solo") and contain(
"requestId" -> requestId.toString
)),
),
)
result.version shouldBe changeTs result.version shouldBe changeTs
result.state shouldBe Right( result.state shouldBe Right(
Map( Map(
@ -709,9 +695,9 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
describe("consortium voting") { describe("consortium voting") {
def testReject() = def testReject() =
LocalRejectError.ConsistencyRejections.LockedContracts.Reject(Seq())( LocalRejectError.ConsistencyRejections.LockedContracts
localVerdictProtocolVersion .Reject(Seq())
) .toLocalReject(testedProtocolVersion)
val fullInformeeTree = val fullInformeeTree =
FullInformeeTree.tryCreate( FullInformeeTree.tryCreate(
@ -1255,16 +1241,15 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
mkResponse( mkResponse(
view1Position, view1Position,
LocalRejectError.MalformedRejects.Payloads LocalRejectError.MalformedRejects.Payloads
.Reject("test4")(localVerdictProtocolVersion), .Reject("test4")
.toLocalReject(testedProtocolVersion),
Set.empty, Set.empty,
rootHash, rootHash,
) )
lazy val result = loggerFactory.assertLogs( lazy val result =
step3 step3
.validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot) .validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot)
.futureValue, .futureValue
_.shouldBeCantonErrorCode(LocalRejectError.MalformedRejects.Payloads),
)
it("should not allow repeated rejection") { it("should not allow repeated rejection") {
result shouldBe None result shouldBe None
} }

View File

@ -159,7 +159,7 @@ class BaseSequencerTest extends AsyncWordSpec with BaseTest {
override protected def timeouts: ProcessingTimeout = ProcessingTimeout() override protected def timeouts: ProcessingTimeout = ProcessingTimeout()
override def setTrafficBalance( override def setTrafficBalance(
member: Member, member: Member,
serial: NonNegativeLong, serial: PositiveInt,
totalTrafficBalance: NonNegativeLong, totalTrafficBalance: NonNegativeLong,
sequencerClient: SequencerClient, sequencerClient: SequencerClient,
)(implicit )(implicit

View File

@ -11,6 +11,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
SequencerRateLimitManager, SequencerRateLimitManager,
SequencerTrafficConfig, SequencerTrafficConfig,
} }
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.TrafficStateUpdateResult
import com.digitalasset.canton.domain.sequencing.traffic.store.memory.InMemoryTrafficBalanceStore import com.digitalasset.canton.domain.sequencing.traffic.store.memory.InMemoryTrafficBalanceStore
import com.digitalasset.canton.sequencing.TrafficControlParameters import com.digitalasset.canton.sequencing.TrafficControlParameters
import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.sequencing.protocol.*
@ -233,13 +234,17 @@ class EnterpriseSequencerRateLimitManagerTest
warnIfApproximate = true, warnIfApproximate = true,
) )
.failOnShutdown .failOnShutdown
_ = state2.get(sender).value shouldBe TrafficState( _ = state2.get(sender).value shouldBe TrafficStateUpdateResult(
extraTrafficRemainder = NonNegativeLong.tryCreate(8L), TrafficState(
extraTrafficConsumed = NonNegativeLong.zero, extraTrafficRemainder = NonNegativeLong.tryCreate(8L),
baseTrafficRemainder = NonNegativeLong.tryCreate( extraTrafficConsumed = NonNegativeLong.zero,
4L // Should have half of the max base rate added back after 1 second
), // Should have half of the max base rate added back after 1 second baseTrafficRemainder = NonNegativeLong.tryCreate(
sequencingTs.immediateSuccessor.plusSeconds(1), 4L
),
sequencingTs.immediateSuccessor.plusSeconds(1),
),
Some(PositiveInt.one),
) )
} yield succeed } yield succeed
} }

View File

@ -12,7 +12,9 @@ message TracedBlockOrderingRequest {
int64 microseconds_since_epoch = 4; int64 microseconds_since_epoch = 4;
} }
// Currently only used by the output module of the BFT sequencer
message TracedBatchedBlockOrderingRequests { message TracedBatchedBlockOrderingRequests {
string traceparent = 1; string traceparent = 1;
repeated TracedBlockOrderingRequest requests = 2; repeated TracedBlockOrderingRequest requests = 2;
int64 last_topology_timestamp_epoch_micros = 3;
} }

View File

@ -171,9 +171,10 @@ object ReferenceBlockOrderer {
final case class TimestampedRequest(tag: String, body: ByteString, timestamp: CantonTimestamp) final case class TimestampedRequest(tag: String, body: ByteString, timestamp: CantonTimestamp)
private[sequencer] def storeMultipleRequests( private[sequencer] def storeBatch(
blockHeight: Long, blockHeight: Long,
timestamp: CantonTimestamp, timestamp: CantonTimestamp,
lastTopologyTimestamp: CantonTimestamp,
sendQueue: SimpleExecutionQueue, sendQueue: SimpleExecutionQueue,
store: ReferenceBlockOrderingStore, store: ReferenceBlockOrderingStore,
requests: Seq[Traced[TimestampedRequest]], requests: Seq[Traced[TimestampedRequest]],
@ -182,33 +183,30 @@ object ReferenceBlockOrderer {
errorLoggingContext: ErrorLoggingContext, errorLoggingContext: ErrorLoggingContext,
traceContext: TraceContext, traceContext: TraceContext,
): Future[Unit] = { ): Future[Unit] = {
val (tag, body) = requests.headOption val batchTraceparent = traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
.filter(_ => requests.sizeIs == 1) val body =
.map(head => head.value.tag -> head.value.body) TracedBatchedBlockOrderingRequests
.getOrElse { .of(
val body = { batchTraceparent,
val traceparent = traceContext.asW3CTraceContext.map(_.parent).getOrElse("") requests.map { case traced @ Traced(request) =>
TracedBatchedBlockOrderingRequests( val requestTraceparent =
traceparent, traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
requests.map { case traced @ Traced(request) => TracedBlockOrderingRequest(
val traceparent = traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("") requestTraceparent,
TracedBlockOrderingRequest( request.tag,
traceparent, request.body,
request.tag, request.timestamp.toMicros,
request.body, )
request.timestamp.toMicros, },
) lastTopologyTimestamp.toMicros,
}, )
) .toByteString
}.toByteString
(BatchTag, body)
}
sendQueue sendQueue
.execute( .execute(
store.insertRequestWithHeight( store.insertRequestWithHeight(
blockHeight, blockHeight,
BlockOrderer.OrderedRequest(timestamp.toMicros, tag, body), BlockOrderer.OrderedRequest(timestamp.toMicros, BatchTag, body),
), ),
s"send request at $timestamp", s"send request at $timestamp",
) )

View File

@ -17,6 +17,7 @@ import com.digitalasset.canton.resource.DbStorage.Profile.{H2, Oracle, Postgres}
import com.digitalasset.canton.resource.{DbStorage, DbStore} import com.digitalasset.canton.resource.{DbStorage, DbStore}
import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.store.db.DbDeserializationException import com.digitalasset.canton.store.db.DbDeserializationException
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX
import com.digitalasset.canton.tracing.{TraceContext, Traced, W3CTraceContext} import com.digitalasset.canton.tracing.{TraceContext, Traced, W3CTraceContext}
import com.digitalasset.canton.util.retry import com.digitalasset.canton.util.retry
import com.digitalasset.canton.util.retry.RetryUtil import com.digitalasset.canton.util.retry.RetryUtil
@ -202,14 +203,19 @@ class DbReferenceBlockOrderingStore(
} }
._2 ._2
requestsUntilFirstGap.map { case (height, tracedRequest, uuid) => requestsUntilFirstGap.map { case (height, tracedRequest, uuid) =>
val orderedRequests = val (orderedRequests, lastTopologyTimestamp) =
if (tracedRequest.value.tag == BatchTag) { if (tracedRequest.value.tag == BatchTag) {
val batchedRequests = proto.TracedBatchedBlockOrderingRequests val tracedBatchedBlockOrderingRequests = proto.TracedBatchedBlockOrderingRequests
.parseFrom(tracedRequest.value.body.toByteArray) .parseFrom(tracedRequest.value.body.toByteArray)
.requests val batchedRequests =
.map(DbReferenceBlockOrderingStore.fromProto) tracedBatchedBlockOrderingRequests.requests
batchedRequests .map(DbReferenceBlockOrderingStore.fromProto)
} else Seq(tracedRequest) batchedRequests -> CantonTimestamp.ofEpochMicro(
tracedBatchedBlockOrderingRequests.lastTopologyTimestampEpochMicros
)
} else {
Seq(tracedRequest) -> SignedTopologyTransactionX.InitialTopologySequencingTime
}
// Logging the UUID to be able to correlate the block on the write side. // Logging the UUID to be able to correlate the block on the write side.
logger.debug(s"Retrieved block at height $height with UUID: $uuid") logger.debug(s"Retrieved block at height $height with UUID: $uuid")
val blockTimestamp = val blockTimestamp =
@ -217,6 +223,7 @@ class DbReferenceBlockOrderingStore(
TimestampedBlock( TimestampedBlock(
BlockOrderer.Block(height, orderedRequests), BlockOrderer.Block(height, orderedRequests),
blockTimestamp, blockTimestamp,
lastTopologyTimestamp,
) )
} }
} }

View File

@ -16,6 +16,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.Refer
import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.v1 as proto import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.v1 as proto
import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.logging.NamedLoggerFactory
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage} import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX
import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.tracing.{TraceContext, Traced}
import scala.concurrent.{ExecutionContext, Future, blocking} import scala.concurrent.{ExecutionContext, Future, blocking}
@ -55,7 +56,11 @@ object ReferenceBlockOrderingStore {
new DbReferenceBlockOrderingStore(dbStorage, timeouts, loggerFactory) new DbReferenceBlockOrderingStore(dbStorage, timeouts, loggerFactory)
} }
final case class TimestampedBlock(block: BlockOrderer.Block, timestamp: CantonTimestamp) final case class TimestampedBlock(
block: BlockOrderer.Block,
timestamp: CantonTimestamp,
lastTopologyTimestamp: CantonTimestamp,
)
} }
class InMemoryReferenceSequencerDriverStore extends ReferenceBlockOrderingStore { class InMemoryReferenceSequencerDriverStore extends ReferenceBlockOrderingStore {
@ -117,24 +122,36 @@ class InMemoryReferenceSequencerDriverStore extends ReferenceBlockOrderingStore
// Get the last elements up until initial height // Get the last elements up until initial height
val iterator = deque.descendingIterator() val iterator = deque.descendingIterator()
val initial = math.max(initialHeight, 0) val initial = math.max(initialHeight, 0)
val requestsWithTimestamps = val requestsWithTimestampsAndLastTopologyTimestamps =
(initial until deque.size().toLong) (initial until deque.size().toLong)
.map(_ => iterator.next()) .map(_ => iterator.next())
.reverse .reverse
.map { request => .map { request =>
if (request.value.tag == BatchTag) { if (request.value.tag == BatchTag) {
val batchedRequests = proto.TracedBatchedBlockOrderingRequests val tracedBatchedBlockOrderingRequests = proto.TracedBatchedBlockOrderingRequests
.parseFrom(request.value.body.toByteArray) .parseFrom(request.value.body.toByteArray)
.requests val batchedRequests = tracedBatchedBlockOrderingRequests.requests
.map(DbReferenceBlockOrderingStore.fromProto) .map(DbReferenceBlockOrderingStore.fromProto)
request.value.microsecondsSinceEpoch -> batchedRequests (
} else request.value.microsecondsSinceEpoch -> Seq(request) request.value.microsecondsSinceEpoch,
batchedRequests,
CantonTimestamp.ofEpochMicro(
tracedBatchedBlockOrderingRequests.lastTopologyTimestampEpochMicros
),
)
} else
(
request.value.microsecondsSinceEpoch,
Seq(request),
SignedTopologyTransactionX.InitialTopologySequencingTime,
)
} }
requestsWithTimestamps.zip(LazyList.from(initial.toInt)).map { requestsWithTimestampsAndLastTopologyTimestamps.zip(LazyList.from(initial.toInt)).map {
case ((blockTimestamp, tracedRequests), blockHeight) => case ((blockTimestamp, tracedRequests, lastTopologyTimestamp), blockHeight) =>
TimestampedBlock( TimestampedBlock(
BlockOrderer.Block(blockHeight.toLong, tracedRequests), BlockOrderer.Block(blockHeight.toLong, tracedRequests),
CantonTimestamp.ofEpochMicro(blockTimestamp), CantonTimestamp.ofEpochMicro(blockTimestamp),
lastTopologyTimestamp,
) )
} }
} }

View File

@ -12,6 +12,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.Refer
sequencedRegisterMember, sequencedRegisterMember,
sequencedSend, sequencedSend,
} }
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX
import com.digitalasset.canton.tracing.{TraceContext, Traced, W3CTraceContext} import com.digitalasset.canton.tracing.{TraceContext, Traced, W3CTraceContext}
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.scalatest.wordspec.AsyncWordSpec import org.scalatest.wordspec.AsyncWordSpec
@ -39,6 +40,7 @@ trait ReferenceBlockOrderingStoreTest extends AsyncWordSpec with BaseTest {
TimestampedBlock( TimestampedBlock(
BlockOrderer.Block(height, Seq(tracedEvent)), BlockOrderer.Block(height, Seq(tracedEvent)),
CantonTimestamp.ofEpochMicro(tracedEvent.value.microsecondsSinceEpoch), CantonTimestamp.ofEpochMicro(tracedEvent.value.microsecondsSinceEpoch),
SignedTopologyTransactionX.InitialTopologySequencingTime,
) )
def referenceBlockOrderingStore(mk: () => ReferenceBlockOrderingStore): Unit = { def referenceBlockOrderingStore(mk: () => ReferenceBlockOrderingStore): Unit = {

View File

@ -12,7 +12,6 @@ import com.daml.lf.value.Value
import com.daml.logging.entries.{LoggingEntry, LoggingValue, ToLoggingValue} import com.daml.logging.entries.{LoggingEntry, LoggingValue, ToLoggingValue}
import com.digitalasset.canton.ledger.api.DeduplicationPeriod import com.digitalasset.canton.ledger.api.DeduplicationPeriod
import com.digitalasset.canton.ledger.configuration.Configuration import com.digitalasset.canton.ledger.configuration.Configuration
import com.digitalasset.canton.logging.pretty.PrettyInstances.*
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
import com.digitalasset.canton.topology.DomainId import com.digitalasset.canton.topology.DomainId
import com.google.rpc.status.Status as RpcStatus import com.google.rpc.status.Status as RpcStatus

View File

@ -1,7 +1,7 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: carbonv1-tests name: carbonv1-tests
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib

View File

@ -1,4 +1,4 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: carbonv2-tests name: carbonv2-tests
data-dependencies: data-dependencies:
- ../../../../scala-2.13/resource_managed/main/carbonv1-tests-3.0.0.dar - ../../../../scala-2.13/resource_managed/main/carbonv1-tests-3.0.0.dar

View File

@ -1,7 +1,7 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: experimental-tests name: experimental-tests
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib

View File

@ -1,7 +1,7 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: model-tests name: model-tests
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib

View File

@ -1,7 +1,7 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: package-management-tests name: package-management-tests
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib

View File

@ -1,7 +1,7 @@
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: semantic-tests name: semantic-tests
source: . source: .
version: 3.0.0 version: 3.0.0
dependencies: dependencies:
- daml-prim - daml-prim
- daml-stdlib - daml-stdlib

View File

@ -1,4 +1,4 @@
sdk-version: 3.0.0-snapshot.20240206.12702.0.v3b408892 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: upgrade-tests name: upgrade-tests
source: . source: .
version: 1.0.0 version: 1.0.0

View File

@ -1,4 +1,4 @@
sdk-version: 3.0.0-snapshot.20240206.12702.0.v3b408892 sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
name: upgrade-tests name: upgrade-tests
source: . source: .
version: 2.0.0 version: 2.0.0

Some files were not shown because too many files have changed in this diff Show More