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

* update canton to 3.0.0-snapshot.100000000.20240301.12744.0.v046ec13c

tell-slack: canton

* fix

---------

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

View File

@ -428,6 +428,7 @@ scala_library(
"@maven//:com_github_blemale_scaffeine_2_13",
"@maven//:com_github_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",

View File

@ -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;
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -18,7 +18,11 @@ import com.digitalasset.canton.crypto.store.{
import com.digitalasset.canton.error.{BaseCantonError, CantonErrorGroups}
import com.digitalasset.canton.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] =

View File

@ -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] =

View File

@ -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](

View File

@ -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)

View File

@ -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),

View File

@ -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)
)
}
}

View File

@ -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]].
*

View File

@ -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,23 +89,22 @@ 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 (confirmingParties.nonEmpty)
throw InvalidConfirmationResponse("Confirming parties must be empty for verdict Malformed.")
case _: LocalApprove | _: LocalReject =>
if (confirmingParties.isEmpty)
throw InvalidConfirmationResponse(
show"Confirming parties must not be empty for verdict $localVerdict"
)
if (rootHash.isEmpty)
throw InvalidConfirmationResponse(
show"Root hash must not be empty for verdict $localVerdict"
)
if (viewPositionO.isEmpty)
throw InvalidConfirmationResponse(
show"View position must not be empty for verdict $localVerdict"
)
if (localVerdict.isMalformed) {
if (confirmingParties.nonEmpty)
throw InvalidConfirmationResponse("Confirming parties must be empty for verdict Malformed.")
} else {
if (confirmingParties.isEmpty)
throw InvalidConfirmationResponse(
show"Confirming parties must not be empty for verdict $localVerdict"
)
if (rootHash.isEmpty)
throw InvalidConfirmationResponse(
show"Root hash must not be empty for verdict $localVerdict"
)
if (viewPositionO.isEmpty)
throw InvalidConfirmationResponse(
show"View position must not be empty for verdict $localVerdict"
)
}
override def signingTimestamp: Option[CantonTimestamp] = Some(requestId.unwrap)

View File

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

View File

@ -3,81 +3,92 @@
package com.digitalasset.canton.protocol.messages
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 verdict the finalized verdict on the 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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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))
}

View File

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

View File

@ -7,8 +7,8 @@ import com.digitalasset.canton.LfPartyId
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
import com.digitalasset.canton.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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

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

View File

@ -101,22 +101,14 @@ object TypedSignedProtocolMessageContent
message <- (messageBytes match {
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]

View File

@ -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)
}
}

View File

@ -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]

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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)))
}

View File

@ -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,

View File

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

View File

@ -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),
)
.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"))
}
ecPrivateKey <- parseAndGetPrivateKey(
signingKey,
{ case k: ECPrivateKey => Right(k) },
SigningError.InvalidSigningKey,
)
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)

View File

@ -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))
}

View File

@ -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,14 +36,33 @@ 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(
error: ProtoDeserializationError,
@ -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")
}
}

View File

@ -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,
)
}
}

View File

@ -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))

View File

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

View File

@ -24,11 +24,12 @@ class JceCryptoTest
with HkdfTest
with 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(),
)
}
}

View File

@ -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
}
}

View File

@ -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(),
)
}
}

View File

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

View File

@ -4,14 +4,12 @@
package com.digitalasset.canton.data
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,
)
)
}

View File

@ -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")),

View File

@ -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,56 +36,49 @@ 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(
LockedContracts.Reject(resources),
InactiveContracts.Reject(resources),
LedgerTime.Reject(details),
SubmissionTime.Reject(details),
LocalTimeout.Reject(),
ActivenessCheckFailed.Reject(details),
ContractAlreadyArchived.Reject(details),
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),
)
val builders = Seq[LocalRejectErrorImpl](
LockedContracts.Reject(resources),
InactiveContracts.Reject(resources),
LedgerTime.Reject(details),
SubmissionTime.Reject(details),
LocalTimeout.Reject(),
ActivenessCheckFailed.Reject(details),
ContractAlreadyArchived.Reject(details),
ContractAlreadyActive.Reject(details),
ContractIsLocked.Reject(details),
AlreadyCompleted.Reject(details),
)
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))

View File

@ -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,38 +60,26 @@ 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 confirmationResultMessageArb: Arbitrary[ConfirmationResultMessage] = Arbitrary(
for {
domainId <- Arbitrary.arbitrary[DomainId]
viewType <- Arbitrary.arbitrary[ViewType]
requestId <- Arbitrary.arbitrary[RequestId]
rootHash <- Arbitrary.arbitrary[RootHash]
verdict <- verdictArb.arbitrary
informees <- Arbitrary.arbitrary[Set[LfPartyId]]
implicit val MalformedMediatorConfirmationRequestResultArb
: Arbitrary[MalformedConfirmationRequestResult] =
Arbitrary(
for {
requestId <- Arbitrary.arbitrary[RequestId]
domainId <- Arbitrary.arbitrary[DomainId]
viewType <- Arbitrary.arbitrary[ViewType]
mediatorReject <- mediatorRejectArb.arbitrary
} yield MalformedConfirmationRequestResult.tryCreate(
requestId,
domainId,
viewType,
mediatorReject,
protocolVersion,
)
// TODO(#14241) Also generate instance that makes pv above cover all the values
} yield ConfirmationResultMessage.create(
domainId,
viewType,
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 {
@ -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],
)
)

View File

@ -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)
}
)

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}

View File

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

View File

@ -18,8 +18,8 @@ import com.digitalasset.canton.error.MediatorError
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, HasCloseContext}
import com.digitalasset.canton.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)

View File

@ -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()

View File

@ -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(

View File

@ -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")
)

View File

@ -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,
crypto.domainId,
viewType,
rejectionReason,
protocolVersion,
)
}
case ViewType.TransferInViewType =>
TransferInResult.create(
requestId,
Set.empty,
TargetDomainId(crypto.domainId),
rejectionReason,
protocolVersion,
)
case ViewType.TransferOutViewType =>
TransferOutResult.create(
requestId,
Set.empty,
SourceDomainId(crypto.domainId),
rejectionReason,
protocolVersion,
)
case _: ViewType =>
MalformedConfirmationRequestResult.tryCreate(
requestId,
crypto.domainId,
viewType,
rejectionReason,
protocolVersion,
)
}): ConfirmationResult
val rejection = ConfirmationResultMessage.create(
crypto.domainId,
viewType,
requestId,
rootHashO = None,
rejectionReason,
Set.empty,
protocolVersion,
)
val recipients = Recipients.recipientGroups(flatRecipients.map(r => NonEmpty(Set, r)))

View File

@ -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

View File

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

View File

@ -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
}

View File

@ -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

View File

@ -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)
)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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]
}
}

View File

@ -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,23 +1031,16 @@ class ConfirmationResponseProcessorTestV5
)(Predef.identity)
// records the request
_ <- loggerFactory.assertLogs(
sequentialTraverse_(malformed)(
sut.processor.processResponse(
ts1,
notSignificantCounter,
ts1.plusSeconds(60),
ts1.plusSeconds(120),
_,
Some(requestId.unwrap),
Recipients.cc(mediatorGroupRecipient),
)
),
isMalformedWarn(participant1),
isMalformedWarn(participant3),
isMalformedWarn(participant3),
isMalformedWarn(participant2),
isMalformedWarn(participant3),
_ <- sequentialTraverse_(malformed)(
sut.processor.processResponse(
ts1,
notSignificantCounter,
ts1.plusSeconds(60),
ts1.plusSeconds(120),
_,
Some(requestId.unwrap),
Recipients.cc(mediatorGroupRecipient),
)
)
finalState <- sut.mediatorState.fetch(requestId).value
@ -1072,9 +1056,11 @@ class ConfirmationResponseProcessorTestV5
// TODO(#5337) These are only the rejections for the first view because this view happens to be finalized first.
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))
}
}

View File

@ -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."
)
)

View File

@ -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,17 +554,12 @@ 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
result.state shouldBe Right(
@ -608,20 +602,12 @@ 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(
Map(
@ -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
}

View File

@ -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

View File

@ -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(
extraTrafficRemainder = NonNegativeLong.tryCreate(8L),
extraTrafficConsumed = NonNegativeLong.zero,
baseTrafficRemainder = NonNegativeLong.tryCreate(
4L
), // Should have half of the max base rate added back after 1 second
sequencingTs.immediateSuccessor.plusSeconds(1),
_ = 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
),
sequencingTs.immediateSuccessor.plusSeconds(1),
),
Some(PositiveInt.one),
)
} yield succeed
}

View File

@ -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;
}

View File

@ -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,
requests.map { case traced @ Traced(request) =>
val traceparent = traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
TracedBlockOrderingRequest(
traceparent,
request.tag,
request.body,
request.timestamp.toMicros,
)
},
)
}.toByteString
(BatchTag, body)
}
val batchTraceparent = traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
val body =
TracedBatchedBlockOrderingRequests
.of(
batchTraceparent,
requests.map { case traced @ Traced(request) =>
val requestTraceparent =
traced.traceContext.asW3CTraceContext.map(_.parent).getOrElse("")
TracedBlockOrderingRequest(
requestTraceparent,
request.tag,
request.body,
request.timestamp.toMicros,
)
},
lastTopologyTimestamp.toMicros,
)
.toByteString
sendQueue
.execute(
store.insertRequestWithHeight(
blockHeight,
BlockOrderer.OrderedRequest(timestamp.toMicros, tag, body),
BlockOrderer.OrderedRequest(timestamp.toMicros, BatchTag, body),
),
s"send request at $timestamp",
)

View File

@ -17,6 +17,7 @@ import com.digitalasset.canton.resource.DbStorage.Profile.{H2, Oracle, Postgres}
import com.digitalasset.canton.resource.{DbStorage, DbStore}
import com.digitalasset.canton.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
.map(DbReferenceBlockOrderingStore.fromProto)
batchedRequests
} else Seq(tracedRequest)
val batchedRequests =
tracedBatchedBlockOrderingRequests.requests
.map(DbReferenceBlockOrderingStore.fromProto)
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,
)
}
}

View File

@ -16,6 +16,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.Refer
import com.digitalasset.canton.domain.sequencing.sequencer.reference.store.v1 as proto
import com.digitalasset.canton.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,
)
}
}

View File

@ -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 = {

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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