mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
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:
parent
6e74d0d4bc
commit
ee73911caf
@ -428,6 +428,7 @@ scala_library(
|
||||
"@maven//:com_github_blemale_scaffeine_2_13",
|
||||
"@maven//:com_github_pathikrit_better_files_2_13",
|
||||
"@maven//:com_github_pureconfig_pureconfig_core_2_13",
|
||||
"@maven//:com_google_crypto_tink_tink",
|
||||
"@maven//:com_google_guava_guava",
|
||||
"@maven//:com_google_protobuf_protobuf_java",
|
||||
"@maven//:com_lihaoyi_fansi_2_13",
|
||||
|
@ -27,7 +27,9 @@ message MemberTrafficStatus {
|
||||
// Total extra traffic consumed
|
||||
uint64 total_extra_traffic_consumed = 3;
|
||||
// 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
|
||||
google.protobuf.Timestamp ts = 5;
|
||||
// Serial number of the balance (total_extra_traffic_limit) of this status
|
||||
google.protobuf.UInt32Value balance_serial = 6;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
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.sequencing.sequencer.SequencerPruningStatus
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerTrafficStatus
|
||||
@ -73,7 +73,7 @@ object SequencerAdminCommands {
|
||||
final case class SetTrafficControlBalance(
|
||||
member: Member,
|
||||
newTrafficBalance: NonNegativeLong,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
) extends BaseSequencerAdministrationCommands[
|
||||
admin.v30.SetTrafficBalanceRequest,
|
||||
admin.v30.SetTrafficBalanceResponse,
|
||||
|
@ -17,7 +17,7 @@ import com.digitalasset.canton.topology.store.{
|
||||
StoredTopologyTransactionX,
|
||||
StoredTopologyTransactionsX,
|
||||
}
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.PositiveSignedTopologyTransactionX
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||
import com.digitalasset.canton.topology.transaction.{
|
||||
SignedTopologyTransactionX,
|
||||
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."
|
||||
)
|
||||
def assign_from_beginning(
|
||||
genesisState: Seq[PositiveSignedTopologyTransactionX],
|
||||
genesisState: Seq[GenericSignedTopologyTransactionX],
|
||||
domainParameters: StaticDomainParameters,
|
||||
): InitializeSequencerResponseX =
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
InitializeX(
|
||||
StoredTopologyTransactionsX[TopologyChangeOpX.Replace, TopologyMappingX](
|
||||
StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX](
|
||||
genesisState.map(signed =>
|
||||
StoredTopologyTransactionX(
|
||||
SequencedTime(SignedTopologyTransactionX.InitialTopologySequencingTime),
|
||||
@ -67,7 +67,7 @@ class SequencerXSetupGroup(parent: ConsoleCommandGroup) extends ConsoleCommandGr
|
||||
|
||||
@Help.Summary(
|
||||
"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(
|
||||
topologySnapshot: GenericStoredTopologyTransactionsX,
|
||||
|
@ -42,6 +42,7 @@ import com.digitalasset.canton.topology.store.{
|
||||
StoredTopologyTransactionX,
|
||||
StoredTopologyTransactionsX,
|
||||
TimeQuery,
|
||||
TopologyStoreId,
|
||||
}
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
||||
@ -1055,12 +1056,13 @@ class TopologyAdministrationGroup(
|
||||
store: String = AuthorizedStore.filterName,
|
||||
): SignedTopologyTransactionX[TopologyChangeOpX, PartyToParticipantX] = {
|
||||
|
||||
val currentO = expectAtMostOneResult(
|
||||
list(
|
||||
filterStore = store,
|
||||
filterParty = party.filterString,
|
||||
).filter(_.item.domainId == domainId)
|
||||
)
|
||||
val currentO = TopologyStoreId(store) match {
|
||||
case TopologyStoreId.DomainStore(domainId, _) =>
|
||||
expectAtMostOneResult(list(domainId, filterParty = party.filterString))
|
||||
|
||||
case TopologyStoreId.AuthorizedStore =>
|
||||
expectAtMostOneResult(list_from_authorized(filterParty = party.filterString))
|
||||
}
|
||||
|
||||
val (existingPermissions, newSerial) = currentO match {
|
||||
case Some(current) if current.context.operation == TopologyChangeOpX.Remove =>
|
||||
@ -1158,14 +1160,13 @@ class TopologyAdministrationGroup(
|
||||
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(
|
||||
"""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.
|
||||
|
||||
filterStore: - "Authorized": Look in the node's authorized store.
|
||||
- "<domain-id>": Look in the specified domain store.
|
||||
domainId: Domain to be considered
|
||||
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.
|
||||
@ -1179,7 +1180,7 @@ class TopologyAdministrationGroup(
|
||||
|"""
|
||||
)
|
||||
def list(
|
||||
filterStore: String = "",
|
||||
domain: DomainId,
|
||||
proposals: Boolean = false,
|
||||
timeQuery: TimeQuery = TimeQuery.HeadState,
|
||||
operation: Option[TopologyChangeOpX] = None,
|
||||
@ -1191,7 +1192,93 @@ class TopologyAdministrationGroup(
|
||||
adminCommand(
|
||||
TopologyAdminCommandsX.Read.ListPartyToParticipant(
|
||||
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,
|
||||
timeQuery,
|
||||
operation,
|
||||
|
@ -5,55 +5,27 @@ syntax = "proto3";
|
||||
|
||||
package com.digitalasset.canton.protocol.v30;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/rpc/status.proto";
|
||||
import "scalapb/scalapb.proto";
|
||||
|
||||
// Definition of the ConfirmationResponse message which is shared between the transaction and transfer protocol
|
||||
|
||||
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 {
|
||||
google.protobuf.Empty local_approve = 1;
|
||||
LocalReject local_reject = 2;
|
||||
VerdictCode code = 1;
|
||||
google.rpc.Status reason = 2; // ok iff code is approve
|
||||
|
||||
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 {
|
||||
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
|
||||
string sender = 2;
|
||||
|
@ -14,19 +14,6 @@ import "scalapb/scalapb.proto";
|
||||
|
||||
// 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 {
|
||||
option (scalapb.message).companion_extends = "com.digitalasset.canton.version.StableProtoVersion";
|
||||
|
||||
@ -39,20 +26,12 @@ message ParticipantReject {
|
||||
|
||||
message RejectionReason {
|
||||
repeated string parties = 1;
|
||||
com.digitalasset.canton.protocol.v30.LocalReject reject = 2;
|
||||
com.digitalasset.canton.protocol.v30.LocalVerdict reject = 2;
|
||||
}
|
||||
|
||||
message MediatorReject {
|
||||
google.rpc.Status reason = 1; // Must not be OK
|
||||
}
|
||||
|
||||
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;
|
||||
bool is_malformed = 2; // True if the request has been recognized as malformed.
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
int64 request_id = 1; // in microseconds of UTC time since Unix epoch
|
||||
Verdict verdict = 2;
|
||||
bytes root_hash = 3;
|
||||
string domain_id = 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;
|
||||
string domain_id = 1;
|
||||
com.digitalasset.canton.protocol.v30.ViewType view_type = 2;
|
||||
int64 request_id = 3;
|
||||
bytes root_hash = 4;
|
||||
Verdict verdict = 5;
|
||||
repeated string informees = 6; // empty for transactions
|
||||
}
|
||||
|
@ -18,11 +18,9 @@ message TypedSignedProtocolMessageContent {
|
||||
|
||||
oneof some_signed_protocol_message {
|
||||
bytes confirmation_response = 2;
|
||||
bytes transaction_result = 3;
|
||||
bytes malformed_mediator_confirmation_request_result = 4;
|
||||
bytes transfer_result = 5;
|
||||
bytes acs_commitment = 6;
|
||||
bytes set_traffic_balance = 7;
|
||||
bytes confirmation_result = 3;
|
||||
bytes acs_commitment = 4;
|
||||
bytes set_traffic_balance = 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ message SetTrafficBalanceMessage {
|
||||
// Member to update the balance for
|
||||
string member = 1;
|
||||
// Serial number - must be unique and monotonically increasing for each new balance update
|
||||
int64 serial = 2;
|
||||
uint32 serial = 2;
|
||||
// New total traffic balance
|
||||
uint64 total_traffic_balance = 4;
|
||||
// Domain Id
|
||||
|
@ -80,6 +80,15 @@ trait CryptoPureApi
|
||||
with RandomOps
|
||||
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 CryptoPrivateStoreApi
|
||||
extends CryptoPrivateApi
|
||||
|
@ -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"),
|
||||
)
|
||||
|
||||
}
|
@ -18,7 +18,11 @@ import com.digitalasset.canton.crypto.store.{
|
||||
import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
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.util.*
|
||||
import com.digitalasset.canton.version.{
|
||||
@ -29,7 +33,10 @@ import com.digitalasset.canton.version.{
|
||||
ProtoVersion,
|
||||
ProtocolVersion,
|
||||
}
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.protobuf.ByteString
|
||||
import monocle.Lens
|
||||
import monocle.macros.GenLens
|
||||
import slick.jdbc.GetResult
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -349,6 +356,15 @@ object EncryptionKeyPair {
|
||||
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(
|
||||
encryptionKeyPairP: v30.EncryptionKeyPair
|
||||
): ParsingResult[EncryptionKeyPair] =
|
||||
@ -373,11 +389,20 @@ final case class EncryptionPublicKey private[crypto] (
|
||||
scheme: EncryptionKeyScheme,
|
||||
) extends PublicKey
|
||||
with PrettyPrinting
|
||||
with HasVersionedWrapper[EncryptionPublicKey]
|
||||
with NoCopy {
|
||||
with HasVersionedWrapper[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
|
||||
|
||||
def toProtoV30: v30.EncryptionPublicKey =
|
||||
@ -398,6 +423,7 @@ final case class EncryptionPublicKey private[crypto] (
|
||||
object EncryptionPublicKey
|
||||
extends HasVersionedMessageCompanion[EncryptionPublicKey]
|
||||
with HasVersionedMessageCompanionDbHelpers[EncryptionPublicKey] {
|
||||
override def name: String = "encryption public key"
|
||||
val supportedProtoVersions: SupportedProtoVersions = SupportedProtoVersions(
|
||||
ProtoVersion(30) -> ProtoCodec(
|
||||
ProtocolVersion.v30,
|
||||
@ -406,15 +432,17 @@ object EncryptionPublicKey
|
||||
)
|
||||
)
|
||||
|
||||
override def name: String = "encryption public key"
|
||||
|
||||
private[this] def apply(
|
||||
private[crypto] def create(
|
||||
id: Fingerprint,
|
||||
format: CryptoKeyFormat,
|
||||
key: ByteString,
|
||||
scheme: EncryptionKeyScheme,
|
||||
): EncryptionPrivateKey =
|
||||
throw new UnsupportedOperationException("Use generate or deserialization methods")
|
||||
): Either[ProtoDeserializationError.CryptoDeserializationError, EncryptionPublicKey] =
|
||||
new EncryptionPublicKey(id, format, key, scheme).validated
|
||||
|
||||
@VisibleForTesting
|
||||
val idUnsafe: Lens[EncryptionPublicKey, Fingerprint] =
|
||||
GenLens[EncryptionPublicKey](_.id)
|
||||
|
||||
def fromProtoV30(
|
||||
publicKeyP: v30.EncryptionPublicKey
|
||||
@ -423,7 +451,13 @@ object EncryptionPublicKey
|
||||
id <- Fingerprint.fromProtoPrimitive(publicKeyP.id)
|
||||
format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format)
|
||||
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(
|
||||
@ -479,14 +513,6 @@ object EncryptionPrivateKey extends HasVersionedMessageCompanion[EncryptionPriva
|
||||
|
||||
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(
|
||||
privateKeyP: v30.EncryptionPrivateKey
|
||||
): ParsingResult[EncryptionPrivateKey] =
|
||||
|
@ -15,8 +15,8 @@ import com.digitalasset.canton.crypto.store.{
|
||||
}
|
||||
import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups}
|
||||
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.{DefaultDeserializationError, ProtoConverter}
|
||||
import com.digitalasset.canton.topology.Member
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.NoCopy
|
||||
@ -27,7 +27,10 @@ import com.digitalasset.canton.version.{
|
||||
ProtoVersion,
|
||||
ProtocolVersion,
|
||||
}
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.protobuf.ByteString
|
||||
import monocle.Lens
|
||||
import monocle.macros.GenLens
|
||||
import slick.jdbc.GetResult
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -290,6 +293,15 @@ object SigningKeyPair {
|
||||
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(
|
||||
signingKeyPairP: v30.SigningKeyPair
|
||||
): ParsingResult[SigningKeyPair] =
|
||||
@ -315,12 +327,21 @@ case class SigningPublicKey private[crypto] (
|
||||
scheme: SigningKeyScheme,
|
||||
) extends PublicKey
|
||||
with PrettyPrinting
|
||||
with NoCopy
|
||||
with HasVersionedWrapper[SigningPublicKey] {
|
||||
override val purpose: KeyPurpose = KeyPurpose.Signing
|
||||
|
||||
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 =
|
||||
v30.SigningPublicKey(
|
||||
id = id.toProtoPrimitive,
|
||||
@ -349,13 +370,17 @@ object SigningPublicKey
|
||||
)
|
||||
)
|
||||
|
||||
private[this] def apply(
|
||||
private[crypto] def create(
|
||||
id: Fingerprint,
|
||||
format: CryptoKeyFormat,
|
||||
key: ByteString,
|
||||
scheme: SigningKeyScheme,
|
||||
): SigningPrivateKey =
|
||||
throw new UnsupportedOperationException("Use keypair generate or deserialization methods")
|
||||
): Either[ProtoDeserializationError.CryptoDeserializationError, SigningPublicKey] =
|
||||
new SigningPublicKey(id, format, key, scheme).validated
|
||||
|
||||
@VisibleForTesting
|
||||
val idUnsafe: Lens[SigningPublicKey, Fingerprint] =
|
||||
GenLens[SigningPublicKey](_.id)
|
||||
|
||||
def fromProtoV30(
|
||||
publicKeyP: v30.SigningPublicKey
|
||||
@ -364,7 +389,13 @@ object SigningPublicKey
|
||||
id <- Fingerprint.fromProtoPrimitive(publicKeyP.id)
|
||||
format <- CryptoKeyFormat.fromProtoEnum("format", publicKeyP.format)
|
||||
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]] =
|
||||
initialKeys.map { case (k, v) =>
|
||||
@ -425,14 +456,6 @@ object SigningPrivateKey extends HasVersionedMessageCompanion[SigningPrivateKey]
|
||||
|
||||
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(
|
||||
privateKeyP: v30.SigningPrivateKey
|
||||
): ParsingResult[SigningPrivateKey] =
|
||||
|
@ -32,22 +32,16 @@ class JceJavaConverter(
|
||||
|
||||
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(
|
||||
publicKey: PublicKey,
|
||||
curveType: CurveType,
|
||||
): Either[JavaKeyConversionError, (AlgorithmIdentifier, JPublicKey)] =
|
||||
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
|
||||
// type to algo id.
|
||||
algoId <- TinkJavaConverter
|
||||
@ -70,7 +64,11 @@ class JceJavaConverter(
|
||||
keyInstance: String,
|
||||
): Either[JavaKeyConversionError, JPublicKey] =
|
||||
for {
|
||||
_ <- ensureFormat(publicKey, format)
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
publicKey.format,
|
||||
Set(format),
|
||||
_ => JavaKeyConversionError.UnsupportedKeyFormat(publicKey.format, format),
|
||||
)
|
||||
x509KeySpec = new X509EncodedKeySpec(x509PublicKey)
|
||||
keyFactory <- Either
|
||||
.catchOnly[NoSuchAlgorithmException](
|
@ -6,7 +6,7 @@ package com.digitalasset.canton.crypto.provider.tink
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.crypto.{Fingerprint, HashAlgorithm}
|
||||
import com.digitalasset.canton.serialization.{DefaultDeserializationError, DeserializationError}
|
||||
import com.google.crypto.tink.{proto, *}
|
||||
import com.google.crypto.tink.*
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
class TinkKeyFingerprintException(message: String) extends RuntimeException(message)
|
@ -6,10 +6,11 @@ package com.digitalasset.canton.error
|
||||
import com.daml.error.*
|
||||
import com.digitalasset.canton.error.CantonErrorGroups.MediatorErrorGroup
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.protocol.v30
|
||||
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 {
|
||||
|
||||
@ -30,6 +31,9 @@ object MediatorError extends MediatorErrorGroup {
|
||||
unresponsiveParties: String = "",
|
||||
) extends BaseCantonError.Impl(cause)
|
||||
with MediatorError {
|
||||
|
||||
override def isMalformed: Boolean = false
|
||||
|
||||
override def pretty: Pretty[Reject] = prettyOfClass(
|
||||
param("code", _.code.id.unquoted),
|
||||
param("cause", _.cause.unquoted),
|
||||
@ -62,10 +66,12 @@ object MediatorError extends MediatorErrorGroup {
|
||||
override def logLevel: Level = Level.WARN
|
||||
|
||||
final case class Reject(
|
||||
override val cause: String,
|
||||
_v0CodeP: v30.MediatorRejection.Code = v30.MediatorRejection.Code.CODE_TIMEOUT,
|
||||
override val cause: String
|
||||
) extends BaseCantonError.Impl(cause)
|
||||
with MediatorError {
|
||||
|
||||
override def isMalformed: Boolean = false
|
||||
|
||||
override def pretty: Pretty[Reject] = prettyOfClass(
|
||||
param("code", _.code.id.unquoted),
|
||||
param("cause", _.cause.unquoted),
|
||||
@ -82,11 +88,13 @@ object MediatorError extends MediatorErrorGroup {
|
||||
object MalformedMessage extends AlarmErrorCode("MEDIATOR_RECEIVED_MALFORMED_MESSAGE") {
|
||||
|
||||
final case class Reject(
|
||||
override val cause: String,
|
||||
_v0CodeP: v30.MediatorRejection.Code = v30.MediatorRejection.Code.CODE_TIMEOUT,
|
||||
override val cause: String
|
||||
) extends Alarm(cause)
|
||||
with MediatorError
|
||||
with BaseCantonError {
|
||||
|
||||
override def isMalformed: Boolean = true
|
||||
|
||||
override def pretty: Pretty[Reject] = prettyOfClass(
|
||||
param("code", _.code.id.unquoted),
|
||||
param("cause", _.cause.unquoted),
|
||||
|
@ -6,43 +6,41 @@ package com.digitalasset.canton.protocol
|
||||
import com.daml.error.*
|
||||
import com.digitalasset.canton.error.CantonErrorGroups.ParticipantErrorGroup.TransactionErrorGroup.LocalRejectionGroup
|
||||
import com.digitalasset.canton.error.{AlarmErrorCode, BaseAlarm, TransactionError}
|
||||
import com.digitalasset.canton.logging.pretty.Pretty
|
||||
import com.digitalasset.canton.protocol.messages.{LocalReject, LocalVerdict}
|
||||
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.protocol.messages.{LocalReject, TransactionRejection}
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.google.rpc.status.Status
|
||||
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.
|
||||
*/
|
||||
abstract class LocalRejectErrorCode(
|
||||
id: String,
|
||||
category: ErrorCategory,
|
||||
override val v30CodeP: v30.LocalReject.Code,
|
||||
)(implicit parent: ErrorClass)
|
||||
extends ErrorCode(id, category)
|
||||
with BaseLocalRejectErrorCode {
|
||||
extends ErrorCode(id, category) {
|
||||
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
|
||||
) extends AlarmErrorCode(id)
|
||||
with BaseLocalRejectErrorCode {
|
||||
) extends AlarmErrorCode(id) {
|
||||
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.
|
||||
*/
|
||||
@ -55,8 +53,7 @@ sealed trait LocalRejectError extends LocalReject with TransactionError {
|
||||
|
||||
override def cause: String = _causePrefix + _details
|
||||
|
||||
// Make sure the ErrorCode has a v0CodeP.
|
||||
override def code: ErrorCode & BaseLocalRejectErrorCode
|
||||
override def code: ErrorCode
|
||||
|
||||
/** 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)] =
|
||||
_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] =
|
||||
prettyOfClass(
|
||||
param("code", _.code.id.unquoted),
|
||||
param("causePrefix", _._causePrefix.doubleQuoted),
|
||||
param("details", _._details.doubleQuoted, _._details.nonEmpty),
|
||||
param("cause", _.cause.doubleQuoted),
|
||||
param("resources", _._resources.map(_.singleQuoted)),
|
||||
paramIfDefined("throwable", _.throwableO),
|
||||
)
|
||||
@ -115,7 +99,11 @@ sealed abstract class Malformed(
|
||||
)(implicit
|
||||
override val code: MalformedErrorCode
|
||||
) extends BaseAlarm
|
||||
with LocalRejectError
|
||||
with LocalRejectError {
|
||||
override def toLocalReject(protocolVersion: ProtocolVersion): LocalReject =
|
||||
LocalReject.create(rpcStatusWithoutLoggingContext(), isMalformed = true, protocolVersion)
|
||||
|
||||
}
|
||||
|
||||
object LocalRejectError extends LocalRejectionGroup {
|
||||
|
||||
@ -129,22 +117,13 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "LOCAL_VERDICT_LOCKED_CONTRACTS",
|
||||
ErrorCategory.ContentionOnSharedResources,
|
||||
v30.LocalReject.Code.CODE_LOCKED_CONTRACTS,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _resources: Seq[String])(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _resources: Seq[String])
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix = s"Rejected transaction is referring to locked contracts ",
|
||||
_resourcesType = Some(ErrorResource.ContractId),
|
||||
)
|
||||
|
||||
object Reject {
|
||||
def apply(resources: Seq[String], protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(resources)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
@ -156,21 +135,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "LOCAL_VERDICT_INACTIVE_CONTRACTS",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
|
||||
v30.LocalReject.Code.CODE_INACTIVE_CONTRACTS,
|
||||
) {
|
||||
final case class Reject(override val _resources: Seq[String])(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _resources: Seq[String])
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix = "Rejected transaction is referring to inactive contracts ",
|
||||
_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(
|
||||
id = "LOCAL_VERDICT_LEDGER_TIME_OUT_OF_BOUND",
|
||||
ErrorCategory.ContentionOnSharedResources,
|
||||
v30.LocalReject.Code.CODE_LEDGER_TIME,
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix =
|
||||
"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(
|
||||
@ -215,21 +176,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "LOCAL_VERDICT_SUBMISSION_TIME_OUT_OF_BOUND",
|
||||
ErrorCategory.ContentionOnSharedResources,
|
||||
v30.LocalReject.Code.CODE_SUBMISSION_TIME,
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix =
|
||||
"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(
|
||||
@ -245,21 +197,14 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "LOCAL_VERDICT_TIMEOUT",
|
||||
ErrorCategory.ContentionOnSharedResources,
|
||||
v30.LocalReject.Code.CODE_LEDGER_TIME,
|
||||
) {
|
||||
override def logLevel: Level = Level.WARN
|
||||
final case class Reject()(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject()
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix = "Rejected transaction due to a participant determined timeout "
|
||||
)
|
||||
|
||||
object Reject {
|
||||
def apply(protocolVersion: ProtocolVersion): Reject =
|
||||
Reject()(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
val status: Status = Reject().rpcStatusWithoutLoggingContext()
|
||||
}
|
||||
|
||||
}
|
||||
@ -272,19 +217,9 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
@Resolution("Please contact support.")
|
||||
object MalformedRequest
|
||||
extends MalformedErrorCode(
|
||||
id = "LOCAL_VERDICT_MALFORMED_REQUEST",
|
||||
v30.LocalReject.Code.CODE_MALFORMED_PAYLOADS,
|
||||
id = "LOCAL_VERDICT_MALFORMED_REQUEST"
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends Malformed(_causePrefix = "")
|
||||
|
||||
object Reject {
|
||||
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
final case class Reject(override val _details: String) extends Malformed(_causePrefix = "")
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
@ -293,21 +228,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
@Resolution("This indicates either malicious or faulty behaviour.")
|
||||
object Payloads
|
||||
extends MalformedErrorCode(
|
||||
id = "LOCAL_VERDICT_MALFORMED_PAYLOAD",
|
||||
v30.LocalReject.Code.CODE_MALFORMED_PAYLOADS,
|
||||
id = "LOCAL_VERDICT_MALFORMED_PAYLOAD"
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends Malformed(
|
||||
final case class Reject(override val _details: String)
|
||||
extends Malformed(
|
||||
_causePrefix = "Rejected transaction due to malformed payload within views "
|
||||
)
|
||||
|
||||
object Reject {
|
||||
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
@ -316,21 +242,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
@Resolution("This indicates either malicious or faulty behaviour.")
|
||||
object ModelConformance
|
||||
extends MalformedErrorCode(
|
||||
id = "LOCAL_VERDICT_FAILED_MODEL_CONFORMANCE_CHECK",
|
||||
v30.LocalReject.Code.CODE_MALFORMED_MODEL,
|
||||
id = "LOCAL_VERDICT_FAILED_MODEL_CONFORMANCE_CHECK"
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends Malformed(
|
||||
final case class Reject(override val _details: String)
|
||||
extends Malformed(
|
||||
_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(
|
||||
@ -341,21 +258,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
)
|
||||
object BadRootHashMessages
|
||||
extends MalformedErrorCode(
|
||||
id = "LOCAL_VERDICT_BAD_ROOT_HASH_MESSAGES",
|
||||
v30.LocalReject.Code.CODE_BAD_ROOT_HASH_MESSAGE,
|
||||
id = "LOCAL_VERDICT_BAD_ROOT_HASH_MESSAGES"
|
||||
) {
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends Malformed(
|
||||
final case class Reject(override val _details: String)
|
||||
extends Malformed(
|
||||
_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(
|
||||
@ -364,22 +272,13 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
@Resolution("This error indicates either faulty or malicious behaviour.")
|
||||
object CreatesExistingContracts
|
||||
extends MalformedErrorCode(
|
||||
id = "LOCAL_VERDICT_CREATES_EXISTING_CONTRACTS",
|
||||
v30.LocalReject.Code.CODE_CREATES_EXISTING_CONTRACT,
|
||||
id = "LOCAL_VERDICT_CREATES_EXISTING_CONTRACTS"
|
||||
) {
|
||||
final case class Reject(override val _resources: Seq[String])(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends Malformed(
|
||||
final case class Reject(override val _resources: Seq[String])
|
||||
extends Malformed(
|
||||
_causePrefix = "Rejected transaction would create contract(s) that already exist ",
|
||||
_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(
|
||||
id = "TRANSFER_OUT_ACTIVENESS_CHECK_FAILED",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
|
||||
v30.LocalReject.Code.CODE_TRANSFER_OUT_ACTIVENESS_CHECK,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(_causePrefix = "Activeness check failed.")
|
||||
|
||||
object Reject {
|
||||
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(_causePrefix = "Activeness check failed.")
|
||||
}
|
||||
|
||||
}
|
||||
@ -422,21 +312,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "TRANSFER_IN_CONTRACT_ALREADY_ARCHIVED",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateResourceMissing,
|
||||
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_ARCHIVED,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix = "Rejected transfer as transferred contract is already archived. "
|
||||
)
|
||||
|
||||
object Reject {
|
||||
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
@ -446,22 +327,13 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "TRANSFER_IN_CONTRACT_ALREADY_ACTIVE",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateResourceExists,
|
||||
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_ACTIVE,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix =
|
||||
"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(
|
||||
@ -471,21 +343,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "TRANSFER_IN_CONTRACT_IS_LOCKED",
|
||||
ErrorCategory.ContentionOnSharedResources,
|
||||
v30.LocalReject.Code.CODE_TRANSFER_IN_LOCKED,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_causePrefix = "Rejected transfer as the transferred contract is locked."
|
||||
)
|
||||
|
||||
object Reject {
|
||||
def apply(details: String, protocolVersion: ProtocolVersion): Reject =
|
||||
Reject(details)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
@ -495,58 +358,12 @@ object LocalRejectError extends LocalRejectionGroup {
|
||||
extends LocalRejectErrorCode(
|
||||
id = "TRANSFER_IN_ALREADY_COMPLETED",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateResourceExists,
|
||||
v30.LocalReject.Code.CODE_TRANSFER_IN_ALREADY_COMPLETED,
|
||||
) {
|
||||
|
||||
final case class Reject(override val _details: String)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
LocalVerdict.type
|
||||
]
|
||||
) extends LocalRejectErrorImpl(
|
||||
final case class Reject(override val _details: String)
|
||||
extends LocalRejectErrorImpl(
|
||||
_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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -36,12 +36,6 @@ trait Phase37Processor[RequestBatch] {
|
||||
traceContext: TraceContext
|
||||
): 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
|
||||
* [[com.digitalasset.canton.participant.event.RecordOrderPublisher]].
|
||||
*
|
||||
|
@ -26,7 +26,7 @@ import monocle.macros.GenLens
|
||||
* @param requestId The unique identifier of the request.
|
||||
* @param sender The identity of the sender.
|
||||
* @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.
|
||||
* @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]]
|
||||
@ -89,11 +89,10 @@ case class ConfirmationResponse private (
|
||||
|
||||
// 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.
|
||||
localVerdict match {
|
||||
case _: Malformed =>
|
||||
if (localVerdict.isMalformed) {
|
||||
if (confirmingParties.nonEmpty)
|
||||
throw InvalidConfirmationResponse("Confirming parties must be empty for verdict Malformed.")
|
||||
case _: LocalApprove | _: LocalReject =>
|
||||
} else {
|
||||
if (confirmingParties.isEmpty)
|
||||
throw InvalidConfirmationResponse(
|
||||
show"Confirming parties must not be empty for verdict $localVerdict"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -3,81 +3,92 @@
|
||||
|
||||
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.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
|
||||
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, ProtocolVersionedMemoizedEvidence}
|
||||
import com.digitalasset.canton.topology.DomainId
|
||||
import com.digitalasset.canton.version.*
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
/** Transaction result message that the mediator sends to all stakeholders of a transaction confirmation request with its verdict.
|
||||
* https://engineering.da-int.net/docs/platform-architecture-handbook/arch/canton/transactions.html#phase-6-broadcast-of-result
|
||||
/** Result message that the mediator sends to all stakeholders of a request with its verdict.
|
||||
*
|
||||
* @param requestId identifier of the confirmation request
|
||||
* @param domainId the domain on which the request is running
|
||||
* @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
|
||||
case class ConfirmationResultMessage private (
|
||||
override val requestId: RequestId,
|
||||
override val verdict: Verdict,
|
||||
rootHash: RootHash,
|
||||
override val domainId: DomainId,
|
||||
viewType: ViewType,
|
||||
override val requestId: RequestId,
|
||||
rootHashO: Option[RootHash],
|
||||
verdict: Verdict,
|
||||
informees: Set[LfPartyId],
|
||||
)(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[
|
||||
ConfirmationResultMessage.type
|
||||
],
|
||||
override val deserializedFrom: Option[ByteString],
|
||||
) extends RegularConfirmationResult
|
||||
) extends ProtocolVersionedMemoizedEvidence
|
||||
with HasDomainId
|
||||
with HasRequestId
|
||||
with SignedProtocolMessageContent
|
||||
with HasProtocolVersionedWrapper[ConfirmationResultMessage]
|
||||
with PrettyPrinting {
|
||||
|
||||
override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap)
|
||||
|
||||
def copy(
|
||||
requestId: RequestId = this.requestId,
|
||||
verdict: Verdict = this.verdict,
|
||||
rootHash: RootHash = this.rootHash,
|
||||
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(requestId, verdict, rootHash, domainId)(
|
||||
ConfirmationResultMessage(domainId, viewType, requestId, rootHashO, verdict, informees)(
|
||||
representativeProtocolVersion,
|
||||
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 =
|
||||
super[HasProtocolVersionedWrapper].toByteString
|
||||
|
||||
@transient override protected lazy val companionObj: ConfirmationResultMessage.type =
|
||||
ConfirmationResultMessage
|
||||
|
||||
protected def toProtoV30: v30.TransactionResultMessage =
|
||||
v30.TransactionResultMessage(
|
||||
requestId = requestId.toProtoPrimitive,
|
||||
verdict = Some(verdict.toProtoV30),
|
||||
rootHash = rootHash.toProtoPrimitive,
|
||||
protected def toProtoV30: v30.ConfirmationResultMessage =
|
||||
v30.ConfirmationResultMessage(
|
||||
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
|
||||
: v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage =
|
||||
v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage.TransactionResult(
|
||||
v30.TypedSignedProtocolMessageContent.SomeSignedProtocolMessage.ConfirmationResult(
|
||||
getCryptographicEvidence
|
||||
)
|
||||
|
||||
override def pretty: Pretty[ConfirmationResultMessage] =
|
||||
prettyOfClass(
|
||||
param("requestId", _.requestId.unwrap),
|
||||
param("verdict", _.verdict),
|
||||
param("rootHash", _.rootHash),
|
||||
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[
|
||||
ConfirmationResultMessage,
|
||||
] {
|
||||
override val name: String = "TransactionResultMessage"
|
||||
override val name: String = "ConfirmationResultMessage"
|
||||
|
||||
val supportedProtoVersions = SupportedProtoVersions(
|
||||
ProtoVersion(30) -> VersionedProtoConverter(ProtocolVersion.v30)(
|
||||
v30.TransactionResultMessage
|
||||
v30.ConfirmationResultMessage
|
||||
)(
|
||||
supportedProtoVersionMemoized(_)(fromProtoV30),
|
||||
_.toProtoV30.toByteString,
|
||||
)
|
||||
)
|
||||
|
||||
def apply(
|
||||
requestId: RequestId,
|
||||
verdict: Verdict,
|
||||
rootHash: RootHash,
|
||||
def create(
|
||||
domainId: DomainId,
|
||||
viewType: ViewType,
|
||||
requestId: RequestId,
|
||||
rootHashO: Option[RootHash],
|
||||
verdict: Verdict,
|
||||
informees: Set[LfPartyId],
|
||||
protocolVersion: ProtocolVersion,
|
||||
): ConfirmationResultMessage =
|
||||
ConfirmationResultMessage(requestId, verdict, rootHash, domainId)(
|
||||
ConfirmationResultMessage(domainId, viewType, requestId, rootHashO, verdict, informees)(
|
||||
protocolVersionRepresentativeFor(protocolVersion),
|
||||
None,
|
||||
)
|
||||
|
||||
private def fromProtoV30(protoResultMessage: v30.TransactionResultMessage)(
|
||||
private def fromProtoV30(protoResultMessage: v30.ConfirmationResultMessage)(
|
||||
bytes: ByteString
|
||||
): ParsingResult[ConfirmationResultMessage] = {
|
||||
val v30.TransactionResultMessage(requestIdP, verdictPO, rootHashP, domainIdP) =
|
||||
protoResultMessage
|
||||
val v30.ConfirmationResultMessage(
|
||||
domainIdP,
|
||||
viewTypeP,
|
||||
requestIdP,
|
||||
rootHashP,
|
||||
verdictPO,
|
||||
informeesP,
|
||||
) = protoResultMessage
|
||||
|
||||
for {
|
||||
requestId <- RequestId.fromProtoPrimitive(requestIdP)
|
||||
transactionResult <- ProtoConverter
|
||||
.required("verdict", verdictPO)
|
||||
.flatMap(Verdict.fromProtoV30)
|
||||
rootHash <- RootHash.fromProtoPrimitive(rootHashP)
|
||||
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))
|
||||
} yield ConfirmationResultMessage(requestId, transactionResult, rootHash, domainId)(
|
||||
rpv,
|
||||
Some(bytes),
|
||||
)
|
||||
} yield ConfirmationResultMessage(
|
||||
domainId,
|
||||
viewType,
|
||||
requestId,
|
||||
rootHashO,
|
||||
verdict,
|
||||
informees.toSet,
|
||||
)(rpv, Some(bytes))
|
||||
}
|
||||
|
||||
implicit val transactionResultMessageCast: SignedMessageContentCast[ConfirmationResultMessage] =
|
||||
SignedMessageContentCast.create[ConfirmationResultMessage]("TransactionResultMessage") {
|
||||
implicit val confirmationResultMessageCast: SignedMessageContentCast[ConfirmationResultMessage] =
|
||||
SignedMessageContentCast.create[ConfirmationResultMessage]("ConfirmationResultMessage") {
|
||||
case m: ConfirmationResultMessage => Some(m)
|
||||
case _ => None
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ package com.digitalasset.canton.protocol.messages
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.functorFilter.*
|
||||
import com.digitalasset.canton.data.ViewType
|
||||
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.{SourceDomainId, TransferId}
|
||||
import com.digitalasset.canton.sequencing.RawProtocolEvent
|
||||
import com.digitalasset.canton.sequencing.protocol.{
|
||||
Batch,
|
||||
@ -19,10 +20,16 @@ import com.digitalasset.canton.sequencing.protocol.{
|
||||
final case class DeliveredTransferOutResult(result: SignedContent[Deliver[DefaultOpenEnvelope]])
|
||||
extends PrettyPrinting {
|
||||
|
||||
val unwrap: TransferOutResult = result.content match {
|
||||
val unwrap: ConfirmationResultMessage = result.content match {
|
||||
case Deliver(_, _, _, _, Batch(envelopes), _) =>
|
||||
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
|
||||
if (size != 1)
|
||||
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.")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -3,13 +3,12 @@
|
||||
|
||||
package com.digitalasset.canton.protocol.messages
|
||||
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
import com.digitalasset.canton.crypto.{HashOps, Signature}
|
||||
import com.digitalasset.canton.data.{FullInformeeTree, Informee, ViewPosition, ViewType}
|
||||
import com.digitalasset.canton.logging.pretty.Pretty
|
||||
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.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
@ -61,19 +60,6 @@ case class InformeeMessage(
|
||||
: Map[ViewPosition, (Set[Informee], NonNegativeInt)] =
|
||||
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.
|
||||
// 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.)
|
||||
@ -97,6 +83,8 @@ case class InformeeMessage(
|
||||
override def pretty: Pretty[InformeeMessage] = prettyOfClass(unnamedParam(_.fullInformeeTree))
|
||||
|
||||
@transient override protected lazy val companionObj: InformeeMessage.type = InformeeMessage
|
||||
|
||||
override def informeesArePublic: Boolean = false
|
||||
}
|
||||
|
||||
object InformeeMessage
|
||||
|
@ -3,22 +3,20 @@
|
||||
|
||||
package com.digitalasset.canton.protocol.messages
|
||||
|
||||
import com.daml.error.{ContextualizedErrorLogger, ErrorCategory, ErrorCode, NoLogging}
|
||||
import com.digitalasset.canton.ProtoDeserializationError.OtherError
|
||||
import com.daml.error.*
|
||||
import com.digitalasset.canton.ProtoDeserializationError.{FieldNotSet, OtherError}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.protocol.LocalRejectError.MalformedRejects.CreatesExistingContracts
|
||||
import com.digitalasset.canton.protocol.LocalRejectError.{
|
||||
ConsistencyRejections,
|
||||
MalformedRejects,
|
||||
TimeRejects,
|
||||
TransferInRejects,
|
||||
TransferOutRejects,
|
||||
import com.digitalasset.canton.protocol.v30.LocalVerdict.VerdictCode.{
|
||||
VERDICT_CODE_LOCAL_APPROVE,
|
||||
VERDICT_CODE_LOCAL_MALFORMED,
|
||||
VERDICT_CODE_LOCAL_REJECT,
|
||||
VERDICT_CODE_UNSPECIFIED,
|
||||
}
|
||||
import com.digitalasset.canton.protocol.messages.LocalVerdict.protocolVersionRepresentativeFor
|
||||
import com.digitalasset.canton.protocol.{LocalRejectError, messages, v30}
|
||||
import com.digitalasset.canton.protocol.{messages, v30}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
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.
|
||||
* The verdict can be `LocalApprove`, `LocalReject` or `Malformed`.
|
||||
@ -29,6 +27,9 @@ sealed trait LocalVerdict
|
||||
with Serializable
|
||||
with PrettyPrinting
|
||||
with HasProtocolVersionedWrapper[LocalVerdict] {
|
||||
|
||||
def isMalformed: Boolean
|
||||
|
||||
private[messages] def toProtoV30: v30.LocalVerdict
|
||||
|
||||
@transient override protected lazy val companionObj: LocalVerdict.type = LocalVerdict
|
||||
@ -51,25 +52,39 @@ object LocalVerdict extends HasProtocolVersionedCompanion[LocalVerdict] {
|
||||
private[messages] def fromProtoV30(
|
||||
localVerdictP: v30.LocalVerdict
|
||||
): ParsingResult[LocalVerdict] = {
|
||||
import v30.LocalVerdict.SomeLocalVerdict as Lv
|
||||
|
||||
val protocolVersion = protocolVersionRepresentativeFor(ProtoVersion(30))
|
||||
val v30.LocalVerdict(someLocalVerdictP) = localVerdictP
|
||||
|
||||
someLocalVerdictP match {
|
||||
case Lv.LocalApprove(empty.Empty(_)) => protocolVersion.map(LocalApprove()(_))
|
||||
case Lv.LocalReject(localRejectP) => LocalReject.fromProtoV30(localRejectP)
|
||||
case Lv.Empty =>
|
||||
Left(OtherError("Unable to deserialize LocalVerdict, as the content is empty"))
|
||||
val v30.LocalVerdict(codeP, reasonPO) = localVerdictP
|
||||
for {
|
||||
rpv <- protocolVersionRepresentativeFor(ProtoVersion(30))
|
||||
reason <- ProtoConverter.required("reason", reasonPO)
|
||||
localVerdict <- codeP match {
|
||||
case VERDICT_CODE_LOCAL_APPROVE => Right(LocalApprove()(rpv))
|
||||
case VERDICT_CODE_LOCAL_REJECT =>
|
||||
Right(LocalReject(reason, isMalformed = false)(rpv))
|
||||
case VERDICT_CODE_LOCAL_MALFORMED =>
|
||||
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()(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[LocalVerdict.type]
|
||||
) extends LocalVerdict {
|
||||
|
||||
override def isMalformed: Boolean = false
|
||||
|
||||
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()
|
||||
}
|
||||
@ -79,70 +94,37 @@ object LocalApprove {
|
||||
LocalApprove()(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
|
||||
// TODO(i16856): temporary intermediate type, until the verdict LocalReject has been separated from the error LocalReject.
|
||||
trait LocalReject extends LocalVerdict with TransactionRejection {
|
||||
final case class LocalReject(reason: com.google.rpc.status.Status, isMalformed: Boolean)(
|
||||
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(
|
||||
overrideCode: Option[io.grpc.Status.Code] = None
|
||||
)(implicit loggingContext: ContextualizedErrorLogger): com.google.rpc.status.Status
|
||||
|
||||
override lazy val reason: com.google.rpc.status.Status = rpcStatus()(NoLogging)
|
||||
|
||||
protected[messages] def toLocalRejectProtoV30: v30.LocalReject
|
||||
|
||||
protected[messages] def toProtoV30: v30.LocalVerdict
|
||||
override private[messages] def toProtoV30: v30.LocalVerdict = {
|
||||
val codeP =
|
||||
if (isMalformed) VERDICT_CODE_LOCAL_MALFORMED else VERDICT_CODE_LOCAL_REJECT
|
||||
v30.LocalVerdict(code = codeP, reason = Some(reason))
|
||||
}
|
||||
|
||||
override def pretty: Pretty[LocalReject] = prettyOfClass(
|
||||
param("reason", _.reason),
|
||||
param("isMalformed", _.isMalformed),
|
||||
)
|
||||
}
|
||||
|
||||
object LocalReject {
|
||||
|
||||
// list of local errors, used to map them during transport
|
||||
// if you add a new error below, you must add it to this list here as well
|
||||
private[messages] def fromProtoV30(
|
||||
localRejectP: v30.LocalReject
|
||||
): ParsingResult[LocalReject] = {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def create(
|
||||
reason: com.google.rpc.status.Status,
|
||||
isMalformed: Boolean,
|
||||
protocolVersion: ProtocolVersion,
|
||||
): LocalReject =
|
||||
LocalReject(reason, isMalformed)(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
import com.digitalasset.canton.crypto.Signature
|
||||
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.{RequestId, RootHash}
|
||||
import com.digitalasset.canton.sequencing.protocol.MediatorsOfDomain
|
||||
import com.digitalasset.canton.topology.ParticipantId
|
||||
|
||||
@ -29,11 +29,8 @@ trait MediatorConfirmationRequest extends UnsignedProtocolMessage {
|
||||
.map(_.party)
|
||||
.toSet
|
||||
|
||||
def createConfirmationResult(
|
||||
requestId: RequestId,
|
||||
verdict: Verdict,
|
||||
recipientParties: Set[LfPartyId],
|
||||
): ConfirmationResult with SignedProtocolMessageContent
|
||||
/** Determines whether the mediator may disclose informees as part of its result message. */
|
||||
def informeesArePublic: Boolean
|
||||
|
||||
def minimumThreshold(informees: Set[Informee]): NonNegativeInt
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
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.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.protocol.messages.SignedProtocolMessageContent.SignedMessageContentCast
|
||||
@ -17,7 +17,7 @@ import com.google.protobuf.ByteString
|
||||
|
||||
final case class SetTrafficBalanceMessage private (
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
domainId: DomainId,
|
||||
)(
|
||||
@ -79,7 +79,7 @@ object SetTrafficBalanceMessage
|
||||
|
||||
def apply(
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
domainId: DomainId,
|
||||
protocolVersion: ProtocolVersion,
|
||||
@ -94,7 +94,7 @@ object SetTrafficBalanceMessage
|
||||
)(bytes: ByteString): ParsingResult[SetTrafficBalanceMessage] = {
|
||||
for {
|
||||
member <- Member.fromProtoPrimitive(proto.member, "member")
|
||||
serial <- ProtoConverter.parseNonNegativeLong(proto.serial)
|
||||
serial <- ProtoConverter.parsePositiveInt(proto.serial)
|
||||
totalTrafficBalance <- ProtoConverter.parseNonNegativeLong(proto.totalTrafficBalance)
|
||||
domainId <- DomainId.fromProtoPrimitive(proto.domainId, "domain_id")
|
||||
rpv <- protocolVersionRepresentativeFor(ProtoVersion(1))
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
package com.digitalasset.canton.protocol.messages
|
||||
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.ProtoDeserializationError.OtherError
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
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 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 =
|
||||
v30.EnvelopeContent.SomeEnvelopeContent.TransferInMediatorMessage(toProtoV30)
|
||||
|
||||
@ -100,6 +80,8 @@ final case class TransferInMediatorMessage(
|
||||
|
||||
@transient override protected lazy val companionObj: TransferInMediatorMessage.type =
|
||||
TransferInMediatorMessage
|
||||
|
||||
override def informeesArePublic: Boolean = true
|
||||
}
|
||||
|
||||
object TransferInMediatorMessage
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
package com.digitalasset.canton.protocol.messages
|
||||
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.ProtoDeserializationError.OtherError
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
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 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 =
|
||||
v30.TransferOutMediatorMessage(
|
||||
tree = Some(tree.toProtoV30),
|
||||
@ -98,6 +78,8 @@ final case class TransferOutMediatorMessage(
|
||||
|
||||
@transient override protected lazy val companionObj: TransferOutMediatorMessage.type =
|
||||
TransferOutMediatorMessage
|
||||
|
||||
override def informeesArePublic: Boolean = true
|
||||
}
|
||||
|
||||
object TransferOutMediatorMessage
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -101,22 +101,14 @@ object TypedSignedProtocolMessageContent
|
||||
message <- (messageBytes match {
|
||||
case Sm.ConfirmationResponse(confirmationResponseBytes) =>
|
||||
ConfirmationResponse.fromByteString(expectedProtocolVersion)(confirmationResponseBytes)
|
||||
case Sm.TransactionResult(transactionResultMessageBytes) =>
|
||||
case Sm.ConfirmationResult(confirmationResultMessageBytes) =>
|
||||
ConfirmationResultMessage.fromByteString(expectedProtocolVersion)(
|
||||
transactionResultMessageBytes
|
||||
confirmationResultMessageBytes
|
||||
)
|
||||
case Sm.TransferResult(transferResultBytes) =>
|
||||
TransferResult.fromByteString(expectedProtocolVersion)(transferResultBytes)
|
||||
case Sm.AcsCommitment(acsCommitmentBytes) =>
|
||||
AcsCommitment.fromByteString(expectedProtocolVersion)(acsCommitmentBytes)
|
||||
case Sm.SetTrafficBalance(setTrafficBalanceBytes) =>
|
||||
SetTrafficBalanceMessage.fromByteString(expectedProtocolVersion)(setTrafficBalanceBytes)
|
||||
case Sm.MalformedMediatorConfirmationRequestResult(
|
||||
malformedMediatorConfirmationRequestResultBytes
|
||||
) =>
|
||||
MalformedConfirmationRequestResult.fromByteString(expectedProtocolVersion)(
|
||||
malformedMediatorConfirmationRequestResultBytes
|
||||
)
|
||||
case Sm.Empty =>
|
||||
Left(OtherError("Deserialization of a SignedMessage failed due to a missing message"))
|
||||
}): ParsingResult[SignedProtocolMessageContent]
|
||||
|
@ -27,13 +27,15 @@ trait TransactionRejection {
|
||||
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
|
||||
extends Product
|
||||
with Serializable
|
||||
with PrettyPrinting
|
||||
with HasProtocolVersionedWrapper[Verdict] {
|
||||
|
||||
def isApprove: Boolean = false
|
||||
|
||||
/** Whether the verdict represents a timeout that the mediator has determined. */
|
||||
def isTimeoutDeterminedByMediator: Boolean
|
||||
|
||||
@ -57,6 +59,9 @@ object Verdict
|
||||
final case class Approve()(
|
||||
override val representativeProtocolVersion: RepresentativeProtocolVersion[Verdict.type]
|
||||
) extends Verdict {
|
||||
|
||||
override def isApprove: Boolean = true
|
||||
|
||||
override def isTimeoutDeterminedByMediator: Boolean = false
|
||||
|
||||
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]
|
||||
) extends Verdict
|
||||
with TransactionRejection {
|
||||
@ -80,10 +88,12 @@ object Verdict
|
||||
private[messages] override def toProtoV30: v30.Verdict =
|
||||
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(
|
||||
unnamedParam(_.reason)
|
||||
unnamedParam(_.reason),
|
||||
param("isMalformed", _.isMalformed),
|
||||
)
|
||||
|
||||
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)
|
||||
def tryCreate(
|
||||
status: com.google.rpc.status.Status,
|
||||
isMalformed: Boolean,
|
||||
protocolVersion: ProtocolVersion,
|
||||
): MediatorReject =
|
||||
MediatorReject(status)(Verdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
MediatorReject(status, isMalformed)(Verdict.protocolVersionRepresentativeFor(protocolVersion))
|
||||
|
||||
private[messages] def fromProtoV30(
|
||||
mediatorRejectP: v30.MediatorReject
|
||||
): ParsingResult[MediatorReject] = {
|
||||
val v30.MediatorReject(statusO) = mediatorRejectP
|
||||
val v30.MediatorReject(statusO, isMalformed) = mediatorRejectP
|
||||
for {
|
||||
status <- ProtoConverter.required("rejection_reason", statusO)
|
||||
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 = {
|
||||
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))
|
||||
}
|
||||
@ -150,7 +161,7 @@ object Verdict
|
||||
val message = show"Request was rejected with multiple reasons. $reasons"
|
||||
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
|
||||
@ -204,10 +215,15 @@ object Verdict
|
||||
private def fromProtoReasonV30(
|
||||
protoReason: v30.RejectionReason
|
||||
): ParsingResult[(Set[LfPartyId], LocalReject)] = {
|
||||
val v30.RejectionReason(partiesP, messageP) = protoReason
|
||||
val v30.RejectionReason(partiesP, rejectP) = protoReason
|
||||
for {
|
||||
parties <- partiesP.traverse(ProtoConverter.parseLfPartyId).map(_.toSet)
|
||||
message <- ProtoConverter.parseRequired(LocalReject.fromProtoV30, "reject", messageP)
|
||||
} yield (parties, message)
|
||||
localVerdict <- ProtoConverter.parseRequired(LocalVerdict.fromProtoV30, "reject", rejectP)
|
||||
localReject <- localVerdict match {
|
||||
case localReject: LocalReject => Right(localReject)
|
||||
case LocalApprove() =>
|
||||
Left(InvariantViolation("RejectionReason.reject must not be approve."))
|
||||
}
|
||||
} yield (parties, localReject)
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,6 @@ import com.digitalasset.canton.sequencing.protocol.{AllMembersOfDomain, OpenEnve
|
||||
*/
|
||||
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 DefaultOpenEnvelope = OpenEnvelope[ProtocolMessage]
|
||||
|
@ -169,12 +169,6 @@ class SequencerAggregator(
|
||||
val messagesToCombine = nonEmptyMessages.map(_._2).toList
|
||||
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
|
||||
.outcome(
|
||||
addEventToQueue(messagesToCombine).map(_ => sequencerIdToNotify)
|
||||
|
@ -758,6 +758,7 @@ class RichSequencerClientImpl(
|
||||
private val handlerIdle: AtomicReference[Promise[Unit]] = new AtomicReference(
|
||||
Promise.successful(())
|
||||
)
|
||||
private val handlerIdleLock: Object = new Object
|
||||
|
||||
override protected def subscribeAfterInternal(
|
||||
priorTimestamp: CantonTimestamp,
|
||||
@ -1024,10 +1025,6 @@ class RichSequencerClientImpl(
|
||||
.leftMap[SequencerClientSubscriptionError](EventAggregationError)
|
||||
} yield
|
||||
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)
|
||||
}).value
|
||||
}
|
||||
@ -1054,11 +1051,13 @@ class RichSequencerClientImpl(
|
||||
// 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
|
||||
// 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(
|
||||
eventHandler: OrdinaryApplicationHandler[ClosedEnvelope]
|
||||
)(implicit traceContext: TraceContext): Unit = performUnlessClosing(functionFullName) {
|
||||
val isIdle = blocking {
|
||||
synchronized {
|
||||
handlerIdleLock.synchronized {
|
||||
val oldPromise = handlerIdle.getAndUpdate(p => if (p.isCompleted) Promise() else p)
|
||||
oldPromise.isCompleted
|
||||
}
|
||||
@ -1074,22 +1073,12 @@ class RichSequencerClientImpl(
|
||||
): Future[Unit] = {
|
||||
val inboxSize = config.eventInboxSize.unwrap
|
||||
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) {
|
||||
import scala.jdk.CollectionConverters.*
|
||||
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 {
|
||||
this.synchronized { val _ = handlerIdle.get().success(()) }
|
||||
handlerIdleLock.synchronized { val _ = handlerIdle.get().success(()) }
|
||||
}
|
||||
|
||||
sendTracker
|
||||
@ -1104,7 +1093,7 @@ class RichSequencerClientImpl(
|
||||
}
|
||||
} else {
|
||||
val stillBusy = blocking {
|
||||
this.synchronized {
|
||||
handlerIdleLock.synchronized {
|
||||
val idlePromise = handlerIdle.get()
|
||||
if (sequencerAggregator.eventQueue.isEmpty) {
|
||||
// signalHandler must not be executed here, because that would lead to lost signals.
|
||||
@ -1118,10 +1107,6 @@ class RichSequencerClientImpl(
|
||||
if (stillBusy) {
|
||||
handleReceivedEventsUntilEmpty(eventHandler)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,18 @@
|
||||
|
||||
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.tracing.TraceContext
|
||||
|
||||
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 */
|
||||
trait DomainTrafficControlStateClient {
|
||||
|
@ -403,12 +403,19 @@ class StoreBasedTopologySnapshotX(
|
||||
.result
|
||||
.groupBy(_.mapping.member)
|
||||
.flatMap { case (member, mappings) =>
|
||||
collectLatestMapping(
|
||||
collectLatestTransaction(
|
||||
TopologyMappingX.Code.TrafficControlStateX,
|
||||
mappings.sortBy(_.validFrom),
|
||||
).map(mapping =>
|
||||
Some(MemberTrafficControlState(totalExtraTrafficLimit = mapping.totalExtraTrafficLimit))
|
||||
).map(member -> _)
|
||||
).map { tx =>
|
||||
val mapping = tx.mapping
|
||||
Some(
|
||||
MemberTrafficControlState(
|
||||
totalExtraTrafficLimit = mapping.totalExtraTrafficLimit,
|
||||
tx.serial,
|
||||
tx.validFrom.value,
|
||||
)
|
||||
)
|
||||
}.map(member -> _)
|
||||
}
|
||||
|
||||
val membersWithoutState = members.toSet.diff(membersWithState.keySet).map(_ -> None).toMap
|
||||
|
@ -160,8 +160,8 @@ object TopologyStoreId {
|
||||
override def isDomainStore: Boolean = false
|
||||
}
|
||||
|
||||
def apply(fName: String): TopologyStoreId = fName match {
|
||||
case "Authorized" => AuthorizedStore
|
||||
def apply(fName: String): TopologyStoreId = fName.toLowerCase match {
|
||||
case "authorized" => AuthorizedStore
|
||||
case domain => DomainStore(DomainId(UniqueIdentifier.tryFromProtoPrimitive(domain)))
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import cats.data.EitherT
|
||||
import cats.syntax.bifunctor.*
|
||||
import cats.syntax.parallel.*
|
||||
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.data.CantonTimestamp
|
||||
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
|
||||
@ -51,7 +51,7 @@ class TrafficBalanceSubmissionHandler(
|
||||
member: Member,
|
||||
domainId: DomainId,
|
||||
protocolVersion: ProtocolVersion,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
sequencerClient: SequencerClientSend,
|
||||
cryptoApi: DomainSyncCryptoClient,
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: CantonExamples
|
||||
|
@ -5,11 +5,12 @@ package com.digitalasset.canton.crypto.provider.jce
|
||||
|
||||
import cats.syntax.either.*
|
||||
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.*
|
||||
import com.digitalasset.canton.crypto.deterministic.encryption.DeterministicRandom
|
||||
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.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.serialization.{
|
||||
@ -33,6 +34,7 @@ import org.bouncycastle.crypto.DataLengthException
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||
import org.bouncycastle.crypto.generators.{Argon2BytesGenerator, HKDFBytesGenerator}
|
||||
import org.bouncycastle.crypto.params.{Argon2Parameters, HKDFParameters}
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
|
||||
import org.bouncycastle.jce.spec.IESParameterSpec
|
||||
import sun.security.ec.ECPrivateKeyImpl
|
||||
|
||||
@ -80,11 +82,14 @@ class JcePureCrypto(
|
||||
with ShowUtil
|
||||
with NamedLogging {
|
||||
|
||||
// TODO(#15632): Make these real caches with an eviction rule
|
||||
// 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
|
||||
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 keyConverter = new CryptoKeyConverter(tinkJavaConverter, javaKeyConverter)
|
||||
@ -119,19 +124,80 @@ class JcePureCrypto(
|
||||
val keySizeInBits = 2048
|
||||
}
|
||||
|
||||
private def checkKeyFormat[E](
|
||||
expected: CryptoKeyFormat,
|
||||
actual: CryptoKeyFormat,
|
||||
errFn: String => E,
|
||||
): Either[E, Unit] =
|
||||
Either.cond(expected == actual, (), errFn(s"Expected key format $expected, but got $actual"))
|
||||
|
||||
private def convertKey[E](
|
||||
/** Parses and converts a public key to a java public key.
|
||||
* We store the deserialization result in a cache.
|
||||
*
|
||||
* @return Either an error or the converted java private key
|
||||
*/
|
||||
private def parseAndGetPublicKey[E](
|
||||
publicKey: PublicKey,
|
||||
expected: CryptoKeyFormat,
|
||||
errFn: String => E,
|
||||
): Either[E, PublicKey] =
|
||||
keyConverter.convert(publicKey, expected).leftMap(errFn)
|
||||
): Either[E, JPublicKey] = {
|
||||
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(
|
||||
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(
|
||||
privateKey: PrivateKey,
|
||||
curveType: CurveType,
|
||||
): Either[JavaKeyConversionError, JPrivateKey] =
|
||||
for {
|
||||
_ <- ensureFormat(privateKey, CryptoKeyFormat.Der)
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
privateKey.format,
|
||||
Set(CryptoKeyFormat.Der),
|
||||
_ => JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, CryptoKeyFormat.Der),
|
||||
)
|
||||
ecPrivateKey <- Either
|
||||
.catchOnly[GeneralSecurityException](
|
||||
EllipticCurves.getEcPrivateKey(curveType, privateKey.key.toByteArray)
|
||||
@ -224,10 +284,10 @@ class JcePureCrypto(
|
||||
keyInstance: String,
|
||||
): Either[JavaKeyConversionError, JPrivateKey] =
|
||||
for {
|
||||
_ <- Either.cond(
|
||||
privateKey.format == format,
|
||||
(),
|
||||
JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, format),
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
privateKey.format,
|
||||
Set(format),
|
||||
_ => JavaKeyConversionError.UnsupportedKeyFormat(privateKey.format, format),
|
||||
)
|
||||
pkcs8KeySpec = new PKCS8EncodedKeySpec(pkcs8PrivateKey)
|
||||
keyFactory <- Either
|
||||
@ -268,22 +328,11 @@ class JcePureCrypto(
|
||||
hashType: HashType,
|
||||
): Either[SigningError, PublicKeySign] =
|
||||
for {
|
||||
_ <- checkKeyFormat(CryptoKeyFormat.Der, signingKey.format, SigningError.InvalidSigningKey)
|
||||
javaPrivateKey <- javaPrivateKeyCache
|
||||
.getOrElseUpdate(
|
||||
signingKey.id,
|
||||
toJava(signingKey),
|
||||
ecPrivateKey <- parseAndGetPrivateKey(
|
||||
signingKey,
|
||||
{ case k: ECPrivateKey => Right(k) },
|
||||
SigningError.InvalidSigningKey,
|
||||
)
|
||||
.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
|
||||
.catchOnly[GeneralSecurityException](
|
||||
new EcdsaSignJce(ecPrivateKey, hashType, EcdsaEncoding.DER)
|
||||
@ -296,22 +345,7 @@ class JcePureCrypto(
|
||||
hashType: HashType,
|
||||
): Either[SignatureCheckError, PublicKeyVerify] =
|
||||
for {
|
||||
pubKey <- convertKey(
|
||||
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")
|
||||
)
|
||||
|
||||
javaPublicKey <- parseAndGetPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
|
||||
ecPublicKey <- javaPublicKey match {
|
||||
case k: ECPublicKey => Right(k)
|
||||
case _ =>
|
||||
@ -373,9 +407,9 @@ class JcePureCrypto(
|
||||
signingKey.scheme match {
|
||||
case SigningKeyScheme.Ed25519 =>
|
||||
for {
|
||||
_ <- checkKeyFormat(
|
||||
CryptoKeyFormat.Raw,
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
signingKey.format,
|
||||
Set(CryptoKeyFormat.Raw),
|
||||
SigningError.InvalidSigningKey,
|
||||
)
|
||||
signer <- Either
|
||||
@ -421,13 +455,17 @@ class JcePureCrypto(
|
||||
_ <- publicKey.scheme match {
|
||||
case SigningKeyScheme.Ed25519 =>
|
||||
for {
|
||||
pubKey <- convertKey(
|
||||
javaPublicKey <- parseAndGetPublicKey(
|
||||
publicKey,
|
||||
CryptoKeyFormat.Raw,
|
||||
SignatureCheckError.InvalidKeyError,
|
||||
)
|
||||
ed25519PublicKey <- javaPublicKey match {
|
||||
case k: BCEdDSAPublicKey =>
|
||||
Right(k.getPointEncoding)
|
||||
case _ => Left(SignatureCheckError.InvalidKeyError("Not an Ed25519 public key"))
|
||||
}
|
||||
verifier <- Either
|
||||
.catchOnly[GeneralSecurityException](new Ed25519Verify(pubKey.key.toByteArray))
|
||||
.catchOnly[GeneralSecurityException](new Ed25519Verify(ed25519PublicKey))
|
||||
.leftMap(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](
|
||||
message: ByteString,
|
||||
publicKey: EncryptionPublicKey,
|
||||
random: SecureRandom,
|
||||
): Either[EncryptionError, AsymmetricEncrypted[M]] =
|
||||
for {
|
||||
ecPublicKey <- checkDerFormatAndGetPublicKey[ECPublicKey](
|
||||
publicKey,
|
||||
{ case k: ECPublicKey =>
|
||||
javaPublicKey <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
|
||||
ecPublicKey <- javaPublicKey match {
|
||||
case k: ECPublicKey =>
|
||||
checkEcKeyInCurve(k, publicKey.id).leftMap(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)
|
||||
* 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
|
||||
.catchOnly[GeneralSecurityException] {
|
||||
val cipher = Cipher
|
||||
.getInstance(EciesP256HmacSha256Aes128CbcParams.jceInternalName, bouncyCastleProvider)
|
||||
.getInstance(
|
||||
EciesP256HmacSha256Aes128CbcParams.jceInternalName,
|
||||
JceSecurityProvider.bouncyCastleProvider,
|
||||
)
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
ecPublicKey,
|
||||
@ -533,12 +552,12 @@ class JcePureCrypto(
|
||||
random: SecureRandom,
|
||||
): Either[EncryptionError, AsymmetricEncrypted[M]] =
|
||||
for {
|
||||
rsaPublicKey <- checkDerFormatAndGetPublicKey[RSAPublicKey](
|
||||
publicKey,
|
||||
{ case k: RSAPublicKey =>
|
||||
javaPublicKey <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
|
||||
rsaPublicKey <- javaPublicKey match {
|
||||
case k: RSAPublicKey =>
|
||||
checkRsaKeySize(k, publicKey.id).leftMap(err => EncryptionError.InvalidEncryptionKey(err))
|
||||
},
|
||||
)
|
||||
case _ => Left(EncryptionError.InvalidEncryptionKey("Not a RSA public key"))
|
||||
}
|
||||
encrypter <- Either
|
||||
.catchOnly[GeneralSecurityException] {
|
||||
val cipher = Cipher
|
||||
@ -565,14 +584,17 @@ class JcePureCrypto(
|
||||
publicKey.scheme match {
|
||||
case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm =>
|
||||
for {
|
||||
ecPublicKey <- checkDerFormatAndGetPublicKey[ECPublicKey](
|
||||
javaPublicKey <- parseAndGetPublicKey(
|
||||
publicKey,
|
||||
{ case k: ECPublicKey =>
|
||||
EncryptionError.InvalidEncryptionKey,
|
||||
)
|
||||
ecPublicKey <- javaPublicKey match {
|
||||
case k: ECPublicKey =>
|
||||
checkEcKeyInCurve(k, publicKey.id).leftMap(err =>
|
||||
EncryptionError.InvalidEncryptionKey(err)
|
||||
)
|
||||
},
|
||||
)
|
||||
case _ => Left(EncryptionError.InvalidEncryptionKey("Not an EC public key"))
|
||||
}
|
||||
encrypter <- Either
|
||||
.catchOnly[GeneralSecurityException](
|
||||
new EciesAeadHkdfHybridEncrypt(
|
||||
@ -660,33 +682,17 @@ class JcePureCrypto(
|
||||
deserialize: ByteString => Either[DeserializationError, 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 {
|
||||
case EncryptionKeyScheme.EciesP256HkdfHmacSha256Aes128Gcm =>
|
||||
for {
|
||||
ecPrivateKey <- checkDerFormatAndGetPrivateKey(
|
||||
ecPrivateKey <- parseAndGetPrivateKey(
|
||||
privateKey,
|
||||
{ case k: ECPrivateKey =>
|
||||
checkEcKeyInCurve(k, privateKey.id).leftMap(err =>
|
||||
DecryptionError.InvalidEncryptionKey(err)
|
||||
)
|
||||
}
|
||||
},
|
||||
DecryptionError.InvalidEncryptionKey,
|
||||
)
|
||||
decrypter <- Either
|
||||
.catchOnly[GeneralSecurityException](
|
||||
@ -709,12 +715,14 @@ class JcePureCrypto(
|
||||
} yield message
|
||||
case EncryptionKeyScheme.EciesP256HmacSha256Aes128Cbc =>
|
||||
for {
|
||||
ecPrivateKey <- checkDerFormatAndGetPrivateKey[ECPrivateKey](
|
||||
ecPrivateKey <- parseAndGetPrivateKey(
|
||||
privateKey,
|
||||
{ case k: ECPrivateKey =>
|
||||
checkEcKeyInCurve(k, privateKey.id).leftMap(err =>
|
||||
DecryptionError.InvalidEncryptionKey(err)
|
||||
)
|
||||
}
|
||||
},
|
||||
DecryptionError.InvalidEncryptionKey,
|
||||
)
|
||||
/* we split at 'ivSizeForAesCbc' (=16) because that is the size of our iv (for AES-128-CBC)
|
||||
* that gets pre-appended to the ciphertext.
|
||||
@ -733,7 +741,7 @@ class JcePureCrypto(
|
||||
val cipher = Cipher
|
||||
.getInstance(
|
||||
EciesP256HmacSha256Aes128CbcParams.jceInternalName,
|
||||
bouncyCastleProvider,
|
||||
JceSecurityProvider.bouncyCastleProvider,
|
||||
)
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
@ -753,12 +761,14 @@ class JcePureCrypto(
|
||||
} yield message
|
||||
case EncryptionKeyScheme.Rsa2048OaepSha256 =>
|
||||
for {
|
||||
rsaPrivateKey <- checkDerFormatAndGetPrivateKey[RSAPrivateKey](
|
||||
rsaPrivateKey <- parseAndGetPrivateKey(
|
||||
privateKey,
|
||||
{ case k: RSAPrivateKey =>
|
||||
checkRsaKeySize(k, privateKey.id).leftMap(err =>
|
||||
DecryptionError.InvalidEncryptionKey(err)
|
||||
)
|
||||
}
|
||||
},
|
||||
DecryptionError.InvalidEncryptionKey,
|
||||
)
|
||||
decrypter <- Either
|
||||
.catchOnly[GeneralSecurityException] {
|
||||
@ -799,9 +809,9 @@ class JcePureCrypto(
|
||||
symmetricKey.scheme match {
|
||||
case SymmetricKeyScheme.Aes128Gcm =>
|
||||
for {
|
||||
_ <- checkKeyFormat(
|
||||
CryptoKeyFormat.Raw,
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
symmetricKey.format,
|
||||
Set(CryptoKeyFormat.Raw),
|
||||
EncryptionError.InvalidSymmetricKey,
|
||||
)
|
||||
encryptedBytes <- encryptAes128Gcm(
|
||||
@ -818,9 +828,9 @@ class JcePureCrypto(
|
||||
symmetricKey.scheme match {
|
||||
case SymmetricKeyScheme.Aes128Gcm =>
|
||||
for {
|
||||
_ <- checkKeyFormat(
|
||||
CryptoKeyFormat.Raw,
|
||||
_ <- CryptoKeyValidation.ensureFormat(
|
||||
symmetricKey.format,
|
||||
Set(CryptoKeyFormat.Raw),
|
||||
DecryptionError.InvalidSymmetricKey,
|
||||
)
|
||||
plaintext <- decryptAes128Gcm(encrypted.ciphertext, symmetricKey.key)
|
||||
|
@ -5,6 +5,8 @@ package com.digitalasset.canton.crypto.provider.tink
|
||||
|
||||
import cats.syntax.either.*
|
||||
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.*
|
||||
import com.digitalasset.canton.crypto.provider.CryptoKeyConverter
|
||||
@ -30,13 +32,95 @@ class TinkPureCrypto private (
|
||||
override val defaultHashAlgorithm: HashAlgorithm,
|
||||
) 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.
|
||||
private val publicKeysetCache: TrieMap[Fingerprint, Either[DeserializationError, KeysetHandle]] =
|
||||
private val publicKeysetCache
|
||||
: TrieMap[Fingerprint, Either[KeyParseAndValidateError, KeysetHandle]] =
|
||||
TrieMap.empty
|
||||
private val privateKeysetCache: TrieMap[Fingerprint, Either[DeserializationError, KeysetHandle]] =
|
||||
private val privateKeysetCache
|
||||
: TrieMap[Fingerprint, Either[KeyParseAndValidateError, KeysetHandle]] =
|
||||
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](
|
||||
message: M,
|
||||
encrypt: Array[Byte] => Array[Byte],
|
||||
@ -80,40 +164,6 @@ class TinkPureCrypto private (
|
||||
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 */
|
||||
override def generateSymmetricKey(
|
||||
scheme: SymmetricKeyScheme = defaultSymmetricKeyScheme
|
||||
@ -184,7 +234,10 @@ class TinkPureCrypto private (
|
||||
version: ProtocolVersion,
|
||||
): Either[EncryptionError, Encrypted[M]] =
|
||||
for {
|
||||
keysetHandle <- keysetNonCached(symmetricKey, EncryptionError.InvalidSymmetricKey)
|
||||
keysetHandle <- parseAndGetPrivateKey(
|
||||
symmetricKey,
|
||||
EncryptionError.InvalidSymmetricKey,
|
||||
)
|
||||
aead <- getPrimitive[tink.Aead, EncryptionError](
|
||||
keysetHandle,
|
||||
EncryptionError.InvalidSymmetricKey,
|
||||
@ -197,7 +250,10 @@ class TinkPureCrypto private (
|
||||
deserialize: ByteString => Either[DeserializationError, M]
|
||||
): Either[DecryptionError, M] =
|
||||
for {
|
||||
keysetHandle <- keysetNonCached(symmetricKey, DecryptionError.InvalidSymmetricKey)
|
||||
keysetHandle <- parseAndGetPrivateKey(
|
||||
symmetricKey,
|
||||
DecryptionError.InvalidSymmetricKey,
|
||||
)
|
||||
aead <- getPrimitive[tink.Aead, DecryptionError](
|
||||
keysetHandle,
|
||||
DecryptionError.InvalidSymmetricKey,
|
||||
@ -212,8 +268,8 @@ class TinkPureCrypto private (
|
||||
version: ProtocolVersion,
|
||||
): Either[EncryptionError, AsymmetricEncrypted[M]] =
|
||||
for {
|
||||
tinkPublicKey <- convertPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
|
||||
keysetHandle <- keysetCached(tinkPublicKey, EncryptionError.InvalidEncryptionKey)
|
||||
keysetHandle <- parseAndGetPublicKey(publicKey, EncryptionError.InvalidEncryptionKey)
|
||||
.leftMap(err => EncryptionError.InvalidEncryptionKey(err.show))
|
||||
hybrid <- getPrimitive[tink.HybridEncrypt, EncryptionError](
|
||||
keysetHandle,
|
||||
EncryptionError.InvalidEncryptionKey,
|
||||
@ -228,7 +284,10 @@ class TinkPureCrypto private (
|
||||
deserialize: ByteString => Either[DeserializationError, M]
|
||||
): Either[DecryptionError, M] =
|
||||
for {
|
||||
keysetHandle <- keysetCached(privateKey, DecryptionError.InvalidEncryptionKey)
|
||||
keysetHandle <- parseAndGetPrivateKey(
|
||||
privateKey,
|
||||
DecryptionError.InvalidEncryptionKey,
|
||||
)
|
||||
hybrid <- getPrimitive[tink.HybridDecrypt, DecryptionError](
|
||||
keysetHandle,
|
||||
DecryptionError.InvalidEncryptionKey,
|
||||
@ -241,7 +300,10 @@ class TinkPureCrypto private (
|
||||
signingKey: SigningPrivateKey,
|
||||
): Either[SigningError, Signature] =
|
||||
for {
|
||||
keysetHandle <- keysetCached(signingKey, SigningError.InvalidSigningKey)
|
||||
keysetHandle <- parseAndGetPrivateKey(
|
||||
signingKey,
|
||||
SigningError.InvalidSigningKey,
|
||||
)
|
||||
verify <- getPrimitive[tink.PublicKeySign, SigningError](
|
||||
keysetHandle,
|
||||
SigningError.InvalidSigningKey,
|
||||
@ -265,8 +327,6 @@ class TinkPureCrypto private (
|
||||
signature: Signature,
|
||||
): Either[SignatureCheckError, Unit] =
|
||||
for {
|
||||
tinkPublicKey <- convertPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
|
||||
keysetHandle <- keysetCached(tinkPublicKey, SignatureCheckError.InvalidKeyError)
|
||||
_ <- Either.cond(
|
||||
signature.signedBy == publicKey.id,
|
||||
(),
|
||||
@ -274,6 +334,7 @@ class TinkPureCrypto private (
|
||||
s"Signature signed by ${signature.signedBy} instead of ${publicKey.id}"
|
||||
),
|
||||
)
|
||||
keysetHandle <- parseAndGetPublicKey(publicKey, SignatureCheckError.InvalidKeyError)
|
||||
verify <- getPrimitive[tink.PublicKeyVerify, SignatureCheckError](
|
||||
keysetHandle,
|
||||
SignatureCheckError.InvalidKeyError,
|
||||
@ -331,7 +392,7 @@ class TinkPureCrypto private (
|
||||
hmacWithSecret(prk, last.concat(info.bytes).concat(chunkByte), algorithm)
|
||||
.bimap(HkdfHmacError, hmac => out.concat(hmac.unwrap) -> hmac.unwrap)
|
||||
}
|
||||
(out, _last) = outputAndLast
|
||||
(out, _) = outputAndLast
|
||||
} yield SecureRandomness(out.substring(0, outputBytes))
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,15 @@ package com.digitalasset.canton.sequencing.handlers
|
||||
|
||||
import cats.instances.either.*
|
||||
import cats.syntax.either.*
|
||||
import com.daml.error.{ContextualizedErrorLogger, Explanation, Resolution}
|
||||
import com.digitalasset.canton.ProtoDeserializationError
|
||||
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.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.{ApplicationHandler, EnvelopeBox}
|
||||
import com.digitalasset.canton.sequencing.{ApplicationHandler, EnvelopeBox, HandlerResult}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
|
||||
@ -33,13 +36,32 @@ class EnvelopeOpener[Box[+_ <: Envelope[_]]](protocolVersion: ProtocolVersion, h
|
||||
object EnvelopeOpener {
|
||||
|
||||
/** 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]
|
||||
)(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)
|
||||
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"))
|
||||
final case class EventDeserializationError(
|
||||
@ -50,4 +72,21 @@ object EnvelopeOpener {
|
||||
s"Failed to deserialize event with protocol version $protocolVersion: $error",
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ package com.digitalasset.canton.traffic
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.ProtoDeserializationError
|
||||
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.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.sequencing.protocol.SequencedEventTrafficState
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.topology.Member
|
||||
@ -15,17 +17,20 @@ final case class MemberTrafficStatus(
|
||||
member: Member,
|
||||
timestamp: CantonTimestamp,
|
||||
trafficState: SequencedEventTrafficState,
|
||||
currentAndFutureTopUps: List[TopUpEvent],
|
||||
) {
|
||||
balanceSerial: Option[PositiveInt],
|
||||
) extends Product
|
||||
with PrettyPrinting {
|
||||
def toProtoV30: MemberTrafficStatusP = {
|
||||
MemberTrafficStatusP(
|
||||
member.toProtoPrimitive,
|
||||
trafficState.extraTrafficLimit.map(_.value),
|
||||
trafficState.extraTrafficConsumed.value,
|
||||
currentAndFutureTopUps.map(_.toProtoV30),
|
||||
List.empty,
|
||||
Some(timestamp.toProtoTimestamp),
|
||||
balanceSerial.map(_.value),
|
||||
)
|
||||
}
|
||||
override def pretty: Pretty[this.type] = adHocPrettyInstance
|
||||
}
|
||||
|
||||
object MemberTrafficStatus {
|
||||
@ -40,13 +45,15 @@ object MemberTrafficStatus {
|
||||
totalExtraTrafficLimitOpt <- trafficStatusP.totalExtraTrafficLimit.traverse(
|
||||
ProtoConverter.parseNonNegativeLong
|
||||
)
|
||||
balanceSerialOpt <- trafficStatusP.balanceSerial.traverse(
|
||||
ProtoConverter.parsePositiveInt
|
||||
)
|
||||
totalExtraTrafficConsumed <- ProtoConverter.parseNonNegativeLong(
|
||||
trafficStatusP.totalExtraTrafficConsumed
|
||||
)
|
||||
totalExtraTrafficRemainder <- ProtoConverter.parseNonNegativeLong(
|
||||
totalExtraTrafficLimitOpt.map(_.value - totalExtraTrafficConsumed.value).getOrElse(0L)
|
||||
)
|
||||
topUps <- trafficStatusP.topUpEvents.toList.traverse(TopUpEvent.fromProtoV30)
|
||||
ts <- ProtoConverter.parseRequired(
|
||||
CantonTimestamp.fromProtoTimestamp,
|
||||
"ts",
|
||||
@ -59,7 +66,7 @@ object MemberTrafficStatus {
|
||||
totalExtraTrafficRemainder,
|
||||
totalExtraTrafficConsumed,
|
||||
),
|
||||
topUps,
|
||||
balanceSerialOpt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -76,16 +76,18 @@ object GeneratorsCrypto {
|
||||
private lazy val privateCrypto = cryptoFactory.crypto.privateCrypto
|
||||
private lazy val pureCryptoApi: CryptoPureApi = cryptoFactory.pureCrypto
|
||||
|
||||
// TODO(#15813): Change arbitrary signing keys to match real keys
|
||||
implicit val signingPublicKeyArb: Arbitrary[SigningPublicKey] = Arbitrary(for {
|
||||
id <- Arbitrary.arbitrary[Fingerprint]
|
||||
format <- Arbitrary.arbitrary[CryptoKeyFormat]
|
||||
format = CryptoKeyFormat.Symbolic
|
||||
key <- Arbitrary.arbitrary[ByteString]
|
||||
scheme <- Arbitrary.arbitrary[SigningKeyScheme]
|
||||
} yield new SigningPublicKey(id, format, key, scheme))
|
||||
|
||||
// TODO(#15813): Change arbitrary encryption keys to match real keys
|
||||
implicit val encryptionPublicKeyArb: Arbitrary[EncryptionPublicKey] = Arbitrary(for {
|
||||
id <- Arbitrary.arbitrary[Fingerprint]
|
||||
format <- Arbitrary.arbitrary[CryptoKeyFormat]
|
||||
format = CryptoKeyFormat.Symbolic
|
||||
key <- Arbitrary.arbitrary[ByteString]
|
||||
scheme <- Arbitrary.arbitrary[EncryptionKeyScheme]
|
||||
} yield new EncryptionPublicKey(id, format, key, scheme))
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -24,11 +24,12 @@ class JceCryptoTest
|
||||
with HkdfTest
|
||||
with PasswordBasedEncryptionTest
|
||||
with RandomTest
|
||||
with JavaPublicKeyConverterTest {
|
||||
with JavaPublicKeyConverterTest
|
||||
with PublicKeyValidationTest {
|
||||
|
||||
"JceCrypto" can {
|
||||
|
||||
def jceCrypto(): Future[Crypto] = {
|
||||
def jceCrypto(): Future[Crypto] =
|
||||
new CommunityCryptoFactory()
|
||||
.create(
|
||||
CommunityCryptoConfig(provider = Jce),
|
||||
@ -40,7 +41,6 @@ class JceCryptoTest
|
||||
NoReportingTracerProvider,
|
||||
)
|
||||
.valueOr(err => throw new RuntimeException(s"failed to create crypto: $err"))
|
||||
}
|
||||
|
||||
behave like signingProvider(Jce.signing.supported, jceCrypto())
|
||||
behave like encryptionProvider(
|
||||
@ -111,5 +111,12 @@ class JceCryptoTest
|
||||
Jce.symmetric.supported,
|
||||
jceCrypto().map(_.pureCrypto),
|
||||
)
|
||||
|
||||
behave like publicKeyValidationProvider(
|
||||
Jce.signing.supported,
|
||||
Jce.encryption.supported,
|
||||
Jce.supportedCryptoKeyFormats,
|
||||
jceCrypto(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ class SymbolicCryptoTest
|
||||
|
||||
// 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 public key validation, thus not tested
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,11 +21,12 @@ class TinkCryptoTest
|
||||
with PrivateKeySerializationTest
|
||||
with HkdfTest
|
||||
with RandomTest
|
||||
with JavaPublicKeyConverterTest {
|
||||
with JavaPublicKeyConverterTest
|
||||
with PublicKeyValidationTest {
|
||||
|
||||
"TinkCrypto" can {
|
||||
|
||||
def tinkCrypto(): Future[Crypto] = {
|
||||
def tinkCrypto(): Future[Crypto] =
|
||||
new CommunityCryptoFactory()
|
||||
.create(
|
||||
CommunityCryptoConfig(provider = Tink),
|
||||
@ -37,7 +38,6 @@ class TinkCryptoTest
|
||||
NoReportingTracerProvider,
|
||||
)
|
||||
.valueOrFail("create crypto")
|
||||
}
|
||||
|
||||
behave like signingProvider(Tink.signing.supported, tinkCrypto())
|
||||
behave like encryptionProvider(
|
||||
@ -67,6 +67,14 @@ class TinkCryptoTest
|
||||
"JCE",
|
||||
new JceJavaConverter(Jce.signing.supported, Jce.encryption.supported),
|
||||
)
|
||||
|
||||
behave like publicKeyValidationProvider(
|
||||
Tink.signing.supported,
|
||||
Tink.encryption.supported,
|
||||
Tink.supportedCryptoKeyFormats,
|
||||
tinkCrypto(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
@ -4,14 +4,12 @@
|
||||
package com.digitalasset.canton.data
|
||||
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
|
||||
import com.digitalasset.canton.crypto.{GeneratorsCrypto, Salt, TestHash}
|
||||
import com.digitalasset.canton.protocol.*
|
||||
import com.digitalasset.canton.protocol.messages.{
|
||||
ConfirmationResultMessage,
|
||||
DeliveredTransferOutResult,
|
||||
SetTrafficBalanceMessage,
|
||||
SignedProtocolMessage,
|
||||
TransferResult,
|
||||
Verdict,
|
||||
}
|
||||
import com.digitalasset.canton.sequencing.protocol.{
|
||||
@ -21,7 +19,7 @@ import com.digitalasset.canton.sequencing.protocol.{
|
||||
SignedContent,
|
||||
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.Transfer.{SourceProtocolVersion, TargetProtocolVersion}
|
||||
import magnolify.scalacheck.auto.*
|
||||
@ -37,7 +35,6 @@ final class GeneratorsTransferData(
|
||||
import com.digitalasset.canton.Generators.*
|
||||
import com.digitalasset.canton.GeneratorsLf.*
|
||||
import com.digitalasset.canton.crypto.GeneratorsCrypto.*
|
||||
import com.digitalasset.canton.config.GeneratorsConfig.*
|
||||
import com.digitalasset.canton.data.GeneratorsDataTime.*
|
||||
import com.digitalasset.canton.topology.GeneratorsTopology.*
|
||||
import org.scalatest.EitherValues.*
|
||||
@ -136,14 +133,18 @@ final class GeneratorsTransferData(
|
||||
sourceDomain <- Arbitrary.arbitrary[SourceDomainId]
|
||||
|
||||
requestId <- Arbitrary.arbitrary[RequestId]
|
||||
rootHash <- Arbitrary.arbitrary[RootHash]
|
||||
protocolVersion = sourceProtocolVersion.v
|
||||
|
||||
verdict = Verdict.Approve(protocolVersion)
|
||||
result = TransferResult.create(
|
||||
|
||||
result = ConfirmationResultMessage.create(
|
||||
sourceDomain.id,
|
||||
ViewType.TransferOutViewType,
|
||||
requestId,
|
||||
contract.metadata.stakeholders,
|
||||
sourceDomain,
|
||||
Some(rootHash),
|
||||
verdict,
|
||||
contract.metadata.stakeholders,
|
||||
protocolVersion,
|
||||
)
|
||||
|
||||
@ -220,20 +221,4 @@ final class GeneratorsTransferData(
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -31,7 +31,9 @@ class ConfirmationResponseTest extends AnyWordSpec with BaseTest with HasCryptog
|
||||
RequestId(CantonTimestamp.now()),
|
||||
topology.ParticipantId(UniqueIdentifier.tryFromProtoPrimitive("da::p1")),
|
||||
None,
|
||||
LocalRejectError.MalformedRejects.Payloads.Reject("test message")(localVerdictProtocolVersion),
|
||||
LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject("test message")
|
||||
.toLocalReject(testedProtocolVersion),
|
||||
Some(RootHash(TestHash.digest("txid3"))),
|
||||
Set.empty,
|
||||
DomainId(UniqueIdentifier.tryFromProtoPrimitive("da::default")),
|
||||
|
@ -11,6 +11,7 @@ import com.digitalasset.canton.protocol.LocalRejectError.ConsistencyRejections.{
|
||||
import com.digitalasset.canton.protocol.LocalRejectError.MalformedRejects.{
|
||||
BadRootHashMessages,
|
||||
CreatesExistingContracts,
|
||||
MalformedRequest,
|
||||
ModelConformance,
|
||||
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.{LocalRejectErrorImpl, Malformed}
|
||||
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion}
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import org.scalacheck.{Arbitrary, Gen}
|
||||
|
||||
final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) {
|
||||
@ -35,12 +36,11 @@ final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) {
|
||||
import com.digitalasset.canton.GeneratorsLf.lfPartyIdArb
|
||||
|
||||
// TODO(#14515) Check that the generator is exhaustive
|
||||
private def localRejectErrorImplGen: Gen[LocalRejectErrorImpl] = {
|
||||
private def localVerdictRejectGen: Gen[LocalReject] = {
|
||||
val resources = List("resource1", "resource2")
|
||||
val details = "details"
|
||||
|
||||
val builders: Seq[RepresentativeProtocolVersion[LocalVerdict.type] => LocalRejectErrorImpl] =
|
||||
Seq(
|
||||
val builders = Seq[LocalRejectErrorImpl](
|
||||
LockedContracts.Reject(resources),
|
||||
InactiveContracts.Reject(resources),
|
||||
LedgerTime.Reject(details),
|
||||
@ -51,40 +51,34 @@ final case class GeneratorsLocalVerdict(protocolVersion: ProtocolVersion) {
|
||||
ContractAlreadyActive.Reject(details),
|
||||
ContractIsLocked.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
|
||||
private def localVerdictMalformedGen: Gen[Malformed] = {
|
||||
private def localVerdictMalformedGen: Gen[LocalReject] = {
|
||||
val resources = List("resource1", "resource2")
|
||||
val details = "details"
|
||||
|
||||
val builders: Seq[RepresentativeProtocolVersion[LocalVerdict.type] => Malformed] = Seq(
|
||||
/*
|
||||
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),
|
||||
val builders = Seq[Malformed](
|
||||
MalformedRequest.Reject(details),
|
||||
Payloads.Reject(details),
|
||||
ModelConformance.Reject(details),
|
||||
BadRootHashMessages.Reject(details),
|
||||
CreatesExistingContracts.Reject(resources),
|
||||
)
|
||||
|
||||
Gen.oneOf(builders).map(_(LocalVerdict.protocolVersionRepresentativeFor(protocolVersion)))
|
||||
Gen
|
||||
.oneOf(builders)
|
||||
.map(_.toLocalReject(protocolVersion))
|
||||
}
|
||||
|
||||
// TODO(#14515) Check that the generator is exhaustive
|
||||
private def localRejectGen: Gen[LocalReject] =
|
||||
Gen.oneOf(localRejectErrorImplGen, localVerdictMalformedGen)
|
||||
Gen.oneOf(localVerdictRejectGen, localVerdictMalformedGen)
|
||||
|
||||
private def localApproveGen: Gen[LocalApprove] =
|
||||
Gen.const(LocalApprove(protocolVersion))
|
||||
|
@ -6,13 +6,7 @@ package com.digitalasset.canton.protocol.messages
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.crypto.Signature
|
||||
import com.digitalasset.canton.data.{CantonTimestampSecond, GeneratorsData, ViewPosition, ViewType}
|
||||
import com.digitalasset.canton.protocol.{
|
||||
GeneratorsProtocol,
|
||||
Malformed,
|
||||
RequestId,
|
||||
RootHash,
|
||||
TransferDomainId,
|
||||
}
|
||||
import com.digitalasset.canton.protocol.{GeneratorsProtocol, RequestId, RootHash}
|
||||
import com.digitalasset.canton.time.PositiveSeconds
|
||||
import com.digitalasset.canton.topology.{DomainId, ParticipantId}
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
@ -66,39 +60,27 @@ final class GeneratorsMessages(
|
||||
)
|
||||
)
|
||||
|
||||
implicit val transferResultArb: Arbitrary[TransferResult[TransferDomainId]] = Arbitrary(for {
|
||||
requestId <- Arbitrary.arbitrary[RequestId]
|
||||
informees <- Gen.containerOf[Set, LfPartyId](Arbitrary.arbitrary[LfPartyId])
|
||||
domain <- Arbitrary.arbitrary[TransferDomainId]
|
||||
verdict <- verdictArb.arbitrary
|
||||
} yield TransferResult.create(requestId, informees, domain, verdict, protocolVersion))
|
||||
|
||||
implicit val MalformedMediatorConfirmationRequestResultArb
|
||||
: Arbitrary[MalformedConfirmationRequestResult] =
|
||||
Arbitrary(
|
||||
implicit val confirmationResultMessageArb: Arbitrary[ConfirmationResultMessage] = Arbitrary(
|
||||
for {
|
||||
requestId <- Arbitrary.arbitrary[RequestId]
|
||||
domainId <- Arbitrary.arbitrary[DomainId]
|
||||
viewType <- Arbitrary.arbitrary[ViewType]
|
||||
mediatorReject <- mediatorRejectArb.arbitrary
|
||||
} yield MalformedConfirmationRequestResult.tryCreate(
|
||||
requestId,
|
||||
requestId <- Arbitrary.arbitrary[RequestId]
|
||||
rootHash <- Arbitrary.arbitrary[RootHash]
|
||||
verdict <- verdictArb.arbitrary
|
||||
informees <- Arbitrary.arbitrary[Set[LfPartyId]]
|
||||
|
||||
// TODO(#14241) Also generate instance that makes pv above cover all the values
|
||||
} yield ConfirmationResultMessage.create(
|
||||
domainId,
|
||||
viewType,
|
||||
mediatorReject,
|
||||
requestId,
|
||||
Some(rootHash),
|
||||
verdict,
|
||||
informees,
|
||||
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(
|
||||
for {
|
||||
requestId <- Arbitrary.arbitrary[RequestId]
|
||||
@ -107,18 +89,11 @@ final class GeneratorsMessages(
|
||||
|
||||
domainId <- Arbitrary.arbitrary[DomainId]
|
||||
|
||||
confirmingParties <- localVerdict match {
|
||||
case _: Malformed =>
|
||||
Gen.const(Set.empty[LfPartyId])
|
||||
case _: LocalApprove | _: LocalReject =>
|
||||
nonEmptySet(implicitly[Arbitrary[LfPartyId]]).arbitrary.map(_.forgetNE)
|
||||
case _ => Gen.containerOf[Set, LfPartyId](Arbitrary.arbitrary[LfPartyId])
|
||||
}
|
||||
confirmingParties <-
|
||||
if (localVerdict.isMalformed) Gen.const(Set.empty[LfPartyId])
|
||||
else nonEmptySet(implicitly[Arbitrary[LfPartyId]]).arbitrary.map(_.forgetNE)
|
||||
|
||||
rootHash <- localVerdict match {
|
||||
case _: LocalApprove | _: LocalReject => Gen.some(Arbitrary.arbitrary[RootHash])
|
||||
case _ => Gen.option(Arbitrary.arbitrary[RootHash])
|
||||
}
|
||||
rootHash <- Arbitrary.arbitrary[RootHash]
|
||||
|
||||
viewPositionO <- localVerdict match {
|
||||
case _: LocalApprove | _: LocalReject =>
|
||||
@ -131,29 +106,19 @@ final class GeneratorsMessages(
|
||||
sender,
|
||||
viewPositionO,
|
||||
localVerdict,
|
||||
rootHash,
|
||||
Some(rootHash),
|
||||
confirmingParties,
|
||||
domainId,
|
||||
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
|
||||
implicit val signedProtocolMessageContentArb: Arbitrary[SignedProtocolMessageContent] = Arbitrary(
|
||||
Gen.oneOf(
|
||||
Gen.oneOf[SignedProtocolMessageContent](
|
||||
Arbitrary.arbitrary[AcsCommitment],
|
||||
Arbitrary.arbitrary[MalformedConfirmationRequestResult],
|
||||
Arbitrary.arbitrary[ConfirmationResponse],
|
||||
Arbitrary.arbitrary[ConfirmationResult],
|
||||
Arbitrary.arbitrary[ConfirmationResultMessage],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,6 @@ package com.digitalasset.canton.protocol.messages
|
||||
import com.digitalasset.canton.Generators.nonEmptyListGen
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import magnolify.scalacheck.auto.*
|
||||
import org.scalacheck.{Arbitrary, Gen}
|
||||
|
||||
final case class GeneratorsVerdict(
|
||||
@ -15,17 +14,13 @@ final case class GeneratorsVerdict(
|
||||
) {
|
||||
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
|
||||
implicit val mediatorRejectArb: Arbitrary[Verdict.MediatorReject] =
|
||||
Arbitrary(
|
||||
// TODO(#14515): do we want randomness here?
|
||||
Gen.const {
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -3,11 +3,11 @@
|
||||
|
||||
package com.digitalasset.canton.sequencing.protocol
|
||||
|
||||
import com.digitalasset.canton.crypto.CryptoPureApi
|
||||
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.{RequestId, TargetDomainId}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.topology.DefaultTestIdentities
|
||||
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
|
||||
val message =
|
||||
SignedProtocolMessage.from(
|
||||
TransferResult.create(
|
||||
ConfirmationResultMessage.create(
|
||||
domainId,
|
||||
ViewType.TransferOutViewType,
|
||||
RequestId(CantonTimestamp.now()),
|
||||
Set.empty,
|
||||
TargetDomainId(domainId),
|
||||
Some(TestHash.dummyRootHash),
|
||||
Verdict.Approve(testedProtocolVersion),
|
||||
Set.empty,
|
||||
testedProtocolVersion,
|
||||
),
|
||||
testedProtocolVersion,
|
||||
|
@ -5,7 +5,7 @@ package com.digitalasset.canton.traffic
|
||||
|
||||
import cats.data.EitherT
|
||||
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.lifecycle.UnlessShutdown
|
||||
import com.digitalasset.canton.protocol.messages.{
|
||||
@ -100,7 +100,7 @@ class TrafficBalanceSubmissionHandlerTest
|
||||
recipient1,
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
NonNegativeLong.tryCreate(5),
|
||||
PositiveInt.tryCreate(5),
|
||||
NonNegativeLong.tryCreate(1000),
|
||||
sequencerClient,
|
||||
crypto,
|
||||
@ -182,7 +182,7 @@ class TrafficBalanceSubmissionHandlerTest
|
||||
recipient1,
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
NonNegativeLong.tryCreate(5),
|
||||
PositiveInt.tryCreate(5),
|
||||
NonNegativeLong.tryCreate(1000),
|
||||
sequencerClient,
|
||||
crypto,
|
||||
@ -236,7 +236,7 @@ class TrafficBalanceSubmissionHandlerTest
|
||||
recipient1,
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
NonNegativeLong.tryCreate(5),
|
||||
PositiveInt.tryCreate(5),
|
||||
NonNegativeLong.tryCreate(1000),
|
||||
sequencerClient,
|
||||
crypto,
|
||||
@ -272,7 +272,7 @@ class TrafficBalanceSubmissionHandlerTest
|
||||
recipient1,
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
NonNegativeLong.tryCreate(5),
|
||||
PositiveInt.tryCreate(5),
|
||||
NonNegativeLong.tryCreate(1000),
|
||||
sequencerClient,
|
||||
crypto,
|
||||
@ -324,7 +324,7 @@ class TrafficBalanceSubmissionHandlerTest
|
||||
recipient1,
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
NonNegativeLong.tryCreate(5),
|
||||
PositiveInt.tryCreate(5),
|
||||
NonNegativeLong.tryCreate(1000),
|
||||
sequencerClient,
|
||||
crypto,
|
||||
|
@ -5,7 +5,7 @@ package com.digitalasset.canton.traffic
|
||||
|
||||
import com.daml.nonempty.NonEmpty
|
||||
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.provider.symbolic.SymbolicCrypto
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
@ -83,7 +83,7 @@ class TrafficControlProcessorTest extends AnyWordSpec with BaseTest with HasExec
|
||||
): SignedProtocolMessage[SetTrafficBalanceMessage] = {
|
||||
val setBalance = SetTrafficBalanceMessage(
|
||||
participantId,
|
||||
NonNegativeLong.one,
|
||||
PositiveInt.one,
|
||||
NonNegativeLong.tryCreate(100),
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
|
@ -55,10 +55,14 @@ class SerializationDeserializationTest
|
||||
generatorsProtocol,
|
||||
generatorsProtocolSeq,
|
||||
)
|
||||
val generatorsTrafficData = new GeneratorsTrafficData(
|
||||
version
|
||||
)
|
||||
|
||||
import generatorsData.*
|
||||
import generatorsMessages.*
|
||||
import generatorsTransferData.*
|
||||
import generatorsTrafficData.*
|
||||
import generatorsVerdict.*
|
||||
import generatorsLocalVerdict.*
|
||||
import generatorsProtocol.*
|
||||
@ -77,8 +81,6 @@ class SerializationDeserializationTest
|
||||
testProtocolVersionedWithCtx(SignedProtocolMessage, version)
|
||||
|
||||
testProtocolVersioned(LocalVerdict)
|
||||
testProtocolVersioned(TransferResult)
|
||||
testProtocolVersioned(MalformedConfirmationRequestResult)
|
||||
testProtocolVersionedWithCtx(EnvelopeContent, (TestHash, version))
|
||||
testMemoizedProtocolVersioned(ConfirmationResultMessage)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: ai-analysis
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: bank
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: doctor
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: health-insurance
|
||||
|
@ -1,4 +1,4 @@
|
||||
sdk-version: 3.0.0-snapshot.20240222.12809.0.v6607acd0
|
||||
sdk-version: 3.0.0-snapshot.20240301.12835.0.v9c5fae54
|
||||
build-options:
|
||||
- --target=2.1
|
||||
name: medical-records
|
||||
|
@ -36,7 +36,7 @@ message TrafficControlStateResponse {
|
||||
|
||||
message SetTrafficBalanceRequest {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1506,7 +1506,9 @@ class BlockUpdateGenerator(
|
||||
)
|
||||
.map { trafficStateUpdates =>
|
||||
ephemeralState
|
||||
.copy(trafficState = ephemeralState.trafficState ++ trafficStateUpdates)
|
||||
.copy(trafficState =
|
||||
ephemeralState.trafficState ++ trafficStateUpdates.view.mapValues(_.state).toMap
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import com.digitalasset.canton.error.MediatorError
|
||||
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, HasCloseContext}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.protocol.*
|
||||
import com.digitalasset.canton.protocol.messages.*
|
||||
import com.digitalasset.canton.protocol.{v30, *}
|
||||
import com.digitalasset.canton.sequencing.HandlerResult
|
||||
import com.digitalasset.canton.sequencing.protocol.*
|
||||
import com.digitalasset.canton.time.DomainTimeTracker
|
||||
@ -333,8 +333,7 @@ private[mediator] class ConfirmationResponseProcessor(
|
||||
.allHaveActiveParticipants(request.allInformees)
|
||||
.leftMap { informeesNoParticipant =>
|
||||
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...",
|
||||
v30.MediatorRejection.Code.CODE_INFORMEES_NOT_HOSTED_ON_ACTIVE_PARTICIPANT,
|
||||
show"Received a mediator confirmation request with id $requestId with some informees not being hosted by an active participant: $informeesNoParticipant. Rejecting request..."
|
||||
)
|
||||
reject.log()
|
||||
Option(MediatorVerdict.MediatorReject(reject))
|
||||
@ -369,8 +368,7 @@ private[mediator] class ConfirmationResponseProcessor(
|
||||
!batchAlsoContainsTopologyXTransaction, {
|
||||
val rejection = MediatorError.MalformedMessage
|
||||
.Reject(
|
||||
s"Received a mediator confirmation request with id $requestId also containing a topology transaction.",
|
||||
v30.MediatorRejection.Code.CODE_UNSPECIFIED,
|
||||
s"Received a mediator confirmation request with id $requestId also containing a topology transaction."
|
||||
)
|
||||
.reported()
|
||||
MediatorVerdict.MediatorReject(rejection)
|
||||
@ -393,8 +391,7 @@ private[mediator] class ConfirmationResponseProcessor(
|
||||
MediatorVerdict.MediatorReject(
|
||||
MediatorError.MalformedMessage
|
||||
.Reject(
|
||||
show"Rejecting mediator confirmation request with $requestId, mediator ${request.mediator}, topology at ${topologySnapshot.timestamp} due to $hint",
|
||||
v30.MediatorRejection.Code.CODE_WRONG_DECLARED_MEDIATOR,
|
||||
show"Rejecting mediator confirmation request with $requestId, mediator ${request.mediator}, topology at ${topologySnapshot.timestamp} due to $hint"
|
||||
)
|
||||
.reported()
|
||||
)
|
||||
@ -514,8 +511,7 @@ private[mediator] class ConfirmationResponseProcessor(
|
||||
unitOrRejectionReason.leftMap { rejectionReason =>
|
||||
val rejection = MediatorError.MalformedMessage
|
||||
.Reject(
|
||||
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,
|
||||
s"Received a mediator confirmation request with id $requestId with invalid root hash messages. Rejecting... Reason: $rejectionReason"
|
||||
)
|
||||
.reported()(loggingContext)
|
||||
MediatorVerdict.MediatorReject(rejection)
|
||||
@ -535,8 +531,7 @@ private[mediator] class ConfirmationResponseProcessor(
|
||||
MediatorVerdict.MediatorReject(
|
||||
MediatorError.MalformedMessage
|
||||
.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...",
|
||||
v30.MediatorRejection.Code.CODE_VIEW_THRESHOLD_BELOW_MINIMUM_THRESHOLD,
|
||||
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..."
|
||||
)
|
||||
.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"Rejecting request. Threshold: $threshold." +
|
||||
insufficientPermissionHint +
|
||||
authorizedPartiesHint,
|
||||
v30.MediatorRejection.Code.CODE_NOT_ENOUGH_CONFIRMING_PARTIES,
|
||||
authorizedPartiesHint
|
||||
)
|
||||
.reported()
|
||||
MediatorVerdict.MediatorReject(rejection)
|
||||
|
@ -12,14 +12,8 @@ import com.digitalasset.canton.domain.mediator.store.MediatorDeduplicationStore
|
||||
import com.digitalasset.canton.error.MediatorError
|
||||
import com.digitalasset.canton.lifecycle.CloseContext
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.protocol.messages.{
|
||||
DefaultOpenEnvelope,
|
||||
MediatorConfirmationRequest,
|
||||
ProtocolMessage,
|
||||
RootHashMessage,
|
||||
SerializedRootHashMessagePayload,
|
||||
}
|
||||
import com.digitalasset.canton.protocol.{DynamicDomainParametersWithValidity, RequestId, v30}
|
||||
import com.digitalasset.canton.protocol.messages.*
|
||||
import com.digitalasset.canton.protocol.{DynamicDomainParametersWithValidity, RequestId}
|
||||
import com.digitalasset.canton.sequencing.TracedProtocolEvent
|
||||
import com.digitalasset.canton.topology.client.DomainTopologyClient
|
||||
import com.digitalasset.canton.tracing.{TraceContext, Traced}
|
||||
@ -176,8 +170,7 @@ class DefaultMediatorEventDeduplicator(
|
||||
case Some(previousUsagesNE) =>
|
||||
val expireAfter = previousUsagesNE.map(_.expireAfter).max1
|
||||
val rejection = MediatorError.MalformedMessage.Reject(
|
||||
s"The request uuid ($uuid) must not be used until $expireAfter.",
|
||||
v30.MediatorRejection.Code.CODE_NON_UNIQUE_REQUEST_UUID,
|
||||
s"The request uuid ($uuid) must not be used until $expireAfter."
|
||||
)
|
||||
rejection.report()
|
||||
|
||||
|
@ -25,8 +25,9 @@ object MediatorVerdict {
|
||||
}
|
||||
type MediatorApprove = MediatorApprove.type
|
||||
|
||||
final case class ParticipantReject(reasons: NonEmpty[List[(Set[LfPartyId], LocalReject)]])
|
||||
extends MediatorVerdict {
|
||||
final case class ParticipantReject(
|
||||
reasons: NonEmpty[List[(Set[LfPartyId], LocalReject)]]
|
||||
) extends MediatorVerdict {
|
||||
override def toVerdict(protocolVersion: ProtocolVersion): Verdict =
|
||||
Verdict.ParticipantReject(reasons, protocolVersion)
|
||||
|
||||
@ -50,7 +51,11 @@ object MediatorVerdict {
|
||||
case invalid: InvalidMessage.Reject => invalid
|
||||
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(
|
||||
|
@ -13,9 +13,10 @@ import com.digitalasset.canton.error.MediatorError
|
||||
import com.digitalasset.canton.logging.pretty.Pretty
|
||||
import com.digitalasset.canton.logging.{HasLoggerName, NamedLoggingContext}
|
||||
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.client.TopologySnapshot
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.ErrorUtil
|
||||
import com.digitalasset.canton.util.FutureInstances.*
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
@ -64,13 +65,13 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
|
||||
ec: ExecutionContext,
|
||||
loggingContext: NamedLoggingContext,
|
||||
): OptionT[Future, List[(VKEY, Set[LfPartyId])]] = {
|
||||
implicit val tc = loggingContext.traceContext
|
||||
implicit val tc: TraceContext = loggingContext.traceContext
|
||||
def authorizedPartiesOfSender(
|
||||
viewKey: VKEY,
|
||||
declaredConfirmingParties: Set[ConfirmingParty],
|
||||
): OptionT[Future, Set[LfPartyId]] =
|
||||
): OptionT[Future, Set[LfPartyId]] = {
|
||||
localVerdict match {
|
||||
case malformed: Malformed =>
|
||||
case malformed: LocalReject if malformed.isMalformed =>
|
||||
malformed.logWithContext(
|
||||
Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender")
|
||||
)
|
||||
@ -86,8 +87,7 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
|
||||
Some(hostedConfirmingParties): Option[Set[LfPartyId]]
|
||||
}
|
||||
OptionT(res)
|
||||
|
||||
case _: LocalApprove | _: LocalReject =>
|
||||
case _: LocalVerdict =>
|
||||
val unexpectedConfirmingParties =
|
||||
confirmingParties -- declaredConfirmingParties.map(_.party)
|
||||
for {
|
||||
@ -126,6 +126,7 @@ trait ResponseAggregator extends HasLoggerName with Product with Serializable {
|
||||
}
|
||||
} yield confirmingParties
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
_ <- 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.
|
||||
// We treat this as a rejection for all parties hosted by the participant.
|
||||
localVerdict match {
|
||||
case malformed: Malformed =>
|
||||
case malformed @ LocalReject(_, true) =>
|
||||
malformed.logWithContext(
|
||||
Map("requestId" -> requestId.toString, "reportedBy" -> show"$sender")
|
||||
)
|
||||
|
@ -11,28 +11,18 @@ import cats.syntax.parallel.*
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
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.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.protocol.RequestId
|
||||
import com.digitalasset.canton.protocol.messages.*
|
||||
import com.digitalasset.canton.protocol.{RequestId, SourceDomainId, TargetDomainId}
|
||||
import com.digitalasset.canton.sequencing.client.{
|
||||
SendCallback,
|
||||
SendResult,
|
||||
SendType,
|
||||
SequencerClientSend,
|
||||
}
|
||||
import com.digitalasset.canton.sequencing.protocol.{
|
||||
AggregationRule,
|
||||
Batch,
|
||||
MediatorsOfDomain,
|
||||
MemberRecipient,
|
||||
OpenEnvelope,
|
||||
ParticipantsOfParty,
|
||||
Recipient,
|
||||
Recipients,
|
||||
SequencerErrors,
|
||||
}
|
||||
import com.digitalasset.canton.sequencing.protocol.*
|
||||
import com.digitalasset.canton.topology.client.TopologySnapshot
|
||||
import com.digitalasset.canton.topology.{MediatorId, ParticipantId, PartyId}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
@ -203,7 +193,15 @@ private[mediator] class DefaultVerdictSender(
|
||||
)
|
||||
(informeesMap, informeesWithGroupAddressing) = result
|
||||
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 =
|
||||
informeesMap.keys.toSeq.map(MemberRecipient) ++ informeesWithGroupAddressing.toSeq
|
||||
.map(p => ParticipantsOfParty(PartyId.tryFromLfParty(p)))
|
||||
@ -324,55 +322,15 @@ private[mediator] class DefaultVerdictSender(
|
||||
snapshot <- crypto.awaitSnapshot(requestId.unwrap)
|
||||
envs <- recipientsByViewType.toSeq
|
||||
.parTraverse { case (viewType, flatRecipients) =>
|
||||
// This is currently a bit messy. We need to a TransactionResultMessage or TransferXResult whenever possible,
|
||||
// because that allows us to easily intercept and change the verdict in tests.
|
||||
// However, in some cases, the required information is not available, so we fall back to MalformedMediatorConfirmationRequestResult.
|
||||
// TODO(i11326): Remove unnecessary fields from the result message types, so we can get rid of MalformedMediatorConfirmationRequestResult and simplify this code.
|
||||
val rejection = (viewType match {
|
||||
case ViewType.TransactionViewType =>
|
||||
requestO match {
|
||||
case Some(request @ InformeeMessage(_, _)) =>
|
||||
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,
|
||||
val rejection = ConfirmationResultMessage.create(
|
||||
crypto.domainId,
|
||||
viewType,
|
||||
rejectionReason,
|
||||
protocolVersion,
|
||||
)
|
||||
}
|
||||
|
||||
case ViewType.TransferInViewType =>
|
||||
TransferInResult.create(
|
||||
requestId,
|
||||
rootHashO = None,
|
||||
rejectionReason,
|
||||
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)))
|
||||
|
||||
|
@ -392,7 +392,7 @@ class DatabaseSequencer(
|
||||
FutureUnlessShutdown.pure(SequencerTrafficStatus(Seq.empty))
|
||||
override def setTrafficBalance(
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
sequencerClient: SequencerClient,
|
||||
)(implicit
|
||||
|
@ -136,7 +136,7 @@ trait Sequencer
|
||||
|
||||
def setTrafficBalance(
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
sequencerClient: SequencerClient,
|
||||
)(implicit
|
||||
|
@ -31,6 +31,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
|
||||
SequencerRateLimitManager,
|
||||
SequencerTrafficStatus,
|
||||
}
|
||||
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.TrafficStateUpdateResult
|
||||
import com.digitalasset.canton.domain.sequencing.traffic.store.TrafficBalanceStore
|
||||
import com.digitalasset.canton.health.admin.data.SequencerHealthStatus
|
||||
import com.digitalasset.canton.lifecycle.*
|
||||
@ -465,7 +466,7 @@ class BlockSequencer(
|
||||
upToDateTrafficStatesForMembers(
|
||||
stateManager.getHeadState.chunk.ephemeral.status.members.map(_.member),
|
||||
Some(clock.now),
|
||||
)
|
||||
).map(_.view.mapValues(_.state).toMap)
|
||||
}
|
||||
|
||||
/** Compute traffic states for the specified members at the provided timestamp,
|
||||
@ -478,14 +479,14 @@ class BlockSequencer(
|
||||
updateTimestamp: Option[CantonTimestamp] = None,
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): FutureUnlessShutdown[Map[Member, TrafficState]] = {
|
||||
): FutureUnlessShutdown[Map[Member, TrafficStateUpdateResult]] = {
|
||||
// Get the parameters for the traffic control
|
||||
OptionUtil.zipWithFDefaultValue(
|
||||
rateLimitManager,
|
||||
FutureUnlessShutdown.outcomeF(
|
||||
cryptoApi.headSnapshot.ipsSnapshot.trafficControlParameters(protocolVersion)
|
||||
),
|
||||
Map.empty[Member, TrafficState],
|
||||
Map.empty[Member, TrafficStateUpdateResult],
|
||||
) { case (rlm, parameters) =>
|
||||
// Use the head ephemeral state to get the known traffic states
|
||||
val headEphemeral = stateManager.getHeadState.chunk.ephemeral
|
||||
@ -521,7 +522,7 @@ class BlockSequencer(
|
||||
|
||||
override def setTrafficBalance(
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
sequencerClient: SequencerClient,
|
||||
)(implicit
|
||||
@ -543,12 +544,12 @@ class BlockSequencer(
|
||||
): FutureUnlessShutdown[SequencerTrafficStatus] = {
|
||||
upToDateTrafficStatesForMembers(requestedMembers)
|
||||
.map { updated =>
|
||||
updated.map { case (member, state) =>
|
||||
updated.map { case (member, TrafficStateUpdateResult(state, balanceUpdateSerial)) =>
|
||||
MemberTrafficStatus(
|
||||
member,
|
||||
state.timestamp,
|
||||
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
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package com.digitalasset.canton.domain.sequencing.sequencer.traffic
|
||||
import cats.data.EitherT
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
|
||||
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.sequencing.TrafficControlParameters
|
||||
import com.digitalasset.canton.sequencing.protocol.{
|
||||
@ -76,7 +77,7 @@ trait SequencerRateLimitManager {
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
tc: TraceContext,
|
||||
): FutureUnlessShutdown[Map[Member, TrafficState]]
|
||||
): FutureUnlessShutdown[Map[Member, TrafficStateUpdateResult]]
|
||||
}
|
||||
|
||||
sealed trait SequencerRateLimitError
|
||||
|
@ -121,7 +121,7 @@ class GrpcSequencerAdministrationService(
|
||||
val result = {
|
||||
for {
|
||||
member <- wrapErrUS(Member.fromProtoPrimitive(requestP.member, "member"))
|
||||
serial <- wrapErrUS(ProtoConverter.parseNonNegativeLong(requestP.serial))
|
||||
serial <- wrapErrUS(ProtoConverter.parsePositiveInt(requestP.serial))
|
||||
totalTrafficBalance <- wrapErrUS(
|
||||
ProtoConverter.parseNonNegativeLong(requestP.totalTrafficBalance)
|
||||
)
|
||||
|
@ -4,7 +4,6 @@
|
||||
package com.digitalasset.canton.domain.sequencing.traffic
|
||||
|
||||
import cats.data.EitherT
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError
|
||||
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.BalanceUpdateClient
|
||||
@ -30,10 +29,9 @@ class BalanceUpdateClientImpl(
|
||||
warnIfApproximate: Boolean,
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong] = {
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]] = {
|
||||
manager
|
||||
.getTrafficBalanceAt(member, timestamp, lastSeen, warnIfApproximate)
|
||||
.map(_.map(_.balance).getOrElse(NonNegativeLong.zero))
|
||||
.leftMap { case TrafficBalanceManager.TrafficBalanceAlreadyPruned(member, timestamp) =>
|
||||
SequencerRateLimitError.UnknownBalance(member, timestamp)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package com.digitalasset.canton.domain.sequencing.traffic
|
||||
|
||||
import cats.data.EitherT
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeLong
|
||||
import com.digitalasset.canton.crypto.{DomainSyncCryptoClient, SyncCryptoClient}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerRateLimitError
|
||||
@ -34,7 +33,7 @@ class BalanceUpdateClientTopologyImpl(
|
||||
warnIfApproximate: Boolean,
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong] = {
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]] = {
|
||||
val balanceFUS = for {
|
||||
topology <- SyncCryptoClient.getSnapshotForTimestampUS(
|
||||
syncCrypto,
|
||||
@ -46,11 +45,26 @@ class BalanceUpdateClientTopologyImpl(
|
||||
trafficBalance <- FutureUnlessShutdown.outcomeF(
|
||||
topology.ipsSnapshot
|
||||
.trafficControlStatus(Seq(member))
|
||||
.map(
|
||||
_.get(member).flatten.map(_.totalExtraTrafficLimit.toNonNegative)
|
||||
.map { statusMap =>
|
||||
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)
|
||||
}
|
||||
|
@ -9,14 +9,17 @@ import cats.syntax.bifunctor.*
|
||||
import cats.syntax.parallel.*
|
||||
import com.digitalasset.canton.concurrent.FutureSupervisor
|
||||
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.domain.metrics.SequencerMetrics
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
|
||||
SequencerRateLimitError,
|
||||
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.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.sequencing.TrafficControlParameters
|
||||
@ -118,7 +121,7 @@ class EnterpriseSequencerRateLimitManager(
|
||||
trafficControlParameters,
|
||||
trafficState,
|
||||
groupToMembers,
|
||||
currentBalance,
|
||||
currentBalance.map(_.balance).getOrElse(NonNegativeLong.zero),
|
||||
)
|
||||
)
|
||||
.leftWiden[SequencerRateLimitError]
|
||||
@ -133,34 +136,47 @@ class EnterpriseSequencerRateLimitManager(
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
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
|
||||
val timestampO = updateTimestamp.orElse(balanceUpdateClient.lastKnownTimestamp)
|
||||
for {
|
||||
updated <- partialTrafficStates.toList
|
||||
.parTraverse { case (member, state) =>
|
||||
.parTraverse { case (member, originalState) =>
|
||||
timestampO match {
|
||||
// Only update if the provided timestamp is in the future compared to the latest known state
|
||||
// We don't provide updates in the past
|
||||
case Some(ts) if ts > state.timestamp =>
|
||||
balanceUpdateClient(member, ts, lastBalanceUpdateTimestamp, warnIfApproximate)
|
||||
.valueOr { err =>
|
||||
logger.warn(s"Failed to obtain the traffic balance for $member at $ts", err)
|
||||
state.extraTrafficLimit.map(_.toNonNegative).getOrElse(NonNegativeLong.zero)
|
||||
}
|
||||
case Some(ts) if ts > originalState.timestamp =>
|
||||
getBalanceOrNone(member, ts)
|
||||
.map { balance =>
|
||||
getOrCreateMemberRateLimiter(member)
|
||||
val state = getOrCreateMemberRateLimiter(member)
|
||||
.updateTrafficState(
|
||||
ts,
|
||||
trafficControlParameters,
|
||||
NonNegativeLong.zero,
|
||||
state,
|
||||
balance,
|
||||
originalState,
|
||||
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
|
||||
TrafficStateUpdateResult(state, balance.map(_.serial))
|
||||
}
|
||||
.map(member -> _)
|
||||
case _ => FutureUnlessShutdown.pure(member -> state)
|
||||
case _ =>
|
||||
getBalanceOrNone(member, originalState.timestamp)
|
||||
.map(balance =>
|
||||
member -> TrafficStateUpdateResult(originalState, balance.map(_.serial))
|
||||
)
|
||||
}
|
||||
}
|
||||
.map(_.toMap)
|
||||
@ -173,6 +189,15 @@ class 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 {
|
||||
def apply(
|
||||
member: Member,
|
||||
@ -181,7 +206,7 @@ object EnterpriseSequencerRateLimitManager {
|
||||
warnIfApproximate: Boolean = true,
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, NonNegativeLong]
|
||||
): EitherT[FutureUnlessShutdown, SequencerRateLimitError, Option[TrafficBalance]]
|
||||
def lastKnownTimestamp: Option[CantonTimestamp]
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import scala.jdk.CollectionConverters.*
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
@nowarn("msg=match may not be exhaustive")
|
||||
class ConfirmationResponseProcessorTestV5
|
||||
class ConfirmationResponseProcessorTest
|
||||
extends AsyncWordSpec
|
||||
with BaseTest
|
||||
with HasTestCloseContext
|
||||
@ -106,9 +106,6 @@ class ConfirmationResponseProcessorTestV5
|
||||
protected val initialDomainParameters: DynamicDomainParameters =
|
||||
TestDomainParameters.defaultDynamic
|
||||
|
||||
private lazy val localVerdictProtocolVersion =
|
||||
LocalVerdict.protocolVersionRepresentativeFor(testedProtocolVersion)
|
||||
|
||||
protected val confirmationResponseTimeout: NonNegativeFiniteDuration =
|
||||
NonNegativeFiniteDuration.tryOfMillis(100L)
|
||||
|
||||
@ -347,9 +344,10 @@ class ConfirmationResponseProcessorTestV5
|
||||
)
|
||||
} yield {
|
||||
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.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 =>
|
||||
envelope.recipients -> Some(
|
||||
envelope.protocolMessage
|
||||
.asInstanceOf[SignedProtocolMessage[ConfirmationResult]]
|
||||
.asInstanceOf[SignedProtocolMessage[ConfirmationResultMessage]]
|
||||
.message
|
||||
.viewType
|
||||
)
|
||||
@ -972,14 +970,6 @@ class ConfirmationResponseProcessorTestV5
|
||||
|
||||
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(
|
||||
participant: ParticipantId
|
||||
): Future[SignedProtocolMessage[ConfirmationResponse]] = {
|
||||
@ -988,7 +978,8 @@ class ConfirmationResponseProcessorTestV5
|
||||
participant,
|
||||
None,
|
||||
LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject(malformedMsg)(localVerdictProtocolVersion),
|
||||
.Reject(malformedMsg)
|
||||
.toLocalReject(testedProtocolVersion),
|
||||
Some(fullInformeeTree.transactionId.toRootHash),
|
||||
Set.empty,
|
||||
factory.domainId,
|
||||
@ -1040,8 +1031,7 @@ class ConfirmationResponseProcessorTestV5
|
||||
)(Predef.identity)
|
||||
|
||||
// records the request
|
||||
_ <- loggerFactory.assertLogs(
|
||||
sequentialTraverse_(malformed)(
|
||||
_ <- sequentialTraverse_(malformed)(
|
||||
sut.processor.processResponse(
|
||||
ts1,
|
||||
notSignificantCounter,
|
||||
@ -1051,12 +1041,6 @@ class ConfirmationResponseProcessorTestV5
|
||||
Some(requestId.unwrap),
|
||||
Recipients.cc(mediatorGroupRecipient),
|
||||
)
|
||||
),
|
||||
isMalformedWarn(participant1),
|
||||
isMalformedWarn(participant3),
|
||||
isMalformedWarn(participant3),
|
||||
isMalformedWarn(participant2),
|
||||
isMalformedWarn(participant3),
|
||||
)
|
||||
|
||||
finalState <- sut.mediatorState.fetch(requestId).value
|
||||
@ -1072,8 +1056,10 @@ class ConfirmationResponseProcessorTestV5
|
||||
// TODO(#5337) These are only the rejections for the first view because this view happens to be finalized first.
|
||||
reasons.length shouldEqual 2
|
||||
reasons.foreach { case (party, reject) =>
|
||||
reject shouldBe LocalRejectError.MalformedRejects.Payloads.Reject(malformedMsg)(
|
||||
localVerdictProtocolVersion
|
||||
reject shouldBe LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject(malformedMsg)
|
||||
.toLocalReject(
|
||||
testedProtocolVersion
|
||||
)
|
||||
party should (contain(submitter) or contain(signatory))
|
||||
}
|
@ -17,8 +17,8 @@ import com.digitalasset.canton.error.MediatorError
|
||||
import com.digitalasset.canton.lifecycle.CloseContext
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.logging.pretty.Pretty
|
||||
import com.digitalasset.canton.protocol.RequestId
|
||||
import com.digitalasset.canton.protocol.messages.*
|
||||
import com.digitalasset.canton.protocol.{RequestId, v30}
|
||||
import com.digitalasset.canton.sequencing.protocol.*
|
||||
import com.digitalasset.canton.topology.DefaultTestIdentities.*
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
@ -123,8 +123,7 @@ class MediatorEventDeduplicatorTest
|
||||
val request = envelope.protocolMessage
|
||||
val reject = MediatorVerdict.MediatorReject(
|
||||
MediatorError.MalformedMessage.Reject(
|
||||
s"The request uuid (${request.requestUuid}) must not be used until $expireAfter.",
|
||||
v30.MediatorRejection.Code.CODE_NON_UNIQUE_REQUEST_UUID,
|
||||
s"The request uuid (${request.requestUuid}) must not be used until $expireAfter."
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -33,11 +33,9 @@ import java.util.UUID
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.language.existentials
|
||||
|
||||
class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
class ResponseAggregationTest extends PathAnyFunSpec with BaseTest {
|
||||
|
||||
private implicit val ec: ExecutionContext = directExecutionContext
|
||||
private lazy val localVerdictProtocolVersion =
|
||||
LocalVerdict.protocolVersionRepresentativeFor(testedProtocolVersion)
|
||||
|
||||
describe(classOf[ResponseAggregation[?]].getSimpleName) {
|
||||
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") {
|
||||
def testReject() =
|
||||
LocalRejectError.ConsistencyRejections.LockedContracts.Reject(Seq())(
|
||||
localVerdictProtocolVersion
|
||||
)
|
||||
LocalRejectError.ConsistencyRejections.LockedContracts
|
||||
.Reject(Seq())
|
||||
.toLocalReject(testedProtocolVersion)
|
||||
|
||||
val fullInformeeTree =
|
||||
FullInformeeTree.tryCreate(
|
||||
@ -430,16 +428,15 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
mkResponse(
|
||||
view1Position,
|
||||
LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject("test4")(localVerdictProtocolVersion),
|
||||
.Reject("test4")
|
||||
.toLocalReject(testedProtocolVersion),
|
||||
Set.empty,
|
||||
rootHash,
|
||||
)
|
||||
lazy val result = loggerFactory.assertLogs(
|
||||
lazy val result =
|
||||
step3
|
||||
.validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot)
|
||||
.futureValue,
|
||||
_.shouldBeCantonErrorCode(LocalRejectError.MalformedRejects.Payloads),
|
||||
)
|
||||
.futureValue
|
||||
it("should not allow repeated rejection") {
|
||||
result shouldBe None
|
||||
}
|
||||
@ -541,7 +538,9 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
lazy val changeTs = requestId.unwrap.plusSeconds(1)
|
||||
|
||||
def testReject(reason: String) =
|
||||
LocalRejectError.MalformedRejects.Payloads.Reject(reason)(localVerdictProtocolVersion)
|
||||
LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject(reason)
|
||||
.toLocalReject(testedProtocolVersion)
|
||||
|
||||
describe("for a single view") {
|
||||
it("should update the pending confirming parties set for all hosted parties") {
|
||||
@ -555,16 +554,11 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
)
|
||||
val result = loggerFactory.assertLogs(
|
||||
val result =
|
||||
valueOrFail(
|
||||
sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue
|
||||
)(
|
||||
"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
|
||||
@ -608,19 +602,11 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
domainId,
|
||||
testedProtocolVersion,
|
||||
)
|
||||
val result = loggerFactory.assertLogs(
|
||||
val result =
|
||||
valueOrFail(
|
||||
sut.validateAndProgress(changeTs, response, topologySnapshot).futureValue
|
||||
)(
|
||||
"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.state shouldBe Right(
|
||||
@ -709,9 +695,9 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
|
||||
describe("consortium voting") {
|
||||
def testReject() =
|
||||
LocalRejectError.ConsistencyRejections.LockedContracts.Reject(Seq())(
|
||||
localVerdictProtocolVersion
|
||||
)
|
||||
LocalRejectError.ConsistencyRejections.LockedContracts
|
||||
.Reject(Seq())
|
||||
.toLocalReject(testedProtocolVersion)
|
||||
|
||||
val fullInformeeTree =
|
||||
FullInformeeTree.tryCreate(
|
||||
@ -1255,16 +1241,15 @@ class ResponseAggregationTestV5 extends PathAnyFunSpec with BaseTest {
|
||||
mkResponse(
|
||||
view1Position,
|
||||
LocalRejectError.MalformedRejects.Payloads
|
||||
.Reject("test4")(localVerdictProtocolVersion),
|
||||
.Reject("test4")
|
||||
.toLocalReject(testedProtocolVersion),
|
||||
Set.empty,
|
||||
rootHash,
|
||||
)
|
||||
lazy val result = loggerFactory.assertLogs(
|
||||
lazy val result =
|
||||
step3
|
||||
.validateAndProgress(requestId.unwrap.plusSeconds(2), response4, topologySnapshot)
|
||||
.futureValue,
|
||||
_.shouldBeCantonErrorCode(LocalRejectError.MalformedRejects.Payloads),
|
||||
)
|
||||
.futureValue
|
||||
it("should not allow repeated rejection") {
|
||||
result shouldBe None
|
||||
}
|
@ -159,7 +159,7 @@ class BaseSequencerTest extends AsyncWordSpec with BaseTest {
|
||||
override protected def timeouts: ProcessingTimeout = ProcessingTimeout()
|
||||
override def setTrafficBalance(
|
||||
member: Member,
|
||||
serial: NonNegativeLong,
|
||||
serial: PositiveInt,
|
||||
totalTrafficBalance: NonNegativeLong,
|
||||
sequencerClient: SequencerClient,
|
||||
)(implicit
|
||||
|
@ -11,6 +11,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.traffic.{
|
||||
SequencerRateLimitManager,
|
||||
SequencerTrafficConfig,
|
||||
}
|
||||
import com.digitalasset.canton.domain.sequencing.traffic.EnterpriseSequencerRateLimitManager.TrafficStateUpdateResult
|
||||
import com.digitalasset.canton.domain.sequencing.traffic.store.memory.InMemoryTrafficBalanceStore
|
||||
import com.digitalasset.canton.sequencing.TrafficControlParameters
|
||||
import com.digitalasset.canton.sequencing.protocol.*
|
||||
@ -233,13 +234,17 @@ class EnterpriseSequencerRateLimitManagerTest
|
||||
warnIfApproximate = true,
|
||||
)
|
||||
.failOnShutdown
|
||||
_ = state2.get(sender).value shouldBe TrafficState(
|
||||
_ = state2.get(sender).value shouldBe TrafficStateUpdateResult(
|
||||
TrafficState(
|
||||
extraTrafficRemainder = NonNegativeLong.tryCreate(8L),
|
||||
extraTrafficConsumed = NonNegativeLong.zero,
|
||||
// Should have half of the max base rate added back after 1 second
|
||||
baseTrafficRemainder = NonNegativeLong.tryCreate(
|
||||
4L
|
||||
), // Should have half of the max base rate added back after 1 second
|
||||
),
|
||||
sequencingTs.immediateSuccessor.plusSeconds(1),
|
||||
),
|
||||
Some(PositiveInt.one),
|
||||
)
|
||||
} yield succeed
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ message TracedBlockOrderingRequest {
|
||||
int64 microseconds_since_epoch = 4;
|
||||
}
|
||||
|
||||
// Currently only used by the output module of the BFT sequencer
|
||||
message TracedBatchedBlockOrderingRequests {
|
||||
string traceparent = 1;
|
||||
repeated TracedBlockOrderingRequest requests = 2;
|
||||
int64 last_topology_timestamp_epoch_micros = 3;
|
||||
}
|
||||
|
@ -171,9 +171,10 @@ object ReferenceBlockOrderer {
|
||||
|
||||
final case class TimestampedRequest(tag: String, body: ByteString, timestamp: CantonTimestamp)
|
||||
|
||||
private[sequencer] def storeMultipleRequests(
|
||||
private[sequencer] def storeBatch(
|
||||
blockHeight: Long,
|
||||
timestamp: CantonTimestamp,
|
||||
lastTopologyTimestamp: CantonTimestamp,
|
||||
sendQueue: SimpleExecutionQueue,
|
||||
store: ReferenceBlockOrderingStore,
|
||||
requests: Seq[Traced[TimestampedRequest]],
|
||||
@ -182,33 +183,30 @@ object ReferenceBlockOrderer {
|
||||
errorLoggingContext: ErrorLoggingContext,
|
||||
traceContext: TraceContext,
|
||||
): Future[Unit] = {
|
||||
val (tag, body) = requests.headOption
|
||||
.filter(_ => requests.sizeIs == 1)
|
||||
.map(head => head.value.tag -> head.value.body)
|
||||
.getOrElse {
|
||||
val body = {
|
||||
val traceparent = traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
|
||||
TracedBatchedBlockOrderingRequests(
|
||||
traceparent,
|
||||
val batchTraceparent = traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
|
||||
val body =
|
||||
TracedBatchedBlockOrderingRequests
|
||||
.of(
|
||||
batchTraceparent,
|
||||
requests.map { case traced @ Traced(request) =>
|
||||
val traceparent = traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
|
||||
val requestTraceparent =
|
||||
traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
|
||||
TracedBlockOrderingRequest(
|
||||
traceparent,
|
||||
requestTraceparent,
|
||||
request.tag,
|
||||
request.body,
|
||||
request.timestamp.toMicros,
|
||||
)
|
||||
},
|
||||
lastTopologyTimestamp.toMicros,
|
||||
)
|
||||
}.toByteString
|
||||
(BatchTag, body)
|
||||
}
|
||||
.toByteString
|
||||
|
||||
sendQueue
|
||||
.execute(
|
||||
store.insertRequestWithHeight(
|
||||
blockHeight,
|
||||
BlockOrderer.OrderedRequest(timestamp.toMicros, tag, body),
|
||||
BlockOrderer.OrderedRequest(timestamp.toMicros, BatchTag, body),
|
||||
),
|
||||
s"send request at $timestamp",
|
||||
)
|
||||
|
@ -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.serialization.ProtoConverter
|
||||
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.util.retry
|
||||
import com.digitalasset.canton.util.retry.RetryUtil
|
||||
@ -202,14 +203,19 @@ class DbReferenceBlockOrderingStore(
|
||||
}
|
||||
._2
|
||||
requestsUntilFirstGap.map { case (height, tracedRequest, uuid) =>
|
||||
val orderedRequests =
|
||||
val (orderedRequests, lastTopologyTimestamp) =
|
||||
if (tracedRequest.value.tag == BatchTag) {
|
||||
val batchedRequests = proto.TracedBatchedBlockOrderingRequests
|
||||
val tracedBatchedBlockOrderingRequests = proto.TracedBatchedBlockOrderingRequests
|
||||
.parseFrom(tracedRequest.value.body.toByteArray)
|
||||
.requests
|
||||
val batchedRequests =
|
||||
tracedBatchedBlockOrderingRequests.requests
|
||||
.map(DbReferenceBlockOrderingStore.fromProto)
|
||||
batchedRequests
|
||||
} 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.
|
||||
logger.debug(s"Retrieved block at height $height with UUID: $uuid")
|
||||
val blockTimestamp =
|
||||
@ -217,6 +223,7 @@ class DbReferenceBlockOrderingStore(
|
||||
TimestampedBlock(
|
||||
BlockOrderer.Block(height, orderedRequests),
|
||||
blockTimestamp,
|
||||
lastTopologyTimestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX
|
||||
import com.digitalasset.canton.tracing.{TraceContext, Traced}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future, blocking}
|
||||
@ -55,7 +56,11 @@ object ReferenceBlockOrderingStore {
|
||||
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 {
|
||||
@ -117,24 +122,36 @@ class InMemoryReferenceSequencerDriverStore extends ReferenceBlockOrderingStore
|
||||
// Get the last elements up until initial height
|
||||
val iterator = deque.descendingIterator()
|
||||
val initial = math.max(initialHeight, 0)
|
||||
val requestsWithTimestamps =
|
||||
val requestsWithTimestampsAndLastTopologyTimestamps =
|
||||
(initial until deque.size().toLong)
|
||||
.map(_ => iterator.next())
|
||||
.reverse
|
||||
.map { request =>
|
||||
if (request.value.tag == BatchTag) {
|
||||
val batchedRequests = proto.TracedBatchedBlockOrderingRequests
|
||||
val tracedBatchedBlockOrderingRequests = proto.TracedBatchedBlockOrderingRequests
|
||||
.parseFrom(request.value.body.toByteArray)
|
||||
.requests
|
||||
val batchedRequests = tracedBatchedBlockOrderingRequests.requests
|
||||
.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 {
|
||||
case ((blockTimestamp, tracedRequests), blockHeight) =>
|
||||
requestsWithTimestampsAndLastTopologyTimestamps.zip(LazyList.from(initial.toInt)).map {
|
||||
case ((blockTimestamp, tracedRequests, lastTopologyTimestamp), blockHeight) =>
|
||||
TimestampedBlock(
|
||||
BlockOrderer.Block(blockHeight.toLong, tracedRequests),
|
||||
CantonTimestamp.ofEpochMicro(blockTimestamp),
|
||||
lastTopologyTimestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.Refer
|
||||
sequencedRegisterMember,
|
||||
sequencedSend,
|
||||
}
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX
|
||||
import com.digitalasset.canton.tracing.{TraceContext, Traced, W3CTraceContext}
|
||||
import com.google.protobuf.ByteString
|
||||
import org.scalatest.wordspec.AsyncWordSpec
|
||||
@ -39,6 +40,7 @@ trait ReferenceBlockOrderingStoreTest extends AsyncWordSpec with BaseTest {
|
||||
TimestampedBlock(
|
||||
BlockOrderer.Block(height, Seq(tracedEvent)),
|
||||
CantonTimestamp.ofEpochMicro(tracedEvent.value.microsecondsSinceEpoch),
|
||||
SignedTopologyTransactionX.InitialTopologySequencingTime,
|
||||
)
|
||||
|
||||
def referenceBlockOrderingStore(mk: () => ReferenceBlockOrderingStore): Unit = {
|
||||
|
@ -12,7 +12,6 @@ import com.daml.lf.value.Value
|
||||
import com.daml.logging.entries.{LoggingEntry, LoggingValue, ToLoggingValue}
|
||||
import com.digitalasset.canton.ledger.api.DeduplicationPeriod
|
||||
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.topology.DomainId
|
||||
import com.google.rpc.status.Status as RpcStatus
|
||||
|
@ -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: carbonv1-tests
|
||||
source: .
|
||||
version: 3.0.0
|
||||
|
@ -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
|
||||
data-dependencies:
|
||||
- ../../../../scala-2.13/resource_managed/main/carbonv1-tests-3.0.0.dar
|
||||
|
@ -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: experimental-tests
|
||||
source: .
|
||||
version: 3.0.0
|
||||
|
@ -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: model-tests
|
||||
source: .
|
||||
version: 3.0.0
|
||||
|
@ -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: package-management-tests
|
||||
source: .
|
||||
version: 3.0.0
|
||||
|
@ -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: semantic-tests
|
||||
source: .
|
||||
version: 3.0.0
|
||||
|
@ -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
|
||||
source: .
|
||||
version: 1.0.0
|
||||
|
@ -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
|
||||
source: .
|
||||
version: 2.0.0
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user