update canton to 20240618.13495.v4816e0c2 (#19407)

* update canton to 20240618.13495.v4816e0c2

tell-slack: canton

* fix build

---------

Co-authored-by: Azure Pipelines Daml Build <support@digitalasset.com>
Co-authored-by: Paul Brauner <paul.brauner@digitalasset.com>
This commit is contained in:
azure-pipelines[bot] 2024-06-19 15:12:29 +02:00 committed by GitHub
parent 351a8c020c
commit 679bdc9cd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1421 additions and 1040 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +0,0 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.canton.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
}
}

View File

@ -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:
* <ul>
* <li>First, `keyVersionAndMaintainers` is used to compute
* `viewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]` from `viewKeyInputs`.</li>
* <li>Second, the result consists of all key-resolution pairs that are in `viewKeyResolutions`,
* but not in `subviewKeyResolutions`.</li>
* </ul>
*
* 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
}
}
}

View File

@ -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:
* <ul>
* <li>First, `keyVersionAndMaintainers` is used to compute
* `viewKeyResolutions: Map[LfGlobalKey, SerializableKeyResolution]` from `viewKeyInputs`.</li>
* <li>Second, the result consists of all key-resolution pairs that are in `viewKeyResolutions`,
* but not in `subviewKeyResolutions`.</li>
* </ul>
*
* 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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
20240617.13482.vc830bc97
20240618.13495.v4816e0c2

View File

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