mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 16:57:40 +03:00
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:
parent
351a8c020c
commit
679bdc9cd2
@ -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;
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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],
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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] = ???
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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]])
|
||||
|
@ -1 +1 @@
|
||||
20240617.13482.vc830bc97
|
||||
20240618.13495.v4816e0c2
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user