diff --git a/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/inspection_service.proto b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/inspection_service.proto
index 161e610146..161cf9f125 100644
--- a/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/inspection_service.proto
+++ b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/inspection_service.proto
@@ -25,6 +25,13 @@ service InspectionService {
rpc LookupOffsetByTime(LookupOffsetByTime.Request) returns (LookupOffsetByTime.Response);
// Look up the ledger offset by an index, e.g. 1 returns the first offset, 2 the second, etc.
rpc LookupOffsetByIndex(LookupOffsetByIndex.Request) returns (LookupOffsetByIndex.Response);
+ // TODO(#18452) R5
+ // Look up the ACS commitments computed and sent by a participant
+ rpc LookupSentAcsCommitments(LookupSentAcsCommitments.Request) returns (LookupSentAcsCommitments.Response);
+ // TODO(#18452) R5
+ // List the counter-participants and their ACS commitments together with the match status
+ // TODO(#18749) R1 Can also be used for R1, to fetch commitments that a counter participant received from myself
+ rpc LookupReceivedAcsCommitments(LookupReceivedAcsCommitments.Request) returns (LookupReceivedAcsCommitments.Response);
// Configure metrics for slow counter-participants (i.e., that are behind in sending commitments) and
// configure thresholds for when a counter-participant is deemed slow.
// TODO(#10436) R7
@@ -90,6 +97,149 @@ message LookupOffsetByIndex {
}
}
+// list the commitments received from counter-participants
+// optional filtering by domain, time ranges, counter participants, commitment state and verbosity
+message LookupReceivedAcsCommitments {
+ message Request {
+ // filter specific time ranges per domain
+ // a domain can appear multiple times with various time ranges, in which case we consider the union of the time ranges.
+ // return only the received commitments with an interval overlapping any of the given time ranges per domain
+ // defaults: if empty, all domains known to the participant are considered
+ repeated DomainTimeRange time_ranges = 1;
+ // retrieve commitments received from specific counter participants
+ // if a specified counter participant is not a counter-participant in some domain, we do not return it in the response
+ // an empty set means we return commitments received from all counter participants on the domains matching the domain filter.
+ repeated string counter_participant_uids = 2;
+ // filter by commitment state: only return commitments with the states below
+ // if no state is given, we return all commitments
+ repeated ReceivedCommitmentState commitment_state = 3;
+ // include the actual commitment in the response
+ bool verbose = 4;
+ }
+
+ // Returns a sequence of commitments for each domain.
+ // Domains should not repeat in the response, otherwise the caller considers the response invalid.
+ // If all commitments received on a domain have been pruned, we return an error.
+ // No streaming, because a response with verbose mode on contains around 1kb to 3kb of data (depending on whether
+ // we ship the LtHash16 bytes directly or just a hash thereof).
+ // Therefore, 1000 commitments fit into a couple of MBs, and we can expect the gRPC admin API to handle messages of
+ // a couple of MBs.
+ // It is the application developer's job to find suitable filter ranges.
+ message Response {
+ repeated ReceivedAcsCommitmentPerDomain received = 1;
+ }
+}
+
+// timestamps *do not* have to fall on commitment period boundaries/ticks
+message TimeRange {
+ google.protobuf.Timestamp from_exclusive = 1;
+ google.protobuf.Timestamp to_inclusive = 2;
+}
+
+message DomainTimeRange {
+ string domain_id = 1;
+ // optional; if not given, the latest reconciliation period the participant knows of for that domain is considered
+ optional TimeRange interval = 2;
+}
+
+// timestamps *do fall* on commitment period boundaries/ticks
+message Interval {
+ google.protobuf.Timestamp start_tick_exclusive = 1;
+ google.protobuf.Timestamp end_tick_inclusive = 2;
+}
+
+message ReceivedAcsCommitment {
+ Interval interval = 1;
+ // the counter participant that computed and sent the commitment, from whom the current participant received the commitment
+ string origin_counter_participant_uid = 2;
+ // the commitment received from the counter participant, unsigned because the admin trusts own participant's reply
+ // populated only if verbose mode is on
+ optional bytes received_commitment = 3;
+ // own commitment of participant that was compared with the received commitment, unsigned because the admin trusts own participant's reply
+ // populated only in case there is a mismatch and verbose mode is on
+ // might not correspond to the same interval as the received commitment, however, the matching timestamp is the end of
+ // the returned interval
+ optional bytes own_commitment = 4;
+ ReceivedCommitmentState state = 5;
+}
+
+message ReceivedAcsCommitmentPerDomain {
+ string domain_id = 1;
+ repeated ReceivedAcsCommitment received = 2;
+}
+
+enum ReceivedCommitmentState {
+ RECEIVED_COMMITMENT_STATE_UNSPECIFIED = 0;
+ RECEIVED_COMMITMENT_STATE_MATCH = 1;
+ RECEIVED_COMMITMENT_STATE_MISMATCH = 2;
+ // buffered commitments were not compared yet with the participant's commitments
+ RECEIVED_COMMITMENT_STATE_BUFFERED = 3;
+ // outstanding commitments were not received yet
+ RECEIVED_COMMITMENT_STATE_OUTSTANDING = 4;
+}
+
+message SentAcsCommitment {
+ Interval interval = 1;
+ // the counter participant to whom we sent the commitment
+ string dest_counter_participant_uid = 2;
+ // own computed commitment sent to counter participant, unsigned because the admin trusts own participant's reply
+ // populated only if verbose mode is on
+ optional bytes own_commitment = 3;
+ // commitment of the counter participant that was compared with own commitment, unsigned because the admin trusts own participant's reply
+ // populated only in case there is a mismatch and verbose mode is on
+ // might not correspond to the same interval as the sent commitment, however, the mismatch timestamp is the end of
+ // the returned interval
+ optional bytes received_commitment = 4;
+ SentCommitmentState state = 5;
+}
+
+message SentAcsCommitmentPerDomain {
+ string domain_id = 1;
+ repeated SentAcsCommitment sent = 2;
+}
+
+enum SentCommitmentState {
+ SENT_COMMITMENT_STATE_UNSPECIFIED = 0;
+ SENT_COMMITMENT_STATE_MATCH = 1;
+ SENT_COMMITMENT_STATE_MISMATCH = 2;
+ // commitment was not compared yet with the counter-participant's commitments, because, e.g., the counter-participant
+ // commitment has not been received yet
+ SENT_COMMITMENT_STATE_NOT_COMPARED = 3;
+}
+
+// list the commitments computed and sent to counter-participants
+// optional filtering by domain, time ranges, counter participants, commitment state and verbosity
+message LookupSentAcsCommitments {
+ message Request {
+ // filter specific time ranges per domain
+ // a domain can appear multiple times with various time ranges, in which case we consider the union of the time ranges.
+ // return only the sent commitments with an interval overlapping any of the given time ranges per domain
+ // defaults: if empty, all domains known to the participant are considered
+ repeated DomainTimeRange time_ranges = 1;
+ // retrieve commitments sent to specific counter participants
+ // if a specified counter participant is not a counter-participant in some domain, we do not return it in the response
+ // an empty set means we return commitments sent to all counter participants on the domains matching the domain filter.
+ repeated string counter_participant_uids = 2;
+ // filter by commitment state: only return commitments with the states below
+ // if no state is given, we return all commitments
+ repeated SentCommitmentState commitment_state = 3;
+ // include the actual commitment in the response
+ bool verbose = 4;
+ }
+
+ // Returns a sequence of commitments for each domain.
+ // Domains should not repeat in the response, otherwise the caller considers the response invalid.
+ // If all commitments sent on a domain have been pruned, we return an error.
+ // No streaming, because a response with verbose mode on contains around 1kb to 3kb of data (depending on whether
+ // we ship the LtHash16 bytes directly or just a hash thereof).
+ // Therefore, 1000 commitments fit into a couple of MBs, and we can expect the gRPC admin API to handle messages of
+ // a couple of MBs.
+ // It is the application developer's job to find suitable filter ranges.
+ message Response {
+ repeated SentAcsCommitmentPerDomain sent = 1;
+ }
+}
+
/*
The configuration concerns the following metrics, and each of the metrics is issued per domain:
- The maximum number of intervals that a distiguished participant falls behind
@@ -100,7 +250,6 @@ The configuration concerns the following metrics, and each of the metrics is iss
reconciliation intervals.
- Selected participants for which we publish independent metrics counting how many intervals they are behind
*/
-
message SlowCounterParticipantDomainConfig {
// the domains for which we apply the settings below
repeated string domain_ids = 1;
diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala
index a111d5d3e0..70fd79ce16 100644
--- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala
+++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala
@@ -31,7 +31,7 @@ import com.digitalasset.canton.admin.participant.v30.{ResourceLimits as _, *}
import com.digitalasset.canton.admin.pruning
import com.digitalasset.canton.admin.pruning.v30.{NoWaitCommitmentsSetup, WaitCommitmentsSetup}
import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt}
-import com.digitalasset.canton.data.CantonTimestamp
+import com.digitalasset.canton.data.{CantonTimestamp, CantonTimestampSecond}
import com.digitalasset.canton.logging.TracedLogger
import com.digitalasset.canton.participant.admin.ResourceLimits
import com.digitalasset.canton.participant.admin.grpc.{
@@ -40,13 +40,19 @@ import com.digitalasset.canton.participant.admin.grpc.{
}
import com.digitalasset.canton.participant.admin.traffic.TrafficStateAdmin
import com.digitalasset.canton.participant.domain.DomainConnectionConfig as CDomainConnectionConfig
-import com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor.SharedContractsState
+import com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor.{
+ ReceivedCmtState,
+ SentCmtState,
+ SharedContractsState,
+}
import com.digitalasset.canton.participant.sync.UpstreamOffsetConvert
import com.digitalasset.canton.protocol.LfContractId
+import com.digitalasset.canton.protocol.messages.{AcsCommitment, CommitmentPeriod}
import com.digitalasset.canton.sequencing.SequencerConnectionValidation
import com.digitalasset.canton.sequencing.protocol.TrafficState
import com.digitalasset.canton.serialization.ProtoConverter
import com.digitalasset.canton.serialization.ProtoConverter.InstantConverter
+import com.digitalasset.canton.time.PositiveSeconds
import com.digitalasset.canton.topology.{DomainId, ParticipantId, PartyId}
import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.BinaryFileUtil
@@ -1106,6 +1112,228 @@ object ParticipantAdminCommands {
Right(response.offset)
}
+ // TODO(#18451) R5: The code below should be sufficient.
+ final case class LookupReceivedAcsCommitments(
+ domainTimeRanges: Seq[DomainTimeRange],
+ counterParticipants: Seq[ParticipantId],
+ commitmentState: Seq[ReceivedCmtState],
+ verboseMode: Boolean,
+ ) extends Base[
+ v30.LookupReceivedAcsCommitments.Request,
+ v30.LookupReceivedAcsCommitments.Response,
+ Map[DomainId, Seq[ReceivedAcsCmt]],
+ ] {
+
+ override def createRequest() = Right(
+ v30.LookupReceivedAcsCommitments
+ .Request(
+ domainTimeRanges.map { case domainTimeRange =>
+ v30.DomainTimeRange(
+ domainTimeRange.domain.toProtoPrimitive,
+ domainTimeRange.timeRange.map { timeRange =>
+ v30.TimeRange(
+ Some(timeRange.startExclusive.toProtoTimestamp),
+ Some(timeRange.endInclusive.toProtoTimestamp),
+ )
+ },
+ )
+ },
+ counterParticipants.map(_.toProtoPrimitive),
+ commitmentState.map(_.toProtoV30),
+ verboseMode,
+ )
+ )
+
+ override def submitRequest(
+ service: InspectionServiceStub,
+ request: v30.LookupReceivedAcsCommitments.Request,
+ ): Future[v30.LookupReceivedAcsCommitments.Response] =
+ service.lookupReceivedAcsCommitments(request)
+
+ override def handleResponse(
+ response: v30.LookupReceivedAcsCommitments.Response
+ ): Either[
+ String,
+ Map[DomainId, Seq[ReceivedAcsCmt]],
+ ] = {
+ if (response.received.size != response.received.map(_.domainId).toSet.size)
+ Left(
+ s"Some domains are not unique in the response: ${response.received}"
+ )
+ else
+ response.received
+ .traverse(receivedCmtPerDomain =>
+ for {
+ domainId <- DomainId.fromString(receivedCmtPerDomain.domainId)
+ receivedCmts <- receivedCmtPerDomain.received
+ .map(fromProtoToReceivedAcsCmt)
+ .sequence
+ } yield domainId -> receivedCmts
+ )
+ .map(_.toMap)
+ }
+ }
+
+ final case class TimeRange(startExclusive: CantonTimestamp, endInclusive: CantonTimestamp)
+
+ final case class DomainTimeRange(domain: DomainId, timeRange: Option[TimeRange])
+
+ final case class ReceivedAcsCmt(
+ receivedCmtPeriod: CommitmentPeriod,
+ originCounterParticipant: ParticipantId,
+ receivedCommitment: Option[AcsCommitment.CommitmentType],
+ localCommitment: Option[AcsCommitment.CommitmentType],
+ state: ReceivedCmtState,
+ )
+
+ private def fromIntervalToCommitmentPeriod(
+ interval: Option[v30.Interval]
+ ): Either[String, CommitmentPeriod] = {
+ interval match {
+ case None => Left("Interval is missing")
+ case Some(v) =>
+ for {
+ from <- v.startTickExclusive
+ .traverse(CantonTimestamp.fromProtoTimestamp)
+ .leftMap(_.toString)
+ fromSecond <- CantonTimestampSecond.fromCantonTimestamp(
+ from.getOrElse(CantonTimestamp.MinValue)
+ )
+ to <- v.endTickInclusive
+ .traverse(CantonTimestamp.fromProtoTimestamp)
+ .leftMap(_.toString)
+ toSecond <- CantonTimestampSecond.fromCantonTimestamp(
+ to.getOrElse(CantonTimestamp.MinValue)
+ )
+ len <- PositiveSeconds.create(
+ java.time.Duration.ofSeconds(
+ toSecond.minusSeconds(fromSecond.getEpochSecond).getEpochSecond
+ )
+ )
+ } yield CommitmentPeriod(fromSecond, len)
+ }
+ }
+
+ private def fromProtoToReceivedAcsCmt(
+ cmt: v30.ReceivedAcsCommitment
+ ): Either[String, ReceivedAcsCmt] = {
+ for {
+ state <- ReceivedCmtState.fromProtoV30(cmt.state).leftMap(_.toString)
+ period <- fromIntervalToCommitmentPeriod(cmt.interval)
+ participantId <- ParticipantId
+ .fromProtoPrimitive(cmt.originCounterParticipantUid, "")
+ .leftMap(_.toString)
+ } yield ReceivedAcsCmt(
+ period,
+ participantId,
+ Option
+ .when(cmt.receivedCommitment.isDefined)(
+ cmt.receivedCommitment.map(AcsCommitment.commitmentTypeFromByteString)
+ )
+ .flatten,
+ Option
+ .when(cmt.ownCommitment.isDefined)(
+ cmt.ownCommitment.map(AcsCommitment.commitmentTypeFromByteString)
+ )
+ .flatten,
+ state,
+ )
+ }
+
+ // TODO(#18451) R5: The code below should be sufficient.
+ final case class LookupSentAcsCommitments(
+ domainTimeRanges: Seq[DomainTimeRange],
+ counterParticipants: Seq[ParticipantId],
+ commitmentState: Seq[SentCmtState],
+ verboseMode: Boolean,
+ ) extends Base[
+ v30.LookupSentAcsCommitments.Request,
+ v30.LookupSentAcsCommitments.Response,
+ Map[DomainId, Seq[SentAcsCmt]],
+ ] {
+
+ override def createRequest() = Right(
+ v30.LookupSentAcsCommitments
+ .Request(
+ domainTimeRanges.map { case domainTimeRange =>
+ v30.DomainTimeRange(
+ domainTimeRange.domain.toProtoPrimitive,
+ domainTimeRange.timeRange.map { timeRange =>
+ v30.TimeRange(
+ Some(timeRange.startExclusive.toProtoTimestamp),
+ Some(timeRange.endInclusive.toProtoTimestamp),
+ )
+ },
+ )
+ },
+ counterParticipants.map(_.toProtoPrimitive),
+ commitmentState.map(_.toProtoV30),
+ verboseMode,
+ )
+ )
+
+ override def submitRequest(
+ service: InspectionServiceStub,
+ request: v30.LookupSentAcsCommitments.Request,
+ ): Future[v30.LookupSentAcsCommitments.Response] =
+ service.lookupSentAcsCommitments(request)
+
+ override def handleResponse(
+ response: v30.LookupSentAcsCommitments.Response
+ ): Either[
+ String,
+ Map[DomainId, Seq[SentAcsCmt]],
+ ] = {
+ if (response.sent.size != response.sent.map(_.domainId).toSet.size)
+ Left(
+ s"Some domains are not unique in the response: ${response.sent}"
+ )
+ else
+ response.sent
+ .traverse(sentCmtPerDomain =>
+ for {
+ domainId <- DomainId.fromString(sentCmtPerDomain.domainId)
+ sentCmts <- sentCmtPerDomain.sent.map(fromProtoToSentAcsCmt).sequence
+ } yield domainId -> sentCmts
+ )
+ .map(_.toMap)
+ }
+ }
+
+ final case class SentAcsCmt(
+ receivedCmtPeriod: CommitmentPeriod,
+ destCounterParticipant: ParticipantId,
+ sentCommitment: Option[AcsCommitment.CommitmentType],
+ receivedCommitment: Option[AcsCommitment.CommitmentType],
+ state: SentCmtState,
+ )
+
+ private def fromProtoToSentAcsCmt(
+ cmt: v30.SentAcsCommitment
+ ): Either[String, SentAcsCmt] = {
+ for {
+ state <- SentCmtState.fromProtoV30(cmt.state).leftMap(_.toString)
+ period <- fromIntervalToCommitmentPeriod(cmt.interval)
+ participantId <- ParticipantId
+ .fromProtoPrimitive(cmt.destCounterParticipantUid, "")
+ .leftMap(_.toString)
+ } yield SentAcsCmt(
+ period,
+ participantId,
+ Option
+ .when(cmt.ownCommitment.isDefined)(
+ cmt.ownCommitment.map(AcsCommitment.commitmentTypeFromByteString)
+ )
+ .flatten,
+ Option
+ .when(cmt.receivedCommitment.isDefined)(
+ cmt.receivedCommitment.map(AcsCommitment.commitmentTypeFromByteString)
+ )
+ .flatten,
+ state,
+ )
+ }
+
// TODO(#10436) R7: The code below should be sufficient.
final case class SetConfigForSlowCounterParticipants(
configs: Seq[SlowCounterParticipantDomainConfig]
@@ -1243,6 +1471,9 @@ object ParticipantAdminCommands {
response.intervalsBehind.map { info =>
for {
domainId <- DomainId.fromString(info.domainId)
+ participantId <- ParticipantId
+ .fromProtoPrimitive(info.counterParticipantUid, "")
+ .leftMap(_.toString)
asOf <- ProtoConverter
.parseRequired(
CantonTimestamp.fromProtoTimestamp,
@@ -1250,10 +1481,11 @@ object ParticipantAdminCommands {
info.asOfSequencingTimestamp,
)
.leftMap(_.toString)
+ intervalsBehind <- PositiveInt.create(info.intervalsBehind.toInt).leftMap(_.toString)
} yield CounterParticipantInfo(
- ParticipantId.tryFromProtoPrimitive(info.counterParticipantUid),
+ participantId,
domainId,
- PositiveInt.tryCreate(info.intervalsBehind.toInt),
+ intervalsBehind,
asOf.toInstant,
)
}.sequence
@@ -1447,8 +1679,11 @@ object ParticipantAdminCommands {
state <- SharedContractsState
.fromProtoV30(setup.counterParticipantState)
.leftMap(_.toString)
+ participantId <- ParticipantId
+ .fromProtoPrimitive(setup.counterParticipantUid, "")
+ .leftMap(_.toString)
} yield NoWaitCommitments(
- ParticipantId.tryFromProtoPrimitive(setup.counterParticipantUid),
+ participantId,
setup.timestampOrOffsetActive match {
case NoWaitCommitmentsSetup.TimestampOrOffsetActive.SequencingTimestamp(_) =>
ts.toInstant.asLeft[ParticipantOffset]
@@ -1463,7 +1698,7 @@ object ParticipantAdminCommands {
s.map(
_.getOrElse(
NoWaitCommitments(
- ParticipantId.tryFromProtoPrimitive("error"),
+ ParticipantId.tryFromProtoPrimitive("PAR::participant::error"),
Left(Instant.EPOCH),
Seq.empty,
SharedContractsState.NoSharedContracts,
@@ -1543,7 +1778,7 @@ object ParticipantAdminCommands {
s.map(
_.getOrElse(
WaitCommitments(
- ParticipantId.tryFromProtoPrimitive("error"),
+ ParticipantId.tryFromProtoPrimitive("PAR::participant::error"),
Seq.empty,
SharedContractsState.NoSharedContracts,
)
diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala
index 527fffee80..a460e9c41f 100644
--- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala
+++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala
@@ -10,8 +10,13 @@ import com.daml.ledger.api.v2.participant_offset.ParticipantOffset
import com.daml.nonempty.NonEmpty
import com.digitalasset.canton.admin.api.client.commands.ParticipantAdminCommands.Inspection.{
CounterParticipantInfo,
+ DomainTimeRange,
GetConfigForSlowCounterParticipants,
GetIntervalsBehindForCounterParticipants,
+ LookupReceivedAcsCommitments,
+ LookupSentAcsCommitments,
+ ReceivedAcsCmt,
+ SentAcsCmt,
SetConfigForSlowCounterParticipants,
SlowCounterParticipantDomainConfig,
}
@@ -64,6 +69,10 @@ import com.digitalasset.canton.participant.admin.ResourceLimits
import com.digitalasset.canton.participant.admin.grpc.TransferSearchResult
import com.digitalasset.canton.participant.admin.inspection.SyncStateInspection
import com.digitalasset.canton.participant.domain.DomainConnectionConfig
+import com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor.{
+ ReceivedCmtState,
+ SentCmtState,
+}
import com.digitalasset.canton.participant.sync.TimestampedEvent
import com.digitalasset.canton.protocol.messages.{
AcsCommitment,
@@ -767,6 +776,94 @@ class LocalCommitmentsAdministrationGroup(
)
}
+ // TODO(#18451) R5
+ @Help.Summary(
+ "List the counter-participants of a participant and the ACS commitments received from them together with" +
+ "the commitment state."
+ )
+ @Help.Description(
+ """Optional filtering through the arguments:
+ | domainTimeRanges: Lists commitments received on the given domains whose period overlaps with any of the given
+ | time ranges per domain.
+ | If the list is empty, considers all domains the participant is connected to.
+ | For domains with an empty time range, considers the latest period the participant knows of for that domain.
+ | Domains can appear multiple times in the list with various time ranges, in which case we consider the
+ | union of the time ranges.
+ |counterParticipants: Lists commitments received only from the given counter-participants. If a counter-participant
+ | is not a counter-participant on some domain, no commitments appear in the reply from that counter-participant
+ | on that domain.
+ |commitmentState: Lists commitments that are in one of the given states. By default considers all states:
+ | - MATCH: the remote commitment matches the local commitment
+ | - MISMATCH: the remote commitment does not match the local commitment
+ | - BUFFERED: the remote commitment is buffered because the corresponding local commitment has not been computed yet
+ | - OUTSTANDING: we expect a remote commitment that has not yet been received
+ |verboseMode: If false, the reply does not contain the commitment bytes. If true, the reply contains:
+ | - In case of a mismatch, the reply contains both the received and the locally computed commitment that do not match.
+ | - In case of outstanding, the reply does not contain any commitment.
+ | - In all other cases (match and buffered), the reply contains the received commitment.
+ """
+ )
+ def lookup_received_acs_commitments(
+ domainTimeRanges: Seq[DomainTimeRange],
+ counterParticipants: Seq[ParticipantId],
+ commitmentState: Seq[ReceivedCmtState],
+ verboseMode: Boolean,
+ ): Map[DomainId, Seq[ReceivedAcsCmt]] =
+ consoleEnvironment.run(
+ runner.adminCommand(
+ LookupReceivedAcsCommitments(
+ domainTimeRanges,
+ counterParticipants,
+ commitmentState,
+ verboseMode,
+ )
+ )
+ )
+
+ // TODO(#18451) R5
+ @Help.Summary(
+ "List the counter-participants of a participant and the ACS commitments that the participant computed and sent to" +
+ "them, together with the commitment state."
+ )
+ @Help.Description(
+ """Optional filtering through the arguments:
+ | domainTimeRanges: Lists commitments received on the given domains whose period overlap with any of the
+ | given time ranges per domain.
+ | If the list is empty, considers all domains the participant is connected to.
+ | For domains with an empty time range, considers the latest period the participant knows of for that domain.
+ | Domains can appear multiple times in the list with various time ranges, in which case we consider the
+ | union of the time ranges.
+ |counterParticipants: Lists commitments sent only to the given counter-participants. If a counter-participant
+ | is not a counter-participant on some domain, no commitments appear in the reply for that counter-participant
+ | on that domain.
+ |commitmentState: Lists sent commitments that are in one of the given states. By default considers all states:
+ | - MATCH: the local commitment matches the remote commitment
+ | - MISMATCH: the local commitment does not match the remote commitment
+ | - NOT_COMPARED: the local commitment has been computed and sent but no corresponding remote commitment has
+ | been received
+ |verboseMode: If false, the reply does not contain the commitment bytes. If true, the reply contains:
+ | - In case of a mismatch, the reply contains both the received and the locally computed commitment that
+ | do not match.
+ | - In all other cases (match and not compared), the reply contains the sent commitment.
+ """
+ )
+ def lookup_sent_acs_commitments(
+ domainTimeRanges: Seq[DomainTimeRange],
+ counterParticipants: Seq[ParticipantId],
+ commitmentState: Seq[SentCmtState],
+ verboseMode: Boolean,
+ ): Map[DomainId, Seq[SentAcsCmt]] =
+ consoleEnvironment.run(
+ runner.adminCommand(
+ LookupSentAcsCommitments(
+ domainTimeRanges,
+ counterParticipants,
+ commitmentState,
+ verboseMode,
+ )
+ )
+ )
+
// TODO(#18453) R6: The code below should be sufficient.
@Help.Summary(
"Disable waiting for commitments from the given counter-participants."
@@ -779,7 +876,7 @@ class LocalCommitmentsAdministrationGroup(
|Returns an error if `startingAt` does not translate to an existing offset.
|If the participant set is empty, the command does nothing."""
)
- def setNoWaitCommitmentsFrom(
+ def set_no_wait_commitments_from(
counterParticipants: Seq[ParticipantId],
domainIds: Seq[DomainId],
startingAt: Either[Instant, ParticipantOffset],
@@ -806,7 +903,7 @@ class LocalCommitmentsAdministrationGroup(
|The command returns a map of counter-participants and the domains for which the setting was changed.
|If the participant set is empty or the domain set is empty, the command does nothing."""
)
- def setWaitCommitmentsFrom(
+ def set_wait_commitments_from(
counterParticipants: Seq[ParticipantId],
domainIds: Seq[DomainId],
): Map[ParticipantId, Seq[DomainId]] = {
@@ -834,7 +931,7 @@ class LocalCommitmentsAdministrationGroup(
|Even if some participants may not be connected to some domains at the time the query executes, the response still
|includes them if they are known to the participant or specified in the arguments."""
)
- def getNoWaitCommitmentsFrom(
+ def get_no_wait_commitments_from(
domains: Seq[DomainId],
counterParticipants: Seq[ParticipantId],
): (Seq[NoWaitCommitments], Seq[WaitCommitments]) =
@@ -863,7 +960,7 @@ class LocalCommitmentsAdministrationGroup(
| reconciliation intervals.
| - Separate metric for each participant in `individualMetrics` argument tracking how many intervals that
|participant is behind""")
- def setConfigForSlowCounterParticipants(
+ def set_config_for_slow_counter_participants(
configs: Seq[SlowCounterParticipantDomainConfig]
): Unit = {
consoleEnvironment.run(
@@ -876,25 +973,25 @@ class LocalCommitmentsAdministrationGroup(
}
// TODO(#10436) R7
- def addConfigForSlowCounterParticipants(
+ def add_config_for_slow_counter_participants(
counterParticipantsDistinguished: Seq[ParticipantId],
domains: Seq[DomainId],
) = ???
// TODO(#10436) R7
- def removeConfigForSlowCounterParticipants(
+ def remove_config_for_slow_counter_participants(
counterParticipantsDistinguished: Seq[ParticipantId],
domains: Seq[DomainId],
) = ???
// TODO(#10436) R7
- def addParticipanttoIndividualMetrics(
+ def add_participant_to_individual_metrics(
individualMetrics: Seq[ParticipantId],
domains: Seq[DomainId],
) = ???
// TODO(#10436) R7
- def removeParticipantFromIndividualMetrics(
+ def remove_participant_from_individual_metrics(
individualMetrics: Seq[ParticipantId],
domains: Seq[DomainId],
) = ???
@@ -914,7 +1011,7 @@ class LocalCommitmentsAdministrationGroup(
| - Parameters `thresholdDistinguished` and `thresholdDefault`
| - The participants in `individualMetrics`, which have individual metrics per participant showing how many
reconciliation intervals that participant is behind""")
- def getConfigForSlowCounterParticipants(
+ def get_config_for_slow_counter_participants(
domains: Seq[DomainId]
): Seq[SlowCounterParticipantDomainConfig] = {
consoleEnvironment.run(
@@ -937,7 +1034,7 @@ class LocalCommitmentsAdministrationGroup(
// TODO(#10436) R7: Return the slow counter participant config for the given domains and counterParticipants
// Filter the gRPC response of `getConfigForSlowCounterParticipants` with `counterParticipants`
- def getConfigForSlowCounterParticipant(
+ def get_config_for_slow_counter_participant(
domains: Seq[DomainId],
counterParticipants: Seq[ParticipantId],
): Seq[SlowCounterParticipantInfo] = Seq.empty
@@ -953,7 +1050,7 @@ class LocalCommitmentsAdministrationGroup(
|Counter-participants that never sent a commitment appear in the output only if they're explicitly given in
|`counterParticipants`. For such counter-participant that never sent a commitment, the output shows they are
|behind by MaxInt""")
- def getIntervalsBehindForCounterParticipants(
+ def get_intervals_behind_for_counter_participants(
counterParticipants: Seq[ParticipantId],
domains: Seq[DomainId],
threshold: Option[NonNegativeInt],
diff --git a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/participant_transaction.proto b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/participant_transaction.proto
index 6b1552643c..77073f51b2 100644
--- a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/participant_transaction.proto
+++ b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/participant_transaction.proto
@@ -200,6 +200,7 @@ message ActionDescription {
string input_contract_id = 1;
repeated string actors = 2;
bool by_key = 3;
+ string template_id = 4;
}
message LookupByKeyActionDescription {
diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ActionDescription.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ActionDescription.scala
index 0c826a18fe..2e41825713 100644
--- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ActionDescription.scala
+++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ActionDescription.scala
@@ -155,7 +155,7 @@ object ActionDescription extends HasProtocolVersionedCompanion[ActionDescription
case LfNodeFetch(
inputContract,
_packageName,
- _templateId,
+ templateId,
actingParties,
_signatories,
_stakeholders,
@@ -174,7 +174,7 @@ object ActionDescription extends HasProtocolVersionedCompanion[ActionDescription
actingParties,
InvalidActionDescription("Fetch node without acting parties"),
)
- } yield FetchActionDescription(inputContract, actors, byKey)(
+ } yield FetchActionDescription(inputContract, actors, byKey, templateId)(
protocolVersionRepresentativeFor(protocolVersion)
)
@@ -275,11 +275,17 @@ object ActionDescription extends HasProtocolVersionedCompanion[ActionDescription
f: v30.ActionDescription.FetchActionDescription,
pv: RepresentativeProtocolVersion[ActionDescription.type],
): ParsingResult[FetchActionDescription] = {
- val v30.ActionDescription.FetchActionDescription(inputContractIdP, actorsP, byKey) = f
+ val v30.ActionDescription.FetchActionDescription(
+ inputContractIdP,
+ actorsP,
+ byKey,
+ templateIdP,
+ ) = f
for {
inputContractId <- ProtoConverter.parseLfContractId(inputContractIdP)
actors <- actorsP.traverse(ProtoConverter.parseLfPartyId).map(_.toSet)
- } yield FetchActionDescription(inputContractId, actors, byKey)(pv)
+ templateId <- RefIdentifierSyntax.fromProtoPrimitive(templateIdP)
+ } yield FetchActionDescription(inputContractId, actors, byKey, templateId)(pv)
}
private[data] def fromProtoV30(
@@ -445,6 +451,7 @@ object ActionDescription extends HasProtocolVersionedCompanion[ActionDescription
inputContractId: LfContractId,
actors: Set[LfPartyId],
override val byKey: Boolean,
+ templateId: LfTemplateId,
)(
override val representativeProtocolVersion: RepresentativeProtocolVersion[
ActionDescription.type
@@ -460,6 +467,7 @@ object ActionDescription extends HasProtocolVersionedCompanion[ActionDescription
inputContractId = inputContractId.toProtoPrimitive,
actors = actors.toSeq,
byKey = byKey,
+ templateId = new RefIdentifierSyntax(templateId).toProtoPrimitive,
)
)
diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ViewParticipantData.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ViewParticipantData.scala
index 5a0c70d1d0..4a0010c46d 100644
--- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ViewParticipantData.scala
+++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/data/ViewParticipantData.scala
@@ -219,14 +219,14 @@ final case class ViewParticipantData private (
}
RootAction(cmd, actors, failed, packagePreference)
- case FetchActionDescription(inputContractId, actors, byKey) =>
+ case FetchActionDescription(inputContractId, actors, byKey, templateId) =>
val inputContract = coreInputs.getOrElse(
inputContractId,
throw InvalidViewParticipantData(
show"Input contract $inputContractId of the Fetch root action is not declared as core input."
),
)
- val templateId = inputContract.contract.contractInstance.unversioned.template
+
val cmd = if (byKey) {
val key = inputContract.contract.metadata.maybeKey
.map(_.key)
diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/data/GeneratorsData.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/data/GeneratorsData.scala
index 39e09caef4..3e7e01bfb0 100644
--- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/data/GeneratorsData.scala
+++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/data/GeneratorsData.scala
@@ -220,7 +220,8 @@ final class GeneratorsData(
inputContractId <- Arbitrary.arbitrary[LfContractId]
actors <- Gen.containerOf[Set, LfPartyId](Arbitrary.arbitrary[LfPartyId])
byKey <- Gen.oneOf(true, false)
- } yield FetchActionDescription(inputContractId, actors, byKey)(rpv)
+ templateId <- Arbitrary.arbitrary[LfTemplateId]
+ } yield FetchActionDescription(inputContractId, actors, byKey, templateId)(rpv)
private def lookupByKeyActionDescriptionGenFor(
rpv: RepresentativeProtocolVersion[ActionDescription.type]
diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala
index 5754d00799..9378137bac 100644
--- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala
+++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala
@@ -71,6 +71,7 @@ object ExampleTransactionFactory {
// Helper methods for Daml-LF types
val languageVersion = LfTransactionBuilder.defaultLanguageVersion
val packageId = LfTransactionBuilder.defaultPackageId
+ val upgradePackageId = LfPackageId.assertFromString("upgraded-pkg-id")
val templateId = LfTransactionBuilder.defaultTemplateId
val packageName = LfTransactionBuilder.defaultPackageName
val someOptUsedPackages = Some(Set(packageId))
@@ -140,6 +141,7 @@ object ExampleTransactionFactory {
key: Option[LfGlobalKeyWithMaintainers] = None,
byKey: Boolean = false,
version: LfTransactionVersion = transactionVersion,
+ templateId: LfTemplateId = templateId,
): LfNodeFetch =
LfNodeFetch(
coid = cid,
@@ -371,7 +373,7 @@ object ExampleTransactionFactory {
packages =
Seq(submittingParticipant, signatoryParticipant, observerParticipant, extraParticipant)
.map(
- _ -> Seq(ExampleTransactionFactory.packageId)
+ _ -> Seq(ExampleTransactionFactory.packageId, upgradePackageId)
)
.toMap,
)
@@ -593,6 +595,7 @@ class ExampleTransactionFactory(
resolvedKeys: Map[LfGlobalKey, SerializableKeyResolution],
seed: Option[LfHash],
isRoot: Boolean,
+ packagePreference: Set[LfPackageId],
subviews: TransactionView*
): TransactionView = {
@@ -637,7 +640,7 @@ class ExampleTransactionFactory(
ActionDescription.tryFromLfActionNode(
LfTransactionUtil.lightWeight(node),
seed,
- packagePreference = Set.empty,
+ packagePreference = packagePreference,
protocolVersion,
)
@@ -904,7 +907,7 @@ class ExampleTransactionFactory(
)
lazy val view0: TransactionView =
- view(node, 0, consumed, used, created, Map.empty, nodeSeed, isRoot = true)
+ view(node, 0, consumed, used, created, Map.empty, nodeSeed, isRoot = true, Set.empty)
override lazy val rootViews: Seq[TransactionView] = Seq(view0)
@@ -1136,7 +1139,7 @@ class ExampleTransactionFactory(
consuming: Boolean = true,
) extends SingleNode(Some(seed)) {
val upgradedTemplateId: canton.protocol.LfTemplateId =
- templateId.copy(packageId = LfPackageId.assertFromString("upgraded"))
+ templateId.copy(packageId = upgradePackageId)
private def genNode(id: LfContractId): LfNodeExercises =
exerciseNode(targetCoid = id, templateId = upgradedTemplateId, signatories = Set(submitter))
override def node: LfNodeExercises = genNode(contractId)
@@ -1254,6 +1257,7 @@ class ExampleTransactionFactory(
Map.empty,
ex.nodeSeed,
isRoot = true,
+ Set.empty,
)
}
@@ -1572,6 +1576,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create0seed),
isRoot = true,
+ Set.empty,
)
val view10: TransactionView =
@@ -1584,6 +1589,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create12seed),
isRoot = false,
+ Set.empty,
)
// TODO(#19611): Merge the informees and quorums of the child action nodes to this view and enable test in standardHappyCases
@@ -1604,6 +1610,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1)),
isRoot = true,
+ Set.empty,
view10,
)
@@ -2004,6 +2011,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create0seed),
isRoot = true,
+ Set.empty,
)
val view10: TransactionView =
view(
@@ -2015,6 +2023,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create130seed),
isRoot = false,
+ Set.empty,
)
val view110: TransactionView =
view(
@@ -2026,6 +2035,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create1310seed),
isRoot = false,
+ Set.empty,
)
val view11: TransactionView =
@@ -2045,6 +2055,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1, 3, 1)),
isRoot = false,
+ Set.empty,
view110,
)
@@ -2068,6 +2079,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1)),
isRoot = true,
+ Set.empty,
view10,
view11,
)
@@ -2561,6 +2573,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create0seed),
isRoot = true,
+ Set.empty,
)
val view100: TransactionView =
@@ -2573,6 +2586,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create100seed),
isRoot = false,
+ Set.empty,
)
val view10: TransactionView = view(
@@ -2591,6 +2605,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1, 0)),
isRoot = false,
+ Set.empty,
view100,
)
@@ -2604,6 +2619,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create120seed),
isRoot = false,
+ Set.empty,
)
val view11: TransactionView =
@@ -2623,6 +2639,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1, 2)),
isRoot = false,
+ Set.empty,
view110,
)
@@ -2646,6 +2663,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1)),
isRoot = true,
+ Set.empty,
view10,
view11,
)
@@ -2660,6 +2678,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create2seed),
isRoot = true,
+ Set.empty,
)
override lazy val rootViews: Seq[TransactionView] = Seq(view0, view1, view2)
@@ -3053,6 +3072,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(create0seed),
isRoot = true,
+ Set.empty,
)
val view10: TransactionView = view(
@@ -3071,6 +3091,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1, 1)),
isRoot = false,
+ Set.empty,
)
val view1: TransactionView = view(
@@ -3089,6 +3110,7 @@ class ExampleTransactionFactory(
Map.empty,
Some(deriveNodeSeed(1)),
isRoot = true,
+ Set.empty,
view10,
)
diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/block/update/SubmissionRequestValidator.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/block/update/SubmissionRequestValidator.scala
index e24cd12553..562700550e 100644
--- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/block/update/SubmissionRequestValidator.scala
+++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/block/update/SubmissionRequestValidator.scala
@@ -480,22 +480,6 @@ private[update] final class SubmissionRequestValidator(
mapping <- EitherT.right[SubmissionRequestOutcome](
topologyOrSequencingSnapshot.ipsSnapshot.activeParticipantsOfParties(parties.toSeq)
)
- _ <- mapping.toList.parTraverse { case (party, participants) =>
- val nonRegistered = participants.filterNot(isMemberRegistered(state))
- EitherT.cond[Future](
- nonRegistered.isEmpty,
- (),
- // TODO(#14322): review if still applicable and consider an error code (SequencerDeliverError)
- invalidSubmissionRequest(
- state,
- submissionRequest,
- sequencingTimestamp,
- SequencerErrors.SubmissionRequestRefused(
- s"The party $party is hosted on non registered participants $nonRegistered"
- ),
- ),
- )
- }
} yield GroupAddressResolver.asGroupRecipientsToMembers(mapping)
}.mapK(FutureUnlessShutdown.outcomeK)
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcInspectionService.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcInspectionService.scala
index 1bbcbebbfa..476c97302e 100644
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcInspectionService.scala
+++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcInspectionService.scala
@@ -13,6 +13,8 @@ import com.digitalasset.canton.admin.participant.v30.{
LookupContractDomain,
LookupOffsetByIndex,
LookupOffsetByTime,
+ LookupReceivedAcsCommitments,
+ LookupSentAcsCommitments,
LookupTransactionDomain,
SetConfigForSlowCounterParticipants,
}
@@ -137,4 +139,19 @@ class GrpcInspectionService(syncStateInspection: SyncStateInspection)(implicit
override def getIntervalsBehindForCounterParticipants(
request: GetIntervalsBehindForCounterParticipants.Request
): Future[GetIntervalsBehindForCounterParticipants.Response] = ???
+
+ /** TODO(#18452) R5
+ * Look up the ACS commitments computed and sent by a participant
+ */
+ override def lookupSentAcsCommitments(
+ request: LookupSentAcsCommitments.Request
+ ): Future[LookupSentAcsCommitments.Response] = ???
+
+ /** TODO(#18452) R5
+ * List the counter-participants of a participant and their ACS commitments together with the match status
+ * TODO(#18749) R1 Can also be used for R1, to fetch commitments that a counter participant received from myself
+ */
+ override def lookupReceivedAcsCommitments(
+ request: LookupReceivedAcsCommitments.Request
+ ): Future[LookupReceivedAcsCommitments.Response] = ???
}
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/ContractEnrichmentFactory.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/ContractEnrichmentFactory.scala
deleted file mode 100644
index 527b8766e7..0000000000
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/ContractEnrichmentFactory.scala
+++ /dev/null
@@ -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.participant.protocol.submission
-
-import com.digitalasset.canton.participant.protocol.submission.ContractEnrichmentFactory.ContractEnrichment
-import com.digitalasset.canton.protocol.RollbackContext.RollbackScope
-import com.digitalasset.canton.protocol.*
-
-/** Prior to protocol version 5 the metadata used in the [[ViewParticipantData.coreInputs]] contract map
- * was derived from `coreOtherNodes` passed to the `createViewParticipantData` method.
- *
- * In the situation where a node is looked up by key but then not resolved into a contract the core other nodes
- * will only contain a [[LfNodeLookupByKey]]. As this does not have signatories or stakeholder fields the global
- * key maintainers where (incorrectly) used.
- *
- * For v5 we have a fully formed [[SerializableContract]] available so do not need to enrich with metadata. Protocol
- * v4 participants should continue to use the maintainer based metadata to avoid model conformance problems.
- */
-private[submission] trait ContractEnrichmentFactory {
- def apply(coreOtherNodes: List[(LfActionNode, RollbackScope)]): ContractEnrichment
-}
-
-private[submission] object ContractEnrichmentFactory {
-
- private type ContractEnrichment = SerializableContract => SerializableContract
-
- def apply(): ContractEnrichmentFactory = PV5
-
- private object PV5 extends ContractEnrichmentFactory {
- override def apply(coreOtherNodes: List[(LfActionNode, RollbackScope)]): ContractEnrichment =
- identity
- }
-
-}
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImpl.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImpl.scala
index 8df7c3517a..b8d8f406c9 100644
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImpl.scala
+++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImpl.scala
@@ -6,17 +6,24 @@ package com.digitalasset.canton.participant.protocol.submission
import cats.data.EitherT
import cats.syntax.bifunctor.*
import cats.syntax.either.*
+import cats.syntax.functor.*
import cats.syntax.functorFilter.*
import cats.syntax.parallel.*
+import cats.syntax.traverse.*
import com.daml.lf.data.Ref.PackageId
+import com.daml.lf.transaction.ContractStateMachine.KeyInactive
+import com.daml.lf.transaction.Transaction.{KeyActive, KeyCreate, KeyInput, NegativeKeyLookup}
+import com.daml.lf.transaction.{ContractKeyUniquenessMode, ContractStateMachine}
import com.digitalasset.canton.*
import com.digitalasset.canton.crypto.{HashOps, HmacOps, Salt, SaltSeed}
+import com.digitalasset.canton.data.TransactionViewDecomposition.{NewView, SameView}
import com.digitalasset.canton.data.ViewConfirmationParameters.InvalidViewConfirmationParameters
import com.digitalasset.canton.data.*
import com.digitalasset.canton.ledger.participant.state.SubmitterInfo
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.participant.protocol.submission.TransactionTreeFactory.*
+import com.digitalasset.canton.participant.protocol.submission.TransactionTreeFactoryImpl.*
import com.digitalasset.canton.protocol.ContractIdSyntax.*
import com.digitalasset.canton.protocol.RollbackContext.RollbackScope
import com.digitalasset.canton.protocol.SerializableContract.LedgerCreateTime
@@ -27,6 +34,7 @@ import com.digitalasset.canton.topology.client.TopologySnapshot
import com.digitalasset.canton.topology.{DomainId, ParticipantId}
import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.FutureInstances.*
+import com.digitalasset.canton.util.ShowUtil.*
import com.digitalasset.canton.util.{ErrorUtil, LfTransactionUtil, MapsUtil, MonadUtil}
import com.digitalasset.canton.version.ProtocolVersion
import io.scalaland.chimney.dsl.*
@@ -45,7 +53,7 @@ import scala.concurrent.{ExecutionContext, Future}
* @param cryptoOps is used to derive Merkle hashes and contract ids [[com.digitalasset.canton.crypto.HashOps]]
* as well as salts and contract ids [[com.digitalasset.canton.crypto.HmacOps]]
*/
-abstract class TransactionTreeFactoryImpl(
+class TransactionTreeFactoryImpl(
participantId: ParticipantId,
domainId: DomainId,
protocolVersion: ProtocolVersion,
@@ -59,26 +67,6 @@ abstract class TransactionTreeFactoryImpl(
private val unicumGenerator = new UnicumGenerator(cryptoOps)
private val cantonContractIdVersion = AuthenticatedContractIdVersionV10
private val transactionViewDecompositionFactory = TransactionViewDecompositionFactory
- private val contractEnrichmentFactory = ContractEnrichmentFactory()
-
- protected type State <: TransactionTreeFactoryImpl.State
-
- protected def stateForSubmission(
- transactionSeed: SaltSeed,
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- keyResolver: LfKeyResolver,
- nextSaltIndex: Int,
- ): State
-
- protected def stateForValidation(
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- salts: Iterable[Salt],
- keyResolver: LfKeyResolver,
- ): State
override def createTransactionTree(
transaction: WellFormedTransaction[WithoutSuffixes],
@@ -103,7 +91,6 @@ abstract class TransactionTreeFactoryImpl(
transactionUuid,
metadata.ledgerTime,
keyResolver,
- nextSaltIndex = 0,
)
// Create salts
@@ -190,112 +177,17 @@ abstract class TransactionTreeFactoryImpl(
} yield rootViews
}
- @SuppressWarnings(Array("org.wartremover.warts.IterableOps"))
- override def tryReconstruct(
- subaction: WellFormedTransaction[WithoutSuffixes],
- rootPosition: ViewPosition,
- confirmationPolicy: ConfirmationPolicy,
+ private def stateForSubmission(
+ transactionSeed: SaltSeed,
mediator: MediatorGroupRecipient,
- submittingParticipantO: Option[ParticipantId],
- viewSalts: Iterable[Salt],
- transactionUuid: UUID,
- topologySnapshot: TopologySnapshot,
- contractOfId: SerializableContractOfId,
- rbContext: RollbackContext,
+ transactionUUID: UUID,
+ ledgerTime: CantonTimestamp,
keyResolver: LfKeyResolver,
- )(implicit traceContext: TraceContext): EitherT[
- Future,
- TransactionTreeConversionError,
- (TransactionView, WellFormedTransaction[WithSuffixes]),
- ] = {
- /* We ship the root node of the view with suffixed contract IDs.
- * If this is a create node, reinterpretation will allocate an unsuffixed contract id instead of the one given in the node.
- * If this is an exercise node, the exercise result may contain unsuffixed contract ids created in the body of the exercise.
- * Accordingly, the reinterpreted transaction must first be suffixed before we can compare it against
- * the shipped views.
- * Ideally we'd ship only the inputs needed to reconstruct the transaction rather than computed data
- * such as exercise results and created contract IDs.
- */
-
- ErrorUtil.requireArgument(
- subaction.unwrap.roots.length == 1,
- s"Subaction must have a single root node, but has ${subaction.unwrap.roots.iterator.mkString(", ")}",
- )
-
- val metadata = subaction.metadata
- val state = stateForValidation(
- mediator,
- transactionUuid,
- metadata.ledgerTime,
- viewSalts,
- keyResolver,
- )
-
- val decompositionsF =
- transactionViewDecompositionFactory.fromTransaction(
- confirmationPolicy,
- topologySnapshot,
- subaction,
- rbContext,
- submittingParticipantO.map(_.adminParty.toLf),
- )
- for {
- decompositions <- EitherT.liftF(decompositionsF)
- decomposition = checked(decompositions.head)
- view <- createView(decomposition, rootPosition, state, contractOfId)
- } yield {
- val suffixedNodes = state.suffixedNodes() transform {
- // Recover the children
- case (nodeId, ne: LfNodeExercises) =>
- checked(subaction.unwrap.nodes(nodeId)) match {
- case ne2: LfNodeExercises =>
- ne.copy(children = ne2.children)
- case _: LfNode =>
- throw new IllegalStateException(
- "Node type changed while constructing the transaction tree"
- )
- }
- case (_, nl: LfLeafOnlyActionNode) => nl
- }
-
- // keep around the rollback nodes (not suffixed as they don't have a contract id), so that we don't orphan suffixed nodes.
- val rollbackNodes = subaction.unwrap.nodes.collect { case tuple @ (_, _: LfNodeRollback) =>
- tuple
- }
-
- val suffixedTx = LfVersionedTransaction(
- subaction.unwrap.version,
- suffixedNodes ++ rollbackNodes,
- subaction.unwrap.roots,
- )
- view -> checked(WellFormedTransaction.normalizeAndAssert(suffixedTx, metadata, WithSuffixes))
- }
- }
-
- override def saltsFromView(view: TransactionView): Iterable[Salt] = {
- val salts = Iterable.newBuilder[Salt]
-
- def addSaltsFrom(subview: TransactionView): Unit = {
- // Salts must be added in the same order as they are requested by checkView
- salts += checked(subview.viewCommonData.tryUnwrap).salt
- salts += checked(subview.viewParticipantData.tryUnwrap).salt
- }
-
- @tailrec
- @nowarn("msg=match may not be exhaustive")
- def go(stack: Seq[TransactionView]): Unit = stack match {
- case Seq() =>
- case subview +: toVisit =>
- addSaltsFrom(subview)
- subview.subviews.assertAllUnblinded(hash =>
- s"View ${subview.viewHash} contains an unexpected blinded subview $hash"
- )
- go(subview.subviews.unblindedElements ++ toVisit)
- }
-
- go(Seq(view))
-
- salts.result()
+ ): State = {
+ val salts = LazyList
+ .from(0)
+ .map(index => Salt.tryDeriveSalt(transactionSeed, index, cryptoOps))
+ new State(mediator, transactionUUID, ledgerTime, salts.iterator, keyResolver)
}
/** compute set of required packages for each party */
@@ -387,16 +279,191 @@ abstract class TransactionTreeFactoryImpl(
}
}
- protected def createView(
+ private def createView(
view: TransactionViewDecomposition.NewView,
viewPosition: ViewPosition,
state: State,
contractOfId: SerializableContractOfId,
)(implicit
traceContext: TraceContext
- ): EitherT[Future, TransactionTreeConversionError, TransactionView]
+ ): EitherT[Future, TransactionTreeConversionError, TransactionView] = {
+ state.signalRollbackScope(view.rbContext.rollbackScope)
- protected def updateStateWithContractCreation(
+ // reset to a fresh state with projected resolver before visiting the subtree
+ val previousCsmState = state.csmState
+ val previousResolver = state.currentResolver
+ state.currentResolver = state.csmState.projectKeyResolver(previousResolver)
+ state.csmState = initialCsmState
+
+ // Process core nodes and subviews
+ val coreCreatedBuilder =
+ List.newBuilder[(LfNodeCreate, RollbackScope)] // contract IDs have already been suffixed
+ val coreOtherBuilder = // contract IDs have not yet been suffixed
+ List.newBuilder[((LfNodeId, LfActionNode), RollbackScope)]
+ val childViewsBuilder = Seq.newBuilder[TransactionView]
+ val subViewKeyResolutions =
+ mutable.Map.empty[LfGlobalKey, LfVersioned[SerializableKeyResolution]]
+
+ @SuppressWarnings(Array("org.wartremover.warts.Var"))
+ var createIndex = 0
+
+ val nbSubViews = view.allNodes.count {
+ case _: TransactionViewDecomposition.NewView => true
+ case _ => false
+ }
+ val subviewIndex = TransactionSubviews.indices(nbSubViews).iterator
+ val createdInView = mutable.Set.empty[LfContractId]
+
+ def fromEither[A <: TransactionTreeConversionError, B](
+ either: Either[A, B]
+ ): EitherT[Future, TransactionTreeConversionError, B] =
+ EitherT.fromEither(either.leftWiden[TransactionTreeConversionError])
+
+ for {
+ // Compute salts
+ viewCommonDataSalt <- fromEither(state.nextSalt())
+ viewParticipantDataSalt <- fromEither(state.nextSalt())
+ _ <- MonadUtil.sequentialTraverse_(view.allNodes) {
+ case childView: TransactionViewDecomposition.NewView =>
+ // Compute subviews, recursively
+ createView(childView, subviewIndex.next() +: viewPosition, state, contractOfId)
+ .map { v =>
+ childViewsBuilder += v
+ val createdInSubview = state.createdContractsInView
+ createdInView ++= createdInSubview
+ val keyResolutionsInSubview = v.globalKeyInputs.fmap(_.map(_.asSerializable))
+ MapsUtil.extendMapWith(subViewKeyResolutions, keyResolutionsInSubview) {
+ (accRes, _) => accRes
+ }
+ }
+
+ case TransactionViewDecomposition.SameView(lfActionNode, nodeId, rbContext) =>
+ val rbScope = rbContext.rollbackScope
+ val suffixedNode = lfActionNode match {
+ case createNode: LfNodeCreate =>
+ val suffixedNode = updateStateWithContractCreation(
+ nodeId,
+ createNode,
+ viewParticipantDataSalt,
+ viewPosition,
+ createIndex,
+ state,
+ )
+ coreCreatedBuilder += (suffixedNode -> rbScope)
+ createdInView += suffixedNode.coid
+ createIndex += 1
+ suffixedNode
+ case lfNode: LfActionNode =>
+ val suffixedNode = trySuffixNode(state)(nodeId -> lfNode)
+ coreOtherBuilder += ((nodeId, lfNode) -> rbScope)
+ suffixedNode
+ }
+
+ suffixedNode.keyOpt.foreach { case LfGlobalKeyWithMaintainers(gkey, maintainers) =>
+ state.keyVersionAndMaintainers += (gkey -> (suffixedNode.version -> maintainers))
+ }
+
+ state.signalRollbackScope(rbScope)
+
+ EitherT.fromEither[Future]({
+ for {
+ resolutionForModeOff <- suffixedNode match {
+ case lookupByKey: LfNodeLookupByKey
+ if state.csmState.mode == ContractKeyUniquenessMode.Off =>
+ val gkey = lookupByKey.key.globalKey
+ state.currentResolver.get(gkey).toRight(MissingContractKeyLookupError(gkey))
+ case _ => Right(KeyInactive) // dummy value, as resolution is not used
+ }
+ nextState <- state.csmState
+ .handleNode((), suffixedNode, resolutionForModeOff)
+ .leftMap(ContractKeyResolutionError)
+ } yield {
+ state.csmState = nextState
+ }
+ })
+ }
+ _ = state.signalRollbackScope(view.rbContext.rollbackScope)
+
+ coreCreatedNodes = coreCreatedBuilder.result()
+ // Translate contract ids in untranslated core nodes
+ // This must be done only after visiting the whole action (rather than just the node)
+ // because an Exercise result may contain an unsuffixed contract ID of a contract
+ // that was created in the consequences of the exercise, i.e., we know the suffix only
+ // after we have visited the create node.
+ coreOtherNodes = coreOtherBuilder.result().map { case (nodeInfo, rbc) =>
+ (checked(trySuffixNode(state)(nodeInfo)), rbc)
+ }
+ childViews = childViewsBuilder.result()
+
+ suffixedRootNode = coreOtherNodes.headOption
+ .orElse(coreCreatedNodes.headOption)
+ .map { case (node, _) => node }
+ .getOrElse(
+ throw new IllegalArgumentException(s"The received view has no core nodes. $view")
+ )
+
+ // Compute the parameters of the view
+ seed = view.rootSeed
+ packagePreference <- EitherT.fromEither[Future](buildPackagePreference(view))
+ actionDescription = createActionDescription(suffixedRootNode, seed, packagePreference)
+ viewCommonData = createViewCommonData(view, viewCommonDataSalt).fold(
+ ErrorUtil.internalError,
+ identity,
+ )
+ viewKeyInputs = state.csmState.globalKeyInputs
+ resolvedK <- EitherT.fromEither[Future](
+ resolvedKeys(
+ viewKeyInputs,
+ state.keyVersionAndMaintainers,
+ subViewKeyResolutions,
+ )
+ )
+ viewParticipantData <- createViewParticipantData(
+ coreCreatedNodes,
+ coreOtherNodes,
+ childViews,
+ state.createdContractInfo,
+ resolvedK,
+ actionDescription,
+ viewParticipantDataSalt,
+ contractOfId,
+ view.rbContext,
+ )
+
+ // fast-forward the former state over the subtree
+ nextCsmState <- EitherT.fromEither[Future](
+ previousCsmState
+ .advance(
+ // advance ignores the resolver in mode Strict
+ if (state.csmState.mode == ContractKeyUniquenessMode.Strict) Map.empty
+ else previousResolver,
+ state.csmState,
+ )
+ .leftMap(ContractKeyResolutionError(_): TransactionTreeConversionError)
+ )
+ } yield {
+ // Compute the result
+ val subviews = TransactionSubviews(childViews)(protocolVersion, cryptoOps)
+ val transactionView =
+ TransactionView.tryCreate(cryptoOps)(
+ viewCommonData,
+ viewParticipantData,
+ subviews,
+ protocolVersion,
+ )
+
+ checkCsmStateMatchesView(state.csmState, transactionView, viewPosition)
+
+ // Update the out parameters in the `State`
+ state.createdContractsInView = createdInView
+ state.csmState = nextCsmState
+ state.currentResolver = previousResolver
+
+ transactionView
+ }
+ }
+
+ private def updateStateWithContractCreation(
nodeId: LfNodeId,
createNode: LfNodeCreate,
viewParticipantDataSalt: Salt,
@@ -461,7 +528,7 @@ abstract class TransactionTreeFactoryImpl(
suffixedNode
}
- protected def trySuffixNode(
+ private def trySuffixNode(
state: State
)(idAndNode: (LfNodeId, LfActionNode)): LfActionNode = {
val (nodeId, node) = idAndNode
@@ -475,7 +542,41 @@ abstract class TransactionTreeFactoryImpl(
suffixedNode
}
- protected def createActionDescription(
+ private[submission] def buildPackagePreference(
+ decomposition: TransactionViewDecomposition
+ ): Either[ConflictingPackagePreferenceError, Set[LfPackageId]] = {
+
+ def nodePref(n: LfActionNode): Set[(LfPackageName, LfPackageId)] = n match {
+ case ex: LfNodeExercises if ex.interfaceId.isDefined =>
+ Set(ex.packageName -> ex.templateId.packageId)
+ case _ => Set.empty
+ }
+
+ @tailrec
+ def go(
+ decompositions: List[TransactionViewDecomposition],
+ resolved: Set[(LfPackageName, LfPackageId)],
+ ): Set[(LfPackageName, LfPackageId)] = {
+ decompositions match {
+ case Nil =>
+ resolved
+ case (v: SameView) :: others =>
+ go(others, resolved ++ nodePref(v.lfNode))
+ case (v: NewView) :: others =>
+ go(v.tailNodes.toList ::: others, resolved ++ nodePref(v.lfNode))
+ }
+ }
+
+ val preferences = go(List(decomposition), Set.empty)
+ MapsUtil
+ .toNonConflictingMap(preferences)
+ .bimap(
+ conflicts => ConflictingPackagePreferenceError(conflicts),
+ map => map.values.toSet,
+ )
+ }
+
+ private def createActionDescription(
actionNode: LfActionNode,
seed: Option[LfHash],
packagePreference: Set[LfPackageId],
@@ -484,7 +585,7 @@ abstract class TransactionTreeFactoryImpl(
ActionDescription.tryFromLfActionNode(actionNode, seed, packagePreference, protocolVersion)
)
- protected def createViewCommonData(
+ private def createViewCommonData(
rootView: TransactionViewDecomposition.NewView,
salt: Salt,
): Either[InvalidViewConfirmationParameters, ViewCommonData] =
@@ -494,7 +595,77 @@ abstract class TransactionTreeFactoryImpl(
protocolVersion,
)
- protected def createViewParticipantData(
+ /** The difference between `viewKeyInputs: Map[LfGlobalKey, KeyInput]` and
+ * `subviewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]`, computed as follows:
+ *
+ * - First, `keyVersionAndMaintainers` is used to compute
+ * `viewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]` from `viewKeyInputs`.
+ * - Second, the result consists of all key-resolution pairs that are in `viewKeyResolutions`,
+ * but not in `subviewKeyResolutions`.
+ *
+ *
+ * Note: The following argument depends on how this method is used.
+ * It just sits here because we can then use scaladoc referencing.
+ *
+ * All resolved contract IDs in the map difference are core input contracts by the following argument:
+ * Suppose that the map difference resolves a key `k` to a contract ID `cid`.
+ * - In mode [[com.daml.lf.transaction.ContractKeyUniquenessMode.Strict]],
+ * the first node (in execution order) involving the key `k` determines the key's resolution for the view.
+ * So the first node `n` in execution order involving `k` is an Exercise, Fetch, or positive LookupByKey node.
+ * - In mode [[com.daml.lf.transaction.ContractKeyUniquenessMode.Off]],
+ * the first by-key node (in execution order, including Creates) determines the global key input of the view.
+ * So the first by-key node `n` is an ExerciseByKey, FetchByKey, or positive LookupByKey node.
+ * In particular, `n` cannot be a Create node because then the resolution for the view
+ * would be [[com.daml.lf.transaction.ContractStateMachine.KeyInactive]].
+ * If this node `n` is in the core of the view, then `cid` is a core input and we are done.
+ * If this node `n` is in a proper subview, then the aggregated global key inputs
+ * [[com.digitalasset.canton.data.TransactionView.globalKeyInputs]]
+ * of the subviews resolve `k` to `cid` (as resolutions from earlier subviews are preferred)
+ * and therefore the map difference does not resolve `k` at all.
+ *
+ * @return `Left(...)` if `viewKeyInputs` contains a key not in the `keyVersionAndMaintainers.keySet`
+ * @throws java.lang.IllegalArgumentException if `subviewKeyResolutions.keySet` is not a subset of `viewKeyInputs.keySet`
+ */
+ private def resolvedKeys(
+ viewKeyInputs: Map[LfGlobalKey, KeyInput],
+ keyVersionAndMaintainers: collection.Map[LfGlobalKey, (LfTransactionVersion, Set[LfPartyId])],
+ subviewKeyResolutions: collection.Map[LfGlobalKey, LfVersioned[SerializableKeyResolution]],
+ )(implicit
+ traceContext: TraceContext
+ ): Either[TransactionTreeConversionError, Map[LfGlobalKey, LfVersioned[
+ SerializableKeyResolution
+ ]]] = {
+ ErrorUtil.requireArgument(
+ subviewKeyResolutions.keySet.subsetOf(viewKeyInputs.keySet),
+ s"Global key inputs of subview not part of the global key inputs of the parent view. Missing keys: ${subviewKeyResolutions.keySet
+ .diff(viewKeyInputs.keySet)}",
+ )
+
+ def resolutionFor(
+ key: LfGlobalKey,
+ keyInput: KeyInput,
+ ): Either[MissingContractKeyLookupError, LfVersioned[SerializableKeyResolution]] = {
+ keyVersionAndMaintainers.get(key).toRight(MissingContractKeyLookupError(key)).map {
+ case (lfVersion, maintainers) =>
+ val resolution = keyInput match {
+ case KeyActive(cid) => AssignedKey(cid)
+ case KeyCreate | NegativeKeyLookup => FreeKey(maintainers)
+ }
+ LfVersioned(lfVersion, resolution)
+ }
+ }
+
+ for {
+ viewKeyResolutionSeq <- viewKeyInputs.toSeq
+ .traverse { case (gkey, keyInput) =>
+ resolutionFor(gkey, keyInput).map(gkey -> _)
+ }
+ } yield {
+ MapsUtil.mapDiff(viewKeyResolutionSeq.toMap, subviewKeyResolutions)
+ }
+ }
+
+ private def createViewParticipantData(
coreCreatedNodes: List[(LfNodeCreate, RollbackScope)],
coreOtherNodes: List[(LfActionNode, RollbackScope)],
childViews: Seq[TransactionView],
@@ -544,8 +715,6 @@ abstract class TransactionTreeFactoryImpl(
val coreInputs = usedCore -- createdInSameViewOrSubviews
val createdInSubviewArchivedInCore = consumedInCore intersect createdInSubviews
- val contractEnricher = contractEnrichmentFactory(coreOtherNodes)
-
def withInstance(
contractId: LfContractId
): EitherT[Future, ContractLookupError, InputContract] = {
@@ -554,7 +723,7 @@ abstract class TransactionTreeFactoryImpl(
case Some(info) =>
EitherT.pure(InputContract(info, cons))
case None =>
- contractOfId(contractId).map { c => InputContract(contractEnricher(c), cons) }
+ contractOfId(contractId).map { c => InputContract(c, cons) }
}
}
@@ -579,6 +748,152 @@ abstract class TransactionTreeFactoryImpl(
.leftMap[TransactionTreeConversionError](ViewParticipantDataError)
} yield viewParticipantData
}
+
+ /** Check that we correctly reconstruct the csm state machine
+ * Canton does not distinguish between the different com.daml.lf.transaction.Transaction.KeyInactive forms right now
+ */
+ private def checkCsmStateMatchesView(
+ csmState: ContractStateMachine.State[Unit],
+ transactionView: TransactionView,
+ viewPosition: ViewPosition,
+ )(implicit traceContext: TraceContext): Unit = {
+ val viewGki = transactionView.globalKeyInputs.fmap(_.unversioned.resolution)
+ val stateGki = csmState.globalKeyInputs.fmap(_.toKeyMapping)
+ ErrorUtil.requireState(
+ viewGki == stateGki,
+ show"""Failed to reconstruct the global key inputs for the view at position $viewPosition.
+ | Reconstructed: $viewGki
+ | Expected: $stateGki""".stripMargin,
+ )
+ val viewLocallyCreated = transactionView.createdContracts.keySet
+ val stateLocallyCreated = csmState.locallyCreated
+ ErrorUtil.requireState(
+ viewLocallyCreated == stateLocallyCreated,
+ show"Failed to reconstruct created contracts for the view at position $viewPosition.\n Reconstructed: $viewLocallyCreated\n Expected: $stateLocallyCreated",
+ )
+ val viewInputContractIds = transactionView.inputContracts.keySet
+ val stateInputContractIds = csmState.inputContractIds
+ ErrorUtil.requireState(
+ viewInputContractIds == stateInputContractIds,
+ s"Failed to reconstruct input contracts for the view at position $viewPosition.\n Reconstructed: $viewInputContractIds\n Expected: $stateInputContractIds",
+ )
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.IterableOps"))
+ override def tryReconstruct(
+ subaction: WellFormedTransaction[WithoutSuffixes],
+ rootPosition: ViewPosition,
+ confirmationPolicy: ConfirmationPolicy,
+ mediator: MediatorGroupRecipient,
+ submittingParticipantO: Option[ParticipantId],
+ viewSalts: Iterable[Salt],
+ transactionUuid: UUID,
+ topologySnapshot: TopologySnapshot,
+ contractOfId: SerializableContractOfId,
+ rbContext: RollbackContext,
+ keyResolver: LfKeyResolver,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ TransactionTreeConversionError,
+ (TransactionView, WellFormedTransaction[WithSuffixes]),
+ ] = {
+ /* We ship the root node of the view with suffixed contract IDs.
+ * If this is a create node, reinterpretation will allocate an unsuffixed contract id instead of the one given in the node.
+ * If this is an exercise node, the exercise result may contain unsuffixed contract ids created in the body of the exercise.
+ * Accordingly, the reinterpreted transaction must first be suffixed before we can compare it against
+ * the shipped views.
+ * Ideally we'd ship only the inputs needed to reconstruct the transaction rather than computed data
+ * such as exercise results and created contract IDs.
+ */
+
+ ErrorUtil.requireArgument(
+ subaction.unwrap.roots.length == 1,
+ s"Subaction must have a single root node, but has ${subaction.unwrap.roots.iterator.mkString(", ")}",
+ )
+
+ val metadata = subaction.metadata
+ val state = stateForValidation(
+ mediator,
+ transactionUuid,
+ metadata.ledgerTime,
+ viewSalts,
+ keyResolver,
+ )
+
+ val decompositionsF =
+ transactionViewDecompositionFactory.fromTransaction(
+ confirmationPolicy,
+ topologySnapshot,
+ subaction,
+ rbContext,
+ submittingParticipantO.map(_.adminParty.toLf),
+ )
+ for {
+ decompositions <- EitherT.liftF(decompositionsF)
+ decomposition = checked(decompositions.head)
+ view <- createView(decomposition, rootPosition, state, contractOfId)
+ } yield {
+ val suffixedNodes = state.suffixedNodes() transform {
+ // Recover the children
+ case (nodeId, ne: LfNodeExercises) =>
+ checked(subaction.unwrap.nodes(nodeId)) match {
+ case ne2: LfNodeExercises =>
+ ne.copy(children = ne2.children)
+ case _: LfNode =>
+ throw new IllegalStateException(
+ "Node type changed while constructing the transaction tree"
+ )
+ }
+ case (_, nl: LfLeafOnlyActionNode) => nl
+ }
+
+ // keep around the rollback nodes (not suffixed as they don't have a contract id), so that we don't orphan suffixed nodes.
+ val rollbackNodes = subaction.unwrap.nodes.collect { case tuple @ (_, _: LfNodeRollback) =>
+ tuple
+ }
+
+ val suffixedTx = LfVersionedTransaction(
+ subaction.unwrap.version,
+ suffixedNodes ++ rollbackNodes,
+ subaction.unwrap.roots,
+ )
+ view -> checked(WellFormedTransaction.normalizeAndAssert(suffixedTx, metadata, WithSuffixes))
+ }
+ }
+
+ private def stateForValidation(
+ mediator: MediatorGroupRecipient,
+ transactionUUID: UUID,
+ ledgerTime: CantonTimestamp,
+ salts: Iterable[Salt],
+ keyResolver: LfKeyResolver,
+ ): State = new State(mediator, transactionUUID, ledgerTime, salts.iterator, keyResolver)
+
+ override def saltsFromView(view: TransactionView): Iterable[Salt] = {
+ val salts = Iterable.newBuilder[Salt]
+
+ def addSaltsFrom(subview: TransactionView): Unit = {
+ // Salts must be added in the same order as they are requested by checkView
+ salts += checked(subview.viewCommonData.tryUnwrap).salt
+ salts += checked(subview.viewParticipantData.tryUnwrap).salt
+ }
+
+ @tailrec
+ @nowarn("msg=match may not be exhaustive")
+ def go(stack: Seq[TransactionView]): Unit = stack match {
+ case Seq() =>
+ case subview +: toVisit =>
+ addSaltsFrom(subview)
+ subview.subviews.assertAllUnblinded(hash =>
+ s"View ${subview.viewHash} contains an unexpected blinded subview $hash"
+ )
+ go(subview.subviews.unblindedElements ++ toVisit)
+ }
+
+ go(Seq(view))
+
+ salts.result()
+ }
}
object TransactionTreeFactoryImpl {
@@ -591,7 +906,7 @@ object TransactionTreeFactoryImpl {
cryptoOps: HashOps & HmacOps,
loggerFactory: NamedLoggerFactory,
)(implicit ex: ExecutionContext): TransactionTreeFactoryImpl =
- new TransactionTreeFactoryImplV3(
+ new TransactionTreeFactoryImpl(
submittingParticipant,
domainId,
protocolVersion,
@@ -612,12 +927,16 @@ object TransactionTreeFactoryImpl {
}
.merge
- trait State {
- def mediator: MediatorGroupRecipient
- def transactionUUID: UUID
- def ledgerTime: CantonTimestamp
+ private val initialCsmState: ContractStateMachine.State[Unit] =
+ ContractStateMachine.initial[Unit](ContractKeyUniquenessMode.Off)
- protected def salts: Iterator[Salt]
+ private class State(
+ val mediator: MediatorGroupRecipient,
+ val transactionUUID: UUID,
+ val ledgerTime: CantonTimestamp,
+ val salts: Iterator[Salt],
+ initialResolver: LfKeyResolver,
+ ) {
def nextSalt(): Either[TransactionTreeFactory.TooFewSalts, Salt] =
Either.cond(salts.hasNext, salts.next(), TooFewSalts)
@@ -671,5 +990,40 @@ object TransactionTreeFactoryImpl {
/** Out parameter for contracts created in the view (including subviews). */
@SuppressWarnings(Array("org.wartremover.warts.Var"))
var createdContractsInView: collection.Set[LfContractId] = Set.empty
+
+ /** An [[com.digitalasset.canton.protocol.LfGlobalKey]] stores neither the
+ * [[com.digitalasset.canton.protocol.LfTransactionVersion]] to be used during serialization
+ * nor the maintainers, which we need to cache in case no contract is found.
+ *
+ * Out parameter that stores version and maintainers for all keys
+ * that have been referenced by an already-processed node.
+ */
+ val keyVersionAndMaintainers: mutable.Map[LfGlobalKey, (LfTransactionVersion, Set[LfPartyId])] =
+ mutable.Map.empty
+
+ /** Out parameter for the [[com.daml.lf.transaction.ContractStateMachine.State]]
+ *
+ * The state of the [[com.daml.lf.transaction.ContractStateMachine]]
+ * after iterating over the following nodes in execution order:
+ * 1. The iteration starts at the root node of the current view.
+ * 2. The iteration includes all processed nodes of the view. This includes the nodes of fully processed subviews.
+ */
+ @SuppressWarnings(Array("org.wartremover.warts.Var"))
+ var csmState: ContractStateMachine.State[Unit] = initialCsmState
+
+ /** This resolver is used to feed [[com.daml.lf.transaction.ContractStateMachine.State.handleLookupWith]].
+ */
+ @SuppressWarnings(Array("org.wartremover.warts.Var"))
+ var currentResolver: LfKeyResolver = initialResolver
+
+ @SuppressWarnings(Array("org.wartremover.warts.Var"))
+ private var rollbackScope: RollbackScope = RollbackScope.empty
+
+ def signalRollbackScope(target: RollbackScope): Unit = {
+ val (pops, pushes) = RollbackScope.popsAndPushes(rollbackScope, target)
+ for (_ <- 1 to pops) { csmState = csmState.endRollback() }
+ for (_ <- 1 to pushes) { csmState = csmState.beginRollback() }
+ rollbackScope = target
+ }
}
}
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImplV3.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImplV3.scala
deleted file mode 100644
index 1d66c2d262..0000000000
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/TransactionTreeFactoryImplV3.scala
+++ /dev/null
@@ -1,478 +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.participant.protocol.submission
-
-import cats.data.EitherT
-import cats.syntax.bifunctor.*
-import cats.syntax.functor.*
-import cats.syntax.traverse.*
-import com.daml.lf.transaction.ContractStateMachine.KeyInactive
-import com.daml.lf.transaction.Transaction.{KeyActive, KeyCreate, KeyInput, NegativeKeyLookup}
-import com.daml.lf.transaction.{ContractKeyUniquenessMode, ContractStateMachine}
-import com.digitalasset.canton.crypto.{HashOps, HmacOps, Salt, SaltSeed}
-import com.digitalasset.canton.data.TransactionViewDecomposition.{NewView, SameView}
-import com.digitalasset.canton.data.*
-import com.digitalasset.canton.logging.NamedLoggerFactory
-import com.digitalasset.canton.participant.protocol.submission.TransactionTreeFactory.{
- ConflictingPackagePreferenceError,
- ContractKeyResolutionError,
- MissingContractKeyLookupError,
- SerializableContractOfId,
- TransactionTreeConversionError,
-}
-import com.digitalasset.canton.protocol.RollbackContext.RollbackScope
-import com.digitalasset.canton.protocol.*
-import com.digitalasset.canton.sequencing.protocol.MediatorGroupRecipient
-import com.digitalasset.canton.topology.{DomainId, ParticipantId}
-import com.digitalasset.canton.tracing.TraceContext
-import com.digitalasset.canton.util.ShowUtil.*
-import com.digitalasset.canton.util.{ErrorUtil, MapsUtil, MonadUtil}
-import com.digitalasset.canton.version.ProtocolVersion
-import com.digitalasset.canton.{
- LfKeyResolver,
- LfPackageId,
- LfPackageName,
- LfPartyId,
- LfVersioned,
- checked,
-}
-
-import java.util.UUID
-import scala.annotation.tailrec
-import scala.collection.mutable
-import scala.concurrent.{ExecutionContext, Future}
-
-/** Generate transaction trees as used from protocol version [[com.digitalasset.canton.version.ProtocolVersion.v31]] on
- */
-class TransactionTreeFactoryImplV3(
- submittingParticipant: ParticipantId,
- domainId: DomainId,
- protocolVersion: ProtocolVersion,
- contractSerializer: LfContractInst => SerializableRawContractInstance,
- cryptoOps: HashOps & HmacOps,
- override protected val loggerFactory: NamedLoggerFactory,
-)(implicit ec: ExecutionContext)
- extends TransactionTreeFactoryImpl(
- submittingParticipant,
- domainId,
- protocolVersion,
- contractSerializer,
- cryptoOps,
- loggerFactory,
- ) {
-
- private val initialCsmState: ContractStateMachine.State[Unit] =
- ContractStateMachine.initial[Unit](ContractKeyUniquenessMode.Off)
-
- private[submission] def buildPackagePreference(
- decomposition: TransactionViewDecomposition
- ): Either[ConflictingPackagePreferenceError, Set[LfPackageId]] = {
-
- def nodePref(n: LfActionNode): Set[(LfPackageName, LfPackageId)] = n match {
- case ex: LfNodeExercises if ex.interfaceId.isDefined =>
- Set(ex.packageName -> ex.templateId.packageId)
- case _ => Set.empty
- }
-
- @tailrec
- def go(
- decompositions: List[TransactionViewDecomposition],
- resolved: Set[(LfPackageName, LfPackageId)],
- ): Set[(LfPackageName, LfPackageId)] = {
- decompositions match {
- case Nil =>
- resolved
- case (v: SameView) :: others =>
- go(others, resolved ++ nodePref(v.lfNode))
- case (v: NewView) :: others =>
- go(v.tailNodes.toList ::: others, resolved ++ nodePref(v.lfNode))
- }
- }
-
- val preferences = go(List(decomposition), Set.empty)
- MapsUtil
- .toNonConflictingMap(preferences)
- .bimap(
- conflicts => ConflictingPackagePreferenceError(conflicts),
- map => map.values.toSet,
- )
- }
-
- protected[submission] class State private (
- override val mediator: MediatorGroupRecipient,
- override val transactionUUID: UUID,
- override val ledgerTime: CantonTimestamp,
- override protected val salts: Iterator[Salt],
- initialResolver: LfKeyResolver,
- ) extends TransactionTreeFactoryImpl.State {
-
- /** An [[com.digitalasset.canton.protocol.LfGlobalKey]] stores neither the
- * [[com.digitalasset.canton.protocol.LfTransactionVersion]] to be used during serialization
- * nor the maintainers, which we need to cache in case no contract is found.
- *
- * Out parameter that stores version and maintainers for all keys
- * that have been referenced by an already-processed node.
- */
- val keyVersionAndMaintainers: mutable.Map[LfGlobalKey, (LfTransactionVersion, Set[LfPartyId])] =
- mutable.Map.empty
-
- /** Out parameter for the [[com.daml.lf.transaction.ContractStateMachine.State]]
- *
- * The state of the [[com.daml.lf.transaction.ContractStateMachine]]
- * after iterating over the following nodes in execution order:
- * 1. The iteration starts at the root node of the current view.
- * 2. The iteration includes all processed nodes of the view. This includes the nodes of fully processed subviews.
- */
- @SuppressWarnings(Array("org.wartremover.warts.Var"))
- var csmState: ContractStateMachine.State[Unit] = initialCsmState
-
- /** This resolver is used to feed [[com.daml.lf.transaction.ContractStateMachine.State.handleLookupWith]].
- */
- @SuppressWarnings(Array("org.wartremover.warts.Var"))
- var currentResolver: LfKeyResolver = initialResolver
-
- @SuppressWarnings(Array("org.wartremover.warts.Var"))
- private var rollbackScope: RollbackScope = RollbackScope.empty
-
- def signalRollbackScope(target: RollbackScope): Unit = {
- val (pops, pushes) = RollbackScope.popsAndPushes(rollbackScope, target)
- for (_ <- 1 to pops) { csmState = csmState.endRollback() }
- for (_ <- 1 to pushes) { csmState = csmState.beginRollback() }
- rollbackScope = target
- }
- }
-
- private[submission] object State {
- private[submission] def submission(
- transactionSeed: SaltSeed,
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- nextSaltIndex: Int,
- keyResolver: LfKeyResolver,
- ): State = {
- val salts = LazyList
- .from(nextSaltIndex)
- .map(index => Salt.tryDeriveSalt(transactionSeed, index, cryptoOps))
- new State(mediator, transactionUUID, ledgerTime, salts.iterator, keyResolver)
- }
-
- private[submission] def validation(
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- salts: Iterable[Salt],
- keyResolver: LfKeyResolver,
- ): State = new State(mediator, transactionUUID, ledgerTime, salts.iterator, keyResolver)
- }
-
- override protected def stateForSubmission(
- transactionSeed: SaltSeed,
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- keyResolver: LfKeyResolver,
- nextSaltIndex: Int,
- ): State =
- State.submission(
- transactionSeed,
- mediator,
- transactionUUID,
- ledgerTime,
- nextSaltIndex,
- keyResolver,
- )
-
- override protected def stateForValidation(
- mediator: MediatorGroupRecipient,
- transactionUUID: UUID,
- ledgerTime: CantonTimestamp,
- salts: Iterable[Salt],
- keyResolver: LfKeyResolver,
- ): State = State.validation(mediator, transactionUUID, ledgerTime, salts, keyResolver)
-
- override protected def createView(
- view: TransactionViewDecomposition.NewView,
- viewPosition: ViewPosition,
- state: State,
- contractOfId: SerializableContractOfId,
- )(implicit
- traceContext: TraceContext
- ): EitherT[Future, TransactionTreeConversionError, TransactionView] = {
- state.signalRollbackScope(view.rbContext.rollbackScope)
-
- // reset to a fresh state with projected resolver before visiting the subtree
- val previousCsmState = state.csmState
- val previousResolver = state.currentResolver
- state.currentResolver = state.csmState.projectKeyResolver(previousResolver)
- state.csmState = initialCsmState
-
- // Process core nodes and subviews
- val coreCreatedBuilder =
- List.newBuilder[(LfNodeCreate, RollbackScope)] // contract IDs have already been suffixed
- val coreOtherBuilder = // contract IDs have not yet been suffixed
- List.newBuilder[((LfNodeId, LfActionNode), RollbackScope)]
- val childViewsBuilder = Seq.newBuilder[TransactionView]
- val subViewKeyResolutions =
- mutable.Map.empty[LfGlobalKey, LfVersioned[SerializableKeyResolution]]
-
- @SuppressWarnings(Array("org.wartremover.warts.Var"))
- var createIndex = 0
-
- val nbSubViews = view.allNodes.count {
- case _: TransactionViewDecomposition.NewView => true
- case _ => false
- }
- val subviewIndex = TransactionSubviews.indices(nbSubViews).iterator
- val createdInView = mutable.Set.empty[LfContractId]
-
- def fromEither[A <: TransactionTreeConversionError, B](
- either: Either[A, B]
- ): EitherT[Future, TransactionTreeConversionError, B] =
- EitherT.fromEither(either.leftWiden[TransactionTreeConversionError])
-
- for {
- // Compute salts
- viewCommonDataSalt <- fromEither(state.nextSalt())
- viewParticipantDataSalt <- fromEither(state.nextSalt())
- _ <- MonadUtil.sequentialTraverse_(view.allNodes) {
- case childView: TransactionViewDecomposition.NewView =>
- // Compute subviews, recursively
- createView(childView, subviewIndex.next() +: viewPosition, state, contractOfId)
- .map { v =>
- childViewsBuilder += v
- val createdInSubview = state.createdContractsInView
- createdInView ++= createdInSubview
- val keyResolutionsInSubview = v.globalKeyInputs.fmap(_.map(_.asSerializable))
- MapsUtil.extendMapWith(subViewKeyResolutions, keyResolutionsInSubview) {
- (accRes, _) => accRes
- }
- }
-
- case TransactionViewDecomposition.SameView(lfActionNode, nodeId, rbContext) =>
- val rbScope = rbContext.rollbackScope
- val suffixedNode = lfActionNode match {
- case createNode: LfNodeCreate =>
- val suffixedNode = updateStateWithContractCreation(
- nodeId,
- createNode,
- viewParticipantDataSalt,
- viewPosition,
- createIndex,
- state,
- )
- coreCreatedBuilder += (suffixedNode -> rbScope)
- createdInView += suffixedNode.coid
- createIndex += 1
- suffixedNode
- case lfNode: LfActionNode =>
- val suffixedNode = trySuffixNode(state)(nodeId -> lfNode)
- coreOtherBuilder += ((nodeId, lfNode) -> rbScope)
- suffixedNode
- }
-
- suffixedNode.keyOpt.foreach { case LfGlobalKeyWithMaintainers(gkey, maintainers) =>
- state.keyVersionAndMaintainers += (gkey -> (suffixedNode.version -> maintainers))
- }
-
- state.signalRollbackScope(rbScope)
-
- EitherT.fromEither[Future]({
- for {
- resolutionForModeOff <- suffixedNode match {
- case lookupByKey: LfNodeLookupByKey
- if state.csmState.mode == ContractKeyUniquenessMode.Off =>
- val gkey = lookupByKey.key.globalKey
- state.currentResolver.get(gkey).toRight(MissingContractKeyLookupError(gkey))
- case _ => Right(KeyInactive) // dummy value, as resolution is not used
- }
- nextState <- state.csmState
- .handleNode((), suffixedNode, resolutionForModeOff)
- .leftMap(ContractKeyResolutionError)
- } yield {
- state.csmState = nextState
- }
- })
- }
- _ = state.signalRollbackScope(view.rbContext.rollbackScope)
-
- coreCreatedNodes = coreCreatedBuilder.result()
- // Translate contract ids in untranslated core nodes
- // This must be done only after visiting the whole action (rather than just the node)
- // because an Exercise result may contain an unsuffixed contract ID of a contract
- // that was created in the consequences of the exercise, i.e., we know the suffix only
- // after we have visited the create node.
- coreOtherNodes = coreOtherBuilder.result().map { case (nodeInfo, rbc) =>
- (checked(trySuffixNode(state)(nodeInfo)), rbc)
- }
- childViews = childViewsBuilder.result()
-
- suffixedRootNode = coreOtherNodes.headOption
- .orElse(coreCreatedNodes.headOption)
- .map { case (node, _) => node }
- .getOrElse(
- throw new IllegalArgumentException(s"The received view has no core nodes. $view")
- )
-
- // Compute the parameters of the view
- seed = view.rootSeed
- packagePreference <- EitherT.fromEither[Future](buildPackagePreference(view))
- actionDescription = createActionDescription(suffixedRootNode, seed, packagePreference)
- viewCommonData = createViewCommonData(view, viewCommonDataSalt).fold(
- ErrorUtil.internalError,
- identity,
- )
- viewKeyInputs = state.csmState.globalKeyInputs
- resolvedK <- EitherT.fromEither[Future](
- resolvedKeys(
- viewKeyInputs,
- state.keyVersionAndMaintainers,
- subViewKeyResolutions,
- )
- )
- viewParticipantData <- createViewParticipantData(
- coreCreatedNodes,
- coreOtherNodes,
- childViews,
- state.createdContractInfo,
- resolvedK,
- actionDescription,
- viewParticipantDataSalt,
- contractOfId,
- view.rbContext,
- )
-
- // fast-forward the former state over the subtree
- nextCsmState <- EitherT.fromEither[Future](
- previousCsmState
- .advance(
- // advance ignores the resolver in mode Strict
- if (state.csmState.mode == ContractKeyUniquenessMode.Strict) Map.empty
- else previousResolver,
- state.csmState,
- )
- .leftMap(ContractKeyResolutionError(_): TransactionTreeConversionError)
- )
- } yield {
- // Compute the result
- val subviews = TransactionSubviews(childViews)(protocolVersion, cryptoOps)
- val transactionView =
- TransactionView.tryCreate(cryptoOps)(
- viewCommonData,
- viewParticipantData,
- subviews,
- protocolVersion,
- )
-
- checkCsmStateMatchesView(state.csmState, transactionView, viewPosition)
-
- // Update the out parameters in the `State`
- state.createdContractsInView = createdInView
- state.csmState = nextCsmState
- state.currentResolver = previousResolver
-
- transactionView
- }
- }
-
- /** Check that we correctly reconstruct the csm state machine
- * Canton does not distinguish between the different com.daml.lf.transaction.Transaction.KeyInactive forms right now
- */
- private def checkCsmStateMatchesView(
- csmState: ContractStateMachine.State[Unit],
- transactionView: TransactionView,
- viewPosition: ViewPosition,
- )(implicit traceContext: TraceContext): Unit = {
- val viewGki = transactionView.globalKeyInputs.fmap(_.unversioned.resolution)
- val stateGki = csmState.globalKeyInputs.fmap(_.toKeyMapping)
- ErrorUtil.requireState(
- viewGki == stateGki,
- show"""Failed to reconstruct the global key inputs for the view at position $viewPosition.
- | Reconstructed: $viewGki
- | Expected: $stateGki""".stripMargin,
- )
- val viewLocallyCreated = transactionView.createdContracts.keySet
- val stateLocallyCreated = csmState.locallyCreated
- ErrorUtil.requireState(
- viewLocallyCreated == stateLocallyCreated,
- show"Failed to reconstruct created contracts for the view at position $viewPosition.\n Reconstructed: $viewLocallyCreated\n Expected: $stateLocallyCreated",
- )
- val viewInputContractIds = transactionView.inputContracts.keySet
- val stateInputContractIds = csmState.inputContractIds
- ErrorUtil.requireState(
- viewInputContractIds == stateInputContractIds,
- s"Failed to reconstruct input contracts for the view at position $viewPosition.\n Reconstructed: $viewInputContractIds\n Expected: $stateInputContractIds",
- )
- }
-
- /** The difference between `viewKeyInputs: Map[LfGlobalKey, KeyInput]` and
- * `subviewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]`, computed as follows:
- *
- * - First, `keyVersionAndMaintainers` is used to compute
- * `viewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]` from `viewKeyInputs`.
- * - Second, the result consists of all key-resolution pairs that are in `viewKeyResolutions`,
- * but not in `subviewKeyResolutions`.
- *
- *
- * Note: The following argument depends on how this method is used.
- * It just sits here because we can then use scaladoc referencing.
- *
- * All resolved contract IDs in the map difference are core input contracts by the following argument:
- * Suppose that the map difference resolves a key `k` to a contract ID `cid`.
- * - In mode [[com.daml.lf.transaction.ContractKeyUniquenessMode.Strict]],
- * the first node (in execution order) involving the key `k` determines the key's resolution for the view.
- * So the first node `n` in execution order involving `k` is an Exercise, Fetch, or positive LookupByKey node.
- * - In mode [[com.daml.lf.transaction.ContractKeyUniquenessMode.Off]],
- * the first by-key node (in execution order, including Creates) determines the global key input of the view.
- * So the first by-key node `n` is an ExerciseByKey, FetchByKey, or positive LookupByKey node.
- * In particular, `n` cannot be a Create node because then the resolution for the view
- * would be [[com.daml.lf.transaction.ContractStateMachine.KeyInactive]].
- * If this node `n` is in the core of the view, then `cid` is a core input and we are done.
- * If this node `n` is in a proper subview, then the aggregated global key inputs
- * [[com.digitalasset.canton.data.TransactionView.globalKeyInputs]]
- * of the subviews resolve `k` to `cid` (as resolutions from earlier subviews are preferred)
- * and therefore the map difference does not resolve `k` at all.
- *
- * @return `Left(...)` if `viewKeyInputs` contains a key not in the `keyVersionAndMaintainers.keySet`
- * @throws java.lang.IllegalArgumentException if `subviewKeyResolutions.keySet` is not a subset of `viewKeyInputs.keySet`
- */
- private def resolvedKeys(
- viewKeyInputs: Map[LfGlobalKey, KeyInput],
- keyVersionAndMaintainers: collection.Map[LfGlobalKey, (LfTransactionVersion, Set[LfPartyId])],
- subviewKeyResolutions: collection.Map[LfGlobalKey, LfVersioned[SerializableKeyResolution]],
- )(implicit
- traceContext: TraceContext
- ): Either[TransactionTreeConversionError, Map[LfGlobalKey, LfVersioned[
- SerializableKeyResolution
- ]]] = {
- ErrorUtil.requireArgument(
- subviewKeyResolutions.keySet.subsetOf(viewKeyInputs.keySet),
- s"Global key inputs of subview not part of the global key inputs of the parent view. Missing keys: ${subviewKeyResolutions.keySet
- .diff(viewKeyInputs.keySet)}",
- )
-
- def resolutionFor(
- key: LfGlobalKey,
- keyInput: KeyInput,
- ): Either[MissingContractKeyLookupError, LfVersioned[SerializableKeyResolution]] = {
- keyVersionAndMaintainers.get(key).toRight(MissingContractKeyLookupError(key)).map {
- case (lfVersion, maintainers) =>
- val resolution = keyInput match {
- case KeyActive(cid) => AssignedKey(cid)
- case KeyCreate | NegativeKeyLookup => FreeKey(maintainers)
- }
- LfVersioned(lfVersion, resolution)
- }
- }
-
- for {
- viewKeyResolutionSeq <- viewKeyInputs.toSeq
- .traverse { case (gkey, keyInput) =>
- resolutionFor(gkey, keyInput).map(gkey -> _)
- }
- } yield {
- MapsUtil.mapDiff(viewKeyResolutionSeq.toMap, subviewKeyResolutions)
- }
- }
-}
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceChecker.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceChecker.scala
index cf4b02f35f..8fea90db20 100644
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceChecker.scala
+++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceChecker.scala
@@ -7,7 +7,6 @@ import cats.data.EitherT
import cats.implicits.toTraverseOps
import cats.syntax.alternative.*
import cats.syntax.bifunctor.*
-import cats.syntax.functor.*
import cats.syntax.parallel.*
import com.daml.lf.data.Ref.{Identifier, PackageId, PackageName}
import com.daml.nonempty.NonEmpty
@@ -36,12 +35,11 @@ import com.digitalasset.canton.participant.protocol.validation.ModelConformanceC
}
import com.digitalasset.canton.participant.store.{
ContractLookup,
- ContractLookupAndVerification,
ExtendedContractLookup,
StoredContract,
}
import com.digitalasset.canton.participant.util.DAMLe
-import com.digitalasset.canton.participant.util.DAMLe.PackageResolver
+import com.digitalasset.canton.participant.util.DAMLe.{HasReinterpret, PackageResolver}
import com.digitalasset.canton.protocol.WellFormedTransaction.{
WithSuffixes,
WithSuffixesAndMerged,
@@ -54,14 +52,7 @@ import com.digitalasset.canton.topology.client.TopologySnapshot
import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.FutureInstances.*
import com.digitalasset.canton.util.{ErrorUtil, MapsUtil}
-import com.digitalasset.canton.{
- LfCommand,
- LfCreateCommand,
- LfKeyResolver,
- LfPartyId,
- RequestCounter,
- checked,
-}
+import com.digitalasset.canton.{LfCreateCommand, LfKeyResolver, RequestCounter, checked}
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}
@@ -69,34 +60,14 @@ import scala.concurrent.{ExecutionContext, Future}
/** Allows for checking model conformance of a list of transaction view trees.
* If successful, outputs the received transaction as LfVersionedTransaction along with TransactionMetadata.
*
- * @param reinterpret reinterprets the lf command to a transaction.
+ * @param reinterpreter reinterprets the lf command to a transaction.
* @param transactionTreeFactory reconstructs a transaction view from the reinterpreted action description.
*/
class ModelConformanceChecker(
- val reinterpret: (
- ContractLookupAndVerification,
- Set[LfPartyId],
- LfCommand,
- CantonTimestamp,
- CantonTimestamp,
- Option[LfHash],
- Boolean,
- ViewHash,
- TraceContext,
- Map[PackageName, PackageId],
- GetEngineAbortStatus,
- ) => EitherT[
- Future,
- DAMLeError,
- (LfVersionedTransaction, TransactionMetadata, LfKeyResolver),
- ],
- val validateContract: (
- SerializableContract,
- GetEngineAbortStatus,
- TraceContext,
- ) => EitherT[Future, ContractValidationFailure, Unit],
+ val reinterpreter: HasReinterpret,
+ val validateContract: SerializableContractValidation,
val transactionTreeFactory: TransactionTreeFactory,
- participantId: ParticipantId,
+ val participantId: ParticipantId,
val serializableContractAuthenticator: SerializableContractAuthenticator,
val packageResolver: PackageResolver,
override protected val loggerFactory: NamedLoggerFactory,
@@ -230,7 +201,7 @@ class ModelConformanceChecker(
getEngineAbortStatus: GetEngineAbortStatus,
)(implicit
traceContext: TraceContext
- ): EitherT[Future, Error, Map[LfContractId, StoredContract]] = {
+ ): EitherT[FutureUnlessShutdown, Error, Map[LfContractId, StoredContract]] = {
view.tryFlattenToParticipantViews
.flatMap(_.viewParticipantData.coreInputs)
.parTraverse { case (cid, InputContract(contract, _)) =>
@@ -244,13 +215,14 @@ class ModelConformanceChecker(
.map(_ => cid -> StoredContract(contract, requestCounter, None))
}
.map(_.toMap)
+ .mapK(FutureUnlessShutdown.outcomeK)
}
private def buildPackageNameMap(
packageIds: Set[PackageId]
)(implicit
traceContext: TraceContext
- ): EitherT[Future, Error, Map[PackageName, PackageId]] = {
+ ): EitherT[FutureUnlessShutdown, Error, Map[PackageName, PackageId]] = {
EitherT(for {
resolvedE <- packageIds.toSeq.parTraverse(pId =>
@@ -264,14 +236,15 @@ class ModelConformanceChecker(
for {
resolved <- resolvedE.separate match {
case (Seq(), resolved) => Right(resolved)
- case (unresolved, _) => Left(PackageNotFound(Map(participantId -> unresolved.toSet)))
+ case (unresolved, _) =>
+ Left(PackageNotFound(Map(participantId -> unresolved.toSet)): Error)
}
resolvedNameBindings = resolved.map({ case (pId, name) => name -> pId })
nameBindings <- MapsUtil.toNonConflictingMap(resolvedNameBindings) leftMap { conflicts =>
ConflictingNameBindings(Map(participantId -> conflicts))
}
} yield nameBindings
- })
+ }).mapK(FutureUnlessShutdown.outcomeK)
}
private def checkView(
@@ -300,12 +273,7 @@ class ModelConformanceChecker(
val rbContext = viewParticipantData.rollbackContext
val seed = viewParticipantData.actionDescription.seedOption
for {
-
- viewInputContracts <- validateInputContracts(view, requestCounter, getEngineAbortStatus).mapK(
- FutureUnlessShutdown.outcomeK
- )
-
- _ <- validatePackageVettings(view, topologySnapshot)
+ viewInputContracts <- validateInputContracts(view, requestCounter, getEngineAbortStatus)
contractLookupAndVerification =
new ExtendedContractLookup(
@@ -316,25 +284,27 @@ class ModelConformanceChecker(
serializableContractAuthenticator,
)
- packagePreference <- buildPackageNameMap(packageIdPreference).mapK(
- FutureUnlessShutdown.outcomeK
- )
+ packagePreference <- buildPackageNameMap(packageIdPreference)
- lfTxAndMetadata <- reinterpret(
- contractLookupAndVerification,
- authorizers,
- cmd,
- ledgerTime,
- submissionTime,
- seed,
- failed,
- view.viewHash,
- traceContext,
- packagePreference,
- getEngineAbortStatus,
- ).leftWiden[Error].mapK(FutureUnlessShutdown.outcomeK)
+ lfTxAndMetadata <- reinterpreter
+ .reinterpret(
+ contractLookupAndVerification,
+ authorizers,
+ cmd,
+ ledgerTime,
+ submissionTime,
+ seed,
+ packagePreference,
+ failed,
+ getEngineAbortStatus,
+ )(traceContext)
+ .leftMap(DAMLeError(_, view.viewHash))
+ .leftWiden[Error]
+ .mapK(FutureUnlessShutdown.outcomeK)
- (lfTx, metadata, resolverFromReinterpretation) = lfTxAndMetadata
+ (lfTx, metadata, resolverFromReinterpretation, usedPackages) = lfTxAndMetadata
+
+ _ <- checkPackageVetting(view, topologySnapshot, usedPackages)
// For transaction views of protocol version 3 or higher,
// the `resolverFromReinterpretation` is the same as the `resolverFromView`.
@@ -378,17 +348,11 @@ class ModelConformanceChecker(
} yield WithRollbackScope(rbContext.rollbackScope, suffixedTx)
}
- private def validatePackageVettings(view: TransactionView, snapshot: TopologySnapshot)(implicit
- traceContext: TraceContext
- ): EitherT[FutureUnlessShutdown, Error, Unit] = {
- val referencedContracts =
- (view.inputContracts.fmap(_.contract) ++ view.createdContracts.fmap(_.contract)).values.toSet
- val packageIdsOfContracts =
- referencedContracts.map(_.contractInstance.unversioned.template.packageId)
-
- val packageIdsOfKeys = view.globalKeyInputs.keySet.flatMap(_.packageId)
-
- val packageIds = packageIdsOfContracts ++ packageIdsOfKeys
+ private def checkPackageVetting(
+ view: TransactionView,
+ snapshot: TopologySnapshot,
+ packageIds: Set[PackageId],
+ )(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, Error, Unit] = {
val informees = view.viewCommonData.tryUnwrap.viewConfirmationParameters.informees
@@ -420,48 +384,19 @@ class ModelConformanceChecker(
}
object ModelConformanceChecker {
+
def apply(
- damle: DAMLe,
+ damlE: DAMLe,
transactionTreeFactory: TransactionTreeFactory,
serializableContractAuthenticator: SerializableContractAuthenticator,
participantId: ParticipantId,
packageResolver: PackageResolver,
loggerFactory: NamedLoggerFactory,
)(implicit executionContext: ExecutionContext): ModelConformanceChecker = {
- def reinterpret(
- contracts: ContractLookupAndVerification,
- submitters: Set[LfPartyId],
- command: LfCommand,
- ledgerTime: CantonTimestamp,
- submissionTime: CantonTimestamp,
- rootSeed: Option[LfHash],
- expectFailure: Boolean,
- viewHash: ViewHash,
- traceContext: TraceContext,
- packageResolution: Map[PackageName, PackageId],
- getEngineAbortStatus: GetEngineAbortStatus,
- ): EitherT[
- Future,
- DAMLeError,
- (LfVersionedTransaction, TransactionMetadata, LfKeyResolver),
- ] =
- damle
- .reinterpret(
- contracts,
- submitters,
- command,
- ledgerTime,
- submissionTime,
- rootSeed,
- expectFailure,
- packageResolution,
- getEngineAbortStatus,
- )(traceContext)
- .leftMap(DAMLeError(_, viewHash))
new ModelConformanceChecker(
- reinterpret,
- validateSerializedContract(damle),
+ damlE,
+ validateSerializedContract(damlE),
transactionTreeFactory,
participantId,
serializableContractAuthenticator,
@@ -478,7 +413,14 @@ object ModelConformanceChecker {
expected: LfNodeCreate,
) extends ContractValidationFailure
- private def validateSerializedContract(damle: DAMLe)(
+ private type SerializableContractValidation =
+ (
+ SerializableContract,
+ GetEngineAbortStatus,
+ TraceContext,
+ ) => EitherT[Future, ContractValidationFailure, Unit]
+
+ private def validateSerializedContract(damlE: DAMLe)(
contract: SerializableContract,
getEngineAbortStatus: GetEngineAbortStatus,
traceContext: TraceContext,
@@ -489,7 +431,7 @@ object ModelConformanceChecker {
val metadata = contract.metadata
for {
- actual <- damle
+ actual <- damlE
.replayCreate(
metadata.signatories,
LfCreateCommand(unversioned.template, unversioned.arg),
@@ -552,7 +494,7 @@ object ModelConformanceChecker {
}
}
- /** Indicates that [[ModelConformanceChecker.reinterpret]] has failed. */
+ /** Indicates that [[ModelConformanceChecker.reinterpreter]] has failed. */
final case class DAMLeError(cause: DAMLe.ReinterpretationError, viewHash: ViewHash)
extends Error {
override def pretty: Pretty[DAMLeError] = prettyOfClass(
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala
index 79292ec0ee..5e3536efca 100644
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala
+++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/AcsCommitmentProcessor.scala
@@ -12,6 +12,7 @@ import cats.syntax.traverse.*
import cats.syntax.validated.*
import com.daml.error.*
import com.daml.nameof.NameOf.functionFullName
+import com.digitalasset.canton.admin.participant.v30.{ReceivedCommitmentState, SentCommitmentState}
import com.digitalasset.canton.admin.pruning
import com.digitalasset.canton.concurrent.{FutureSupervisor, Threading}
import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt, PositiveNumeric}
@@ -2268,4 +2269,83 @@ object AcsCommitmentProcessor extends HasLoggerName {
}
}
+ sealed trait ReceivedCmtState {
+ def toProtoV30: ReceivedCommitmentState
+ }
+
+ object ReceivedCmtState {
+ object Match extends ReceivedCmtState {
+ override val toProtoV30: ReceivedCommitmentState =
+ ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_MATCH
+ }
+
+ object Mismatch extends ReceivedCmtState {
+ override val toProtoV30: ReceivedCommitmentState =
+ ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_MISMATCH
+ }
+
+ object Buffered extends ReceivedCmtState {
+ override val toProtoV30: ReceivedCommitmentState =
+ ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_BUFFERED
+ }
+
+ object Outstanding extends ReceivedCmtState {
+ override val toProtoV30: ReceivedCommitmentState =
+ ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_OUTSTANDING
+ }
+
+ def fromProtoV30(
+ proto: ReceivedCommitmentState
+ ): ParsingResult[ReceivedCmtState] =
+ proto match {
+ case ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_MATCH => Right(Match)
+ case ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_MISMATCH => Right(Mismatch)
+ case ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_BUFFERED => Right(Buffered)
+ case ReceivedCommitmentState.RECEIVED_COMMITMENT_STATE_OUTSTANDING => Right(Outstanding)
+ case _ =>
+ Left(
+ ProtoDeserializationError.ValueConversionError(
+ "received commitment state",
+ s"Unknown value: $proto",
+ )
+ )
+ }
+ }
+
+ sealed trait SentCmtState {
+ def toProtoV30: SentCommitmentState
+ }
+
+ object SentCmtState {
+ object Match extends SentCmtState {
+ override val toProtoV30: SentCommitmentState =
+ SentCommitmentState.SENT_COMMITMENT_STATE_MATCH
+ }
+
+ object Mismatch extends SentCmtState {
+ override val toProtoV30: SentCommitmentState =
+ SentCommitmentState.SENT_COMMITMENT_STATE_MISMATCH
+ }
+
+ object NotCompared extends SentCmtState {
+ override val toProtoV30: SentCommitmentState =
+ SentCommitmentState.SENT_COMMITMENT_STATE_NOT_COMPARED
+ }
+
+ def fromProtoV30(
+ proto: SentCommitmentState
+ ): ParsingResult[SentCmtState] =
+ proto match {
+ case SentCommitmentState.SENT_COMMITMENT_STATE_MATCH => Right(Match)
+ case SentCommitmentState.SENT_COMMITMENT_STATE_MISMATCH => Right(Mismatch)
+ case SentCommitmentState.SENT_COMMITMENT_STATE_NOT_COMPARED => Right(NotCompared)
+ case _ =>
+ Left(
+ ProtoDeserializationError.ValueConversionError(
+ "sent commitment state",
+ s"Unknown value: $proto",
+ )
+ )
+ }
+ }
}
diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala
index 05e3429972..072a711037 100644
--- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala
+++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala
@@ -6,7 +6,7 @@ package com.digitalasset.canton.participant.util
import cats.data.EitherT
import cats.syntax.either.*
import com.daml.lf.VersionRange
-import com.daml.lf.data.Ref.PackageId
+import com.daml.lf.data.Ref.{PackageId, PackageName}
import com.daml.lf.data.{ImmArray, Ref, Time}
import com.daml.lf.engine.*
import com.daml.lf.interpretation.Error as LfInterpretationError
@@ -19,7 +19,11 @@ import com.digitalasset.canton.logging.{LoggingContextUtil, NamedLoggerFactory,
import com.digitalasset.canton.participant.admin.PackageService
import com.digitalasset.canton.participant.protocol.EngineController.GetEngineAbortStatus
import com.digitalasset.canton.participant.store.ContractLookupAndVerification
-import com.digitalasset.canton.participant.util.DAMLe.{ContractWithMetadata, PackageResolver}
+import com.digitalasset.canton.participant.util.DAMLe.{
+ ContractWithMetadata,
+ HasReinterpret,
+ PackageResolver,
+}
import com.digitalasset.canton.platform.apiserver.configuration.EngineLoggingConfig
import com.digitalasset.canton.platform.apiserver.execution.AuthorityResolver
import com.digitalasset.canton.protocol.SerializableContract.LedgerCreateTime
@@ -103,6 +107,25 @@ object DAMLe {
packageService: PackageService
): PackageId => TraceContext => Future[Option[Package]] =
pkgId => traceContext => packageService.getPackage(pkgId)(traceContext)
+
+ trait HasReinterpret {
+ def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[Ref.PackageName, Ref.PackageId] = Map.empty,
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ]
+ }
+
}
/** Represents a Daml runtime instance for interpreting commands. Provides an abstraction for the Daml engine
@@ -120,27 +143,27 @@ class DAMLe(
engineLoggingConfig: EngineLoggingConfig,
protected val loggerFactory: NamedLoggerFactory,
)(implicit ec: ExecutionContext)
- extends NamedLogging {
+ extends NamedLogging
+ with HasReinterpret {
+
import DAMLe.{ReinterpretationError, EngineError, EngineAborted}
logger.debug(engine.info.show)(TraceContext.empty)
- def reinterpret(
+ override def reinterpret(
contracts: ContractLookupAndVerification,
submitters: Set[LfPartyId],
command: LfCommand,
ledgerTime: CantonTimestamp,
submissionTime: CantonTimestamp,
rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
expectFailure: Boolean,
- packageResolution: Map[Ref.PackageName, Ref.PackageId] = Map.empty,
getEngineAbortStatus: GetEngineAbortStatus,
- )(implicit
- traceContext: TraceContext
- ): EitherT[
+ )(implicit traceContext: TraceContext): EitherT[
Future,
ReinterpretationError,
- (LfVersionedTransaction, TransactionMetadata, LfKeyResolver),
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
] = {
def peelAwayRootLevelRollbackNode(
@@ -210,6 +233,7 @@ class DAMLe(
txNoRootRollback,
TransactionMetadata.fromLf(ledgerTime, metadata),
metadata.globalKeyMapping,
+ metadata.usedPackages,
)
}
@@ -273,7 +297,7 @@ class DAMLe(
expectFailure = false,
getEngineAbortStatus = getEngineAbortStatus,
)
- (transaction, _, _) = transactionWithMetadata
+ (transaction, _, _, _) = transactionWithMetadata
md = transaction.nodes(transaction.roots(0)) match {
case nc: LfNodeCreate =>
ContractWithMetadata(
@@ -404,4 +428,5 @@ class DAMLe(
handleResultInternal(contracts, result)
}
+
}
diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ExtractUsedAndCreatedTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ExtractUsedAndCreatedTest.scala
index ed58ceccb9..c58b2072f1 100644
--- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ExtractUsedAndCreatedTest.scala
+++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ExtractUsedAndCreatedTest.scala
@@ -47,7 +47,7 @@ class ExtractUsedAndCreatedTest extends BaseTestWordSpec with HasExecutionContex
val tree = etf.rootTransactionViewTree(singleCreate.view0)
val transactionViewTrees = NonEmpty(Seq, (tree, Option.empty[Signature]))
- val transactionViews = transactionViewTrees.map { case (viewTree, _signature) => viewTree.view }
+ val transactionViews = transactionViewTrees.map { case (viewTree, _) => viewTree.view }
val actual = underTest.usedAndCreated(transactionViews)
@@ -172,6 +172,7 @@ class ExtractUsedAndCreatedTest extends BaseTestWordSpec with HasExecutionContex
resolvedKeys = Map.empty,
seed = singleCreate.nodeSeed,
isRoot = true,
+ packagePreference = Set.empty,
)
val viewData = ViewData(
diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceCheckerTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceCheckerTest.scala
index 4619f093ff..d9d71c964d 100644
--- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceCheckerTest.scala
+++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/validation/ModelConformanceCheckerTest.scala
@@ -7,16 +7,11 @@ import cats.data.EitherT
import cats.syntax.parallel.*
import com.daml.lf.data.ImmArray
import com.daml.lf.data.Ref.{PackageId, PackageName}
-import com.daml.lf.engine
+import com.daml.lf.engine.Error as LfError
import com.daml.lf.language.Ast.{Expr, GenPackage, PackageMetadata}
import com.daml.lf.language.LanguageVersion
import com.daml.nonempty.{NonEmpty, NonEmptyUtil}
-import com.digitalasset.canton.data.{
- CantonTimestamp,
- FreeKey,
- FullTransactionViewTree,
- TransactionView,
-}
+import com.digitalasset.canton.data.*
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
import com.digitalasset.canton.logging.pretty.Pretty
import com.digitalasset.canton.participant.protocol.EngineController.{
@@ -29,14 +24,13 @@ import com.digitalasset.canton.participant.protocol.{
SerializableContractAuthenticator,
TransactionProcessingSteps,
}
-import com.digitalasset.canton.participant.store.ContractLookup
-import com.digitalasset.canton.participant.util.DAMLe.{EngineError, PackageResolver}
-import com.digitalasset.canton.protocol.ExampleTransactionFactory.{lfHash, submittingParticipant}
+import com.digitalasset.canton.participant.store.ContractLookupAndVerification
+import com.digitalasset.canton.participant.util.DAMLe
+import com.digitalasset.canton.participant.util.DAMLe.{EngineError, HasReinterpret, PackageResolver}
+import com.digitalasset.canton.protocol.ExampleTransactionFactory.*
import com.digitalasset.canton.protocol.*
import com.digitalasset.canton.topology.client.TopologySnapshot
import com.digitalasset.canton.topology.store.PackageDependencyResolverUS
-import com.digitalasset.canton.topology.transaction.VettedPackages
-import com.digitalasset.canton.topology.{TestingIdentityFactory, TestingTopology}
import com.digitalasset.canton.tracing.TraceContext
import com.digitalasset.canton.util.FutureInstances.*
import com.digitalasset.canton.{
@@ -48,11 +42,11 @@ import com.digitalasset.canton.{
LfPartyId,
RequestCounter,
}
-import org.scalatest.Assertion
import org.scalatest.wordspec.AsyncWordSpec
import pprint.Tree
import java.time.Duration
+import scala.annotation.unused
import scala.concurrent.{ExecutionContext, Future}
class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
@@ -66,60 +60,66 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
val ledgerTimeRecordTimeTolerance: Duration = Duration.ofSeconds(10)
def validateContractOk(
- contract: SerializableContract,
- getEngineAbortStatus: GetEngineAbortStatus,
- context: TraceContext,
+ @unused _contract: SerializableContract,
+ @unused _getEngineAbortStatus: GetEngineAbortStatus,
+ @unused _context: TraceContext,
): EitherT[Future, ContractValidationFailure, Unit] = EitherT.pure(())
- def reinterpret(example: ExampleTransaction)(
- _contracts: ContractLookup,
- _submitters: Set[LfPartyId],
- cmd: LfCommand,
- ledgerTime: CantonTimestamp,
- submissionTime: CantonTimestamp,
- rootSeed: Option[LfHash],
- _inRollback: Boolean,
- _viewHash: ViewHash,
- _traceContext: TraceContext,
- _packageResolution: Map[PackageName, PackageId],
- _getEngineAbortStatus: GetEngineAbortStatus,
- ): EitherT[
- Future,
- DAMLeError,
- (LfVersionedTransaction, TransactionMetadata, LfKeyResolver),
- ] = {
+ def reinterpretExample(
+ example: ExampleTransaction,
+ usedPackages: Set[PackageId] = Set.empty,
+ ): HasReinterpret = new HasReinterpret {
- ledgerTime shouldEqual factory.ledgerTime
- submissionTime shouldEqual factory.submissionTime
+ override def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ DAMLe.ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ] = {
+ ledgerTime shouldEqual factory.ledgerTime
+ submissionTime shouldEqual factory.submissionTime
- val (_viewTree, (reinterpretedTx, metadata, keyResolver), _witnesses) =
- example.reinterpretedSubtransactions.find { case (viewTree, (tx, md, keyResolver), _) =>
- viewTree.viewParticipantData.rootAction.command == cmd &&
- // Commands are otherwise not sufficiently unique (whereas with nodes, we can produce unique nodes).
- rootSeed == md.seeds.get(tx.roots(0))
- }.value
+ val (_, (reinterpretedTx, metadata, keyResolver), _) = {
+ // The code below assumes that for reinterpretedSubtransactions the combination
+ // of command and root-seed wil be unique. In the examples used to date this is
+ // the case. A limitation of this approach is that only one LookupByKey transaction
+ // can be returned as the root seed is unset in the ReplayCommand.
+ example.reinterpretedSubtransactions.find { case (viewTree, (tx, md, _), _) =>
+ viewTree.viewParticipantData.rootAction.command == command &&
+ md.seeds.get(tx.roots(0)) == rootSeed
+ }.value
+ }
- EitherT.rightT[Future, DAMLeError]((reinterpretedTx, metadata, keyResolver))
+ EitherT.rightT((reinterpretedTx, metadata, keyResolver, usedPackages))
+ }
}
- def failOnReinterpret(
- _contracts: ContractLookup,
- _submitters: Set[LfPartyId],
- cmd: LfCommand,
- ledgerTime: CantonTimestamp,
- submissionTime: CantonTimestamp,
- rootSeed: Option[LfHash],
- _inRollback: Boolean,
- _viewHash: ViewHash,
- _traceContext: TraceContext,
- _packageResolution: Map[PackageName, PackageId],
- _getEngineAbortStatus: GetEngineAbortStatus,
- ): EitherT[
- Future,
- DAMLeError,
- (LfVersionedTransaction, TransactionMetadata, LfKeyResolver),
- ] =
- fail("Reinterpret should not be called by this test case.")
+ val failOnReinterpret: HasReinterpret = new HasReinterpret {
+ override def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ DAMLe.ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ] = fail("Reinterpret should not be called by this test case.")
+ }
def viewsWithNoInputKeys(
rootViews: Seq[FullTransactionViewTree]
@@ -127,7 +127,7 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
NonEmptyUtil.fromUnsafe(rootViews.map { viewTree =>
// Include resolvers for all the subviews
val resolvers =
- viewTree.view.allSubviewsWithPosition(viewTree.viewPosition).map { case (view, _viewPos) =>
+ viewTree.view.allSubviewsWithPosition(viewTree.viewPosition).map { case (view, _) =>
view -> (Map.empty: LfKeyResolver)
}
(viewTree, resolvers)
@@ -158,7 +158,7 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
): EitherT[Future, ErrorWithSubTransaction, Result] = {
val rootViewTrees = views.map(_._1)
val commonData = TransactionProcessingSteps.tryCommonData(rootViewTrees)
- val keyResolvers = views.forgetNE.flatMap { case (_vt, resolvers) => resolvers }.toMap
+ val keyResolvers = views.forgetNE.flatMap { case (_, resolvers) => resolvers }.toMap
mcc
.check(
rootViewTrees,
@@ -176,8 +176,18 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
val packageMetadata: PackageMetadata = PackageMetadata(packageName, packageVersion, None)
val genPackage: GenPackage[Expr] =
GenPackage(Map.empty, Set.empty, LanguageVersion.default, packageMetadata)
- val packageResolver: PackageResolver = pkgId =>
- traceContext => Future.successful(Some(genPackage))
+ val packageResolver: PackageResolver = _ => _ => Future.successful(Some(genPackage))
+
+ def buildUnderTest(reinterpretCommand: HasReinterpret): ModelConformanceChecker =
+ new ModelConformanceChecker(
+ reinterpretCommand,
+ validateContractOk,
+ transactionTreeFactory,
+ submittingParticipant,
+ dummyAuthenticator,
+ packageResolver,
+ loggerFactory,
+ )
"A model conformance checker" when {
val relevantExamples = factory.standardHappyCases.filter {
@@ -189,16 +199,7 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
forEvery(relevantExamples) { example =>
s"checking $example" must {
- val sut =
- new ModelConformanceChecker(
- reinterpret(example),
- validateContractOk,
- transactionTreeFactory,
- submittingParticipant,
- dummyAuthenticator,
- packageResolver,
- loggerFactory,
- )
+ val sut = buildUnderTest(reinterpretExample(example))
"yield the correct result" in {
for {
@@ -240,15 +241,7 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
}
"transaction id is inconsistent" must {
- val sut = new ModelConformanceChecker(
- failOnReinterpret,
- validateContractOk,
- transactionTreeFactory,
- submittingParticipant,
- dummyAuthenticator,
- packageResolver,
- loggerFactory,
- )
+ val sut = buildUnderTest(failOnReinterpret)
val singleCreate = factory.SingleCreate(seed = ExampleTransactionFactory.lfHash(0))
val viewTreesWithInconsistentTransactionIds = Seq(
@@ -272,59 +265,53 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
override def treeOf(t: ViewHash): Tree = Apply("[ViewHash]", Seq.empty.iterator)
})
- val error = DAMLeError(EngineError(mock[engine.Error]), mockViewHash)
+ val lfError = mock[LfError]
+ val error = EngineError(lfError)
+
+ val sut = buildUnderTest(new HasReinterpret {
+ override def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ DAMLe.ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ] = EitherT.leftT(error)
+ })
- val sut = new ModelConformanceChecker(
- (_, _, _, _, _, _, _, _, _, _, _) =>
- EitherT.leftT[Future, (LfVersionedTransaction, TransactionMetadata, LfKeyResolver)](
- error
- ),
- validateContractOk,
- transactionTreeFactory,
- submittingParticipant,
- dummyAuthenticator,
- packageResolver,
- loggerFactory,
- )
val example = factory.MultipleRootsAndViewNestings
- def countLeaves(views: NonEmpty[Seq[TransactionView]]): Int =
- views.foldLeft(0)((count, view) => {
- NonEmpty.from(view.subviews.unblindedElements) match {
- case Some(subviewsNE) => count + countLeaves(subviewsNE)
- case None => count + 1
- }
- })
- val nbLeafViews = countLeaves(NonEmptyUtil.fromUnsafe(example.rootViews))
-
"yield an error" in {
+ val exampleTree = example.transactionViewTree0
for {
failure <- leftOrFail(
check(
sut,
- viewsWithNoInputKeys(example.rootTransactionViewTrees),
+ viewsWithNoInputKeys(Seq(exampleTree)),
)
)("reinterpretation fails")
- } yield failure.errors shouldBe Seq.fill(nbLeafViews)(error) // One error per leaf
+ } yield {
+ failure.errors.forgetNE shouldBe Seq(
+ DAMLeError(EngineError(lfError), exampleTree.viewHash)
+ )
+ }
}
}
- "contract upgrading is enabled" should {
+ "checking an upgraded contract" should {
val example: factory.UpgradedSingleExercise = factory.UpgradedSingleExercise(lfHash(0))
- "the choice package may differ from the contract package" in {
+ "allow the choice package to differ from the contract package" in {
- val sut =
- new ModelConformanceChecker(
- reinterpret(example),
- validateContractOk,
- transactionTreeFactory,
- submittingParticipant,
- dummyAuthenticator,
- packageResolver,
- loggerFactory,
- )
+ val sut = buildUnderTest(reinterpretExample(example))
valueOrFail(
check(sut, viewsWithNoInputKeys(example.rootTransactionViewTrees))
@@ -347,18 +334,27 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
signatories = Set(submitter, extra),
),
)
- val sut = new ModelConformanceChecker(
- (_, _, _, _, _, _, _, _, _, _, _) =>
- EitherT.pure[Future, DAMLeError](
- (reinterpreted, subviewMissing.metadata, subviewMissing.keyResolver)
- ),
- validateContractOk,
- transactionTreeFactory,
- submittingParticipant,
- dummyAuthenticator,
- packageResolver,
- loggerFactory,
- )
+
+ val sut = buildUnderTest(new HasReinterpret {
+ override def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ DAMLe.ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ] = EitherT.pure(
+ (reinterpreted, subviewMissing.metadata, subviewMissing.keyResolver, Set.empty)
+ )
+ })
+
for {
result <- leftOrFail(
check(sut, viewsWithNoInputKeys(subviewMissing.rootTransactionViewTrees))
@@ -379,101 +375,82 @@ class ModelConformanceCheckerTest extends AsyncWordSpec with BaseTest {
*/
}
- "a package (referenced by create) is not vetted by some participant" must {
- "yield an error" in {
- import ExampleTransactionFactory.*
- testVettingError(
- NonEmpty.from(factory.SingleCreate(lfHash(0)).rootTransactionViewTrees).value,
- // The package is not vetted for signatoryParticipant
- vettings = Seq(VettedPackages(submittingParticipant, None, Seq(packageId))),
- packageDependenciesLookup = new TestPackageResolver(Right(Set.empty)),
- expectedError = UnvettedPackages(Map(signatoryParticipant -> Set(packageId))),
+ "reinterpretation used package vetting" must {
+
+ "fail conformance if an un-vetted package is used" in {
+ val unexpectedPackageId = PackageId.assertFromString("unexpected-pkg")
+ val example = factory.SingleCreate(seed = factory.deriveNodeSeed(0))
+ val sut =
+ buildUnderTest(reinterpretExample(example, usedPackages = Set(unexpectedPackageId)))
+ val expected = Left(
+ ErrorWithSubTransaction(
+ NonEmpty(
+ Seq,
+ UnvettedPackages(
+ Map(
+ submittingParticipant -> Set(unexpectedPackageId),
+ observerParticipant -> Set(unexpectedPackageId),
+ )
+ ),
+ ),
+ None,
+ Seq.empty,
+ )
)
+ for {
+ actual <- check(sut, viewsWithNoInputKeys(example.rootTransactionViewTrees)).value
+ } yield {
+ actual shouldBe expected
+ }
}
}
- "a package (referenced by key lookup) is not vetted by some participant" must {
- "yield an error" in {
- import ExampleTransactionFactory.*
-
- val key = defaultGlobalKey
- val maintainers = Set(submitter)
- val view = factory.view(
- lookupByKeyNode(key, Set(submitter), None),
- 0,
- Set.empty,
- Seq.empty,
- Seq.empty,
- Map(key -> FreeKey(maintainers)),
- None,
- isRoot = true,
- )
- val viewTree = factory.rootTransactionViewTree(view)
-
- testVettingError(
- NonEmpty(Seq, viewTree),
- // The package is not vetted for submittingParticipant
- vettings = Seq.empty,
- packageDependenciesLookup = new TestPackageResolver(Right(Set.empty)),
- expectedError = UnvettedPackages(Map(submittingParticipant -> Set(key.packageId.value))),
- )
- }
- }
-
- def testVettingError(
- rootViewTrees: NonEmpty[Seq[FullTransactionViewTree]],
- vettings: Seq[VettedPackages],
- packageDependenciesLookup: PackageDependencyResolverUS,
- expectedError: UnvettedPackages,
- ): Future[Assertion] = {
- import ExampleTransactionFactory.*
-
- val sut = new ModelConformanceChecker(
- reinterpret = failOnReinterpret,
- validateContract = validateContractOk,
- transactionTreeFactory = transactionTreeFactory,
- participantId = submittingParticipant,
- serializableContractAuthenticator = dummyAuthenticator,
- packageResolver = packageResolver,
- loggerFactory,
- )
-
- val snapshot = TestingIdentityFactory(
- TestingTopology(
- ).withTopology(Map(submitter -> submittingParticipant, observer -> signatoryParticipant))
- .withPackages(vettings.map(vetting => vetting.participantId -> vetting.packageIds).toMap),
- loggerFactory,
- TestDomainParameters.defaultDynamic,
- ).topologySnapshot(packageDependencyResolver = packageDependenciesLookup)
-
- for {
- error <- check(sut, viewsWithNoInputKeys(rootViewTrees), snapshot).value
- } yield error shouldBe Left(
- ErrorWithSubTransaction(
- NonEmpty(Seq, expectedError),
- None,
- Seq.empty,
- )
- )
- }
-
"a package is not found in the package store" must {
- "yield an error" in {
- import ExampleTransactionFactory.*
- testVettingError(
- NonEmpty.from(factory.SingleCreate(lfHash(0)).rootTransactionViewTrees).value,
- vettings = Seq(
- VettedPackages(submittingParticipant, None, Seq(packageId)),
- VettedPackages(signatoryParticipant, None, Seq(packageId)),
- ),
- // Submitter participant is unable to lookup dependencies.
- // Therefore, the validation concludes that the package is not in the store
- // and thus that the package is not vetted.
- packageDependenciesLookup = new TestPackageResolver(Left(packageId)),
- expectedError = UnvettedPackages(Map(submittingParticipant -> Set(packageId))),
+
+ "fail with an engine error" in {
+
+ val missingPackageId = PackageId.assertFromString("missing-pkg")
+ val example = factory.SingleCreate(seed = factory.deriveNodeSeed(0))
+ val engineError =
+ EngineError(new LfError.Package(LfError.Package.MissingPackage(missingPackageId)))
+ val sut =
+ buildUnderTest(new HasReinterpret {
+ override def reinterpret(
+ contracts: ContractLookupAndVerification,
+ submitters: Set[LfPartyId],
+ command: LfCommand,
+ ledgerTime: CantonTimestamp,
+ submissionTime: CantonTimestamp,
+ rootSeed: Option[LfHash],
+ packageResolution: Map[PackageName, PackageId],
+ expectFailure: Boolean,
+ getEngineAbortStatus: GetEngineAbortStatus,
+ )(implicit traceContext: TraceContext): EitherT[
+ Future,
+ DAMLe.ReinterpretationError,
+ (LfVersionedTransaction, TransactionMetadata, LfKeyResolver, Set[PackageId]),
+ ] = EitherT.fromEither(Left(engineError))
+ })
+ val viewHash = example.transactionViewTrees.head.viewHash
+ val expected = Left(
+ ErrorWithSubTransaction(
+ NonEmpty(
+ Seq,
+ DAMLeError(engineError, viewHash),
+ ),
+ None,
+ Seq.empty,
+ )
)
+ for {
+ actual <- check(sut, viewsWithNoInputKeys(example.rootTransactionViewTrees)).value
+ } yield {
+ actual shouldBe expected
+ }
}
+
}
+
}
class TestPackageResolver(result: Either[PackageId, Set[PackageId]])
diff --git a/sdk/canton/ref b/sdk/canton/ref
index 62ccb8f93a..eedc551ebe 100644
--- a/sdk/canton/ref
+++ b/sdk/canton/ref
@@ -1 +1 @@
-20240617.13482.vc830bc97
+20240618.13495.v4816e0c2
diff --git a/sdk/daml-script/runner/BUILD.bazel b/sdk/daml-script/runner/BUILD.bazel
index cb76e0437f..87bfcb8ac3 100644
--- a/sdk/daml-script/runner/BUILD.bazel
+++ b/sdk/daml-script/runner/BUILD.bazel
@@ -34,6 +34,7 @@ da_scala_library(
if copt not in [
# scalapb does not like those
"-P:wartremover:traverser:org.wartremover.warts.JavaSerializable",
+ "-P:wartremover:traverser:org.wartremover.warts.NonUnitStatements",
"-P:wartremover:traverser:org.wartremover.warts.Product",
"-P:wartremover:traverser:org.wartremover.warts.Serializable",
# ProtocolVersionAnnotation.scala violates this warning