diff --git a/sdk/bazel-java-deps.bzl b/sdk/bazel-java-deps.bzl index ff8bd93d6e75..802a9f8f70dd 100644 --- a/sdk/bazel-java-deps.bzl +++ b/sdk/bazel-java-deps.bzl @@ -52,6 +52,7 @@ protobuf_version = "3.24.0" pekko_version = "1.0.1" pekko_http_version = "1.0.0" tapir_version = "1.8.5" +ujson_version = "2.0.0" guava_version = "31.1-jre" @@ -98,9 +99,13 @@ def install_java_deps(): "com.lihaoyi:ammonite-util_{}:2.5.9".format(scala_major_version), "com.lihaoyi:ammonite_{}:2.5.9".format(scala_version), "com.lihaoyi:fansi_{}:0.4.0".format(scala_major_version), + "com.lihaoyi:geny_{}:1.1.1".format(scala_major_version), "com.lihaoyi:os-lib_{}:0.8.0".format(scala_major_version), "com.lihaoyi:pprint_{}:0.8.1".format(scala_major_version), "com.lihaoyi:sourcecode_{}:0.3.0".format(scala_major_version), + "com.lihaoyi:ujson_{}:{}".format(scala_major_version, ujson_version), + "com.lihaoyi:ujson-circe_{}:{}".format(scala_major_version, ujson_version), + "com.lihaoyi:upickle-core_{}:{}".format(scala_major_version, ujson_version), "com.oracle.database.jdbc.debug:ojdbc8_g:19.18.0.0", "com.oracle.database.jdbc:ojdbc8:19.18.0.0", "com.softwaremill.sttp.tapir:tapir-json-circe_{}:{}".format(scala_major_version, tapir_version), diff --git a/sdk/canton/BUILD.bazel b/sdk/canton/BUILD.bazel index a7bd44237d33..1b062cbd9393 100644 --- a/sdk/canton/BUILD.bazel +++ b/sdk/canton/BUILD.bazel @@ -824,6 +824,7 @@ scala_library( ":bindings-java", ":community_base", ":community_ledger_ledger-common", + ":community_ledger_transcode", ":community_util-logging", ":daml-common-staging_daml-errors", ":daml-common-staging_daml-jwt", @@ -858,6 +859,10 @@ scala_library( "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:com_lihaoyi_geny_2_13", + "@maven//:com_lihaoyi_ujson_2_13", + "@maven//:com_lihaoyi_ujson_circe_2_13", + "@maven//:com_lihaoyi_upickle_core_2_13", "@maven//:com_softwaremill_magnolia1_2_magnolia_2_13", "@maven//:com_softwaremill_sttp_model_core_2_13", "@maven//:com_softwaremill_sttp_shared_core_2_13", @@ -889,6 +894,34 @@ scala_library( ], ) +### community/ledger/transcode ### + +scala_library( + name = "community_ledger_transcode", + srcs = glob(["community/ledger/transcode/src/main/scala/**/*.scala"]), + plugins = [kind_projector_plugin], + resource_strip_prefix = "canton/community/ledger/transcode/src/main/resources", + resources = glob(["community/ledger/transcode/src/main/resources/**"]), + scalacopts = [ + "-Xsource:3", + "-language:postfixOps", + ], + unused_dependency_checker_mode = "error", + deps = [ + ":bindings-java", + ":ledger_api_proto_scala", + "//daml-lf/data", + "//daml-lf/language", + "@maven//:com_lihaoyi_geny_2_13", + "@maven//:com_lihaoyi_ujson_2_13", + "@maven//:com_lihaoyi_upickle_core_2_13", + "@maven//:com_thesamet_scalapb_scalapb_runtime_2_13", + "@maven//:junit_junit", + "@maven//:org_scala_lang_scala_reflect", + "@maven//:org_scalaz_scalaz_core_2_13", + ], +) + ### community/domain ### proto_library( diff --git a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/health/admin/v30/status_service.proto b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/health/v30/status_service.proto similarity index 99% rename from sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/health/admin/v30/status_service.proto rename to sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/health/v30/status_service.proto index b4b822101152..8c61dd995065 100644 --- a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/health/admin/v30/status_service.proto +++ b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/health/v30/status_service.proto @@ -3,7 +3,7 @@ syntax = "proto3"; -package com.digitalasset.canton.health.admin.v30; +package com.digitalasset.canton.admin.health.v30; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; diff --git a/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/domain_connectivity.proto b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/domain_connectivity.proto index 57a5686a8e89..1be4771ec9ca 100644 --- a/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/domain_connectivity.proto +++ b/sdk/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/domain_connectivity.proto @@ -32,6 +32,8 @@ service DomainConnectivityService { rpc ListConfiguredDomains(ListConfiguredDomainsRequest) returns (ListConfiguredDomainsResponse); // Get the domain id of the given domain alias rpc GetDomainId(GetDomainIdRequest) returns (GetDomainIdResponse); + // Revoke the authentication tokens for all the sequencers on a domain and disconnect the sequencer clients + rpc Logout(LogoutRequest) returns (LogoutResponse); } message DomainConnectionConfig { @@ -134,3 +136,10 @@ message GetDomainIdRequest { message GetDomainIdResponse { string domain_id = 2; } + +message LogoutRequest { + string domain_alias = 1; +} + +message LogoutResponse { +} diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/EnterpriseSequencerConnectionAdminCommands.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/EnterpriseSequencerConnectionAdminCommands.scala index 26ef9ff1306d..54883d13d2a6 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/EnterpriseSequencerConnectionAdminCommands.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/EnterpriseSequencerConnectionAdminCommands.scala @@ -70,4 +70,21 @@ object EnterpriseSequencerConnectionAdminCommands { override def handleResponse(response: v30.SetConnectionResponse): Either[String, Unit] = Either.unit } + + final case class Logout() + extends BaseSequencerConnectionAdminCommand[v30.LogoutRequest, v30.LogoutResponse, Unit] { + + override def createRequest(): Either[String, v30.LogoutRequest] = + Right(v30.LogoutRequest()) + + override def submitRequest( + service: v30.SequencerConnectionServiceGrpc.SequencerConnectionServiceStub, + request: v30.LogoutRequest, + ): Future[v30.LogoutResponse] = + service.logout(request) + + override def handleResponse(response: v30.LogoutResponse): Either[String, Unit] = Right( + () + ) + } } diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala index 386f72a07a06..e30319ac827c 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala @@ -911,6 +911,23 @@ object ParticipantAdminCommands { override def handleResponse(response: ModifyDomainResponse): Either[String, Unit] = Right(()) } + + final case class Logout(domainAlias: DomainAlias) + extends Base[LogoutRequest, LogoutResponse, Unit] { + + override def createRequest(): Either[String, LogoutRequest] = + Right(LogoutRequest(domainAlias.toProtoPrimitive)) + + override def submitRequest( + service: DomainConnectivityServiceStub, + request: LogoutRequest, + ): Future[LogoutResponse] = + service.logout(request) + + override def handleResponse(response: LogoutResponse): Either[String, Unit] = Right( + () + ) + } } object Resources { diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/StatusAdminCommands.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/StatusAdminCommands.scala index 4e13b1751cba..4fb8048b42c9 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/StatusAdminCommands.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/StatusAdminCommands.scala @@ -6,13 +6,14 @@ package com.digitalasset.canton.admin.api.client.commands import cats.syntax.either.* import ch.qos.logback.classic.Level import com.digitalasset.canton.ProtoDeserializationError -import com.digitalasset.canton.health.admin.data.WaitingForExternalInput -import com.digitalasset.canton.health.admin.v30.{ +import com.digitalasset.canton.admin.health.v30 +import com.digitalasset.canton.admin.health.v30.{ HealthDumpRequest, HealthDumpResponse, StatusServiceGrpc, } -import com.digitalasset.canton.health.admin.{data, v30} +import com.digitalasset.canton.health.admin.data +import com.digitalasset.canton.health.admin.data.WaitingForExternalInput import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import io.grpc.Context.CancellableContext import io.grpc.stub.StreamObserver diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleMacros.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleMacros.scala index e9f1595fe447..22078b88006e 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleMacros.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleMacros.scala @@ -679,7 +679,7 @@ trait ConsoleMacros extends NamedLogging with NoTracing { .map(_.transaction) existingDnsO.getOrElse( - owner.topology.decentralized_namespaces.propose( + owner.topology.decentralized_namespaces.propose_new( owners.map(_.namespace).toSet, PositiveInt.tryCreate(1.max(owners.size - 1)), store = store, diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala index 830beb9169bb..07459225ea5b 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala @@ -6,11 +6,12 @@ package com.digitalasset.canton.console import better.files.File import com.digitalasset.canton.admin.api.client.commands.StatusAdminCommands import com.digitalasset.canton.admin.api.client.data.CantonStatus +import com.digitalasset.canton.admin.health.v30 import com.digitalasset.canton.config.LocalNodeConfig import com.digitalasset.canton.console.CommandErrors.CommandError import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.health.admin.data import com.digitalasset.canton.health.admin.data.NodeStatus -import com.digitalasset.canton.health.admin.{data, v30} import com.digitalasset.canton.metrics.MetricsSnapshot import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.version.ReleaseVersion diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala index c4c76314f7da..5418e839ecfe 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala @@ -5,6 +5,7 @@ package com.digitalasset.canton.console import com.digitalasset.canton.admin.api.client.commands.EnterpriseSequencerAdminCommands.LocatePruningTimestampCommand import com.digitalasset.canton.admin.api.client.commands.* +import com.digitalasset.canton.admin.api.client.data.topology.ListParticipantDomainPermissionResult import com.digitalasset.canton.admin.api.client.data.StaticDomainParameters as ConsoleStaticDomainParameters import com.digitalasset.canton.config.RequireTypes.{ExistingFile, NonNegativeInt, Port, PositiveInt} import com.digitalasset.canton.config.* @@ -596,8 +597,35 @@ abstract class ParticipantReference( override protected def participantIsActiveOnDomain( domainId: DomainId, participantId: ParticipantId, - ): Boolean = topology.domain_trust_certificates.active(domainId, participantId) + ): Boolean = { + val hasDomainTrustCertificate = + topology.domain_trust_certificates.active(domainId, participantId) + val isDomainRestricted = topology.domain_parameters + .get_dynamic_domain_parameters(domainId) + .onboardingRestriction + .isRestricted + val domainPermission = topology.participant_domain_permissions.find(domainId, participantId) + + // notice the `exists`, expressing the requirement of a permission to exist + val hasRequiredDomainPermission = domainPermission.exists(noLoginRestriction) + // notice the forall, expressing optionality for the permission to exist + val hasOptionalDomainPermission = domainPermission.forall(noLoginRestriction) + + // for a participant to be considered active, it must have a domain trust certificate + hasDomainTrustCertificate && + ( + // if the domain is restricted, the participant MUST have the permission + (isDomainRestricted && hasRequiredDomainPermission) || + // if the domain is UNrestricted, the participant may still be restricted by the domain + (!isDomainRestricted && hasOptionalDomainPermission) + ) + } + private def noLoginRestriction(result: ListParticipantDomainPermissionResult): Boolean = + result.item.loginAfter + .forall( + _ <= consoleEnvironment.environment.clock.now + ) } object ParticipantReference { val InstanceType = "Participant" @@ -1167,7 +1195,7 @@ class LocalSequencerReference( override def adminToken: Option[String] = underlying.map(_.adminToken.secret) - @Help.Summary("Returns the sequencerx configuration") + @Help.Summary("Returns the sequencer configuration") override def config: SequencerNodeConfigCommon = consoleEnvironment.environment.config.sequencersByString(name) diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/HealthAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/HealthAdministration.scala index d7a66fd18345..bebb7dabcadc 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/HealthAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/HealthAdministration.scala @@ -9,6 +9,7 @@ import com.digitalasset.canton.admin.api.client.commands.{ StatusAdminCommands, TopologyAdminCommands, } +import com.digitalasset.canton.admin.health.v30 import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration} import com.digitalasset.canton.console.CommandErrors.CommandError import com.digitalasset.canton.console.ConsoleMacros.utils @@ -22,8 +23,8 @@ import com.digitalasset.canton.console.{ Helpful, } import com.digitalasset.canton.grpc.FileStreamObserver +import com.digitalasset.canton.health.admin.data import com.digitalasset.canton.health.admin.data.NodeStatus -import com.digitalasset.canton.health.admin.{data, v30} import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import io.grpc.Context diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala index c42b453ff086..1dcd9ad363de 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala @@ -1274,8 +1274,6 @@ trait ParticipantAdministration extends FeatureFlagFilter { ) def active(domainAlias: DomainAlias): Boolean = list_connected().exists { r => - // TODO(#14053): Filter out participants that are not permissioned on the domain. The TODO is because the daml 2.x - // also asks the domain whether the participant is permissioned, i.e. do we need to for a ParticipantDomainPermission? r.domainAlias == domainAlias && r.healthy && participantIsActiveOnDomain(r.domainId, id) @@ -1705,6 +1703,22 @@ trait ParticipantAdministration extends FeatureFlagFilter { ).toEither } yield () } + + @Help.Summary( + "Revoke this participant's authentication tokens and close all the sequencer connections in the given domain" + ) + @Help.Description(""" + domainAlias: the domain alias from which to logout + On all the sequencers from the specified domain, all existing authentication tokens for this participant + will be revoked. + Note that the participant is not disconnected from the domain; only the connections to the sequencers are closed. + The participant will automatically reopen connections, perform a challenge-response and obtain new tokens. + """) + def logout(domainAlias: DomainAlias): Unit = consoleEnvironment.run { + adminCommand( + ParticipantAdminCommands.DomainConnectivity.Logout(domainAlias) + ) + } } @Help.Summary("Functionality for managing resources") @@ -1764,7 +1778,6 @@ trait ParticipantHealthAdministrationCommon extends FeatureFlagFilter { participantId: ParticipantId, timeout: NonNegativeDuration, domainId: Option[DomainId], - workflowId: String, id: String, ): Either[String, Duration] = consoleEnvironment.run { @@ -1793,7 +1806,7 @@ trait ParticipantHealthAdministrationCommon extends FeatureFlagFilter { id: String = "", ): Duration = { val adminApiRes: Either[String, Duration] = - ping_internal(participantId, timeout, domainId, "", id) + ping_internal(participantId, timeout, domainId, id) consoleEnvironment.runE( adminApiRes.leftMap { reason => s"Unable to ping $participantId within ${LoggerUtil @@ -1813,7 +1826,7 @@ trait ParticipantHealthAdministrationCommon extends FeatureFlagFilter { domainId: Option[DomainId] = None, id: String = "", ): Option[Duration] = check(FeatureFlag.Testing) { - ping_internal(participantId, timeout, domainId, "", id).toOption + ping_internal(participantId, timeout, domainId, id).toOption } } diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala index ea8d43bf6a72..7305b2812a67 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala @@ -27,9 +27,9 @@ class SequencerAdministration(node: SequencerReference) extends ConsoleCommandGr @Help.Summary( "Download sequencer snapshot at given point in time to bootstrap another sequencer" ) + @Help.Description("""It is recommended to use onboarding_state_for_sequencer for onboarding + |a new sequencer.""") def snapshot(timestamp: CantonTimestamp): SequencerSnapshot = - // TODO(#14074) add something like "snapshot for sequencer-id", rather than timestamp based - // we still need to keep the timestamp based such that we can provide recovery for corrupted sequencers consoleEnvironment.run { runner.adminCommand(EnterpriseSequencerAdminCommands.Snapshot(timestamp)) } diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerConnectionAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerConnectionAdministration.scala index 708b2e1c7706..10adf17664f0 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerConnectionAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerConnectionAdministration.scala @@ -96,5 +96,19 @@ trait SequencerConnectionAdministration extends Helpful { } yield () } + @Help.Summary( + "Revoke this sequencer client node's authentication tokens and close all the sequencers connections." + ) + @Help.Description(""" + On all the sequencers, all existing authentication tokens for this sequencer client node will be revoked. + Note that the node is not disconnected from the domain; only the connections to the sequencers are closed. + The node will automatically reopen connections, perform a challenge-response and obtain new tokens. + """) + def logout(): Unit = consoleEnvironment.run { + adminCommand( + EnterpriseSequencerConnectionAdminCommands.Logout() + ) + } + } } diff --git a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministration.scala b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministration.scala index 95389bf1fe04..3f866711905b 100644 --- a/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministration.scala +++ b/sdk/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministration.scala @@ -64,6 +64,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} import scala.math.Ordering.Implicits.infixOrderingOps import scala.reflect.ClassTag +import scala.util.control.NoStackTrace class TopologyAdministrationGroup( instance: InstanceReference, @@ -595,7 +596,7 @@ class TopologyAdministrationGroup( This transaction will be rejected if another fully authorized transaction with the same serial already exists, or if there is a gap between this serial and the most recently used serial. If None, the serial will be automatically selected by the node.""") - def propose( + def propose_new( owners: Set[Namespace], threshold: PositiveInt, store: String, @@ -623,7 +624,7 @@ class TopologyAdministrationGroup( ownersNE, ) .valueOr(error => consoleEnvironment.run(GenericCommandError(error))) - authorize( + propose( decentralizedNamespace, store, mustFullyAuthorize, @@ -633,7 +634,25 @@ class TopologyAdministrationGroup( ) } - def authorize( + @Help.Summary("Propose changes to a decentralized namespace") + @Help.Description(""" + decentralizedNamespace: the DecentralizedNamespaceDefinition to propose + + store: - "Authorized": the topology transaction will be stored in the node's authorized store and automatically + propagated to connected domains, if applicable. + - "": the topology transaction will be directly submitted to the specified domain without + storing it locally first. This also means it will _not_ be synchronized to other domains + automatically. + mustFullyAuthorize: when set to true, the proposal's previously received signatures and the signature of this node must be + sufficient to fully authorize the topology transaction. if this is not the case, the request fails. + when set to false, the proposal retains the proposal status until enough signatures are accumulated to + satisfy the mapping's authorization requirements. + signedBy: the fingerprint of the key to be used to sign this proposal + serial: the expected serial this topology transaction should have. Serials must be contiguous and start at 1. + This transaction will be rejected if another fully authorized transaction with the same serial already + exists, or if there is a gap between this serial and the most recently used serial. + If None, the serial will be automatically selected by the node.""") + def propose( decentralizedNamespace: DecentralizedNamespaceDefinition, store: String, mustFullyAuthorize: Boolean = false, @@ -656,18 +675,6 @@ class TopologyAdministrationGroup( synchronisation.runAdminCommand(synchronize)(command) } - - def join( - decentralizedNamespace: Fingerprint, - owner: Option[Fingerprint] = Some(instance.id.fingerprint), - ): GenericSignedTopologyTransaction = - ??? - - def leave( - decentralizedNamespace: Fingerprint, - owner: Option[Fingerprint] = Some(instance.id.fingerprint), - ): ByteString = - ByteString.EMPTY } @Help.Summary("Manage namespace delegations") @@ -1624,7 +1631,11 @@ class TopologyAdministrationGroup( // TODO(#14057) document console command def active(domainId: DomainId, participantId: ParticipantId): Boolean = - list(filterStore = domainId.filterString).exists { x => + list( + filterStore = domainId.filterString, + filterUid = participantId.filterString, + operation = Some(TopologyChangeOp.Replace), + ).exists { x => x.item.domainId == domainId && x.item.participantId == participantId } @@ -1717,6 +1728,7 @@ class TopologyAdministrationGroup( store: Option[String] = None, mustFullyAuthorize: Boolean = false, serial: Option[PositiveInt] = None, + change: TopologyChangeOp = TopologyChangeOp.Replace, ): SignedTopologyTransaction[TopologyChangeOp, ParticipantDomainPermission] = { val cmd = TopologyAdminCommands.Write.Propose( mapping = ParticipantDomainPermission( @@ -1730,11 +1742,66 @@ class TopologyAdministrationGroup( serial = serial, store = store.getOrElse(domainId.filterString), mustFullyAuthorize = mustFullyAuthorize, + change = change, ) synchronisation.runAdminCommand(synchronize)(cmd) } + @Help.Summary("Revokes the domain permissions of a participant.") + @Help.Description( + """Domain operators may use this command to revoke a participant's permissions on a domain. + + domainId: the target domain + participantId: the participant whose permissions should be revoked + + store: - "Authorized": the topology transaction will be stored in the node's authorized store and automatically + propagated to connected domains, if applicable. + - "": the topology transaction will be directly submitted to the specified domain without + storing it locally first. This also means it will _not_ be synchronized to other domains + automatically. + mustFullyAuthorize: when set to true, the proposal's previously received signatures and the signature of this node must be + sufficient to fully authorize the topology transaction. if this is not the case, the request fails. + when set to false, the proposal retains the proposal status until enough signatures are accumulated to + satisfy the mapping's authorization requirements.""" + ) + def revoke( + domainId: DomainId, + participantId: ParticipantId, + synchronize: Option[NonNegativeDuration] = Some( + consoleEnvironment.commandTimeouts.bounded + ), + mustFullyAuthorize: Boolean = false, + store: Option[String] = None, + ): SignedTopologyTransaction[TopologyChangeOp, ParticipantDomainPermission] = + list( + filterStore = store.getOrElse(domainId.filterString), + filterUid = participantId.filterString, + ) match { + case Seq() => + throw new IllegalStateException( + s"No ParticipantDomainPermission found for participant $participantId." + ) with NoStackTrace + case Seq(result) => + val item = result.item + propose( + item.domainId, + item.participantId, + item.permission, + item.loginAfter, + item.limits, + synchronize, + store = store, + serial = Some(result.context.serial.increment), + mustFullyAuthorize = mustFullyAuthorize, + change = TopologyChangeOp.Remove, + ) + case otherwise => + throw new IllegalStateException( + s"Found more than one ParticipantDomainPermission for participant $participantId on domain $domainId" + ) with NoStackTrace + } + def list( filterStore: String = "", proposals: Boolean = false, @@ -1758,6 +1825,16 @@ class TopologyAdministrationGroup( ) ) } + + @Help.Summary("Looks up the participant permission for a participant on a domain") + @Help.Description("""Returns the optional participant domain permission.""") + def find( + domainId: DomainId, + participantId: ParticipantId, + ): Option[ListParticipantDomainPermissionResult] = + expectAtMostOneResult( + list(filterStore = domainId.filterString, filterUid = participantId.filterString) + ).filter(p => p.item.participantId == participantId && p.item.domainId == domainId) } @Help.Summary("Inspect participant domain states") @@ -1871,7 +1948,7 @@ class TopologyAdministrationGroup( instance.id.fingerprint ), // TODO(#12945) don't use the instance's root namespace key by default. force: ForceFlags = ForceFlags.none, - ): SignedTopologyTransaction[TopologyChangeOp, VettedPackages] = { + ): Unit = { // compute the diff and then call the propose method val current0 = expectAtMostOneResult( @@ -1893,17 +1970,20 @@ class TopologyAdministrationGroup( case None => (PositiveInt.one, (adds.diff(removes)).distinct) } - propose( - participant = participant, - packageIds = newDiffPackageIds, - domainId, - store, - mustFullyAuthorize, - synchronize, - Some(newSerial), - signedBy, - force, - ) + if (current0.exists(_.item.packageIds.toSet == newDiffPackageIds.toSet)) + () // means no change + else + propose( + participant = participant, + packageIds = newDiffPackageIds, + domainId, + store, + mustFullyAuthorize, + synchronize, + Some(newSerial), + signedBy, + force, + ) } } @Help.Summary("Replace package vettings") @@ -1944,7 +2024,7 @@ class TopologyAdministrationGroup( instance.id.fingerprint ), // TODO(#12945) don't use the instance's root namespace key by default. force: ForceFlags = ForceFlags.none, - ): SignedTopologyTransaction[TopologyChangeOp, VettedPackages] = { + ): Unit = { val topologyChangeOp = if (packageIds.isEmpty) TopologyChangeOp.Remove else TopologyChangeOp.Replace @@ -1963,7 +2043,7 @@ class TopologyAdministrationGroup( forceChanges = force, ) - synchronisation.runAdminCommand(synchronize)(command) + synchronisation.runAdminCommand(synchronize)(command).discard } def list( @@ -2562,7 +2642,6 @@ class TopologyAdministrationGroup( val newParameters = update(ConsoleDynamicDomainParameters(previousParameters.item)) // Avoid topology manager ALREADY_EXISTS error by not submitting a no-op proposal. - // TODO(#15817): Move such ux-resilience avoiding error to write_service if (ConsoleDynamicDomainParameters(previousParameters.item) != newParameters) { propose( domainId, diff --git a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/domain/api/v30/sequencer_authentication_service.proto b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/domain/api/v30/sequencer_authentication_service.proto index 0c7de0a74da6..f68bf4a60b62 100644 --- a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/domain/api/v30/sequencer_authentication_service.proto +++ b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/domain/api/v30/sequencer_authentication_service.proto @@ -8,14 +8,16 @@ package com.digitalasset.canton.domain.api.v30; import "com/digitalasset/canton/crypto/v30/crypto.proto"; import "google/protobuf/timestamp.proto"; -// Operations to generate an authentication token for calling sequencer operations +// Operations related to authentication tokens for calling sequencer operations service SequencerAuthenticationService { - // If provided with a supported protocol version and crypto type, + // If provided with a supported protocol version, // will return a nonce and fingerprint of the expected key to sign this nonce rpc Challenge(SequencerAuthentication.ChallengeRequest) returns (SequencerAuthentication.ChallengeResponse) {} - // If provided with a correctly signed nonce, will return a authentication token + // If provided with a correctly signed nonce, will return an authentication token // to be supplied to SequencerService operations rpc Authenticate(SequencerAuthentication.AuthenticateRequest) returns (SequencerAuthentication.AuthenticateResponse) {} + // Unconditionally revoke a member's authentication tokens and disconnect it + rpc Logout(SequencerAuthentication.LogoutRequest) returns (SequencerAuthentication.LogoutResponse) {} } message SequencerAuthentication { @@ -52,6 +54,7 @@ message SequencerAuthentication { // nonce value that was signed is sent back to identify the challenge bytes nonce = 3; } + message AuthenticateResponse { message Success { bytes token = 1; @@ -66,4 +69,11 @@ message SequencerAuthentication { Failure failure = 2; } } + + message LogoutRequest { + // token to identify the member to invalidate + bytes token = 1; + } + + message LogoutResponse {} } diff --git a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto index b157bff7c328..19f577845073 100644 --- a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto +++ b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto @@ -148,7 +148,6 @@ message ParticipantDomainPermission { // optional earliest time when participant can log in (again) // used to temporarily disable participants // In microseconds of UTC time since Unix epoch - // TODO(#14049) implement participant deny list google.protobuf.Int64Value login_after = 5; } diff --git a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/topology_manager_write_service.proto b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/topology_manager_write_service.proto index 524157708b27..25bd8aa069a2 100644 --- a/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/topology_manager_write_service.proto +++ b/sdk/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/topology_manager_write_service.proto @@ -175,5 +175,9 @@ enum ForceFlag { /** Required when increasing the ledger time record time tolerance */ FORCE_FLAG_LEDGER_TIME_RECORD_TIME_TOLERANCE_INCREASE = 2; /** Required when revoking the vetting of a package */ - FORCE_FLAG_PACKAGE_VETTING_REVOCATION = 3; + FORCE_FLAG_ALLOW_UNVET_PACKAGE = 3; + /** Required when vetting unknown packages (not uploaded). */ + FORCE_FLAG_ALLOW_UNKNOWN_PACKAGE = 4; + /** Required when vetting a package with unvetted dependencies */ + FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES = 5; } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentHealthState.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentHealthState.scala index b6c764330ed2..1bade614221c 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentHealthState.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentHealthState.scala @@ -4,8 +4,8 @@ package com.digitalasset.canton.health import com.daml.error.BaseError +import com.digitalasset.canton.admin.health.v30 as proto import com.digitalasset.canton.health.ComponentHealthState.{Degraded, Failed, Fatal, Ok} -import com.digitalasset.canton.health.admin.v30 as proto import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting, PrettyUtil} import com.digitalasset.canton.util.ShowUtil diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentStatus.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentStatus.scala index 6d9487cdd91c..9070a1501998 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentStatus.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/health/ComponentStatus.scala @@ -5,8 +5,8 @@ package com.digitalasset.canton.health import cats.implicits.catsSyntaxEitherId import com.digitalasset.canton.* +import com.digitalasset.canton.admin.health.v30 as proto import com.digitalasset.canton.health.ComponentHealthState.* -import com.digitalasset.canton.health.admin.v30 as proto import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import io.circe.Encoder diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/AuthenticationTokenProvider.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/AuthenticationTokenProvider.scala index a21f6a6a8349..10e03b451e39 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/AuthenticationTokenProvider.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/AuthenticationTokenProvider.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.domain.api.v30.SequencerAuthentication.{ AuthenticateResponse, ChallengeRequest, ChallengeResponse, + LogoutRequest, } import com.digitalasset.canton.domain.api.v30.SequencerAuthenticationServiceGrpc.SequencerAuthenticationServiceStub import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} @@ -27,9 +28,10 @@ import com.digitalasset.canton.topology.{DomainId, Member} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.retry.{NoExceptionRetryPolicy, Pause} import com.digitalasset.canton.version.ProtocolVersion -import io.grpc.Status +import io.grpc.{Status, StatusRuntimeException} import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} /** Configures authentication token fetching * @@ -179,4 +181,22 @@ class AuthenticationTokenProvider( }.mapK(FutureUnlessShutdown.outcomeK) } yield token + def logout( + authenticationClient: SequencerAuthenticationServiceStub + ): EitherT[FutureUnlessShutdown, Status, Unit] = + for { + // Generate a new token to use as "entry point" to invalidate all tokens + tokenWithExpiry <- generateToken(authenticationClient) + token = tokenWithExpiry.token + + _ <- EitherT( + authenticationClient + .logout(LogoutRequest(token.toProtoPrimitive)) + .transform { + case Failure(exc: StatusRuntimeException) => Success(Left(exc.getStatus)) + case Failure(exc) => Success(Left(Status.INTERNAL.withDescription(exc.getMessage))) + case Success(_) => Success(Right(())) + } + ).mapK(FutureUnlessShutdown.outcomeK) + } yield () } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/MemberAuthentication.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/MemberAuthentication.scala index c35593da7e01..5f74aad170e7 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/MemberAuthentication.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/MemberAuthentication.scala @@ -81,18 +81,17 @@ object MemberAuthentication extends MemberAuthentication { s"Due to an internal error, the server side token lookup for member $member failed", "VerifyTokenTimeout", ) + final case object LogoutTokenDoesNotExist + extends AuthenticationError( + s"The token provided for logging out does not exist", + "LogoutTokenDoesNotExist", + ) final case class AuthenticationNotSupportedForMember(member: Member) extends AuthenticationError( reason = s"Authentication for member type is not supported: $member", code = "UnsupportedMember", ) - final object PassiveSequencer - extends AuthenticationError( - reason = - "Sequencer is currently passive. Connect to a different sequencer and retry the request or wait for the sequencer to become active again.", - code = "PassiveSequencer", - ) def hashDomainNonce( nonce: Nonce, diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/AuthenticationTokenManager.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/AuthenticationTokenManager.scala index cdfcd822d6ea..fca5b92c31f2 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/AuthenticationTokenManager.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/AuthenticationTokenManager.scala @@ -66,7 +66,7 @@ class AuthenticationTokenManager( def getToken: EitherT[FutureUnlessShutdown, Status, AuthenticationToken] = refreshToken(refreshWhenHaveToken = false) - /** Invalid the current token if it matches the provided value. + /** Invalidate the current token if it matches the provided value. * Although unlikely, the token must be provided here in case a response terminates after a new token has already been generated. */ def invalidateToken(invalidToken: AuthenticationToken): Unit = { diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/SequencerClientAuthentication.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/SequencerClientAuthentication.scala index 08db60b93ee2..86797c008bda 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/SequencerClientAuthentication.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/authentication/grpc/SequencerClientAuthentication.scala @@ -95,10 +95,6 @@ private[grpc] class SequencerClientTokenAuthentication( applier.apply(generateMetadata(token, maybeEndpoint)) } } - - override def thisUsesUnstableApi(): Unit = { - // yes, we know - cheers grpc - } } /** Will invalidate the current token if an UNAUTHORIZED response is observed. diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SendTracker.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SendTracker.scala index 0149ff90494b..b710e350ab30 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SendTracker.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SendTracker.scala @@ -55,7 +55,6 @@ class SendTracker( with FlagCloseableAsync with AutoCloseable { - private implicit val metricsContext: MetricsContext = MetricsContext("sender" -> member.toString) private implicit val directExecutionContext: DirectExecutionContext = DirectExecutionContext( noTracingLogger ) @@ -70,6 +69,7 @@ class SendTracker( callback: SendCallback, startedAt: Option[Instant], traceContext: TraceContext, + metricsContext: MetricsContext, ) private val pendingSends: TrieMap[MessageId, PendingSend] = @@ -81,6 +81,7 @@ class SendTracker( SendCallback.empty, startedAt = None, TraceContext.empty, + MetricsContext.Empty, ) }).result() @@ -89,7 +90,8 @@ class SendTracker( maxSequencingTime: CantonTimestamp, callback: SendCallback = SendCallback.empty, )(implicit - traceContext: TraceContext + traceContext: TraceContext, + metricsContext: MetricsContext, ): EitherT[FutureUnlessShutdown, SavePendingSendError, Unit] = performUnlessClosingEitherU(s"track $messageId") { for { @@ -102,6 +104,7 @@ class SendTracker( callback, startedAt = Some(Instant.now()), traceContext, + metricsContext, ), ) match { case Some(previousMaxSequencingTime) => @@ -161,7 +164,7 @@ class SendTracker( timestamp: CantonTimestamp ): Future[Unit] = { val timedOut = pendingSends.collect { - case (messageId, PendingSend(maxSequencingTime, _, _, traceContext)) + case (messageId, PendingSend(maxSequencingTime, _, _, traceContext, _)) if maxSequencingTime < timestamp => Traced(messageId)(traceContext) }.toList @@ -241,16 +244,35 @@ class SendTracker( None } + // Metrics context extracted from the pending send + // This allows to get labels such as the request type and application ID back and use them to update + // event specific metrics + val eventSpecificMetricsContext = current + .map(_.metricsContext) + .getOrElse( + // If we there's no pending send, set the application id and type labels to unknown to get consistent + // labelling even during crash recovery (when we may not have corresponding pending sends for the receipts) + MetricsContext( + "application-id" -> "unknown", + "type" -> "unknown", + ) + ) // Update the traffic controller with the traffic consumed in the receipt (trafficStateController, resultO) match { case (Some(tsc), Some(UnlessShutdown.Outcome(Success(deliver)))) => - deliver.trafficReceipt.foreach(tsc.updateWithReceipt(_, deliver.timestamp, None)) + deliver.trafficReceipt.foreach( + tsc.updateWithReceipt(_, deliver.timestamp, None, eventSpecificMetricsContext) + ) case (Some(tsc), Some(UnlessShutdown.Outcome(Error(deliverError)))) => deliverError.trafficReceipt.foreach( tsc.updateWithReceipt( _, deliverError.timestamp, - BaseCantonError.statusErrorCodes(deliverError.reason).headOption.orElse(Some("unknown")), + BaseCantonError + .statusErrorCodes(deliverError.reason) + .headOption + .orElse(Some("unknown")), + eventSpecificMetricsContext, ) ) case (Some(tsc), Some(UnlessShutdown.Outcome(Timeout(timestamp)))) => diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala index 84c6ff643f28..94bc1887ee06 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala @@ -77,6 +77,7 @@ import com.digitalasset.canton.util.retry.AllExceptionRetryPolicy import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{SequencerAlias, SequencerCounter, time} import com.google.common.annotations.VisibleForTesting +import io.grpc.Status import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.scaladsl.{Flow, Keep, Sink, Source} import org.apache.pekko.stream.{KillSwitch, KillSwitches, Materializer} @@ -93,6 +94,11 @@ import scala.util.{Failure, Success, Try} trait SequencerClient extends SequencerClientSend with FlagCloseable { + /** Provides an entry point to revoke all the authentication tokens for a member on a given + * sequencer, and close the connection to that sequencer. + */ + def logout(): EitherT[FutureUnlessShutdown, Status, Unit] + /** The sequencer client computes the cost of submission requests sent to the sequencer, * and update the traffic state when receiving confirmation that the event has been sequenced. * This is done via the traffic state controller. @@ -208,6 +214,11 @@ abstract class SequencerClientImpl( with Spanning with HasCloseContext { + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + sequencerTransports.sequencerIdToTransportMap.values.toSeq.parTraverse_ { transport => + transport.clientTransport.logout() + } + protected val sequencersTransportState: SequencersTransportState = new SequencersTransportState( sequencerTransports, diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientAuth.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientAuth.scala index 8cfd4a7f9479..fa0a755561a2 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientAuth.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientAuth.scala @@ -3,12 +3,13 @@ package com.digitalasset.canton.sequencing.client.transports +import cats.data.EitherT import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.domain.api.v30.SequencerAuthenticationServiceGrpc.SequencerAuthenticationServiceStub import com.digitalasset.canton.lifecycle.Lifecycle.CloseableChannel -import com.digitalasset.canton.lifecycle.{FlagCloseable, Lifecycle} +import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.Endpoint import com.digitalasset.canton.sequencing.authentication.grpc.SequencerClientTokenAuthentication @@ -20,8 +21,8 @@ import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.{DomainId, Member} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.version.ProtocolVersion -import io.grpc.ManagedChannel import io.grpc.stub.AbstractStub +import io.grpc.{ManagedChannel, Status} import scala.concurrent.ExecutionContext @@ -51,6 +52,11 @@ class GrpcSequencerClientAuth( loggerFactory, ) + def logout(channel: ManagedChannel): EitherT[FutureUnlessShutdown, Status, Unit] = { + val authenticationClient = new SequencerAuthenticationServiceStub(channel) + tokenProvider.logout(authenticationClient) + } + /** Wrap a grpc client with components to appropriately perform authentication */ def apply[S <: AbstractStub[S]](client: S): S = { val obtainTokenPerEndpoint = channelPerEndpoint.transform { case (_, channel) => diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala index 601046234627..ea44b4f3f5f8 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSequencerClientTransport.scala @@ -38,7 +38,7 @@ import com.digitalasset.canton.util.EitherTUtil.syntax.* import com.digitalasset.canton.util.EitherUtil import com.digitalasset.canton.version.ProtocolVersion import io.grpc.Context.CancellableContext -import io.grpc.{CallOptions, Context, ManagedChannel} +import io.grpc.{CallOptions, Context, ManagedChannel, Status} import org.apache.pekko.stream.Materializer import org.apache.pekko.stream.scaladsl.Source @@ -59,6 +59,9 @@ private[transports] abstract class GrpcSequencerClientTransportCommon( ) extends SequencerClientTransportCommon with NamedLogging { + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + clientAuth.logout(channel) + protected val sequencerServiceClient: SequencerServiceStub = clientAuth( new SequencerServiceStub(channel, options = callOptions) ) diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/SequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/SequencerClientTransport.scala index 1def677e5d55..bbfde8ee168c 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/SequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/SequencerClientTransport.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.sequencing.client.{ } import com.digitalasset.canton.sequencing.protocol.* import com.digitalasset.canton.tracing.TraceContext +import io.grpc.Status import scala.concurrent.Future import scala.concurrent.duration.Duration @@ -20,6 +21,10 @@ import scala.concurrent.duration.Duration /** Implementation dependent operations for a client to write to a domain sequencer. */ trait SequencerClientTransportCommon extends FlagCloseable { + /** Revoke all the authentication tokens on this sequencer and close the connection. + */ + def logout(): EitherT[FutureUnlessShutdown, Status, Unit] + /** Sends a signed submission request to the sequencer. * If we failed to make the request, an error will be returned. * If the sequencer accepted (or may have accepted) the request this call will return successfully. diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingEventsSequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingEventsSequencerClientTransport.scala index 30068b7b8998..2e44e502292d 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingEventsSequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingEventsSequencerClientTransport.scala @@ -33,6 +33,7 @@ import com.digitalasset.canton.tracing.{TraceContext, Traced} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.{ErrorUtil, FutureUtil, MonadUtil} import com.digitalasset.canton.version.ProtocolVersion +import io.grpc.Status import java.nio.file.Path import java.time.Duration as JDuration @@ -53,6 +54,9 @@ class ReplayingEventsSequencerClientTransport( with SequencerClientTransportPekko with NamedLogging { + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + EitherT.pure(()) + /** Does nothing */ override def sendAsyncSigned( request: SignedContent[SubmissionRequest], diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala index 753f0ab59045..802759e98505 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/replay/ReplayingSendsSequencerClientTransport.scala @@ -36,6 +36,7 @@ import com.digitalasset.canton.tracing.{NoTracing, TraceContext, Traced} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.{ErrorUtil, OptionUtil, PekkoUtil} import com.digitalasset.canton.version.ProtocolVersion +import io.grpc.Status import io.opentelemetry.sdk.metrics.data.MetricData import org.apache.pekko.NotUsed import org.apache.pekko.stream.Materializer @@ -435,6 +436,9 @@ class ReplayingSendsSequencerClientTransportImpl( ) with SequencerClientTransport with SequencerClientTransportPekko { + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + EitherT.pure(()) + override def subscribe[E](request: SubscriptionRequest, handler: SerializedEventHandler[E])( implicit traceContext: TraceContext ): SequencerSubscription[E] = new SequencerSubscription[E] { diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficConsumedManager.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficConsumedManager.scala index 4b6068ced61d..cb75cf7a0ddd 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficConsumedManager.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficConsumedManager.scala @@ -34,11 +34,15 @@ class TrafficConsumedManager( def getTrafficConsumed: TrafficConsumed = trafficConsumed.get /** Update the traffic consumed state with the provided receipt only if it is more recent. + * @return true if the state was updated. */ def updateWithReceipt(trafficReceipt: TrafficReceipt, timestamp: CantonTimestamp)(implicit metricsContext: MetricsContext - ): TrafficConsumed = - updateAndGet { + ): Boolean = { + // We need to get the traffic consumed before the update to compare its timestamp with the input + // timestamp and know for sure if it was updated or not. That's why we don't use "updateAndGet" here + // but instead trafficConsumed.getAndUpdate + val oldTrafficConsumed = trafficConsumed.getAndUpdate { case current if current.sequencingTimestamp < timestamp => current.copy( extraTrafficConsumed = trafficReceipt.extraTrafficConsumed, @@ -49,6 +53,20 @@ class TrafficConsumedManager( case current => current } + val trafficConsumedUpdated = oldTrafficConsumed.sequencingTimestamp < timestamp + + // And then update the metrics if the state was updated + if (trafficConsumedUpdated) { + updateTrafficConsumedMetrics( + trafficReceipt.extraTrafficConsumed, + trafficReceipt.baseTrafficRemainder, + timestamp, + ) + } + + trafficConsumedUpdated + } + /** Validate that the event cost is below the traffic limit at the provided timestamp. * DOES NOT debit the cost from the traffic state. */ @@ -113,16 +131,28 @@ class TrafficConsumedManager( f: UnaryOperator[TrafficConsumed] )(implicit metricsContext: MetricsContext) = { val newTrafficConsumed = trafficConsumed.updateAndGet(f) + updateTrafficConsumedMetrics( + newTrafficConsumed.extraTrafficConsumed, + newTrafficConsumed.baseTrafficRemainder, + newTrafficConsumed.sequencingTimestamp, + ) + newTrafficConsumed + } + + private def updateTrafficConsumedMetrics( + extraTrafficConsumed: NonNegativeLong, + baseTrafficRemainder: NonNegativeLong, + lastTrafficUpdateTimestamp: CantonTimestamp, + )(implicit metricsContext: MetricsContext): Unit = { metrics .extraTrafficConsumed(metricsContext) - .updateValue(newTrafficConsumed.extraTrafficConsumed.value) + .updateValue(extraTrafficConsumed.value) metrics .baseTrafficRemainder(metricsContext) - .updateValue(newTrafficConsumed.baseTrafficRemainder.value) + .updateValue(baseTrafficRemainder.value) metrics .lastTrafficUpdateTimestamp(metricsContext) - .updateValue(newTrafficConsumed.sequencingTimestamp.getEpochSecond) - newTrafficConsumed + .updateValue(lastTrafficUpdateTimestamp.getEpochSecond) } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficControlProcessor.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficControlProcessor.scala index 31c450bf920f..4e0324154356 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficControlProcessor.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficControlProcessor.scala @@ -20,13 +20,11 @@ import com.digitalasset.canton.protocol.messages.{ SetTrafficPurchasedMessage, SignedProtocolMessage, } -import com.digitalasset.canton.sequencing.SubscriptionStart import com.digitalasset.canton.sequencing.protocol.{ Deliver, DeliverError, OpenEnvelope, Recipient, - SequencedEvent, SequencersOfDomain, } import com.digitalasset.canton.sequencing.traffic.TrafficControlErrors.{ @@ -34,8 +32,16 @@ import com.digitalasset.canton.sequencing.traffic.TrafficControlErrors.{ TrafficControlError, } import com.digitalasset.canton.sequencing.traffic.TrafficControlProcessor.TrafficControlSubscriber +import com.digitalasset.canton.sequencing.{ + BoxedEnvelope, + HandlerResult, + SubscriptionStart, + UnsignedEnvelopeBox, + UnsignedProtocolEventHandler, +} +import com.digitalasset.canton.time.DomainTimeTracker import com.digitalasset.canton.topology.DomainId -import com.digitalasset.canton.tracing.{TraceContext, Traced} +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.FutureInstances.* import com.digitalasset.canton.util.MonadUtil @@ -49,15 +55,18 @@ class TrafficControlProcessor( override protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext -) extends NamedLogging { +) extends UnsignedProtocolEventHandler + with NamedLogging { + + override val name: String = s"traffic-control-processor-$domainId" private val listeners = new AtomicReference[List[TrafficControlSubscriber]](List.empty) def subscribe(subscriber: TrafficControlSubscriber): Unit = listeners.updateAndGet(subscriber :: _).discard - def subscriptionStartsAt(start: SubscriptionStart)(implicit - traceContext: TraceContext + override def subscriptionStartsAt(start: SubscriptionStart, domainTimeTracker: DomainTimeTracker)( + implicit traceContext: TraceContext ): FutureUnlessShutdown[Unit] = { import SubscriptionStart.* @@ -81,10 +90,10 @@ class TrafficControlProcessor( FutureUnlessShutdown.unit } - def handle( - tracedEvents: NonEmpty[Seq[Traced[SequencedEvent[DefaultOpenEnvelope]]]] - ): FutureUnlessShutdown[Unit] = - MonadUtil.sequentialTraverseMonoid(tracedEvents) { tracedEvent => + override def apply( + tracedBatch: BoxedEnvelope[UnsignedEnvelopeBox, DefaultOpenEnvelope] + ): HandlerResult = + MonadUtil.sequentialTraverseMonoid(tracedBatch.value) { tracedEvent => implicit val tracContext: TraceContext = tracedEvent.traceContext tracedEvent.value match { @@ -102,11 +111,13 @@ class TrafficControlProcessor( }, ) - processSetTrafficPurchasedEnvelopes(ts, topologyTimestampO, domainEnvelopes) + HandlerResult.synchronous( + processSetTrafficPurchasedEnvelopes(ts, topologyTimestampO, domainEnvelopes) + ) case DeliverError(_sc, ts, _domainId, _messageId, _status, _trafficReceipt) => notifyListenersOfTimestamp(ts) - FutureUnlessShutdown.unit + HandlerResult.done } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala index 9a34aaca8b1c..11756b048afb 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficStateController.scala @@ -10,7 +10,6 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt} import com.digitalasset.canton.crypto.{SyncCryptoApi, SyncCryptoClient} import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.metrics.TrafficConsumptionMetrics @@ -53,6 +52,10 @@ class TrafficStateController( metrics, ) + private implicit val memberMetricsContext: MetricsContext = MetricsContext( + "sender" -> member.toString + ) + def getTrafficConsumed: TrafficConsumed = trafficConsumedManager.getTrafficConsumed def getState: TrafficState = getTrafficConsumed.toTrafficState(currentTrafficPurchased.get()) @@ -100,7 +103,6 @@ class TrafficStateController( def tickStateAt(sequencingTimestamp: CantonTimestamp)(implicit executionContext: ExecutionContext, traceContext: TraceContext, - metricsContext: MetricsContext, ): Unit = FutureUtil.doNotAwaitUnlessShutdown( for { topology <- topologyClient.awaitSnapshotUS(sequencingTimestamp) @@ -126,21 +128,29 @@ class TrafficStateController( trafficReceipt: TrafficReceipt, timestamp: CantonTimestamp, deliverErrorReason: Option[String], - )(implicit - metricsContext: MetricsContext - ): Unit = { - deliverErrorReason match { - case Some(reason) => - metrics.trafficCostOfNotDeliveredSequencedEvent.mark(trafficReceipt.consumedCost.value)( - metricsContext.withExtraLabels("reason" -> reason) - ) - metrics.deliveredEventCounter.inc() - case None => - metrics.trafficCostOfDeliveredSequencedEvent.mark(trafficReceipt.consumedCost.value) - metrics.rejectedEventCounter.inc() + eventSpecificMetricsContext: MetricsContext, + ): Unit = + // Only update the event-specific traffic metrics if the state was updated (to avoid double reporting cost consumption) + // This is especially relevant during crash recovery if the member replays events from the sequencer subscription + if (trafficConsumedManager.updateWithReceipt(trafficReceipt, timestamp)) { + // For event specific metrics, we merge the member metrics context with the metrics context containing + // additional labels such as application ID and event type. It's possible we don't have such context + // during crash recovery though as this context is kept in the send tracker in memory cache, which is not persisted + val mergedEventSpecificMetricsContext = + memberMetricsContext.merge(eventSpecificMetricsContext) + deliverErrorReason match { + case Some(reason) => + metrics.trafficCostOfNotDeliveredSequencedEvent.mark(trafficReceipt.consumedCost.value)( + mergedEventSpecificMetricsContext.withExtraLabels("reason" -> reason) + ) + metrics.deliveredEventCounter.inc()(mergedEventSpecificMetricsContext) + case None => + metrics.trafficCostOfDeliveredSequencedEvent.mark(trafficReceipt.consumedCost.value)( + mergedEventSpecificMetricsContext + ) + metrics.rejectedEventCounter.inc()(mergedEventSpecificMetricsContext) + } } - trafficConsumedManager.updateWithReceipt(trafficReceipt, timestamp).discard - } /** Compute the cost of a batch of envelopes. * Does NOT debit the cost from the current traffic purchased. diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ForceFlags.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ForceFlags.scala index 176af6659548..a04fff63f7d5 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ForceFlags.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ForceFlags.scala @@ -21,8 +21,12 @@ object ForceFlag { case object LedgerTimeRecordTimeToleranceIncrease extends ForceFlag(v30.ForceFlag.FORCE_FLAG_LEDGER_TIME_RECORD_TIME_TOLERANCE_INCREASE) - case object PackageVettingRevocation - extends ForceFlag(v30.ForceFlag.FORCE_FLAG_PACKAGE_VETTING_REVOCATION) + case object AllowUnvetPackage extends ForceFlag(v30.ForceFlag.FORCE_FLAG_ALLOW_UNVET_PACKAGE) + + case object AllowUnknownPackage extends ForceFlag(v30.ForceFlag.FORCE_FLAG_ALLOW_UNKNOWN_PACKAGE) + + case object AllowUnvettedDependencies + extends ForceFlag(v30.ForceFlag.FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES) /** This should only be used internally in situations where * */ val all: Map[v30.ForceFlag, ForceFlag] = - Seq[ForceFlag](AlienMember, LedgerTimeRecordTimeToleranceIncrease, PackageVettingRevocation) + Seq[ForceFlag]( + AlienMember, + LedgerTimeRecordTimeToleranceIncrease, + AllowUnvetPackage, + AllowUnknownPackage, + AllowUnvettedDependencies, + ) .map(ff => ff.toProtoV30 -> ff) .toMap diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala index 94a42596fe1d..54f7b7658ff4 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala @@ -157,6 +157,20 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( def clearObservers(): Unit = observers.set(Seq.empty) + /** Allows the participant to override this method to enable additional checks on the VettedPackages transaction. + * Only the participant has access to the package store. + */ + def validatePackages( + currentlyVettedPackages: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], + forceFlags: ForceFlags, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = { + val _ = traceContext + EitherT.rightT(()) + } + /** Authorizes a new topology transaction by signing it and adding it to the topology state * * @param op the operation that should be performed @@ -447,7 +461,6 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( transactions <- addMissingOtkSignaturesForSigningKeys(transactionsToAdd) _ <- MonadUtil .sequentialTraverse_(transactions)(transactionIsNotDangerous(_, forceChanges)) - .mapK(FutureUnlessShutdown.outcomeK) transactionsInStore <- EitherT .liftF( @@ -518,7 +531,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( forceChanges: ForceFlags, )(implicit traceContext: TraceContext - ): EitherT[Future, TopologyManagerError, Unit] = transaction.mapping match { + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = transaction.mapping match { case DomainParametersState(domainId, newDomainParameters) => checkLedgerTimeRecordTimeToleranceNotIncreasing(domainId, newDomainParameters, forceChanges) case OwnerToKeyMapping(member, _, _) => @@ -539,7 +552,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( topologyMappingCode: TopologyMapping.Code, )(implicit traceContext: TraceContext - ): EitherT[Future, TopologyManagerError, Unit] = + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = EitherTUtil.condUnitET( member.uid == nodeId || forceChanges.permits(ForceFlag.AlienMember), DangerousCommandRequiresForce.AlienMember(member, topologyMappingCode), @@ -549,17 +562,21 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( domainId: DomainId, newDomainParameters: DynamicDomainParameters, forceChanges: ForceFlags, - )(implicit traceContext: TraceContext): EitherT[Future, TopologyManagerError, Unit] = + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = // See i9028 for a detailed design. EitherT(for { - headTransactions <- store.findPositiveTransactions( - asOf = CantonTimestamp.MaxValue, - asOfInclusive = false, - isProposal = false, - types = Seq(DomainParametersState.code), - filterUid = Some(Seq(domainId.uid)), - filterNamespace = None, + headTransactions <- FutureUnlessShutdown.outcomeF( + store.findPositiveTransactions( + asOf = CantonTimestamp.MaxValue, + asOfInclusive = false, + isProposal = false, + types = Seq(DomainParametersState.code), + filterUid = Some(Seq(domainId.uid)), + filterNamespace = None, + ) ) } yield { headTransactions @@ -595,43 +612,47 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId]( newPackageIds: Set[LfPackageId], forceChanges: ForceFlags, topologyMappingCode: TopologyMapping.Code, - )(implicit traceContext: TraceContext): EitherT[Future, TopologyManagerError, Unit] = + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = for { - addedAndRemoved <- EitherT.right( - store - .findPositiveTransactions( - asOf = CantonTimestamp.MaxValue, - asOfInclusive = false, - isProposal = false, - types = Seq(VettedPackages.code), - filterUid = Some(Seq(participantId.uid)), - filterNamespace = None, - ) - .map { headTransactions => - val headPackages = headTransactions - .collectOfMapping[VettedPackages] - .collectLatestByUniqueKey - .toTopologyState - .collectFirst { case VettedPackages(_, _, existingPackageIds) => - existingPackageIds - } - .getOrElse(Nil) - .toSet - val packagesToUnvet = headPackages -- newPackageIds - val addedPackages = newPackageIds -- headPackages - (addedPackages, packagesToUnvet) - } - ) - (added, removed) = (addedAndRemoved._1, addedAndRemoved._2) - _ <- checkPackageVettingRevocation(removed, forceChanges) + headPackageIds <- EitherT + .right( + store + .findPositiveTransactions( + asOf = CantonTimestamp.MaxValue, + asOfInclusive = false, + isProposal = false, + types = Seq(VettedPackages.code), + filterUid = Some(Seq(participantId.uid)), + filterNamespace = None, + ) + ) + .mapK( + FutureUnlessShutdown.outcomeK + ) + .map { + _.collectOfMapping[VettedPackages].collectLatestByUniqueKey.toTopologyState + .collectFirst { case VettedPackages(_, _, existingPackageIds) => + existingPackageIds + } + .getOrElse(Nil) + .toSet + } + _ <- checkPackageVettingRevocation(headPackageIds, newPackageIds, forceChanges) _ <- checkTransactionIsForCurrentNode(participantId, forceChanges, topologyMappingCode) + _ <- validatePackages(headPackageIds, newPackageIds, forceChanges) } yield () private def checkPackageVettingRevocation( - removed: Set[LfPackageId], + headPackageIds: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], forceChanges: ForceFlags, - )(implicit traceContext: TraceContext): EitherT[Future, TopologyManagerError, Unit] = { - val force = forceChanges.permits(ForceFlag.PackageVettingRevocation) + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = { + val removed = headPackageIds -- nextPackageIds + val force = forceChanges.permits(ForceFlag.AllowUnvetPackage) val changeIdDangerous = removed.nonEmpty EitherT.cond( !changeIdDangerous || force, diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala index 6c26c224fb1f..049bae3e01de 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala @@ -5,7 +5,7 @@ package com.digitalasset.canton.topology import com.daml.error.* import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong} +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt, PositiveLong} import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.error.CantonErrorGroups.TopologyManagementErrorGroup.TopologyManagerErrorGroup @@ -641,6 +641,30 @@ object TopologyManagerError extends TopologyManagerErrorGroup { with TopologyManagerError } + @Explanation( + """This error indicates that a topology transaction attempts to add mediators to multiple mediator groups.""" + ) + @Resolution( + "Either first remove the mediators from their current groups or choose other mediators to add." + ) + object MediatorsAlreadyInOtherGroups + extends ErrorCode( + id = "MEDIATORS_ALREADY_IN_OTHER_GROUPS", + ErrorCategory.InvalidGivenCurrentSystemStateOther, + ) { + final case class Reject( + group: NonNegativeInt, + mediators: Map[MediatorId, NonNegativeInt], + )(implicit override val loggingContext: ErrorLoggingContext) + extends CantonError.Impl( + cause = + s"Tried to add mediators to group $group, but they are already assigned to other groups: ${mediators.toSeq + .sortBy(_._1.toProtoPrimitive) + .mkString(", ")}" + ) + with TopologyManagerError + } + @Explanation( """This error indicates that the namespace is already used by another entity.""" ) @@ -751,7 +775,7 @@ object TopologyManagerError extends TopologyManagerErrorGroup { |will not be able to process a transaction relying on a particular package.""" ) @Resolution( - "Ensure that the package exists locally before issuing such a transaction." + "Upload the package locally first before issuing such a transaction." ) object CannotVetDueToMissingPackages extends ErrorCode( diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyStateProcessor.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyStateProcessor.scala index f0cc7614208d..1f60261860c6 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyStateProcessor.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyStateProcessor.scala @@ -67,7 +67,6 @@ class TopologyStateProcessor( ValidatedTopologyTransaction(currentTx, rejection.get(), expireImmediately.get()) } - // TODO(#14063) use cache instead and remember empty private val txForMapping = TrieMap[MappingHash, MaybePending]() private val proposalsByMapping = TrieMap[MappingHash, Seq[TxHash]]() private val proposalsForTx = TrieMap[TxHash, MaybePending]() @@ -110,7 +109,6 @@ class TopologyStateProcessor( val preloadTxsForMappingF = preloadTxsForMapping(effective, transactions) val preloadProposalsForTxF = preloadProposalsForTx(effective, transactions) val duplicatesF = findDuplicates(effective, transactions) - // TODO(#14064) preload authorization data val ret = for { _ <- EitherT.right[Lft](preloadProposalsForTxF) _ <- EitherT.right[Lft](preloadTxsForMappingF) diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/client/StoreBasedTopologySnapshot.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/client/StoreBasedTopologySnapshot.scala index a3414b5b6104..99efb23c3063 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/client/StoreBasedTopologySnapshot.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/client/StoreBasedTopologySnapshot.scala @@ -86,42 +86,17 @@ class StoreBasedTopologySnapshot( } ) - val requiredPackagesF = store.storeId match { - case _: TopologyStoreId.DomainStore => - FutureUnlessShutdown.outcomeF( - findTransactions( - asOfInclusive = false, - types = Seq(TopologyMapping.Code.DomainParametersState), - filterUid = None, - filterNamespace = None, - ).map { transactions => - collectLatestMapping( - TopologyMapping.Code.DomainParametersState, - transactions.collectOfMapping[DomainParametersState].result, - ).getOrElse(throw new IllegalStateException("Unable to locate domain parameters state")) - .discard - // TODO(#14054) Once the non-proto DynamicDomainParameters is available, use it - // _.parameters.requiredPackages - Set.empty[PackageId] - } - ) - - case TopologyStoreId.AuthorizedStore => - FutureUnlessShutdown.pure(Set.empty) - } - lazy val dependenciesET = packageDependencyResolver.packageDependencies(packageId).value EitherT(for { vetted <- vettedF - requiredPackages <- requiredPackagesF // check that the main package is vetted res <- if (!vetted.contains(packageId)) FutureUnlessShutdown.pure(Right(Set(packageId))) // main package is not vetted else { // check which of the dependencies aren't vetted - dependenciesET.map(_.map(_ ++ requiredPackages -- vetted)) + dependenciesET.map(_.map(_ -- vetted)) } } yield res) @@ -570,15 +545,31 @@ class StoreBasedTopologySnapshot( // 3. Attempt to look up permissions/trust from participant domain permission val participantDomainPermissions = getParticipantDomainPermissions(storedTxs, participantsWithCertAndKeys) - // 4. Apply default permissions/trust of submission/ordinary if missing participant domain permission and - // grab rate limits from dynamic domain parameters if not specified - val participantIdDomainPermissionsMap = participantsWithCertAndKeys.map { pid => - pid -> participantDomainPermissions - .getOrElse( - pid, - ParticipantDomainPermission.default(domainParametersState.domain, pid), + + val participantIdDomainPermissionsMap = participantsWithCertAndKeys.toSeq.mapFilter { pid => + if ( + domainParametersState.parameters.onboardingRestriction.isRestricted && !participantDomainPermissions + .contains(pid) + ) { + // 4a. If the domain is restricted, we must have found a ParticipantDomainPermission for the participants, otherwise + // the participants shouldn't have been able to onboard to the domain in the first place. + // In case we don't find a ParticipantDomainPermission, we don't return the participant with default permissions, but we skip it. + logger.warn( + s"Unable to find ParticipantDomainPermission for participant $pid on domain ${domainParametersState.domain} with onboarding restrictions ${domainParametersState.parameters.onboardingRestriction} at $referenceTime" ) - .setDefaultLimitIfNotSet(DynamicDomainParameters.defaultParticipantDomainLimits) + None + } else { + // 4b. Apply default permissions/trust of submission/ordinary if missing participant domain permission and + // grab rate limits from dynamic domain parameters if not specified + Some( + pid -> participantDomainPermissions + .getOrElse( + pid, + ParticipantDomainPermission.default(domainParametersState.domain, pid), + ) + .setDefaultLimitIfNotSet(DynamicDomainParameters.defaultParticipantDomainLimits) + ) + } }.toMap participantIdDomainPermissionsMap } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStateForInititalizationService.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStateForInititalizationService.scala index 8a52e2d50b00..3b536e2526af 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStateForInititalizationService.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStateForInititalizationService.scala @@ -31,8 +31,8 @@ final class StoreBasedTopologyStateForInitializationService( * 1. Determine the first MediatorDomainState or DomainTrustCertificate that mentions the member to onboard. * 2. Take its effective time (here t0') * 3. Find all transactions with sequence time <= t0' - * 4. Find the maximum effective time of the transactions returned in 3. (here ts1') - * 5. Set all validUntil > ts1' to None + * 4. Find the maximum effective time of the transactions returned in 3. (here t1') + * 5. Set all validUntil > t1' to None * * {{{ * diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala index 284e58f95c6e..7734edeb0f4c 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala @@ -436,7 +436,6 @@ object TopologyStore { lazy val initialParticipantDispatchingSet = Set( TopologyMapping.Code.DomainTrustCertificate, TopologyMapping.Code.OwnerToKeyMapping, - // TODO(#14060) - potentially revisit this once we implement TopologyStore.filterInitialParticipantDispatchingTransactions TopologyMapping.Code.NamespaceDelegation, ) @@ -445,39 +444,11 @@ object TopologyStore { domainId: DomainId, transactions: Seq[GenericStoredTopologyTransaction], ): Seq[GenericSignedTopologyTransaction] = - // TODO(#14060): Extend filtering along the lines of: - // TopologyStore.filterInitialParticipantDispatchingTransactions - transactions.map(_.transaction).collect { - case tx @ SignedTopologyTransaction( - TopologyTransaction(_, _, DomainTrustCertificate(`participantId`, `domainId`)), - _, - _, - ) => - tx - case tx @ SignedTopologyTransaction( - TopologyTransaction(_, _, OwnerToKeyMapping(`participantId`, _, _)), - _, - _, - ) => - tx - case tx @ SignedTopologyTransaction( - TopologyTransaction(_, _, NamespaceDelegation(ns, _, _)), - _, - _, - ) if ns == participantId.namespace => - tx - case tx @ SignedTopologyTransaction( - TopologyTransaction(_, _, IdentifierDelegation(uid, _)), - _, - _, - ) if uid == participantId.uid => - tx - case tx @ SignedTopologyTransaction( - TopologyTransaction(_, _, _: DecentralizedNamespaceDefinition), - _, - _, - ) => - tx + transactions.map(_.transaction).filter { signedTx => + initialParticipantDispatchingSet.contains(signedTx.mapping.code) && + signedTx.mapping.maybeUid.forall(_ == participantId.uid) && + signedTx.mapping.namespace == participantId.namespace && + signedTx.mapping.restrictedToDomain.forall(_ == domainId) } /** convenience method waiting until the last eligible transaction inserted into the source store has been dispatched successfully to the target domain */ diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionRejection.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionRejection.scala index 1e5cf819ff14..f2015f781d05 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionRejection.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionRejection.scala @@ -4,7 +4,7 @@ package com.digitalasset.canton.topology.store import com.digitalasset.canton.config.CantonRequireTypes.String256M -import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong} +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt, PositiveLong} import com.digitalasset.canton.crypto.{Fingerprint, SignatureCheckError} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.ErrorLoggingContext @@ -236,4 +236,17 @@ object TopologyTransactionRejection { param("partyId", _.partyId), ) } + + final case class MediatorsAlreadyInOtherGroups( + group: NonNegativeInt, + mediators: Map[MediatorId, NonNegativeInt], + ) extends TopologyTransactionRejection { + override def asString: String = + s"Tried to add mediators to group $group, but they are already assigned to other groups: ${mediators.toSeq + .sortBy(_._1.toProtoPrimitive) + .mkString(", ")}" + + override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError = + TopologyManagerError.MediatorsAlreadyInOtherGroups.Reject(group, mediators) + } } diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala index fb6ffe84782e..1c2f27c6c691 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala @@ -340,9 +340,6 @@ class DbTopologyStore[StoreId <: TopologyStoreId]( .map( _.result.toSet .flatMap[PartyId](_.mapping match { - // TODO(#14061): post-filtering for participantId non-columns results in fewer than limit results being returned - // - add indexed secondary uid and/or namespace columns for participant-ids - also to support efficient lookup - // of "what parties a particular participant hosts" (ParticipantId => Set[PartyId]) case ptp: PartyToParticipant if filterParticipant.isEmpty || ptp.participants .exists( @@ -606,8 +603,6 @@ class DbTopologyStore[StoreId <: TopologyStoreId]( } } - // TODO(#14061): Decide whether we want additional indices by mapping_key_hash and tx_hash (e.g. for update/removal and lookups) - // TODO(#14061): Come up with columns/indexing for efficient ParticipantId => Seq[PartyId] lookup storage.profile match { case _: DbStorage.Profile.Postgres | _: DbStorage.Profile.H2 => (sql"""INSERT INTO common_topology_transactions (store_id, sequenced, valid_from, valid_until, transaction_type, namespace, diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala index bc04690c10cc..f43783c64598 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala @@ -22,6 +22,7 @@ import com.digitalasset.canton.protocol.v30.TopologyMapping.Mapping import com.digitalasset.canton.protocol.{DynamicDomainParameters, DynamicSequencingParameters, v30} import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult +import com.digitalasset.canton.topology.MediatorGroup.MediatorGroupIndex import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction import com.digitalasset.canton.topology.transaction.TopologyMapping.RequiredAuth.* @@ -1516,12 +1517,12 @@ object DynamicSequencingParametersState { /** Mediator definition for a domain * * Each domain needs at least one mediator (group), but can have multiple. - * Mediators can be temporarily be turned off by making them observers. This way, + * Mediators can be temporarily turned off by making them observers. This way, * they get informed but they don't have to reply. */ final case class MediatorDomainState private ( domain: DomainId, - group: NonNegativeInt, + group: MediatorGroupIndex, threshold: PositiveInt, active: NonEmpty[Seq[MediatorId]], observers: Seq[MediatorId], @@ -1561,14 +1562,14 @@ final case class MediatorDomainState private ( object MediatorDomainState { - def uniqueKey(domainId: DomainId, group: NonNegativeInt): MappingHash = + def uniqueKey(domainId: DomainId, group: MediatorGroupIndex): MappingHash = TopologyMapping.buildUniqueKey(code)(_.add(domainId.toProtoPrimitive).add(group.unwrap)) def code: TopologyMapping.Code = Code.MediatorDomainState def create( domain: DomainId, - group: NonNegativeInt, + group: MediatorGroupIndex, threshold: PositiveInt, active: Seq[MediatorId], observers: Seq[MediatorId], @@ -1578,10 +1579,17 @@ object MediatorDomainState { (), s"threshold ($threshold) of mediator domain state higher than number of mediators ${active.length}", ) + mediatorsBothActiveAndObserver = active.intersect(observers) + _ <- Either.cond( + mediatorsBothActiveAndObserver.isEmpty, + (), + s"the following mediators were defined both as active and observer: ${mediatorsBothActiveAndObserver + .mkString(", ")}", + ) activeNE <- NonEmpty - .from(active) + .from(active.distinct) .toRight("mediator domain state requires at least one active mediator") - } yield MediatorDomainState(domain, group, threshold, activeNE, observers) + } yield MediatorDomainState(domain, group, threshold, activeNE, observers.distinct) def fromProtoV30( value: v30.MediatorDomainState @@ -1671,10 +1679,17 @@ object SequencerDomainState { (), s"threshold ($threshold) of sequencer domain state higher than number of active sequencers ${active.length}", ) + sequencersBothActiveAndObserver = active.intersect(observers) + _ <- Either.cond( + sequencersBothActiveAndObserver.isEmpty, + (), + s"the following sequencers were defined both as active and observer: ${sequencersBothActiveAndObserver + .mkString(", ")}", + ) activeNE <- NonEmpty - .from(active) + .from(active.distinct) .toRight("sequencer domain state requires at least one active sequencer") - } yield SequencerDomainState(domain, threshold, activeNE, observers) + } yield SequencerDomainState(domain, threshold, activeNE, observers.distinct) def fromProtoV30( value: v30.SequencerDomainState diff --git a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMappingChecks.scala b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMappingChecks.scala index 963347cf3335..2059fdc3a1b0 100644 --- a/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMappingChecks.scala +++ b/sdk/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMappingChecks.scala @@ -540,7 +540,28 @@ class ValidatingTopologyMappingChecks( val newMediators = (toValidate.mapping.allMediatorsInGroup.toSet -- inStore.toList.flatMap( _.mapping.allMediatorsInGroup )).map(identity[Member]) - checkMissingNsdAndOtkMappings(effectiveTime, newMediators) + for { + _ <- checkMissingNsdAndOtkMappings(effectiveTime, newMediators) + + result <- loadFromStore(effectiveTime, MediatorDomainState.code) + mediatorsAlreadyAssignedToGroups = result.collectLatestByUniqueKey + .collectOfMapping[MediatorDomainState] + .collectOfType[TopologyChangeOp.Replace] + .result + .flatMap(tx => + tx.mapping.allMediatorsInGroup.collect { + case med if newMediators.contains(med) => med -> tx.mapping.group + } + ) + .toMap + _ <- EitherTUtil.condUnitET[Future]( + mediatorsAlreadyAssignedToGroups.isEmpty, + TopologyTransactionRejection.MediatorsAlreadyInOtherGroups( + toValidate.mapping.group, + mediatorsAlreadyAssignedToGroups, + ): TopologyTransactionRejection, + ) + } yield () } private def checkSequencerDomainStateReplace( diff --git a/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml b/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml index 40b462883d15..ea57ca8f9dc2 100644 --- a/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml +++ b/sdk/canton/community/common/src/main/daml/CantonExamples/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: CantonExamples diff --git a/sdk/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V1_1__initial.sql b/sdk/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V1_1__initial.sql index c3e6544c2b85..bdee65be4189 100644 --- a/sdk/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V1_1__initial.sql +++ b/sdk/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V1_1__initial.sql @@ -674,26 +674,6 @@ create table sequencer_events ( -- Sequence of local offsets used by the participant event publisher create sequence participant_event_publisher_local_offsets minvalue 0 start with 0; - --- store nonces that have been requested for authentication challenges -create table sequencer_authentication_nonces ( - nonce varchar(300) primary key, - member varchar(300) not null, - generated_at_ts bigint not null, - expire_at_ts bigint not null -); - -create index idx_nonces_for_member on sequencer_authentication_nonces (member, nonce); - --- store tokens that have been generated for successful authentication requests -create table sequencer_authentication_tokens ( - token varchar(300) primary key, - member varchar(300) not null, - expire_at_ts bigint not null -); - -create index idx_tokens_for_member on sequencer_authentication_tokens (member); - -- store in-flight submissions create table par_in_flight_submission ( -- hash of the change ID as a hex string diff --git a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_1__initial.sql b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_1__initial.sql index 0aede8db22fc..57829726ab31 100644 --- a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_1__initial.sql +++ b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_1__initial.sql @@ -706,25 +706,6 @@ CREATE TABLE par_pruning_schedules ( prune_internally_only boolean NOT NULL DEFAULT false -- whether to prune only canton-internal stores not visible to ledger api ); --- store nonces that have been requested for authentication challenges -create table sequencer_authentication_nonces ( - nonce varchar(300) collate "C" primary key, - member varchar(300) collate "C" not null, - generated_at_ts bigint not null, - expire_at_ts bigint not null -); - -create index idx_nonces_for_member on sequencer_authentication_nonces (member, nonce); - --- store tokens that have been generated for successful authentication requests -create table sequencer_authentication_tokens ( - token varchar(300) collate "C" primary key, - member varchar(300) collate "C" not null, - expire_at_ts bigint not null -); - -create index idx_tokens_for_member on sequencer_authentication_tokens (member); - -- store in-flight submissions create table par_in_flight_submission ( -- hash of the change ID as a hex string diff --git a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sha256 b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sha256 index 05f51bfacee5..decdad7f21d6 100644 --- a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sha256 +++ b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sha256 @@ -1 +1 @@ -5f3540852e13e037ae21a13bdd6539f57edb10016bd57f155346287ea6493b7a +2998bdd56d5a9d905233ad0aa5b6615c9738214612e2e8eb487396f8a18a67da diff --git a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sql b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sql index 1ea5393fc2b2..d86b4dacddd0 100644 --- a/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sql +++ b/sdk/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V1_2__initial_views.sql @@ -536,21 +536,6 @@ create or replace view debug.par_pruning_schedules as prune_internally_only from par_pruning_schedules; -create or replace view debug.sequencer_authentication_nonces as - select - nonce, - member, - debug.canton_timestamp(generated_at_ts) as generated_at_ts, - debug.canton_timestamp(expire_at_ts) as expire_at_ts - from sequencer_authentication_nonces; - -create or replace view debug.sequencer_authentication_tokens as - select - token, - member, - debug.canton_timestamp(expire_at_ts) as expire_at_ts - from sequencer_authentication_tokens; - create or replace view debug.par_in_flight_submission as select change_id_hash, diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNode.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNode.scala index ae9e1fa62eb8..63e7f91782b1 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNode.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNode.scala @@ -5,11 +5,9 @@ package com.digitalasset.canton.environment import com.digitalasset.canton.health.admin.data.NodeStatus -import scala.concurrent.Future - /** A running instance of a canton node */ trait CantonNode extends AutoCloseable { - def status: Future[NodeStatus.Status] + def status: NodeStatus.Status def isActive: Boolean } diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala index 07e341845769..d1dc581d8e9f 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala @@ -11,6 +11,7 @@ import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.daml.metrics.api.MetricName import com.daml.metrics.grpc.GrpcServerMetrics import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.admin.health.v30.StatusServiceGrpc import com.digitalasset.canton.concurrent.{ ExecutionContextIdlenessExecutorService, FutureSupervisor, @@ -41,7 +42,6 @@ import com.digitalasset.canton.health.admin.data.{ WaitingForNodeTopology, } import com.digitalasset.canton.health.admin.grpc.GrpcStatusService -import com.digitalasset.canton.health.admin.v30.StatusServiceGrpc import com.digitalasset.canton.health.{ DependenciesHealthService, GrpcHealthReporter, @@ -78,7 +78,7 @@ import com.digitalasset.canton.topology.client.{ DomainTopologyClient, IdentityProvidingServiceClient, } -import com.digitalasset.canton.topology.store.TopologyStoreId.DomainStore +import com.digitalasset.canton.topology.store.TopologyStoreId.{AuthorizedStore, DomainStore} import com.digitalasset.canton.topology.store.{InitializationStore, TopologyStore, TopologyStoreId} import com.digitalasset.canton.topology.transaction.{ NamespaceDelegation, @@ -185,6 +185,22 @@ abstract class CantonNodeBootstrapImpl[ override def timeouts: ProcessingTimeout = arguments.parameterConfig.processingTimeouts override def loggerFactory: NamedLoggerFactory = arguments.loggerFactory protected def futureSupervisor: FutureSupervisor = arguments.futureSupervisor + protected def createAuthorizedTopologyManager( + nodeId: UniqueIdentifier, + crypto: Crypto, + authorizedStore: TopologyStore[AuthorizedStore], + storage: Storage, + ): AuthorizedTopologyManager = + new AuthorizedTopologyManager( + nodeId, + clock, + crypto, + authorizedStore, + exitOnFatalFailures = parameters.exitOnFatalFailures, + bootstrapStageCallback.timeouts, + futureSupervisor, + bootstrapStageCallback.loggerFactory, + ) protected val cryptoConfig: CryptoConfig = config.crypto protected val initConfig: InitConfigBase = config.init @@ -205,12 +221,11 @@ abstract class CantonNodeBootstrapImpl[ private val adminApiConfig = config.adminApi - private def status: Future[NodeStatus[NodeStatus.Status]] = + private def status: NodeStatus[NodeStatus.Status] = getNode - .map(_.status.map(NodeStatus.Success(_))) - .getOrElse( - Future.successful(NodeStatus.NotInitialized(isActive, waitingFor)) - ) + .map(_.status) + .map(NodeStatus.Success(_)) + .getOrElse(NodeStatus.NotInitialized(isActive, waitingFor)) private def waitingFor: Option[WaitingForExternalInput] = { def nextStage(stage: BootstrapStage[?, ?]): Option[BootstrapStage[?, ?]] = @@ -229,7 +244,7 @@ abstract class CantonNodeBootstrapImpl[ arguments.metrics.healthMetrics .registerHealthGauge( name.toProtoPrimitive, - () => getNode.map(_.status.map(_.active)).getOrElse(Future(false)), + () => getNode.exists(_.status.active), ) .discard // we still want to report the health even if the node is closed @@ -573,16 +588,7 @@ abstract class CantonNodeBootstrapImpl[ ) { private val topologyManager: AuthorizedTopologyManager = - new AuthorizedTopologyManager( - nodeId, - clock, - crypto, - authorizedStore, - exitOnFatalFailures = parameters.exitOnFatalFailures, - bootstrapStageCallback.timeouts, - futureSupervisor, - bootstrapStageCallback.loggerFactory, - ) + createAuthorizedTopologyManager(nodeId, crypto, authorizedStore, storage) addCloseable(topologyManager) adminServerRegistry .addServiceU( diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/data/NodeStatus.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/data/NodeStatus.scala index 5a6c5666dbf8..204c7e7db30b 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/data/NodeStatus.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/data/NodeStatus.scala @@ -8,11 +8,11 @@ import cats.syntax.functor.* import cats.syntax.option.* import cats.syntax.traverse.* import com.digitalasset.canton.ProtoDeserializationError.{InvariantViolation, UnrecognizedEnum} +import com.digitalasset.canton.admin.health.v30 +import com.digitalasset.canton.admin.health.v30.StatusResponse.NotInitialized.WaitingForExternalInput as V30WaitingForExternalInput import com.digitalasset.canton.config.RequireTypes.Port import com.digitalasset.canton.health.ComponentHealthState.UnhealthyState import com.digitalasset.canton.health.admin.data.NodeStatus.{multiline, portsString} -import com.digitalasset.canton.health.admin.v30 -import com.digitalasset.canton.health.admin.v30.StatusResponse.NotInitialized.WaitingForExternalInput as V30WaitingForExternalInput import com.digitalasset.canton.health.{ ComponentHealthState, ComponentStatus, diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/grpc/GrpcStatusService.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/grpc/GrpcStatusService.scala index ff1136fdeafc..c33703f494cb 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/grpc/GrpcStatusService.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/health/admin/grpc/GrpcStatusService.scala @@ -4,9 +4,10 @@ package com.digitalasset.canton.health.admin.grpc import better.files.* +import com.digitalasset.canton.admin.health.v30 +import com.digitalasset.canton.admin.health.v30.{HealthDumpRequest, HealthDumpResponse} import com.digitalasset.canton.config.ProcessingTimeout -import com.digitalasset.canton.health.admin.v30.{HealthDumpRequest, HealthDumpResponse} -import com.digitalasset.canton.health.admin.{data, v30} +import com.digitalasset.canton.health.admin.data import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, NodeLoggingUtil} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.GrpcStreamingUtils @@ -21,7 +22,7 @@ object GrpcStatusService { } class GrpcStatusService( - status: => Future[data.NodeStatus[_]], + status: => data.NodeStatus[_], healthDump: File => Future[Unit], processingTimeout: ProcessingTimeout, val loggerFactory: NamedLoggerFactory, @@ -30,8 +31,8 @@ class GrpcStatusService( ) extends v30.StatusServiceGrpc.StatusService with NamedLogging { - override def status(request: v30.StatusRequest): Future[v30.StatusResponse] = - status.map { + override def status(request: v30.StatusRequest): Future[v30.StatusResponse] = { + val protoStatus = status match { case data.NodeStatus.Success(status) => v30.StatusResponse(v30.StatusResponse.Response.Success(status.toProtoV30)) case data.NodeStatus.NotInitialized(active, waitingFor) => @@ -51,6 +52,8 @@ class GrpcStatusService( // The node's status should never return a Failure here. v30.StatusResponse(v30.StatusResponse.Response.Empty) } + Future.successful(protoStatus) + } override def healthDump( request: HealthDumpRequest, diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala index 7a288a89619c..496a889a5954 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala @@ -69,7 +69,6 @@ class GrpcTopologyManagerWriteService( .leftMap(ProtoDeserializationFailure.Wrap(_): CantonError) ) signedTopoTx <- - // TODO(#14067) understand when and why force needs to come in effect manager .accept( txHash, diff --git a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/version/ProtocolVersionCompatibility.scala b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/version/ProtocolVersionCompatibility.scala index 5a4208bdafb4..749a5d5ef85f 100644 --- a/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/version/ProtocolVersionCompatibility.scala +++ b/sdk/canton/community/common/src/main/scala/com/digitalasset/canton/version/ProtocolVersionCompatibility.scala @@ -20,9 +20,11 @@ import pureconfig.{ConfigReader, ConfigWriter} object ProtocolVersionCompatibility { - /** Returns the protocol versions supported by the participant of the current release. + /** Returns the protocol versions supported by the canton node parameters and the release. + * + * @param release defaults to the current release */ - def supportedProtocolsParticipant( + def supportedProtocols( cantonNodeParameters: CantonNodeParameters, release: ReleaseVersion = ReleaseVersion.current, ): NonEmpty[List[ProtocolVersion]] = { @@ -34,69 +36,20 @@ object ProtocolVersionCompatibility { ReleaseVersionToProtocolVersions.getBetaProtocolVersions(release) else List.empty - ReleaseVersionToProtocolVersions.getOrElse( + val supportedPVs = ReleaseVersionToProtocolVersions.getOrElse( release, sys.error( - s"Please add the supported protocol versions of a participant of release version $release to `majorMinorToProtocolVersions` in `ReleaseVersionToProtocolVersions.scala`." + s"Please review the supported protocol versions of release version $release in `ReleaseVersionToProtocolVersions.scala`." ), ) ++ unstableAndBeta - } - - /** Returns the protocol versions supported by the participant of the specified release. - * includeAlphaVersions: include alpha versions - * includeBetaVersions: include beta versions - */ - def supportedProtocolsParticipant( - includeAlphaVersions: Boolean, - includeBetaVersions: Boolean, - release: ReleaseVersion, - ): NonEmpty[List[ProtocolVersion]] = { - val beta = - if (includeBetaVersions) - ReleaseVersionToProtocolVersions.getBetaProtocolVersions(release) - else List.empty - - val alpha = - if (includeAlphaVersions) - ProtocolVersion.alpha.forgetNE - else List.empty - - ReleaseVersionToProtocolVersions.getOrElse( - release, - sys.error( - s"Please add the supported protocol versions of a participant of release version $release to `majorMinorToProtocolVersions` in `ReleaseVersionToProtocolVersions.scala`." - ), - ) ++ beta ++ alpha - } - - /** Returns the protocol versions supported by the domain of the current release. - * Fails if no stable protocol versions are found - */ - def trySupportedProtocolsDomain( - cantonNodeParameters: CantonNodeParameters, - release: ReleaseVersion = ReleaseVersion.current, - ): NonEmpty[List[ProtocolVersion]] = { - val unstableAndBeta = - if (cantonNodeParameters.alphaVersionSupport && cantonNodeParameters.nonStandardConfig) - ProtocolVersion.alpha.forgetNE ++ ReleaseVersionToProtocolVersions - .getBetaProtocolVersions(release) - else if (cantonNodeParameters.betaVersionSupport) - ReleaseVersionToProtocolVersions.getBetaProtocolVersions(release) - else List.empty - ReleaseVersionToProtocolVersions.getOrElse( - release, - sys.error( - s"Please add the supported protocol versions of domain nodes of release version $release to `majorMinorToProtocolVersions` in `ReleaseVersionToProtocolVersions.scala`." - ), - ) ++ unstableAndBeta + // If the release contains an unstable, alpha or beta protocol version, it is mentioned twice in the result + supportedPVs.distinct } - /** Returns the protocol versions supported by the domain of the specified release. - * includeAlphaVersions: include alpha versions - * includeBetaVersions: include beta versions + /** Returns the protocol versions supported by the release. */ - def trySupportedProtocolsDomain( + def supportedProtocols( includeAlphaVersions: Boolean, includeBetaVersions: Boolean, release: ReleaseVersion, @@ -111,12 +64,15 @@ object ProtocolVersionCompatibility { ProtocolVersion.alpha.forgetNE else List.empty - ReleaseVersionToProtocolVersions.getOrElse( + val supportedPVs = ReleaseVersionToProtocolVersions.getOrElse( release, sys.error( - s"Please add the supported protocol versions of domain nodes of release version $release to `majorMinorToProtocolVersions` in `ReleaseVersionToProtocolVersions.scala`." + s"Please review the supported protocol versions of release version $release in `ReleaseVersionToProtocolVersions.scala`." ), ) ++ beta ++ alpha + + // If the release contains an unstable, alpha or beta protocol version, it is mentioned twice in the result + supportedPVs.distinct } final case class UnsupportedVersion(version: ProtocolVersion, supported: Seq[ProtocolVersion]) @@ -128,34 +84,27 @@ object ProtocolVersionCompatibility { /** Returns successfully if the client and server should be compatible. * Otherwise returns an error message. * - * The client and server are compatible if the protocol version required by the server is not lower than - * the clientMinimumVersion and the protocol version required by the server is among the protocol versions supported - * by the client (exact string match). + * The client and server are compatible if both of the following conditions are true: + * - The protocol version required by the server is among the protocol versions supported by the client. + * - The protocol version required by the server is not lower than `clientMinimumVersion`. * - * Note that this compatibility check cannot be implemented by simply verifying whether the supported - * version by the client is larger than the required version by the server as this may lead to issues with - * patches for old minor versions. - * For example, if the latest release version is 1.3.0 but we release patch release version 1.1.1 after - * the release of version 1.3.0, a node on version 1.3.0 which only checks whether - * are versions are smaller, would mistakenly indicate that it is compatible with a node running version 1.1.1. - * This issue is avoided if the client sends all protocol versions it supports and an exact string match is required. - * Generally, this sort of error can occur because Canton is operated in a distributed environment where not every - * node is on the same version. + * Note that the second condition is not enforced if support for development versions is active for both + * client and server. */ def canClientConnectToServer( clientSupportedVersions: Seq[ProtocolVersion], - server: ProtocolVersion, - clientMinimumProtocolVersion: Option[ProtocolVersion], + serverVersion: ProtocolVersion, + clientMinimumVersion: Option[ProtocolVersion], ): Either[HandshakeError, Unit] = { - val clientSupportsRequiredVersion = clientSupportedVersions.contains(server) - val clientMinVersionLargerThanReqVersion = clientMinimumProtocolVersion.exists(_ > server) + val clientSupportsRequiredVersion = clientSupportedVersions.contains(serverVersion) + val clientMinVersionLargerThanReqVersion = clientMinimumVersion.exists(_ > serverVersion) // if dev-version support is on for participant and domain, ignore the min protocol version - if (clientSupportsRequiredVersion && server.isAlpha) + if (clientSupportsRequiredVersion && serverVersion.isAlpha) Right(()) else if (clientMinVersionLargerThanReqVersion) - Left(MinProtocolError(server, clientMinimumProtocolVersion, clientSupportsRequiredVersion)) + Left(MinProtocolError(serverVersion, clientMinimumVersion, clientSupportsRequiredVersion)) else if (!clientSupportsRequiredVersion) - Left(VersionNotSupportedError(server, clientSupportedVersions)) + Left(VersionNotSupportedError(serverVersion, clientSupportedVersions)) else Right(()) } } @@ -241,7 +190,7 @@ object DomainProtocolVersion { // we support development versions when parsing, but catch dev versions without // the safety flag during config validation ProtocolVersionCompatibility - .trySupportedProtocolsDomain( + .supportedProtocols( includeAlphaVersions = true, includeBetaVersions = true, release = ReleaseVersion.current, @@ -250,7 +199,7 @@ object DomainProtocolVersion { (), UnsupportedVersion( version, - ProtocolVersionCompatibility.trySupportedProtocolsDomain( + ProtocolVersionCompatibility.supportedProtocols( includeAlphaVersions = true, includeBetaVersions = true, release = ReleaseVersion.current, @@ -281,7 +230,7 @@ object ParticipantProtocolVersion { _ <- Either.cond( // same as domain: support parsing of dev ProtocolVersionCompatibility - .supportedProtocolsParticipant( + .supportedProtocols( includeAlphaVersions = true, includeBetaVersions = true, release = ReleaseVersion.current, @@ -290,7 +239,7 @@ object ParticipantProtocolVersion { (), UnsupportedVersion( version, - ProtocolVersionCompatibility.supportedProtocolsParticipant( + ProtocolVersionCompatibility.supportedProtocols( includeAlphaVersions = true, includeBetaVersions = true, release = ReleaseVersion.current, diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoaderTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoaderTest.scala index e059c0b9163c..d9d6f63f2b53 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoaderTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/common/domain/grpc/SequencerInfoLoaderTest.scala @@ -213,7 +213,7 @@ class SequencerInfoLoaderTest extends BaseTestWordSpec with HasExecutionContext val sequencerInfoLoader = new SequencerInfoLoader( ProcessingTimeout(), TracingConfig.Propagation.Disabled, - clientProtocolVersions = ProtocolVersionCompatibility.supportedProtocolsParticipant( + clientProtocolVersions = ProtocolVersionCompatibility.supportedProtocols( includeAlphaVersions = true, includeBetaVersions = true, release = ReleaseVersion.current, diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/metrics/MetricsUtils.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/metrics/MetricsUtils.scala index d2366496cfeb..870e217a04a5 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/metrics/MetricsUtils.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/metrics/MetricsUtils.scala @@ -36,6 +36,14 @@ trait MetricsUtils { this: BaseTest => .flatMap(_.attributes.get(key)) shouldBe Some(value) } + def assertNotInContext(name: String, key: String)(implicit + onDemandMetricsReader: OnDemandMetricsReader + ): Assertion = + clue(s"metric $name has value $value for key $key in context") { + getMetricValues[MetricValue.LongPoint](name).headOption + .flatMap(_.attributes.get(key)) shouldBe empty + } + def assertSenderIsInContext(name: String, sender: Member)(implicit onDemandMetricsReader: OnDemandMetricsReader ): Assertion = diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SendTrackerTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SendTrackerTest.scala index 29a4f2c41121..d006532f17cf 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SendTrackerTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SendTrackerTest.scala @@ -3,13 +3,17 @@ package com.digitalasset.canton.sequencing.client +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.crypto.provider.symbolic.SymbolicCrypto import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.metrics.{ CommonMockMetrics, + MetricsUtils, + OpenTelemetryOnDemandMetricsReader, SequencerClientMetrics, TrafficConsumptionMetrics, } @@ -36,7 +40,7 @@ import org.scalatest.wordspec.AsyncWordSpec import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.{ExecutionContext, Future, Promise} -class SendTrackerTest extends AsyncWordSpec with BaseTest { +class SendTrackerTest extends AsyncWordSpec with BaseTest with MetricsUtils { val metrics = CommonMockMetrics.sequencerClient val msgId1 = MessageId.tryCreate("msgId1") val msgId2 = MessageId.tryCreate("msgId2") @@ -133,22 +137,31 @@ class SendTrackerTest extends AsyncWordSpec with BaseTest { } + implicit private val onDemandMetricsReader: OpenTelemetryOnDemandMetricsReader = + new OpenTelemetryOnDemandMetricsReader() + + private val initialTrafficState = TrafficState.empty def mkSendTracker(timeoutHandler: MessageId => Future[Unit] = _ => Future.unit): Env = { val store = new InMemorySendTrackerStore() val topologyClient = TestingTopology(Set(DefaultTestIdentities.domainId)) .build(loggerFactory) .forOwnerAndDomain(participant1, domainId) + val factory = testableMetricsFactory( + "ref-sequencer-with-traffic-control", + onDemandMetricsReader, + new HistogramInventory().registered().map(_.name.toString()).toSet, + ) val trafficStateController = new TrafficStateController( DefaultTestIdentities.participant1, loggerFactory, topologyClient, - TrafficState.empty, + initialTrafficState, testedProtocolVersion, new EventCostCalculator(loggerFactory), futureSupervisor, timeouts, - TrafficConsumptionMetrics.noop, + new TrafficConsumptionMetrics(MetricName("test"), factory), ) val tracker = new MySendTracker( @@ -164,6 +177,10 @@ class SendTrackerTest extends AsyncWordSpec with BaseTest { Env(tracker, store) } + implicit private val eventSpecificMetricsContext: MetricsContext = MetricsContext( + "test" -> "value" + ) + "tracking sends" should { "error if there's a previously tracked send with the same message id" in { val Env(tracker, _) = mkSendTracker() @@ -187,6 +204,103 @@ class SendTrackerTest extends AsyncWordSpec with BaseTest { ) } yield tracker.assertNotCalled } + + "propagate metrics context" in { + val Env(tracker, _) = mkSendTracker() + + for { + _ <- tracker.track(msgId1, CantonTimestamp.MinValue).valueOrFailShutdown("track first") + _ <- tracker.update( + Seq( + deliver( + msgId1, + initialTrafficState.timestamp.immediateSuccessor, + trafficReceipt = Some( + TrafficReceipt( + consumedCost = NonNegativeLong.tryCreate(1), + extraTrafficConsumed = NonNegativeLong.tryCreate(2), + baseTrafficRemainder = NonNegativeLong.tryCreate(3), + ) + ), + ) + ) + ) + } yield { + assertLongValue("test.event-delivered-cost", 1L) + assertInContext( + "test.event-delivered-cost", + "sender", + DefaultTestIdentities.participant1.toString, + ) + // Event specific metrics should contain the event specific metrics context + assertInContext("test.event-delivered-cost", "test", "value") + assertLongValue("test.extra-traffic-consumed", 2L) + assertInContext( + "test.extra-traffic-consumed", + "sender", + DefaultTestIdentities.participant1.toString, + ) + // But not the event agnostic metrics + assertNotInContext("test.extra-traffic-consumed", "test") + } + } + + "not re-export metrics when replaying events older than current state" in { + val Env(tracker, _) = mkSendTracker() + + for { + _ <- tracker.track(msgId1, CantonTimestamp.MinValue).valueOrFailShutdown("track first") + _ <- tracker.update( + Seq( + deliver( + msgId1, + initialTrafficState.timestamp, + trafficReceipt = Some( + TrafficReceipt( + consumedCost = NonNegativeLong.tryCreate(1), + extraTrafficConsumed = NonNegativeLong.tryCreate(2), + baseTrafficRemainder = NonNegativeLong.tryCreate(3), + ) + ), + ) + ) + ) + } yield { + assertNoValue("test.event-delivered-cost") + } + } + + "metrics should contain default labels for unknown sends" in { + val Env(tracker, _) = mkSendTracker() + + for { + _ <- tracker.update( + Seq( + deliver( + msgId1, + initialTrafficState.timestamp.immediateSuccessor, + trafficReceipt = Some( + TrafficReceipt( + consumedCost = NonNegativeLong.tryCreate(1), + extraTrafficConsumed = NonNegativeLong.tryCreate(2), + baseTrafficRemainder = NonNegativeLong.tryCreate(3), + ) + ), + ) + ) + ) + } yield { + assertLongValue("test.event-delivered-cost", 1L) + assertInContext( + "test.event-delivered-cost", + "sender", + DefaultTestIdentities.participant1.toString, + ) + // Check there are labels for application-id and type + assertInContext("test.event-delivered-cost", "application-id", "unknown") + assertInContext("test.event-delivered-cost", "type", "unknown") + } + } } "updating" should { diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala index e6a38ec207d5..232351fa4062 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala @@ -88,6 +88,7 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.EitherTUtil import com.digitalasset.canton.util.PekkoUtil.syntax.* import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion} +import io.grpc.Status import org.apache.pekko.actor.ActorSystem import org.apache.pekko.stream.scaladsl.{Keep, Source} import org.apache.pekko.stream.{BoundedSourceQueue, Materializer, QueueOfferResult} @@ -1101,6 +1102,9 @@ class SequencerClientTest with SequencerClientTransportPekko with NamedLogging { + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + EitherT.pure(()) + override protected def timeouts: ProcessingTimeout = DefaultProcessingTimeouts.testing private val subscriberRef = new AtomicReference[Option[Subscriber[_]]](None) diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/topology/transaction/ValidatingTopologyMappingChecksTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/topology/transaction/ValidatingTopologyMappingChecksTest.scala index f02192059572..d79b776bac22 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/topology/transaction/ValidatingTopologyMappingChecksTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/topology/transaction/ValidatingTopologyMappingChecksTest.scala @@ -604,6 +604,55 @@ class ValidatingTopologyMappingChecksTest ) ) } + + "report MediatorsAlreadyAssignedToGroups for duplicate mediator assignments" in { + val (checks, store) = mk() + val (Seq(med1, med2, med3), transactions) = generateMemberIdentities(3, MediatorId(_)) + + val Seq(group0, group1, group2) = Seq( + NonNegativeInt.zero -> Seq(med1), + NonNegativeInt.one -> Seq(med2), + NonNegativeInt.two -> Seq(med1, med2, med3), + ).map { case (group, mediators) => + factory.mkAdd( + MediatorDomainState + .create( + domainId, + group, + PositiveInt.one, + active = mediators, + Seq.empty, + ) + .value, + // the signing key is not relevant for the test + factory.SigningKeys.key1, + ) + } + + addToStore(store, (transactions :+ group0 :+ group1)*) + + checkTransaction(checks, group2, None) shouldBe Left( + TopologyTransactionRejection.MediatorsAlreadyInOtherGroups( + NonNegativeInt.two, + Map(med1 -> NonNegativeInt.zero, med2 -> NonNegativeInt.one), + ) + ) + } + + "report mediators defined both as active and observers" in { + val (Seq(med1, med2), _transactions) = generateMemberIdentities(2, MediatorId(_)) + + MediatorDomainState + .create( + domainId, + NonNegativeInt.zero, + PositiveInt.one, + active = Seq(med1, med2), + observers = Seq(med1), + ) shouldBe Left( + s"the following mediators were defined both as active and observer: $med1" + ) + } } "validating SequencerDomainState" should { @@ -681,6 +730,20 @@ class ValidatingTopologyMappingChecksTest ) ) } + + "report sequencers defined both as active and observers" in { + val (Seq(seq1, seq2), _transactions) = generateMemberIdentities(2, SequencerId(_)) + + SequencerDomainState + .create( + domainId, + PositiveInt.one, + active = Seq(seq1, seq2), + observers = Seq(seq1), + ) shouldBe Left( + s"the following sequencers were defined both as active and observer: $seq1" + ) + } } "validating OwnerToKeyMapping" should { diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala index 6a2e2d1d98fc..4aa326fb815e 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala @@ -3,7 +3,6 @@ package com.digitalasset.canton.traffic -import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.CantonRequireTypes.String255 import com.digitalasset.canton.config.RequireTypes.{NonNegativeLong, PositiveInt} import com.digitalasset.canton.crypto.Signature @@ -160,16 +159,17 @@ class TrafficControlProcessorTest extends AnyWordSpec with BaseTest with HasExec "the traffic control processor" should { "notify subscribers of all event timestamps" in { val batch = Batch.of(testedProtocolVersion, mkTopoTx() -> Recipients.cc(participantId)) - val events = NonEmpty( - Seq, - mkDeliver(sc1, ts1, batch), - mkDeliverError(sc2, ts2), - mkDeliver(sc3, ts3, batch), - ).map(v => Traced(v)) + val events = Traced( + Seq( + mkDeliver(sc1, ts1, batch), + mkDeliverError(sc2, ts2), + mkDeliver(sc3, ts3, batch), + ).map(v => Traced(v)) + ) val (tcp, observedTs, updates) = mkTrafficProcessor() - tcp.handle(events).futureValueUS + tcp(events).futureValueUS.unwrap.futureValueUS observedTs.get().result() shouldBe Seq(ts1, ts2, ts3) updates.get().result() shouldBe Seq.empty diff --git a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/version/ProtocolVersionCompatibilityTest.scala b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/version/ProtocolVersionCompatibilityTest.scala index ff114f9728ed..a1fa35e7ff42 100644 --- a/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/version/ProtocolVersionCompatibilityTest.scala +++ b/sdk/canton/community/common/src/test/scala/com/digitalasset/canton/version/ProtocolVersionCompatibilityTest.scala @@ -13,7 +13,7 @@ class ProtocolVersionCompatibilityTest extends AnyWordSpec with BaseTest { "be successful for matching versions" in { canClientConnectToServer( clientSupportedVersions = Seq(ProtocolVersion.v32, ProtocolVersion.dev), - server = ProtocolVersion.dev, + serverVersion = ProtocolVersion.dev, None, ) shouldBe Right(()) } @@ -21,7 +21,7 @@ class ProtocolVersionCompatibilityTest extends AnyWordSpec with BaseTest { "fail with a nice message if incompatible" in { canClientConnectToServer( clientSupportedVersions = Seq(ProtocolVersion.v32), - server = ProtocolVersion.dev, + serverVersion = ProtocolVersion.dev, None, ).left.value shouldBe (VersionNotSupportedError( ProtocolVersion.dev, diff --git a/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml b/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml index 7dbdd38e6493..dc51e1c7410a 100644 --- a/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/ai-analysis/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: ai-analysis diff --git a/sdk/canton/community/demo/src/main/daml/bank/daml.yaml b/sdk/canton/community/demo/src/main/daml/bank/daml.yaml index 05fb8f93d2f2..13d26654e519 100644 --- a/sdk/canton/community/demo/src/main/daml/bank/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/bank/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: bank diff --git a/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml b/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml index 780d825dcbbf..1242cd7f5b63 100644 --- a/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/doctor/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: doctor diff --git a/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml b/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml index 105a8758ba44..a005c9e519b0 100644 --- a/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/health-insurance/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: health-insurance diff --git a/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml b/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml index 609b071af96b..323436e01846 100644 --- a/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml +++ b/sdk/canton/community/demo/src/main/daml/medical-records/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: medical-records diff --git a/sdk/canton/community/domain/src/main/protobuf/com/digitalasset/canton/mediator/admin/v30/sequencer_connection_service.proto b/sdk/canton/community/domain/src/main/protobuf/com/digitalasset/canton/mediator/admin/v30/sequencer_connection_service.proto index 78e8bf17abfe..0a37486c75ba 100644 --- a/sdk/canton/community/domain/src/main/protobuf/com/digitalasset/canton/mediator/admin/v30/sequencer_connection_service.proto +++ b/sdk/canton/community/domain/src/main/protobuf/com/digitalasset/canton/mediator/admin/v30/sequencer_connection_service.proto @@ -12,6 +12,8 @@ import "com/digitalasset/canton/admin/domain/v30/sequencer_connection.proto"; service SequencerConnectionService { rpc GetConnection(GetConnectionRequest) returns (GetConnectionResponse); rpc SetConnection(SetConnectionRequest) returns (SetConnectionResponse); + // Revoke the authentication tokens for all the sequencers on the domain and disconnect the sequencer clients + rpc Logout(LogoutRequest) returns (LogoutResponse); } message GetConnectionRequest {} @@ -26,3 +28,7 @@ message SetConnectionRequest { } message SetConnectionResponse {} + +message LogoutRequest {} + +message LogoutResponse {} diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/MediatorNode.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/MediatorNode.scala index 267660b49051..b5cfc6a0cb69 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/MediatorNode.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/mediator/MediatorNode.scala @@ -591,27 +591,10 @@ class MediatorNodeBootstrap( arguments.metrics.sequencerClient, parameters.loggingConfig, domainLoggerFactory, - ProtocolVersionCompatibility.trySupportedProtocolsDomain(parameters), + ProtocolVersionCompatibility.supportedProtocols(parameters), None, ) - sequencerClientRef = - GrpcSequencerConnectionService.setup[MediatorDomainConfiguration](mediatorId)( - adminServerRegistry, - fetchConfig, - saveConfig, - Lens[MediatorDomainConfiguration, SequencerConnections](_.sequencerConnections)( - connection => conf => conf.copy(sequencerConnections = connection) - ), - RequestSigner( - syncCryptoApi, - domainConfig.domainParameters.protocolVersion, - loggerFactory, - ), - sequencerClientFactory, - sequencerInfoLoader, - domainAlias, - domainId, - ) + // we wait here until the sequencer becomes active. this allows to reconfigure the // sequencer client address info <- GrpcSequencerConnectionService @@ -638,6 +621,27 @@ class MediatorNodeBootstrap( ) .mapK(FutureUnlessShutdown.outcomeK) + sequencerClientRef = + GrpcSequencerConnectionService.setup[MediatorDomainConfiguration](mediatorId)( + adminServerRegistry, + fetchConfig, + saveConfig, + Lens[MediatorDomainConfiguration, SequencerConnections](_.sequencerConnections)( + connection => conf => conf.copy(sequencerConnections = connection) + ), + RequestSigner( + syncCryptoApi, + domainConfig.domainParameters.protocolVersion, + loggerFactory, + ), + sequencerClientFactory, + sequencerInfoLoader, + domainAlias, + domainId, + sequencerClient, + loggerFactory, + ) + _ <- { val headSnapshot = topologyClient.headSnapshot for { @@ -738,18 +742,17 @@ class MediatorNode( def isActive: Boolean = replicaManager.isActive - def status: Future[MediatorNodeStatus] = { + def status: MediatorNodeStatus = { val ports = Map("admin" -> config.adminApi.port) - Future.successful( - MediatorNodeStatus( - mediatorId.uid, - domainId, - uptime(), - ports, - replicaManager.isActive, - replicaManager.getTopologyQueueStatus, - healthData, - ) + + MediatorNodeStatus( + mediatorId.uid, + domainId, + uptime(), + ports, + replicaManager.isActive, + replicaManager.getTopologyQueueStatus, + healthData, ) } diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/metrics/BftOrderingMetrics.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/metrics/BftOrderingMetrics.scala index 3ba2a44b5b92..4bfd84013590 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/metrics/BftOrderingMetrics.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/metrics/BftOrderingMetrics.scala @@ -571,6 +571,7 @@ class BftOrderingMetrics( case class Empty(from: SequencerId) extends SourceValue case class Availability(from: SequencerId) extends SourceValue case class Consensus(from: SequencerId) extends SourceValue + case class StateTransfer(from: SequencerId) extends SourceValue } } } diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerNode.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerNode.scala index bc6c73e528e9..ba2e5fc86d23 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerNode.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerNode.scala @@ -28,7 +28,7 @@ import com.digitalasset.canton.domain.sequencing.sequencer.store.{ SequencerDomainConfigurationStore, } import com.digitalasset.canton.domain.sequencing.service.GrpcSequencerInitializationService -import com.digitalasset.canton.domain.server.DynamicDomainGrpcServer +import com.digitalasset.canton.domain.server.DynamicGrpcServer import com.digitalasset.canton.environment.* import com.digitalasset.canton.health.admin.data.{ SequencerHealthStatus, @@ -97,7 +97,7 @@ import org.apache.pekko.actor.ActorSystem import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.{ExecutionContextExecutorService, Future} +import scala.concurrent.Future object SequencerNodeBootstrap { trait Factory[C <: SequencerNodeConfigCommon] { @@ -211,7 +211,7 @@ class SequencerNodeBootstrap( // Holds the gRPC server started when the node is started, even when non initialized // If non initialized the server will expose the gRPC health service only protected val nonInitializedSequencerNodeServer = - new AtomicReference[Option[DynamicDomainGrpcServer]](None) + new AtomicReference[Option[DynamicGrpcServer]](None) addCloseable(new AutoCloseable() { override def close(): Unit = nonInitializedSequencerNodeServer.getAndSet(None).foreach(_.publicServer.close()) @@ -248,7 +248,7 @@ class SequencerNodeBootstrap( nonInitializedSequencerNodeServer .set( Some( - makeDynamicDomainServer( + makeDynamicGrpcServer( // We use max value for the request size here as this is the default for a non initialized sequencer MaxRequestSize(NonNegativeInt.maxValue), healthReporter, @@ -435,7 +435,7 @@ class SequencerNodeBootstrap( staticDomainParameters: StaticDomainParameters, authorizedTopologyManager: AuthorizedTopologyManager, domainTopologyManager: DomainTopologyManager, - preinitializedServer: Option[DynamicDomainGrpcServer], + preinitializedServer: Option[DynamicGrpcServer], sequencerSnapshot: Option[SequencerSnapshot], healthReporter: GrpcHealthReporter, healthService: DependenciesHealthService, @@ -509,9 +509,9 @@ class SequencerNodeBootstrap( // When the sequencer is started on a fresh domain there's no sequencer snapshot, // so we need to register all members present in the topology snapshot if (topologyClient.approximateTimestamp == tsInit) { - // this sequencer node was started for the first time an initialized with a topology state. - // therefore we fetch all members who have a registered role on the domain and pass them - // to the underlying sequencer driver to register them as known members + // This sequencer node was started for the first time and initialized with a topology state. + // Therefore, we fetch all members who have a registered role on the domain and pass them + // to the underlying sequencer driver to register them as known members. EitherT.right[String]( domainTopologyStore .findPositiveTransactions( @@ -696,7 +696,6 @@ class SequencerNodeBootstrap( runtimeReadyPromise, ) _ <- sequencerRuntime.initializeAll() - // TODO(#14073) subscribe to processor BEFORE sequencer client is created _ = addCloseable(sequencer) server <- createSequencerServer( sequencerRuntime, @@ -766,18 +765,17 @@ class SequencerNodeBootstrap( (readiness, liveness) } - // Creates a dynamic domain server that initially only exposes a health endpoint, and can later be - // setup with the sequencer runtime to provide the full sequencer domain API - private def makeDynamicDomainServer( + // Creates a dynamic GRPC server that initially only exposes a health endpoint, and can later be + // setup with the sequencer runtime to provide the full sequencer API + private def makeDynamicGrpcServer( maxRequestSize: MaxRequestSize, grpcHealthReporter: GrpcHealthReporter, ) = - new DynamicDomainGrpcServer( + new DynamicGrpcServer( loggerFactory, maxRequestSize, arguments.parameterConfig, config.publicApi, - arguments.metrics.openTelemetryMetricsFactory, arguments.metrics.grpcMetrics, grpcHealthReporter, sequencerPublicApiHealthService, @@ -786,10 +784,10 @@ class SequencerNodeBootstrap( private def createSequencerServer( runtime: SequencerRuntime, domainParamsLookup: DynamicDomainParametersLookup[SequencerDomainParameters], - server: Option[DynamicDomainGrpcServer], + server: Option[DynamicGrpcServer], healthReporter: GrpcHealthReporter, adminServerRegistry: CantonMutableHandlerRegistry, - ): EitherT[Future, String, DynamicDomainGrpcServer] = { + ): EitherT[Future, String, DynamicGrpcServer] = { runtime.registerAdminGrpcServices(service => adminServerRegistry.addServiceU(service)) for { maxRequestSize <- EitherT @@ -799,7 +797,7 @@ class SequencerNodeBootstrap( ) sequencerNodeServer = server .getOrElse( - makeDynamicDomainServer(maxRequestSize, healthReporter) + makeDynamicGrpcServer(maxRequestSize, healthReporter) ) .initialize(runtime) // wait for the server to be initialized before reporting a serving health state @@ -814,10 +812,9 @@ class SequencerNode( val sequencer: SequencerRuntime, val adminToken: CantonAdminToken, protected val loggerFactory: NamedLoggerFactory, - sequencerNodeServer: DynamicDomainGrpcServer, + sequencerNodeServer: DynamicGrpcServer, healthData: => Seq[ComponentStatus], -)(implicit executionContext: ExecutionContextExecutorService) - extends CantonNode +) extends CantonNode with NamedLogging with HasUptime { @@ -825,15 +822,16 @@ class SequencerNode( override def isActive = true - override def status: Future[SequencerNodeStatus] = - for { - healthStatus <- sequencer.health - activeMembers <- sequencer.fetchActiveMembers() - ports = Map("public" -> config.publicApi.port, "admin" -> config.adminApi.port) - participants = activeMembers.collect { case participant: ParticipantId => - participant - } - } yield SequencerNodeStatus( + override def status: SequencerNodeStatus = { + val healthStatus = sequencer.health + val activeMembers = sequencer.fetchActiveMembers() + + val ports = Map("public" -> config.publicApi.port, "admin" -> config.adminApi.port) + val participants = activeMembers.collect { case participant: ParticipantId => + participant + } + + SequencerNodeStatus( sequencer.domainId.unwrap, sequencer.domainId, uptime(), @@ -844,6 +842,7 @@ class SequencerNode( admin = sequencer.adminStatus, healthData, ) + } override def close(): Unit = Lifecycle.close( diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerRuntime.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerRuntime.scala index 7e79d0c1d44d..4e918ac4b6ca 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerRuntime.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/SequencerRuntime.scala @@ -4,9 +4,7 @@ package com.digitalasset.canton.domain.sequencing import cats.data.EitherT -import cats.syntax.foldable.* import cats.syntax.parallel.* -import com.daml.nonempty.NonEmpty import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.connection.GrpcApiInfoService @@ -44,7 +42,6 @@ import com.digitalasset.canton.lifecycle.{ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil import com.digitalasset.canton.protocol.DomainParametersLookup.SequencerDomainParameters -import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope import com.digitalasset.canton.protocol.{ DomainParametersLookup, DynamicDomainParametersLookup, @@ -61,15 +58,7 @@ import com.digitalasset.canton.sequencing.handlers.{ EnvelopeOpener, StripSignature, } -import com.digitalasset.canton.sequencing.protocol.SequencedEvent import com.digitalasset.canton.sequencing.traffic.TrafficControlProcessor -import com.digitalasset.canton.sequencing.{ - BoxedEnvelope, - HandlerResult, - SubscriptionStart, - UnsignedEnvelopeBox, - UnsignedProtocolEventHandler, -} import com.digitalasset.canton.store.{IndexedDomain, SequencerCounterTrackerStore} import com.digitalasset.canton.time.{Clock, DomainTimeTracker} import com.digitalasset.canton.topology.* @@ -195,7 +184,7 @@ class SequencerRuntime( } yield () } - protected val sequencerDomainParamsLookup + private val sequencerDomainParamsLookup : DynamicDomainParametersLookup[SequencerDomainParameters] = DomainParametersLookup.forSequencerDomainParameters( staticDomainParameters, @@ -213,7 +202,6 @@ class SequencerRuntime( sequencerDomainParamsLookup, localNodeParameters, staticDomainParameters.protocolVersion, - domainTopologyManager, topologyStateForInitializationService, loggerFactory, ) @@ -249,7 +237,7 @@ class SequencerRuntime( private val authenticationServices = { val authenticationService = memberAuthenticationServiceFactory.createAndSubscribe( syncCrypto, - MemberAuthenticationStore(storage, timeouts, loggerFactory, closeContext), + new MemberAuthenticationStore(), // closing the subscription when the token expires will force the client to try to reconnect // immediately and notice it is unauthenticated, which will cause it to also start reauthenticating // it's important to disconnect the member AFTER we expired the token, as otherwise, the member @@ -277,8 +265,7 @@ class SequencerRuntime( ) } - def health: Future[SequencerHealthStatus] = - Future.successful(sequencer.getState) + def health: SequencerHealthStatus = sequencer.getState def topologyQueue: TopologyQueueStatus = TopologyQueueStatus( manager = topologyManagerStatusO.map(_.queueSize).getOrElse(0), @@ -289,8 +276,8 @@ class SequencerRuntime( def adminStatus: SequencerAdminStatus = sequencer.adminStatus - def fetchActiveMembers(): Future[Seq[Member]] = - Future.successful(sequencerService.membersWithActiveSubscriptions) + def fetchActiveMembers(): Seq[Member] = + sequencerService.membersWithActiveSubscriptions def registerAdminGrpcServices( register: ServerServiceDefinition => Unit @@ -322,7 +309,7 @@ class SequencerRuntime( ) } - def domainServices(implicit ec: ExecutionContext): Seq[ServerServiceDefinition] = Seq( + def sequencerServices(implicit ec: ExecutionContext): Seq[ServerServiceDefinition] = Seq( ServerInterceptors.intercept( v30.SequencerConnectServiceGrpc.bindService( new GrpcSequencerConnectService( @@ -427,37 +414,7 @@ class SequencerRuntime( sequencer.rateLimitManager.foreach(rlm => trafficProcessor.subscribe(rlm.balanceUpdateSubscriber)) - // TODO(i17434): Use topologyHandler.combineWith(trafficProcessorHandler) - private def handler(domainId: DomainId): UnsignedProtocolEventHandler = - new UnsignedProtocolEventHandler { - override def name: String = s"sequencer-runtime-$domainId" - - override def subscriptionStartsAt( - start: SubscriptionStart, - domainTimeTracker: DomainTimeTracker, - )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = - Seq( - topologyProcessor.subscriptionStartsAt(start, domainTimeTracker), - trafficProcessor.subscriptionStartsAt(start), - ).sequence_ - - override def apply( - tracedEvents: BoxedEnvelope[UnsignedEnvelopeBox, DefaultOpenEnvelope] - ): HandlerResult = - tracedEvents.withTraceContext { implicit traceContext => events => - NonEmpty.from(events).fold(HandlerResult.done)(handle) - } - - private def handle(tracedEvents: NonEmpty[Seq[Traced[SequencedEvent[DefaultOpenEnvelope]]]])( - implicit traceContext: TraceContext - ): HandlerResult = - for { - topology <- topologyHandler(Traced(tracedEvents)) - _ <- trafficProcessor.handle(tracedEvents) - } yield topology - } - - private val eventHandler = StripSignature(handler(domainId)) + private val eventHandler = StripSignature(topologyHandler.combineWith(trafficProcessor)) private val sequencerAdministrationService = new GrpcSequencerAdministrationService( diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCache.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCache.scala deleted file mode 100644 index e0ed0ea2c496..000000000000 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCache.scala +++ /dev/null @@ -1,89 +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.domain.sequencing.authentication - -import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.sequencing.authentication.AuthenticationToken -import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.topology.Member -import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.FutureUtil - -import scala.collection.concurrent.TrieMap -import scala.concurrent.{ExecutionContext, Future} - -/** Provides a read and write through cache for authentication tokens while also - * enforcing expiry timestamps based on the local clock. - */ -class AuthenticationTokenCache( - clock: Clock, - store: MemberAuthenticationStore, - protected val loggerFactory: NamedLoggerFactory, -)(implicit executionContext: ExecutionContext) - extends NamedLogging { - - private val tokenCache = TrieMap[AuthenticationToken, StoredAuthenticationToken]() - - def lookupMatchingToken(member: Member, providedToken: AuthenticationToken)(implicit - traceContext: TraceContext - ): Future[Option[StoredAuthenticationToken]] = { - val now = clock.now - - def lookupFromStore(): Future[Option[StoredAuthenticationToken]] = - for { - matchedTokenO <- store - .fetchTokens(member) - .map(_.filter(_.expireAt > now).find(_.token == providedToken)) - // cache it - _ = matchedTokenO.foreach(cacheToken) - } yield matchedTokenO - - // lookup from cache, otherwise fetch from store (and then cache that) - tokenCache.get(providedToken) match { - case Some(token) if token.member == member && token.expireAt > now => - Future.successful(Some(token)) - case _ => lookupFromStore() - } - } - - /** Will persist and locally cache a new token */ - def saveToken( - storedToken: StoredAuthenticationToken - )(implicit traceContext: TraceContext): Future[Unit] = - for { - _ <- store.saveToken(storedToken) - } yield cacheToken(storedToken) - - /** Removes all tokens for the given member from the persisted store and cache. - * Expected to be used when the member is disabled on the domain. - */ - def invalidateAllTokensForMember( - member: Member - )(implicit traceContext: TraceContext): Future[Unit] = - for { - _ <- store.invalidateMember(member) - _ = tokenCache - .filter(_._2.member == member) - .keys - .foreach(tokenCache.remove(_).discard[Option[StoredAuthenticationToken]]) - } yield () - - private def cacheToken(stored: StoredAuthenticationToken): Unit = { - tokenCache.putIfAbsent(stored.token, stored).discard - - val _ = clock.scheduleAt( - _ => { - TraceContext.withNewTraceContext { implicit traceContext => - logger.debug(s"Expiring token for ${stored.member}@${stored.expireAt}") - tokenCache.remove(stored.token).discard - FutureUtil - .doNotAwait(store.expireNoncesAndTokens(clock.now), "Expiring old nonces and tokens") - } - }, - stored.expireAt, - ) - } - -} diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationService.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationService.scala index f0cfbad2d953..07ea800d5342 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationService.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationService.scala @@ -14,9 +14,8 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, Lifecycle} +import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.resource.DbStorage.PassiveInstanceException import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.* import com.digitalasset.canton.sequencing.authentication.grpc.AuthenticationTokenWithExpiry import com.digitalasset.canton.sequencing.authentication.{AuthenticationToken, MemberAuthentication} @@ -64,8 +63,6 @@ class MemberAuthenticationService( extends NamedLogging with FlagCloseable { - protected val tokenCache = new AuthenticationTokenCache(clock, store, loggerFactory) - /** Domain generates nonce that he expects the participant to use to concatenate with the domain's id and sign * to proceed with the authentication (step 2). */ @@ -85,7 +82,7 @@ class MemberAuthenticationService( ) nonce = Nonce.generate(cryptoApi.pureCrypto) storedNonce = StoredNonce(member, nonce, clock.now, nonceExpirationInterval) - _ <- handlePassiveInstanceException(store.saveNonce(storedNonce)) + _ = store.saveNonce(storedNonce) } yield { scheduleExpirations(storedNonce.expireAt) (nonce, fingerprints) @@ -102,19 +99,7 @@ class MemberAuthenticationService( } } - private def handlePassiveInstanceException[A]( - future: Future[A] - ): EitherT[Future, AuthenticationError, A] = - EitherT( - future - .map(Right(_)) - .recover { case _: PassiveInstanceException => - Left(PassiveSequencer: AuthenticationError) - } - ) - /** Domain checks that the signature given by the member matches and returns a token if it does (step 4) - * Al */ def validateSignature(member: Member, signature: Signature, providedNonce: Nonce)(implicit traceContext: TraceContext @@ -122,10 +107,10 @@ class MemberAuthenticationService( for { _ <- EitherT.right(waitForInitialized) _ <- isActive(member) - value <- - handlePassiveInstanceException(store.fetchAndRemoveNonce(member, providedNonce)) - .map(ignoreExpired) - .subflatMap(_.toRight(MissingNonce(member): AuthenticationError)) + value <- EitherT.fromEither( + ignoreExpired(store.fetchAndRemoveNonce(member, providedNonce)) + .toRight(MissingNonce(member): AuthenticationError) + ) StoredNonce(_, nonce, generatedAt, _expireAt) = value authentication <- EitherT.fromEither(MemberAuthentication(member)) hash = authentication.hashDomainNonce(nonce, domain, cryptoApi.pureCrypto) @@ -133,7 +118,7 @@ class MemberAuthenticationService( _ <- snapshot.verifySignature(hash, member, signature).leftMap { err => logger.warn(s"Member $member provided invalid signature: $err") - InvalidSignature(member) + InvalidSignature(member): AuthenticationError } token = AuthenticationToken.generate(cryptoApi.pureCrypto) maybeRandomTokenExpirationTime = @@ -149,7 +134,7 @@ class MemberAuthenticationService( } tokenExpiry = clock.now.add(maybeRandomTokenExpirationTime) storedToken = StoredAuthenticationToken(member, tokenExpiry, token) - _ <- handlePassiveInstanceException(tokenCache.saveToken(storedToken)) + _ = store.saveToken(storedToken) } yield { logger.info( s"$member authenticated new token with expiry $tokenExpiry" @@ -162,31 +147,46 @@ class MemberAuthenticationService( * this domain's id, it means the participant was previously connected to a different domain on the same address and * now should be informed that this address now hosts a different domain. */ - def validateToken(intendedDomain: DomainId, member: Member, token: AuthenticationToken)(implicit - traceContext: TraceContext - ): EitherT[Future, AuthenticationError, StoredAuthenticationToken] = + def validateToken( + intendedDomain: DomainId, + member: Member, + token: AuthenticationToken, + ): Either[AuthenticationError, StoredAuthenticationToken] = for { - _ <- EitherT.fromEither[Future](correctDomain(member, intendedDomain)) - validTokenO <- handlePassiveInstanceException(tokenCache.lookupMatchingToken(member, token)) - validToken <- EitherT - .fromEither[Future](validTokenO.toRight(MissingToken(member))) - .leftWiden[AuthenticationError] + _ <- correctDomain(member, intendedDomain) + validTokenO = store.fetchTokens(member).filter(_.expireAt > clock.now).find(_.token == token) + validToken <- validTokenO.toRight(MissingToken(member)).leftWiden[AuthenticationError] } yield validToken + def invalidateMemberWithToken( + token: AuthenticationToken + )(implicit traceContext: TraceContext): Future[Either[LogoutTokenDoesNotExist.type, Unit]] = + for { + _ <- waitForInitialized + } yield { + store + .fetchToken(token) + .toRight(LogoutTokenDoesNotExist) + .map(token => + // Force invalidation, whether the member is actually active or not + invalidateAndExpire(isActiveCheck = (_: Member) => Future.successful(false))( + token.member + ) + ) + } + private def ignoreExpired[A <: HasExpiry](itemO: Option[A]): Option[A] = itemO.filter(_.expireAt > clock.now) private def scheduleExpirations( timestamp: CantonTimestamp )(implicit traceContext: TraceContext): Unit = { - def run(): Unit = FutureUtil.doNotAwait( - performUnlessClosingF(functionFullName) { - val now = clock.now - logger.debug(s"Expiring nonces and tokens up to $now") - handlePassiveInstanceException(store.expireNoncesAndTokens(now)).value - }.unwrap, - "Expiring nonces and tokens failed", - ) + def run(): Unit = performUnlessClosing(functionFullName) { + val now = clock.now + logger.debug(s"Expiring nonces and tokens up to $now") + store.expireNoncesAndTokens(now) + }.onShutdown(()) + clock.scheduleAt(_ => run(), timestamp).discard } @@ -215,12 +215,11 @@ class MemberAuthenticationService( protected def isMemberActive(check: TopologySnapshot => Future[Boolean])(implicit traceContext: TraceContext ): Future[Boolean] = - cryptoApi.snapshot(cryptoApi.topologyKnownUntilTimestamp).flatMap { snapshot => - // we are a bit more conservative here. a member needs to be active NOW and the head state (i.e. effective in the future) - Seq(snapshot.ipsSnapshot, cryptoApi.currentSnapshotApproximation.ipsSnapshot) - .parTraverse(check(_)) - .map(_.forall(identity)) - } + // we are a bit more conservative here. a member needs to be active NOW and the head state (i.e. effective in the future) + Seq(cryptoApi.headSnapshot, cryptoApi.currentSnapshotApproximation) + .map(_.ipsSnapshot) + .parTraverse(check(_)) + .map(_.forall(identity)) protected def isParticipantActive(participant: ParticipantId)(implicit traceContext: TraceContext @@ -233,15 +232,14 @@ class MemberAuthenticationService( protected def invalidateAndExpire[T <: Member]( isActiveCheck: T => Future[Boolean] )(memberId: T)(implicit traceContext: TraceContext): Unit = { - val invalidateF = isActiveCheck(memberId).flatMap { isActive => + val invalidateF = isActiveCheck(memberId).map { isActive => if (!isActive) { logger.debug(s"Expiring all auth-tokens of $memberId") - tokenCache - // first, remove all auth tokens - .invalidateAllTokensForMember(memberId) - // second, ensure the sequencer client gets disconnected - .map(_ => invalidateMemberCallback(Traced(memberId))) - } else Future.unit + // first, remove all auth tokens + store.invalidateMember(memberId) + // second, ensure the sequencer client gets disconnected + invalidateMemberCallback(Traced(memberId)) + } } FutureUtil.doNotAwait( invalidateF, @@ -327,11 +325,20 @@ class MemberAuthenticationServiceImpl( s"$participant is disabled until ${cert.loginAfter}. Removing any token and booting the participant" ) invalidateAndExpire(isParticipantActive)(participant) + case TopologyTransaction( + TopologyChangeOp.Remove, + _serial, + cert: ParticipantDomainPermission, + ) => + val participant = cert.participantId + logger.info( + s"$participant's access has been revoked by the domain. Removing any token and booting the participant" + ) + invalidateAndExpire(isParticipantActive)(participant) + case _ => } }) - - override def onClosed(): Unit = Lifecycle.close(store)(logger) } trait MemberAuthenticationServiceFactory { diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStore.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStore.scala index 4a0604816057..bedc7fd15dcd 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStore.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStore.scala @@ -3,21 +3,14 @@ package com.digitalasset.canton.domain.sequencing.authentication -import com.daml.nameof.NameOf.functionFullName -import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.crypto.Nonce import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.lifecycle.CloseContext -import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.resource.IdempotentInsert.insertIgnoringConflicts -import com.digitalasset.canton.resource.{DbStorage, DbStore, MemoryStorage, Storage} import com.digitalasset.canton.sequencing.authentication.AuthenticationToken import com.digitalasset.canton.topology.Member -import com.digitalasset.canton.tracing.TraceContext import java.time.Duration import scala.collection.mutable -import scala.concurrent.{ExecutionContext, Future, blocking} +import scala.concurrent.blocking trait HasExpiry { val expireAt: CantonTimestamp @@ -43,52 +36,9 @@ final case class StoredAuthenticationToken( member: Member, expireAt: CantonTimestamp, token: AuthenticationToken, -) extends HasExpiry - -trait MemberAuthenticationStore extends AutoCloseable { - - /** Save the provided nonce */ - def saveNonce(storedNonce: StoredNonce)(implicit traceContext: TraceContext): Future[Unit] - - /** Fetch and if found immediately remove the nonce for the member and nonce provided. */ - def fetchAndRemoveNonce(member: Member, nonce: Nonce)(implicit - traceContext: TraceContext - ): Future[Option[StoredNonce]] - - /** Save the provided authentication token */ - def saveToken(token: StoredAuthenticationToken)(implicit traceContext: TraceContext): Future[Unit] - - /** Fetch all saved tokens for the provided member */ - def fetchTokens(member: Member)(implicit - traceContext: TraceContext - ): Future[Seq[StoredAuthenticationToken]] - - /** Expire all nonces and tokens up to and including the provided timestamp */ - def expireNoncesAndTokens(timestamp: CantonTimestamp)(implicit - traceContext: TraceContext - ): Future[Unit] +) - /** Remove any token or nonce for participant. */ - def invalidateMember(member: Member)(implicit traceContext: TraceContext): Future[Unit] -} - -object MemberAuthenticationStore { - def apply( - storage: Storage, - timeouts: ProcessingTimeout, - loggerFactory: NamedLoggerFactory, - closeContext: CloseContext, - )(implicit - executionContext: ExecutionContext - ): MemberAuthenticationStore = - storage match { - case _: MemoryStorage => new InMemoryMemberAuthenticationStore - case dbStorage: DbStorage => - new DbMemberAuthenticationStore(dbStorage, timeouts, loggerFactory, closeContext) - } -} - -class InMemoryMemberAuthenticationStore extends MemberAuthenticationStore { +class MemberAuthenticationStore { // we use a variety of data access and modification patterns that aren't well suited to lockless datastructures // so as the numbers of items is typically always small just use a single coarse lock for all accessing and modifications // to nonces and tokens @@ -96,164 +46,48 @@ class InMemoryMemberAuthenticationStore extends MemberAuthenticationStore { private val nonces = mutable.Buffer[StoredNonce]() private val tokens = mutable.Buffer[StoredAuthenticationToken]() - override def saveNonce( - storedNonce: StoredNonce - )(implicit traceContext: TraceContext): Future[Unit] = { + def saveNonce(storedNonce: StoredNonce): Unit = blocking(lock.synchronized { nonces += storedNonce () }) - Future.unit - } - override def fetchAndRemoveNonce(member: Member, nonce: Nonce)(implicit - traceContext: TraceContext - ): Future[Option[StoredNonce]] = { - val storedNonce = blocking(lock.synchronized { + def fetchAndRemoveNonce(member: Member, nonce: Nonce): Option[StoredNonce] = + blocking(lock.synchronized { val storedNonce = nonces.find(n => n.member == member && n.nonce == nonce) storedNonce.foreach(nonces.-=) // remove the nonce storedNonce }) - Future.successful(storedNonce) - } - override def saveToken( - token: StoredAuthenticationToken - )(implicit traceContext: TraceContext): Future[Unit] = { + def saveToken(token: StoredAuthenticationToken): Unit = blocking(lock.synchronized { tokens += token () }) - Future.unit - } - override def fetchTokens(member: Member)(implicit - traceContext: TraceContext - ): Future[Seq[StoredAuthenticationToken]] = { - val memberTokens = blocking(lock.synchronized { + def fetchTokens(member: Member): Seq[StoredAuthenticationToken] = + blocking(lock.synchronized { tokens.filter(_.member == member).toSeq }) - Future.successful(memberTokens) - } - override def expireNoncesAndTokens( - timestamp: CantonTimestamp - )(implicit traceContext: TraceContext): Future[Unit] = { + def fetchToken(token: AuthenticationToken): Option[StoredAuthenticationToken] = + blocking { + lock.synchronized { + tokens.find(_.token == token) + } + } + + def expireNoncesAndTokens(timestamp: CantonTimestamp): Unit = blocking(lock.synchronized { nonces --= nonces.filter(_.expireAt <= timestamp) tokens --= tokens.filter(_.expireAt <= timestamp) () }) - Future.unit - } - override def invalidateMember( - member: Member - )(implicit traceContext: TraceContext): Future[Unit] = { + def invalidateMember(member: Member): Unit = blocking(lock.synchronized { nonces --= nonces.filter(_.member == member) tokens --= tokens.filter(_.member == member) () }) - Future.unit - } - - override def close(): Unit = () -} - -class DbMemberAuthenticationStore( - override protected val storage: DbStorage, - override protected val timeouts: ProcessingTimeout, - override protected val loggerFactory: NamedLoggerFactory, - override implicit val closeContext: CloseContext, -)(implicit executionContext: ExecutionContext) - extends MemberAuthenticationStore - with DbStore { - - import storage.api.* - import Member.DbStorageImplicits.* - - override def saveNonce(storedNonce: StoredNonce)(implicit - traceContext: TraceContext - ): Future[Unit] = - storage.update_( - // it is safe to use insertIgnoringConflicts here for saving the nonce as the only conflicts that are likely - // to occur are DB retries from this call - insertIgnoringConflicts( - storage, - "sequencer_authentication_nonces(nonce)", - sql"""sequencer_authentication_nonces (nonce, member, generated_at_ts, expire_at_ts) - values (${storedNonce.nonce}, ${storedNonce.member}, ${storedNonce.generatedAt}, ${storedNonce.expireAt})""", - ), - functionFullName, - ) - - override def fetchAndRemoveNonce(member: Member, nonce: Nonce)(implicit - traceContext: TraceContext - ): Future[Option[StoredNonce]] = - for { - timestampsO <- storage.query( - sql"""select generated_at_ts, expire_at_ts - from sequencer_authentication_nonces - where nonce = $nonce and member = $member - """.as[(CantonTimestamp, CantonTimestamp)].headOption, - s"$functionFullName:nonce-lookup", - ) - storedNonceO = timestampsO.map { case (generatedAt, expireAt) => - StoredNonce(member, nonce, generatedAt, expireAt) - } - _ <- storedNonceO.fold(Future.unit)(_ => - storage.update_( - sqlu"""delete from sequencer_authentication_nonces where nonce = $nonce and member = $member""", - s"$functionFullName:nonce-removal", - ) - ) - } yield storedNonceO - - override def saveToken(token: StoredAuthenticationToken)(implicit - traceContext: TraceContext - ): Future[Unit] = - storage.update_( - // tokens are also securely generated so conflicts are also unlikely and can be safely ignored - insertIgnoringConflicts( - storage, - "sequencer_authentication_tokens(token)", - sql"""sequencer_authentication_tokens (token, member, expire_at_ts) - values (${token.token}, ${token.member}, ${token.expireAt})""", - ), - functionFullName, - ) - - override def fetchTokens(member: Member)(implicit - traceContext: TraceContext - ): Future[Seq[StoredAuthenticationToken]] = - storage.query( - sql""" - select token, expire_at_ts from sequencer_authentication_tokens where member = $member - """ - .as[(AuthenticationToken, CantonTimestamp)] - .map(_.map { case (token, expireAt) => - StoredAuthenticationToken(member, expireAt, token) - }), - functionFullName, - ) - override def expireNoncesAndTokens(timestamp: CantonTimestamp)(implicit - traceContext: TraceContext - ): Future[Unit] = - storage.update_( - DBIO.seq( - sqlu"delete from sequencer_authentication_nonces where expire_at_ts <= $timestamp", - sqlu"delete from sequencer_authentication_tokens where expire_at_ts <= $timestamp", - ), - functionFullName, - ) - - override def invalidateMember(member: Member)(implicit traceContext: TraceContext): Future[Unit] = - storage.update_( - DBIO.seq( - sqlu"delete from sequencer_authentication_nonces where member = $member", - sqlu"delete from sequencer_authentication_tokens where member = $member", - ), - functionFullName, - ) } diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptor.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptor.scala index 29e9228b5447..57e9bff93d7e 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptor.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptor.scala @@ -3,7 +3,6 @@ package com.digitalasset.canton.domain.sequencing.authentication.grpc -import cats.data.EitherT import cats.implicits.* import com.digitalasset.canton.auth.AsyncForwardingListener import com.digitalasset.canton.domain.sequencing.authentication.grpc.SequencerAuthenticationServerInterceptor.VerifyTokenError @@ -15,22 +14,16 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.sequencing.authentication.AuthenticationToken import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.{ AuthenticationError, - PassiveSequencer, TokenVerificationException, } import com.digitalasset.canton.sequencing.authentication.grpc.Constant import com.digitalasset.canton.topology.{DomainId, Member, UniqueIdentifier} -import com.digitalasset.canton.tracing.TraceContext import io.grpc.* -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - class SequencerAuthenticationServerInterceptor( tokenValidator: MemberAuthenticationService, val loggerFactory: NamedLoggerFactory, -)(implicit ec: ExecutionContext) - extends ServerInterceptor +) extends ServerInterceptor with NamedLogging { override def interceptCall[ReqT, RespT]( @@ -53,58 +46,48 @@ class SequencerAuthenticationServerInterceptor( // any original context values val originalContext = Context.current() - val listenerET = for { - member <- Member - .fromProtoPrimitive(memberId, "memberId") - .leftMap(err => s"Failed to deserialize member id: $err") - .leftMap(VerifyTokenError.GeneralError) - .toEitherT[Future] - intendedDomainId <- UniqueIdentifier - .fromProtoPrimitive_(intendedDomain) - .map(DomainId(_)) - .leftMap(err => VerifyTokenError.GeneralError(err.message)) - .toEitherT[Future] - storedTokenO <- - for { - token <- tokenO - .toRight[VerifyTokenError]( - VerifyTokenError.GeneralError("Authentication headers are missing for token") - ) - .toEitherT[Future] - storedToken <- verifyToken(member, intendedDomainId, token) - } yield Some(storedToken) - } yield { - val contextWithAuthorizedMember = originalContext - .withValue(IdentityContextHelper.storedAuthenticationTokenContextKey, storedTokenO) - .withValue(IdentityContextHelper.storedMemberContextKey, Some(member)) - Contexts.interceptCall(contextWithAuthorizedMember, serverCall, headers, next) - } + try { + val listenerE = for { + member <- Member + .fromProtoPrimitive(memberId, "memberId") + .leftMap(err => s"Failed to deserialize member id: $err") + .leftMap(VerifyTokenError.GeneralError) + intendedDomainId <- UniqueIdentifier + .fromProtoPrimitive_(intendedDomain) + .map(DomainId(_)) + .leftMap(err => VerifyTokenError.GeneralError(err.message)) + storedTokenO <- + for { + token <- tokenO + .toRight[VerifyTokenError]( + VerifyTokenError.GeneralError("Authentication headers are missing for token") + ) + storedToken <- verifyToken(member, intendedDomainId, token) + } yield Some(storedToken) + } yield { + val contextWithAuthorizedMember = originalContext + .withValue(IdentityContextHelper.storedAuthenticationTokenContextKey, storedTokenO) + .withValue(IdentityContextHelper.storedMemberContextKey, Some(member)) + Contexts.interceptCall(contextWithAuthorizedMember, serverCall, headers, next) + } - listenerET.value.onComplete { - case Success(Right(listener)) => - setNextListener(listener) - case Success(Left(VerifyTokenError.AuthError(PassiveSequencer))) => - logger.debug("Authentication not possible with passive sequencer.") - setNextListener( - failVerification( - s"Verification failed for member $memberId: ${PassiveSequencer.reason}", - serverCall, - headers, - Some(PassiveSequencer.code), - Status.UNAVAILABLE, - ) - ) - case Success(Left(err)) => - logger.debug(s"Authentication token verification failed: $err") - setNextListener( - failVerification( - s"Verification failed for member $memberId: ${err.message}", - serverCall, - headers, - err.errorCode, + listenerE match { + case Right(listener) => + setNextListener(listener) + case Left(err) => + logger.debug(s"Authentication token verification failed: $err") + setNextListener( + failVerification( + s"Verification failed for member $memberId: ${err.message}", + serverCall, + headers, + err.errorCode, + ) ) - ) - case Failure(ex) => + } + } catch { + // To be on the safe side, in case `verifyToken` throws an exception + case ex: Throwable => logger.warn(s"Authentication token verification caused an unexpected exception", ex) setNextListener( failVerification( @@ -114,6 +97,7 @@ class SequencerAuthenticationServerInterceptor( Some(TokenVerificationException(memberId).code), ) ) + } } } @@ -124,9 +108,7 @@ class SequencerAuthenticationServerInterceptor( member: Member, intendedDomain: DomainId, token: AuthenticationToken, - )(implicit - traceContext: TraceContext - ): EitherT[Future, VerifyTokenError, StoredAuthenticationToken] = + ): Either[VerifyTokenError, StoredAuthenticationToken] = tokenValidator .validateToken(intendedDomain, member, token) .leftMap[VerifyTokenError](VerifyTokenError.AuthError) diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DirectSequencerClientTransport.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DirectSequencerClientTransport.scala index 1af6419121d3..42152e9561f4 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DirectSequencerClientTransport.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/sequencer/DirectSequencerClientTransport.scala @@ -38,6 +38,7 @@ import com.digitalasset.canton.util.PekkoUtil.syntax.* import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.{ErrorUtil, FutureUtil, PekkoUtil} import com.digitalasset.canton.version.ProtocolVersion +import io.grpc.Status import org.apache.pekko.stream.Materializer import org.apache.pekko.stream.scaladsl.Source import org.apache.pekko.{Done, NotUsed} @@ -61,6 +62,10 @@ class DirectSequencerClientTransport( with NamedLogging { import DirectSequencerClientTransport.* + override def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + // In-process connection is not authenticated + EitherT.pure(()) + private val subscriptionFactory = new DirectSequencerSubscriptionFactory(sequencer, timeouts, loggerFactory) diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerAuthenticationService.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerAuthenticationService.scala index 3b7451bdd7e2..98ba62beb6d9 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerAuthenticationService.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerAuthenticationService.scala @@ -14,6 +14,8 @@ import com.digitalasset.canton.domain.api.v30.SequencerAuthentication.{ AuthenticateResponse, ChallengeRequest, ChallengeResponse, + LogoutRequest, + LogoutResponse, } import com.digitalasset.canton.domain.api.v30.SequencerAuthenticationServiceGrpc.SequencerAuthenticationService import com.digitalasset.canton.domain.sequencing.authentication.MemberAuthenticationService @@ -24,9 +26,12 @@ import com.digitalasset.canton.domain.sequencing.service.GrpcSequencerAuthentica import com.digitalasset.canton.domain.service.HandshakeValidator import com.digitalasset.canton.error.{Alarm, AlarmErrorCode, CantonError} import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.sequencing.authentication.MemberAuthentication -import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.AuthenticationError +import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.{ + AuthenticationError, + LogoutTokenDoesNotExist, +} import com.digitalasset.canton.sequencing.authentication.grpc.AuthenticationTokenWithExpiry +import com.digitalasset.canton.sequencing.authentication.{AuthenticationToken, MemberAuthentication} import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.topology.Member import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} @@ -76,7 +81,7 @@ class GrpcSequencerAuthenticationService( .discard true } else { - // create error message to appropriate log this incident + // create error message to appropriately log this incident SequencerAuthenticationFailure .AuthenticationFailure(request.member, error) .discard @@ -161,13 +166,12 @@ class GrpcSequencerAuthenticationService( private def handleAuthError(err: AuthenticationError): Status = { def maliciousOrFaulty(): Status = Status.INTERNAL.withDescription(err.reason) + err match { case MemberAuthentication.MemberAccessDisabled(_) => Status.PERMISSION_DENIED.withDescription(err.reason) case MemberAuthentication.NonMatchingDomainId(_, _) => Status.FAILED_PRECONDITION.withDescription(err.reason) - case MemberAuthentication.PassiveSequencer => - Status.UNAVAILABLE.withDescription(err.reason) case MemberAuthentication.NoKeysRegistered(_) => maliciousOrFaulty() case MemberAuthentication.FailedToSign(_, _) => maliciousOrFaulty() case MemberAuthentication.MissingNonce(_) => maliciousOrFaulty() @@ -175,6 +179,7 @@ class GrpcSequencerAuthenticationService( case MemberAuthentication.MissingToken(_) => maliciousOrFaulty() case MemberAuthentication.TokenVerificationException(_) => maliciousOrFaulty() case MemberAuthentication.AuthenticationNotSupportedForMember(_) => maliciousOrFaulty() + case MemberAuthentication.LogoutTokenDoesNotExist => maliciousOrFaulty() } } @@ -194,6 +199,35 @@ class GrpcSequencerAuthenticationService( .clientIsCompatible(protocolVersion, request.memberProtocolVersions, minClientVersionP = None) .leftMap(err => Status.FAILED_PRECONDITION.withDescription(err)) + /** Unconditionally revoke a member's authentication tokens and disconnect it + */ + override def logout(request: LogoutRequest): Future[LogoutResponse] = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + + val result = for { + providedToken <- AuthenticationToken.fromProtoPrimitive(request.token) match { + case Right(token) => Future.successful(token) + case Left(err) => + Future.failed( + Status.INVALID_ARGUMENT + .withDescription(s"Failed to deserialize token: $err") + .asRuntimeException() + ) + } + logoutResult <- authenticationService.invalidateMemberWithToken(providedToken) + _ <- logoutResult match { + case Right(()) => Future.successful(()) + case Left(err @ LogoutTokenDoesNotExist) => + Future.failed( + Status.FAILED_PRECONDITION + .withDescription(s"Failed to logout: $err") + .asRuntimeException() + ) + } + } yield () + + result.map(_ => LogoutResponse()) + } } object GrpcSequencerAuthenticationService extends GrpcSequencerAuthenticationErrorGroup { @@ -228,8 +262,8 @@ object GrpcSequencerAuthenticationService extends GrpcSequencerAuthenticationErr s"Authentication for $member rejected with ${response.getCode}/${response.getDescription}" ) with CantonError - } + @Explanation( """This error indicates that a client failed to authenticate with the sequencer due to a reason possibly |pointing out to faulty or malicious behaviour. The message is logged on the server in order to support an diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerService.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerService.scala index d09d90b7ec0a..e1693c9f5e54 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerService.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerService.scala @@ -115,7 +115,6 @@ object GrpcSequencerService { domainParamsLookup: DynamicDomainParametersLookup[SequencerDomainParameters], parameters: SequencerParameters, protocolVersion: ProtocolVersion, - domainTopologyManager: DomainTopologyManager, topologyStateForInitializationService: TopologyStateForInitializationService, loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext, materializer: Materializer): GrpcSequencerService = @@ -137,7 +136,6 @@ object GrpcSequencerService { ), domainParamsLookup, parameters, - domainTopologyManager, topologyStateForInitializationService, protocolVersion, ) @@ -166,7 +164,6 @@ class GrpcSequencerService( directSequencerSubscriptionFactory: DirectSequencerSubscriptionFactory, domainParamsLookup: DynamicDomainParametersLookup[SequencerDomainParameters], parameters: SequencerParameters, - domainTopologyManager: DomainTopologyManager, topologyStateForInitializationService: TopologyStateForInitializationService, protocolVersion: ProtocolVersion, maxItemsInTopologyResponse: PositiveInt = PositiveInt.tryCreate(100), diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicDomainGrpcServer.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicGrpcServer.scala similarity index 76% rename from sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicDomainGrpcServer.scala rename to sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicGrpcServer.scala index 6b45155f8c52..7be0690ccab1 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicDomainGrpcServer.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/server/DynamicGrpcServer.scala @@ -3,7 +3,6 @@ package com.digitalasset.canton.domain.server -import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.daml.metrics.grpc.GrpcServerMetrics import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.domain.config.PublicServerConfig @@ -22,34 +21,33 @@ import io.grpc.protobuf.services.ProtoReflectionService import scala.concurrent.ExecutionContextExecutorService -/** Creates a dynamic public domain server to which domain services can be added at a later time, +/** Creates a dynamic public server to which services can be added at a later time, * while providing a gRPC health service from the start. - * This is useful to bring up the embedded domain or sequencer node endpoint with health service, prior to them being + * This is useful to bring up the sequencer node endpoint with health service, prior to being * initialized. */ -class DynamicDomainGrpcServer( +class DynamicGrpcServer( val loggerFactory: NamedLoggerFactory, maxRequestSize: MaxRequestSize, nodeParameters: HasGeneralCantonNodeParameters, serverConfig: PublicServerConfig, - metrics: LabeledMetricsFactory, grpcMetrics: GrpcServerMetrics, grpcHealthReporter: GrpcHealthReporter, - domainHealthService: DependenciesHealthService, + healthService: DependenciesHealthService, )(implicit executionContext: ExecutionContextExecutorService) extends NamedLogging { - private lazy val grpcDomainHealthManager = + private lazy val grpcHealthManager = ServiceHealthStatusManager( - "Domain API", + "Health API", new io.grpc.protobuf.services.HealthStatusManager(), - Set(domainHealthService), + Set(healthService), ) - grpcHealthReporter.registerHealthManager(grpcDomainHealthManager) + grpcHealthReporter.registerHealthManager(grpcHealthManager) // Upon initialization, register all gRPC services into their dynamic slot - def initialize(runtime: SequencerRuntime): DynamicDomainGrpcServer = { - runtime.domainServices.foreach(registry.addServiceU(_)) + def initialize(runtime: SequencerRuntime): DynamicGrpcServer = { + runtime.sequencerServices.foreach(registry.addServiceU(_)) this } @@ -72,7 +70,7 @@ class DynamicDomainGrpcServer( val registry = serverBuilder.mutableHandlerRegistry() serverBuilder - .addService(grpcDomainHealthManager.manager.getHealthService.bindService()) + .addService(grpcHealthManager.manager.getHealthService.bindService()) .addService(ProtoReflectionService.newInstance(), withLogging = false) .discard[CantonServerBuilder] diff --git a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/service/GrpcSequencerConnectionService.scala b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/service/GrpcSequencerConnectionService.scala index 914173f37e0c..dc5626057657 100644 --- a/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/service/GrpcSequencerConnectionService.scala +++ b/sdk/canton/community/domain/src/main/scala/com/digitalasset/canton/domain/service/GrpcSequencerConnectionService.scala @@ -10,15 +10,22 @@ import com.digitalasset.canton.DomainAlias import com.digitalasset.canton.common.domain.grpc.SequencerInfoLoader import com.digitalasset.canton.common.domain.grpc.SequencerInfoLoader.SequencerAggregatedInfo import com.digitalasset.canton.concurrent.FutureSupervisor -import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable, PromiseUnlessShutdown} -import com.digitalasset.canton.logging.ErrorLoggingContext +import com.digitalasset.canton.lifecycle.{ + CloseContext, + FlagCloseable, + FutureUnlessShutdown, + PromiseUnlessShutdown, +} +import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.mediator.admin.v30 import com.digitalasset.canton.mediator.admin.v30.SequencerConnectionServiceGrpc.SequencerConnectionService +import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown import com.digitalasset.canton.networking.grpc.CantonMutableHandlerRegistry import com.digitalasset.canton.sequencing.client.SequencerClient.SequencerTransports import com.digitalasset.canton.sequencing.client.{ RequestSigner, RichSequencerClient, + SequencerClient, SequencerClientTransportFactory, } import com.digitalasset.canton.sequencing.{ @@ -28,7 +35,7 @@ import com.digitalasset.canton.sequencing.{ } import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.topology.{DomainId, Member} -import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.retry.NoExceptionRetryPolicy import com.digitalasset.canton.util.{EitherTUtil, retry} import io.grpc.{Status, StatusException} @@ -45,8 +52,11 @@ class GrpcSequencerConnectionService( String, Unit, ], + logout: () => EitherT[FutureUnlessShutdown, Status, Unit], + protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext) - extends v30.SequencerConnectionServiceGrpc.SequencerConnectionService { + extends v30.SequencerConnectionServiceGrpc.SequencerConnectionService + with NamedLogging { override def getConnection(request: v30.GetConnectionRequest): Future[v30.GetConnectionResponse] = fetchConnection() .map { @@ -115,6 +125,22 @@ class GrpcSequencerConnectionService( .asException() ) } + + /** Revoke the authentication tokens on a sequencer and disconnect the sequencer client + */ + override def logout( + request: v30.LogoutRequest + ): Future[v30.LogoutResponse] = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + + val ret = for { + _ <- logout() + .leftMap(err => err.asRuntimeException()) + .onShutdown(Left(AbortedDueToShutdown.Error().asGrpcError)) + } yield v30.LogoutResponse() + + EitherTUtil.toFuture(ret) + } } object GrpcSequencerConnectionService { @@ -133,6 +159,8 @@ object GrpcSequencerConnectionService { sequencerInfoLoader: SequencerInfoLoader, domainAlias: DomainAlias, domainId: DomainId, + sequencerClient: SequencerClient, + loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContextExecutor, executionServiceFactory: ExecutionSequencerFactory, @@ -194,6 +222,8 @@ object GrpcSequencerConnectionService { }(_.changeTransport(sequencerTransports)) ) } yield (), + sequencerClient.logout _, + loggerFactory, ), executionContext, ) diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCacheTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCacheTest.scala deleted file mode 100644 index f105776be017..000000000000 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/AuthenticationTokenCacheTest.scala +++ /dev/null @@ -1,129 +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.domain.sequencing.authentication - -import com.digitalasset.canton.BaseTest -import com.digitalasset.canton.crypto.provider.symbolic.SymbolicPureCrypto -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.sequencing.authentication.AuthenticationToken -import com.digitalasset.canton.time.SimClock -import com.digitalasset.canton.topology.DefaultTestIdentities -import org.mockito.ArgumentMatchers -import org.scalatest.wordspec.AsyncWordSpec - -class AuthenticationTokenCacheTest extends AsyncWordSpec with BaseTest { - class Env() { - val store = new InMemoryMemberAuthenticationStore - val storeSpy = spy(store) - val clock = new SimClock(loggerFactory = loggerFactory) - val cache = new AuthenticationTokenCache(clock, storeSpy, loggerFactory) - } - - lazy val participant1 = DefaultTestIdentities.participant1 - lazy val participant2 = DefaultTestIdentities.participant2 - lazy val crypto = new SymbolicPureCrypto - lazy val token1 = AuthenticationToken.generate(crypto) - lazy val token2 = AuthenticationToken.generate(crypto) - def ts(n: Int): CantonTimestamp = CantonTimestamp.Epoch.plusSeconds(n.toLong) - - "lookup" should { - "return empty if not in store" in { - val env = new Env() - - for { - result <- env.cache.lookupMatchingToken(participant1, token1) - } yield result shouldBe empty - } - - "return from store if present" in { - val env = new Env() - val storedToken = StoredAuthenticationToken(participant1, ts(5), token1) - - for { - _ <- env.store.saveToken(storedToken) - result <- env.cache.lookupMatchingToken(participant1, token1) - } yield result.value shouldBe storedToken - } - - "return empty if the store contains a token but it has already expired" in { - val env = new Env() - val tokenExpiry = ts(5) - val storedToken = StoredAuthenticationToken(participant1, tokenExpiry, token1) - - env.clock.advanceTo(tokenExpiry) - - for { - _ <- env.store.saveToken(storedToken) - result <- env.cache.lookupMatchingToken(participant1, token1) - } yield result shouldBe empty - } - - "cache if found in store and then return from cache on next call" in { - val env = new Env() - val storedToken = StoredAuthenticationToken(participant1, ts(5), token1) - - for { - _ <- env.store.saveToken(storedToken) - firstTokenResult <- env.cache.lookupMatchingToken(participant1, token1) - secondTokenResult <- env.cache.lookupMatchingToken(participant1, token1) - } yield { - firstTokenResult.value shouldBe storedToken - secondTokenResult.value shouldBe storedToken - - // ensure the token lookup was only called once as on the second call it should be available in the cache - verify(env.storeSpy, times(1)).fetchTokens(ArgumentMatchers.eq(participant1))( - anyTraceContext - ) - - succeed - } - } - } - - "invaliding all tokens" should { - "invalid all the tokens for a member" in { - val stored1 = StoredAuthenticationToken(participant1, ts(4), token1) - val stored2 = StoredAuthenticationToken(participant1, ts(5), token2) - val env = new Env() - - for { - _ <- env.cache.saveToken(stored1) - _ <- env.cache.saveToken(stored2) - _ <- env.cache.invalidateAllTokensForMember(participant1) - token1ResultO <- env.cache.lookupMatchingToken(participant1, token1) - token2ResultO <- env.cache.lookupMatchingToken(participant1, token2) - storedTokens <- env.store.fetchTokens(participant1) - } yield { - token1ResultO shouldBe empty - token2ResultO shouldBe empty - storedTokens shouldBe empty - } - } - } - - "expiry" should { - "clear cache and all tokens from persisted store when expiration is reached" in { - val env = new Env() - val expireAt = ts(5) - val storedToken = StoredAuthenticationToken(participant1, expireAt, token1) - - for { - _ <- env.cache.saveToken(storedToken) - _ = env.clock.advanceTo(ts(4)) - resultBeforeO <- env.cache.lookupMatchingToken(participant1, token1) - _ = resultBeforeO.value shouldBe storedToken - _ = env.clock.advanceTo(expireAt) - resultAfterO <- env.cache.lookupMatchingToken(participant1, token1) - storedTokens <- env.store.fetchTokens(participant1) - } yield { - verify(env.storeSpy, times(1)).expireNoncesAndTokens(ArgumentMatchers.eq(expireAt))( - anyTraceContext - ) - - resultAfterO shouldBe empty - storedTokens shouldBe empty - } - } - } -} diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationServiceTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationServiceTest.scala index dd176978bfac..423b36b5bf8a 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationServiceTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationServiceTest.scala @@ -3,17 +3,19 @@ package com.digitalasset.canton.domain.sequencing.authentication +import cats.data.EitherT import cats.implicits.* import com.digitalasset.canton.BaseTest import com.digitalasset.canton.config.DefaultProcessingTimeouts -import com.digitalasset.canton.crypto.{Nonce, Signature} +import com.digitalasset.canton.crypto.Nonce +import com.digitalasset.canton.sequencing.authentication.MemberAuthentication import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.{ + AuthenticationError, MemberAccessDisabled, MissingToken, NonMatchingDomainId, } import com.digitalasset.canton.sequencing.authentication.grpc.AuthenticationTokenWithExpiry -import com.digitalasset.canton.sequencing.authentication.{AuthenticationToken, MemberAuthentication} import com.digitalasset.canton.time.SimClock import com.digitalasset.canton.topology.* import com.digitalasset.canton.tracing.TraceContext @@ -27,20 +29,20 @@ class MemberAuthenticationServiceTest extends AsyncWordSpec with BaseTest { import DefaultTestIdentities.* - val p1 = participant1 + private val p1 = participant1 - val clock: SimClock = new SimClock(loggerFactory = loggerFactory) + private val clock: SimClock = new SimClock(loggerFactory = loggerFactory) - val topology = TestingTopology().withSimpleParticipants(participant1).build() - val syncCrypto = topology.forOwnerAndDomain(participant1, domainId) + private val topology = TestingTopology().withSimpleParticipants(participant1).build() + private val syncCrypto = topology.forOwnerAndDomain(participant1, domainId) - def service( + private def service( participantIsActive: Boolean, useExponentialRandomTokenExpiration: Boolean = false, nonceDuration: JDuration = JDuration.ofMinutes(1), tokenDuration: JDuration = JDuration.ofHours(1), invalidateMemberCallback: Member => Unit = _ => (), - store: MemberAuthenticationStore = new InMemoryMemberAuthenticationStore(), + store: MemberAuthenticationStore = new MemberAuthenticationStore(), ): MemberAuthenticationService = new MemberAuthenticationService( domainId, @@ -61,10 +63,10 @@ class MemberAuthenticationServiceTest extends AsyncWordSpec with BaseTest { Future.successful(participantIsActive) } - def getMemberAuthentication(member: Member) = + private def getMemberAuthentication(member: Member) = MemberAuthentication(member).getOrElse(fail("unsupported")) - "ParticipantAuthenticationService" should { + "MemberAuthenticationService" should { def generateToken(sut: MemberAuthenticationService) = for { @@ -76,49 +78,45 @@ class MemberAuthenticationServiceTest extends AsyncWordSpec with BaseTest { tokenAndExpiry <- sut.validateSignature(p1, signature, nonce) } yield tokenAndExpiry + def fetchTokens( + store: MemberAuthenticationStore, + members: Seq[Member], + ): Map[Member, Seq[StoredAuthenticationToken]] = + members.flatMap(store.fetchTokens).groupBy(_.member) + "generate nonce, verify signature, generate token, verify token, and verify expiry" in { val sut = service(participantIsActive = true) - (for { + for { tokenAndExpiry <- generateToken(sut) AuthenticationTokenWithExpiry(token, expiry) = tokenAndExpiry - _ <- sut.validateToken(domainId, p1, token) - } yield expiry).value.map { - case Right(expiry) => - expiry should be(clock.now.plus(JDuration.ofHours(1))) - case Left(error) => fail(s"Failed with error: $error") + _ <- EitherT.fromEither[Future](sut.validateToken(domainId, p1, token)) + } yield { + expiry should be(clock.now.plus(JDuration.ofHours(1))) } } "generate nonce, verify signature, generate token, verify token, and verify exponential expiry" in { val sut = service(participantIsActive = true, useExponentialRandomTokenExpiration = true) - (for { + for { tokenAndExpiry <- generateToken(sut) AuthenticationTokenWithExpiry(token, expiry) = tokenAndExpiry - _ <- sut.validateToken(domainId, p1, token) - } yield expiry).value.map { - case Right(expiry) => - expiry should be >= clock.now.plus(JDuration.ofMinutes(30)) - expiry should be <= clock.now.plus(JDuration.ofHours(1)) - case Left(error) => fail(s"Failed with error: $error") + _ <- EitherT.fromEither[Future](sut.validateToken(domainId, p1, token)) + } yield { + expiry should be >= clock.now.plus(JDuration.ofMinutes(30)) + expiry should be <= clock.now.plus(JDuration.ofHours(1)) } } "use random expiry" in { val sut = service(participantIsActive = true, useExponentialRandomTokenExpiration = true) - Seq - .fill(10) { - generateToken(sut).map(_.expiresAt) - } - .sequence - .value - .map { - case Right(expireTimes) => - expireTimes.distinct.size should be > 1 - case Left(error) => fail(s"Failed with error: $error") - } + for { + expireTimes <- Seq.fill(10)(generateToken(sut).map(_.expiresAt)).sequence + } yield { + expireTimes.distinct.size should be > 1 + } } - "should fail every method if participant is not active" in { + "fail every method if participant is not active" in { val sut = service(false) for { generateNonceError <- leftOrFail(sut.generateNonce(p1))("generating nonce") @@ -127,7 +125,7 @@ class MemberAuthenticationServiceTest extends AsyncWordSpec with BaseTest { )( "validateSignature" ) - validateTokenError <- leftOrFail(sut.validateToken(domainId, p1, null))( + validateTokenError = leftOrFail(sut.validateToken(domainId, p1, null))( "token validation should fail" ) } yield { @@ -137,40 +135,33 @@ class MemberAuthenticationServiceTest extends AsyncWordSpec with BaseTest { } } - "should check whether the intended domain is the one the participant is connecting to" in { + "check whether the intended domain is the one the participant is connecting to" in { val sut = service(false) val wrongDomainId = DomainId(UniqueIdentifier.tryFromProtoPrimitive("wrong::domain")) - for { - error <- leftOrFail(sut.validateToken(wrongDomainId, p1, null))("should fail domain check") - } yield error shouldBe NonMatchingDomainId(p1, wrongDomainId) + val error = leftOrFail(sut.validateToken(wrongDomainId, p1, null))("should fail domain check") + error shouldBe NonMatchingDomainId(p1, wrongDomainId) } - "properly handle becoming a passive node" in { - val sut = service(true, store = new PassiveSequencerMemberAuthenticationStore()) + "invalidate all tokens from a member when logging out" in { + val store = new MemberAuthenticationStore() + val sut = service(participantIsActive = true, store = store) for { - generateNonceError <- leftOrFail(sut.generateNonce(p1))("generateNonce should fail") - validateTokenError <- - leftOrFail( - sut.validateToken( - domainId, - p1, - AuthenticationToken.generate(syncCrypto.crypto.pureCrypto), - ) - )("validateToken should fail") + tokenAndExpiry <- generateToken(sut) + AuthenticationTokenWithExpiry(token, _expiry) = tokenAndExpiry + _ <- EitherT.fromEither[Future](sut.validateToken(domainId, p1, token)) + // Generate a second token for p1 + _ <- generateToken(sut) - validateSignatureError <- leftOrFail( - sut.validateSignature( - p1, - Signature.noSignature, - Nonce.generate(syncCrypto.crypto.pureCrypto), - ) - )("validateSignature should fail") + tokensBefore = fetchTokens(store, Seq(p1)) + + // Use the first token to invalidate them all + _ <- EitherT(sut.invalidateMemberWithToken(token)).leftWiden[AuthenticationError] + tokensAfter = fetchTokens(store, Seq(p1)) } yield { - generateNonceError shouldBe MemberAuthentication.PassiveSequencer - validateTokenError shouldBe MemberAuthentication.PassiveSequencer - validateSignatureError shouldBe MemberAuthentication.PassiveSequencer + tokensBefore(p1) should have size 2 + tokensAfter shouldBe empty } } } diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStoreTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStoreTest.scala index 36e46b78ad5b..cc0b81661988 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStoreTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/MemberAuthenticationStoreTest.scala @@ -3,168 +3,127 @@ package com.digitalasset.canton.domain.sequencing.authentication -import cats.syntax.parallel.* -import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.BaseTest import com.digitalasset.canton.crypto.Nonce import com.digitalasset.canton.crypto.provider.symbolic.SymbolicPureCrypto import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.sequencing.authentication.AuthenticationToken -import com.digitalasset.canton.store.db.{DbTest, H2Test, PostgresTest} import com.digitalasset.canton.topology.{DefaultTestIdentities, Member} -import com.digitalasset.canton.util.FutureInstances.* import org.scalatest.wordspec.AsyncWordSpec -import scala.concurrent.Future - -trait MemberAuthenticationStoreTest extends AsyncWordSpec with BaseTest { +class MemberAuthenticationStoreTest extends AsyncWordSpec with BaseTest { lazy val participant1 = DefaultTestIdentities.participant1 lazy val participant2 = DefaultTestIdentities.participant2 lazy val participant3 = DefaultTestIdentities.participant3 lazy val defaultExpiry = CantonTimestamp.Epoch.plusSeconds(120) lazy val crypto = new SymbolicPureCrypto - def memberAuthenticationStore(mk: () => MemberAuthenticationStore): Unit = { - "invalid member" should { - "work fine if the member has no active token" in { - val store = mk() - - for { - _ <- store.invalidateMember(participant1) - tokens <- store.fetchTokens(participant1) - } yield tokens shouldBe empty - } - - "remove token and nonce for participant" in { - val store = mk() - for { - _ <- store.saveToken(generateToken(participant1)) - storedNonce = generateNonce(participant1) - _ <- store.saveNonce(storedNonce) - _ <- store.invalidateMember(participant1) - tokens <- store.fetchTokens(participant1) - nonceO <- store.fetchAndRemoveNonce(participant1, storedNonce.nonce) - } yield { - tokens shouldBe empty - nonceO shouldBe empty - } - } - } + "invalid member" should { + "work fine if the member has no active token" in { + val store = mk() - "saving nonces" should { - "support many for a member and remove the nonce when fetched" in { - val store = mk() - - val nonce1 = generateNonce(participant1) - val nonce2 = generateNonce(participant1) - val unstoredNonce = generateNonce(participant1) - - for { - _ <- store.saveNonce(nonce1) - _ <- store.saveNonce(nonce2) - nonExistentNonceO <- store.fetchAndRemoveNonce(participant1, unstoredNonce.nonce) - fetchedNonce1O <- store.fetchAndRemoveNonce(participant1, nonce1.nonce) - fetchedNonce2O <- store.fetchAndRemoveNonce(participant1, nonce2.nonce) - refetchedNonce1O <- store.fetchAndRemoveNonce(participant1, nonce1.nonce) - refetchedNonce2O <- store.fetchAndRemoveNonce(participant1, nonce2.nonce) - } yield { - nonExistentNonceO shouldBe empty - fetchedNonce1O.value shouldBe nonce1 - fetchedNonce2O.value shouldBe nonce2 - refetchedNonce1O shouldBe empty - refetchedNonce2O shouldBe empty - } - } - } + store.invalidateMember(participant1) + val tokens = store.fetchTokens(participant1) - "saving tokens" should { - "support many for a member" in { - val store = mk() - - val p1t1 = generateToken(participant1) - val p1t2 = generateToken(participant1) - val p2t1 = generateToken(participant2) - - for { - _ <- List(p1t1, p1t2, p2t1).parTraverse(store.saveToken) - p1Tokens <- store.fetchTokens(participant1) - p2Tokens <- store.fetchTokens(participant2) - p3Tokens <- store.fetchTokens(participant3) - } yield { - p1Tokens should contain.only(p1t1, p1t2) - p2Tokens should contain.only(p2t1) - p3Tokens shouldBe empty - } - } + tokens shouldBe empty } - "expire" should { - "expire all nonces and tokens at or before the given timestamp" in { - val store = mk() - - val n1 = generateNonce(participant1, defaultExpiry.plusSeconds(-1)) - val n2 = generateNonce(participant1, defaultExpiry) - val n3 = generateNonce(participant1, defaultExpiry.plusSeconds(1)) - val t1 = generateToken(participant1, defaultExpiry.plusSeconds(-1)) - val t2 = generateToken(participant1, defaultExpiry) - val t3 = generateToken(participant1, defaultExpiry.plusSeconds(1)) - - for { - _ <- List(n1, n2, n3).parTraverse(store.saveNonce) - _ <- List(t1, t2, t3).parTraverse(store.saveToken) - _ <- store.expireNoncesAndTokens(defaultExpiry) - fn1O <- store.fetchAndRemoveNonce(participant1, n1.nonce) - fn2O <- store.fetchAndRemoveNonce(participant1, n2.nonce) - fn3O <- store.fetchAndRemoveNonce(participant1, n3.nonce) - tokens <- store.fetchTokens(participant1) - } yield { - fn1O shouldBe empty - fn2O shouldBe empty - fn3O.value shouldBe n3 - tokens should contain.only(t3) - } - } + "remove token and nonce for participant" in { + val store = mk() + + store.saveToken(generateToken(participant1)) + val storedNonce = generateNonce(participant1) + store.saveNonce(storedNonce) + + store.invalidateMember(participant1) + val tokens = store.fetchTokens(participant1) + val nonceO = store.fetchAndRemoveNonce(participant1, storedNonce.nonce) + + tokens shouldBe empty + nonceO shouldBe empty } } - def generateToken( - member: Member, - expiry: CantonTimestamp = defaultExpiry, - ): StoredAuthenticationToken = - StoredAuthenticationToken(member, expiry, AuthenticationToken.generate(crypto)) - def generateNonce(member: Member, expiry: CantonTimestamp = defaultExpiry): StoredNonce = - StoredNonce(member, Nonce.generate(crypto), CantonTimestamp.Epoch, expiry) -} + "saving nonces" should { + "support many for a member and remove the nonce when fetched" in { + val store = mk() + + val nonce1 = generateNonce(participant1) + val nonce2 = generateNonce(participant1) + val unstoredNonce = generateNonce(participant1) + + store.saveNonce(nonce1) + store.saveNonce(nonce2) + + val nonExistentNonceO = store.fetchAndRemoveNonce(participant1, unstoredNonce.nonce) + val fetchedNonce1O = store.fetchAndRemoveNonce(participant1, nonce1.nonce) + val fetchedNonce2O = store.fetchAndRemoveNonce(participant1, nonce2.nonce) + val refetchedNonce1O = store.fetchAndRemoveNonce(participant1, nonce1.nonce) + val refetchedNonce2O = store.fetchAndRemoveNonce(participant1, nonce2.nonce) -class MemberAuthenticationStoreTestInMemory extends MemberAuthenticationStoreTest { - "InMemoryMemberAuthenticationStore" should { - behave like (memberAuthenticationStore(() => new InMemoryMemberAuthenticationStore)) + nonExistentNonceO shouldBe empty + fetchedNonce1O.value shouldBe nonce1 + fetchedNonce2O.value shouldBe nonce2 + refetchedNonce1O shouldBe empty + refetchedNonce2O shouldBe empty + } } -} -trait DbMemberAuthenticationStoreTest extends MemberAuthenticationStoreTest { - this: DbTest => + "saving tokens" should { + "support many for a member" in { + val store = mk() + + val p1t1 = generateToken(participant1) + val p1t2 = generateToken(participant1) + val p2t1 = generateToken(participant2) + + List(p1t1, p1t2, p2t1).foreach(store.saveToken) - override def cleanDb(storage: DbStorage): Future[Unit] = { - import storage.api.* + val p1Tokens = store.fetchTokens(participant1) + val p2Tokens = store.fetchTokens(participant2) + val p3Tokens = store.fetchTokens(participant3) - storage.update_( - DBIO.seq( - Seq("nonces", "tokens").map(name => sqlu"truncate table sequencer_authentication_#$name")* - ), - functionFullName, - ) + p1Tokens should contain.only(p1t1, p1t2) + p2Tokens should contain.only(p2t1) + p3Tokens shouldBe empty + } } - "DbMemberAuthenticationStore" should { - behave like memberAuthenticationStore(() => - new DbMemberAuthenticationStore(storage, timeouts, loggerFactory, closeContext) - ) + "expire" should { + "expire all nonces and tokens at or before the given timestamp" in { + val store = mk() + + val n1 = generateNonce(participant1, defaultExpiry.plusSeconds(-1)) + val n2 = generateNonce(participant1, defaultExpiry) + val n3 = generateNonce(participant1, defaultExpiry.plusSeconds(1)) + val t1 = generateToken(participant1, defaultExpiry.plusSeconds(-1)) + val t2 = generateToken(participant1, defaultExpiry) + val t3 = generateToken(participant1, defaultExpiry.plusSeconds(1)) + + List(n1, n2, n3).foreach(store.saveNonce) + List(t1, t2, t3).foreach(store.saveToken) + + store.expireNoncesAndTokens(defaultExpiry) + val fn1O = store.fetchAndRemoveNonce(participant1, n1.nonce) + val fn2O = store.fetchAndRemoveNonce(participant1, n2.nonce) + val fn3O = store.fetchAndRemoveNonce(participant1, n3.nonce) + val tokens = store.fetchTokens(participant1) + + fn1O shouldBe empty + fn2O shouldBe empty + fn3O.value shouldBe n3 + tokens should contain.only(t3) + } } -} -class MemberAuthenticationStoreTestH2 extends DbMemberAuthenticationStoreTest with H2Test -class MemberAuthenticationStoreTestPostgres - extends DbMemberAuthenticationStoreTest - with PostgresTest + private def mk(): MemberAuthenticationStore = new MemberAuthenticationStore() + + private def generateToken( + member: Member, + expiry: CantonTimestamp = defaultExpiry, + ): StoredAuthenticationToken = + StoredAuthenticationToken(member, expiry, AuthenticationToken.generate(crypto)) + + private def generateNonce(member: Member, expiry: CantonTimestamp = defaultExpiry): StoredNonce = + StoredNonce(member, Nonce.generate(crypto), CantonTimestamp.Epoch, expiry) +} diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/PassiveSequencerMemberAuthenticationStore.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/PassiveSequencerMemberAuthenticationStore.scala deleted file mode 100644 index 45bd19b3a67f..000000000000 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/PassiveSequencerMemberAuthenticationStore.scala +++ /dev/null @@ -1,42 +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.domain.sequencing.authentication - -import com.digitalasset.canton.crypto.Nonce -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.resource.DbStorage.PassiveInstanceException -import com.digitalasset.canton.topology.Member -import com.digitalasset.canton.tracing.TraceContext - -import scala.concurrent.Future - -class PassiveSequencerMemberAuthenticationStore extends MemberAuthenticationStore { - override def saveNonce(storedNonce: StoredNonce)(implicit - traceContext: TraceContext - ): Future[Unit] = Future.failed(new PassiveInstanceException("passive sequencer")) - - override def fetchAndRemoveNonce(member: Member, nonce: Nonce)(implicit - traceContext: TraceContext - ): Future[Option[StoredNonce]] = - Future.failed(new PassiveInstanceException("passive sequencer")) - - override def saveToken(token: StoredAuthenticationToken)(implicit - traceContext: TraceContext - ): Future[Unit] = Future.failed(new PassiveInstanceException("passive sequencer")) - - override def fetchTokens( - member: Member - )(implicit traceContext: TraceContext): Future[Seq[StoredAuthenticationToken]] = - Future.failed(new PassiveInstanceException("passive sequencer")) - - override def expireNoncesAndTokens(timestamp: CantonTimestamp)(implicit - traceContext: TraceContext - ): Future[Unit] = Future.failed(new PassiveInstanceException("passive sequencer")) - - override def invalidateMember(member: Member)(implicit - traceContext: TraceContext - ): Future[Unit] = Future.failed(new PassiveInstanceException("passive sequencer")) - - override def close(): Unit = () -} diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptorTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptorTest.scala index 3271b549f298..dcdf62b77107 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptorTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/authentication/grpc/SequencerAuthenticationServerInterceptorTest.scala @@ -28,7 +28,7 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.{BaseTest, HasExecutionContext} import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder} import io.grpc.stub.StreamObserver -import io.grpc.{ManagedChannel, ServerInterceptors, Status, StatusRuntimeException} +import io.grpc.{ManagedChannel, ServerInterceptors, Status} import org.scalatest.BeforeAndAfterEach import org.scalatest.wordspec.AnyWordSpec @@ -56,7 +56,7 @@ class SequencerAuthenticationServerInterceptorTest lazy val service = new GrpcHelloService() - lazy val store: MemberAuthenticationStore = new InMemoryMemberAuthenticationStore() + lazy val store: MemberAuthenticationStore = new MemberAuthenticationStore() lazy val domainId = DomainId(UniqueIdentifier.tryFromProtoPrimitive("popo::pipi")) lazy val authService = new MemberAuthenticationService( @@ -114,7 +114,6 @@ class SequencerAuthenticationServerInterceptorTest loggerFactory.suppressWarningsAndErrors(new GrpcContext { store .saveToken(StoredAuthenticationToken(participantId, neverExpire, token.token)) - .futureValue channel = InProcessChannelBuilder.forName(channelName).build() val client = HelloServiceGrpc.stub(channel) @@ -128,7 +127,6 @@ class SequencerAuthenticationServerInterceptorTest "succeed request if participant use interceptor with correct token information" in new GrpcContext { store .saveToken(StoredAuthenticationToken(participantId, neverExpire, token.token)) - .futureValue val obtainToken = NonEmpty .mk( @@ -164,7 +162,6 @@ class SequencerAuthenticationServerInterceptorTest "fail request if participant use interceptor with incorrect token information" in new GrpcContext { store .saveToken(StoredAuthenticationToken(participantId, neverExpire, token.token)) - .futureValue val obtainToken = NonEmpty .mk( @@ -202,46 +199,5 @@ class SequencerAuthenticationServerInterceptorTest status.getStatus.getCode shouldBe io.grpc.Status.UNAUTHENTICATED.getCode } } - - "fail if the sequencer has become passive" in new GrpcContext { - override lazy val store: MemberAuthenticationStore = - new PassiveSequencerMemberAuthenticationStore() - - val obtainToken = NonEmpty - .mk( - Seq, - ( - Endpoint("localhost", Port.tryCreate(10)), - (_ => EitherT.pure[FutureUnlessShutdown, Status](token)): TraceContext => EitherT[ - FutureUnlessShutdown, - Status, - AuthenticationTokenWithExpiry, - ], - ), - ) - .toMap - - val clientAuthentication = - SequencerClientTokenAuthentication( - domainId, - participantId, - obtainToken, - isClosed = false, - AuthenticationTokenManagerConfig(), - AuthenticationTokenManagerTest.mockClock, - loggerFactory, - ) - channel = InProcessChannelBuilder - .forName(channelName) - .build() - val client = clientAuthentication(HelloServiceGrpc.stub(channel)) - - val exception = client.hello(Hello.Request("hi")).failed.futureValue - exception shouldBe a[StatusRuntimeException] - - val status = exception.asInstanceOf[StatusRuntimeException].getStatus - status.getCode shouldBe Status.UNAVAILABLE.getCode - status.getDescription shouldBe s"Verification failed for member $participantId: Sequencer is currently passive. Connect to a different sequencer and retry the request or wait for the sequencer to become active again." - } } } diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerApiTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerApiTest.scala index 9d2d13be80f5..85427444c752 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerApiTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/sequencer/SequencerApiTest.scala @@ -206,6 +206,7 @@ abstract class SequencerApiTest include regex "Creating .* at block height None" or // TODO(#20288): Remove the log line below include("Creating block sequencer with unified mode") or + include("Completing init") or include("Subscribing to block source from") or include("Advancing sim clock") or (include("Creating ForkJoinPool with parallelism") and include( diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerIntegrationTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerIntegrationTest.scala index 40c2997bcb49..6341a62a633e 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerIntegrationTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerIntegrationTest.scala @@ -177,7 +177,6 @@ final case class Env(loggerFactory: NamedLoggerFactory)(implicit sequencerSubscriptionFactory, domainParamsLookup, params, - mockDomainTopologyManager, topologyStateForInitializationService, BaseTest.testedProtocolVersion, ) @@ -222,6 +221,10 @@ final case class Env(loggerFactory: NamedLoggerFactory)(implicit ) ) ) + override def logout( + request: v30.SequencerAuthentication.LogoutRequest + ): Future[v30.SequencerAuthentication.LogoutResponse] = + Future.successful(v30.SequencerAuthentication.LogoutResponse()) } private val serverPort = UniquePortGenerator.next logger.debug(s"Using port $serverPort for integration test") @@ -264,7 +267,7 @@ final case class Env(loggerFactory: NamedLoggerFactory)(implicit CommonMockMetrics.sequencerClient, LoggingConfig(), loggerFactory, - ProtocolVersionCompatibility.supportedProtocolsParticipant( + ProtocolVersionCompatibility.supportedProtocols( includeAlphaVersions = BaseTest.testedProtocolVersion.isAlpha, includeBetaVersions = BaseTest.testedProtocolVersion.isBeta, release = ReleaseVersion.current, diff --git a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerServiceTest.scala b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerServiceTest.scala index babea0941755..3e829ab608ba 100644 --- a/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerServiceTest.scala +++ b/sdk/canton/community/domain/src/test/scala/com/digitalasset/canton/domain/sequencing/service/GrpcSequencerServiceTest.scala @@ -97,7 +97,6 @@ class GrpcSequencerServiceTest val sequencerSubscriptionFactory = mock[DirectSequencerSubscriptionFactory] private val topologyClient = mock[DomainTopologyClient] private val mockTopologySnapshot = mock[TopologySnapshot] - private val mockDomainTopologyManager = mock[DomainTopologyManager] when(topologyClient.currentSnapshotApproximation(any[TraceContext])) .thenReturn(mockTopologySnapshot) when( @@ -166,7 +165,6 @@ class GrpcSequencerServiceTest sequencerSubscriptionFactory, domainParamLookup, params, - mockDomainTopologyManager, topologyInitService, BaseTest.testedProtocolVersion, maxItemsInTopologyResponse = PositiveInt.tryCreate(maxItemsInTopologyBatch), diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml index 7de4a83ba823..dcaaa9b3c63a 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv1/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --enable-interfaces=yes name: carbonv1-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml index 54d09bd81ad0..3f39bdd515ae 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/carbonv2/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --enable-interfaces=yes name: carbonv2-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml index fe56da51b5c5..e1bee45e5f2a 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/experimental/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c name: experimental-tests source: . version: 3.1.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml index c48fa7b73cf3..8c1e2d1d521b 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/model/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --enable-interfaces=yes name: model-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml index 5b9dfa11aa90..f0d52aa7dbd5 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/package_management/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c name: package-management-tests source: . version: 3.1.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml index 7af99c5b58f9..d123f372e98b 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/semantic/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --enable-interfaces=yes name: semantic-tests diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml index eb68cf560bd6..fd500bc35419 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/1.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c name: upgrade-tests source: . version: 1.0.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml index 3ffd176ebca6..3aff85226db7 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/2.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c name: upgrade-tests source: . version: 2.0.0 diff --git a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml index 424afe920e70..a95d438ae369 100644 --- a/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml +++ b/sdk/canton/community/ledger/ledger-common-dars/src/main/daml/upgrade/3.0.0/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c name: upgrade-tests source: . version: 3.0.0 diff --git a/sdk/canton/community/ledger/ledger-common/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala b/sdk/canton/community/ledger/ledger-common/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala index 68c4b59bda66..327ca9840ea4 100644 --- a/sdk/canton/community/ledger/ledger-common/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala +++ b/sdk/canton/community/ledger/ledger-common/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala @@ -18,6 +18,7 @@ import com.daml.ledger.api.v2.state_service.StateServiceGrpc import com.daml.ledger.api.v2.trace_context.TraceContext as LedgerApiTraceContext import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc import com.daml.ledger.api.v2.version_service.VersionServiceGrpc +import com.digitalasset.canton.ledger.client.LedgerClient.stubWithTracing import com.digitalasset.canton.ledger.client.configuration.{ LedgerClientChannelConfiguration, LedgerClientConfiguration, @@ -102,6 +103,11 @@ final class LedgerClient private ( ) override def close(): Unit = GrpcChannel.close(channel) + + def serviceClient[A <: AbstractStub[A]](stub: Channel => A, token: Option[String])(implicit + traceContext: TraceContext + ): A = + stubWithTracing(stub(channel), token) } object LedgerClient { diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/fetchcontracts/util/IdentifierConverters.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/fetchcontracts/util/IdentifierConverters.scala index 6ddf2b0b7d60..2c3cf5d1e306 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/fetchcontracts/util/IdentifierConverters.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/fetchcontracts/util/IdentifierConverters.scala @@ -5,9 +5,10 @@ package com.digitalasset.canton.fetchcontracts.util import com.digitalasset.daml.lf import com.daml.ledger.api.v2 as lav2 +import com.digitalasset.daml.lf.data.Ref.{DottedName, ModuleName, PackageId, QualifiedName} import com.digitalasset.canton.http.domain.ContractTypeId - object IdentifierConverters { +object IdentifierConverters { def apiIdentifier(a: lf.data.Ref.Identifier): lav2.value.Identifier = lav2.value.Identifier( packageId = a.packageId, @@ -15,6 +16,15 @@ import com.digitalasset.canton.http.domain.ContractTypeId entityName = a.qualifiedName.name.dottedName, ) + def lfIdentifier(a: com.daml.ledger.api.v2.value.Identifier): lf.data.Ref.Identifier = + lf.data.Ref.Identifier( + packageId = PackageId.assertFromString(a.packageId), + qualifiedName = QualifiedName( + module = ModuleName.assertFromString(a.moduleName), + name = DottedName.assertFromString(a.entityName), + ), + ) + def apiIdentifier(a: ContractTypeId.RequiredPkg): lav2.value.Identifier = lav2.value.Identifier( packageId = a.packageId, diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/CommandService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/CommandService.scala index c7fb2f2840b5..26b1346ce5a4 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/CommandService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/CommandService.scala @@ -187,7 +187,7 @@ class CommandService( case -\/((templateId, contractKey)) => Commands.exerciseByKey( templateId = refApiIdentifier(templateId), - // TODO #14549 somehow pass choiceSource + // TODO daml-14549 somehow pass choiceSource contractKey = contractKey, choice = input.choice, argument = input.argument, @@ -202,7 +202,7 @@ class CommandService( } } - // TODO #14549 somehow use the choiceInterfaceId + // TODO daml-14549 somehow use the choiceInterfaceId private def createAndExerciseCommand( input: CreateAndExerciseCommand.LAVResolved ): lav2.commands.Command.Command.CreateAndExercise = diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/Endpoints.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/Endpoints.scala index 29f77576e300..e7f232ad4205 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/Endpoints.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/Endpoints.scala @@ -33,7 +33,7 @@ import com.daml.metrics.Timed import org.apache.pekko.http.scaladsl.server.Directives.* import com.digitalasset.canton.http.endpoints.{MeteringReportEndpoint, RouteSetup} import com.daml.jwt.Jwt -import com.digitalasset.canton.http.json2.V2Routes +import com.digitalasset.canton.http.json.v2.V2Routes import com.digitalasset.canton.ledger.client.services.admin.UserManagementClient import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.NoTracing diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/HttpService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/HttpService.scala index 7f92591b86b0..594017942a66 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/HttpService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/HttpService.scala @@ -4,29 +4,22 @@ package com.digitalasset.canton.http import com.daml.grpc.adapter.ExecutionSequencerFactory -import com.daml.jwt.JwtDecoder -import com.daml.jwt.Jwt +import com.daml.jwt.{Jwt, JwtDecoder} +import com.daml.ledger.api.v2.command_submission_service.CommandSubmissionServiceGrpc +import com.daml.ledger.api.v2.state_service.StateServiceGrpc import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner} import com.daml.logging.LoggingContextOf import com.daml.metrics.pekkohttp.HttpMetricsInterceptor import com.daml.ports.{Port, PortFiles} import com.digitalasset.canton.concurrent.DirectExecutionContext -import com.digitalasset.canton.http.json.{ - ApiValueToJsValueConverter, - DomainJsonDecoder, - DomainJsonEncoder, - JsValueToApiValueConverter, -} +import com.digitalasset.canton.http.json.{ApiValueToJsValueConverter, DomainJsonDecoder, DomainJsonEncoder, JsValueToApiValueConverter} import com.digitalasset.canton.http.metrics.HttpApiMetrics import com.digitalasset.canton.http.util.ApiValueToLfValueConverter import com.digitalasset.canton.http.util.FutureUtil.* import com.digitalasset.canton.http.util.Logging.InstanceUUID import com.digitalasset.canton.ledger.api.refinements.ApiTypes.ApplicationId import com.digitalasset.canton.ledger.client.LedgerClient as DamlLedgerClient -import com.digitalasset.canton.ledger.client.configuration.{ - CommandClientConfiguration, - LedgerClientConfiguration, -} +import com.digitalasset.canton.ledger.client.configuration.{CommandClientConfiguration, LedgerClientConfiguration} import com.digitalasset.canton.ledger.client.services.pkg.PackageClient import com.digitalasset.canton.ledger.service.LedgerReader import com.digitalasset.canton.ledger.service.LedgerReader.PackageStore @@ -47,7 +40,7 @@ import java.nio.file.{Files, Path} import java.security.{Key, KeyStore} import javax.net.ssl.SSLContext import com.daml.tls.TlsConfiguration -import com.digitalasset.canton.http.json2.V2Routes +import com.digitalasset.canton.http.json.v2.V2Routes import scala.concurrent.{ExecutionContext, Future} import scala.util.Using @@ -86,6 +79,7 @@ class HttpService( val ledgerClient: DamlLedgerClient = DamlLedgerClient.withoutToken(channel, clientConfig, loggerFactory) + import org.apache.pekko.http.scaladsl.server.Directives.* val bindingEt: EitherT[Future, HttpService.Error, ServerBinding] = for { _ <- eitherT(Future.successful(\/-(ledgerClient))) @@ -158,6 +152,7 @@ class HttpService( v2Routes = V2Routes( ledgerClient, + packageService, mat.executionContext, mat, loggerFactory, diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/PackageService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/PackageService.scala index 2a786d9d8b4d..9fc0681349cf 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/PackageService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/PackageService.scala @@ -14,7 +14,7 @@ import com.digitalasset.canton.http.domain.ContractTypeId.ResolvedOf import com.digitalasset.canton.http.domain.Choice import com.digitalasset.canton.http.util.IdentifierConverters import com.digitalasset.canton.http.util.Logging.InstanceUUID -import com.digitalasset.canton.ledger.service.LedgerReader.PackageStore +import com.digitalasset.canton.ledger.service.LedgerReader.{PackageStore, Signatures} import com.digitalasset.canton.ledger.service.{LedgerReader, TemplateIds} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.NoTracing @@ -48,6 +48,7 @@ class PackageService( def append(diff: PackageStore): State = { val newPackageStore = this.packageStore ++ resolveChoicesIn(diff) + val (tpIdMap, ifaceIdMap) = getTemplateIdInterfaceMaps(newPackageStore) State( packageIds = newPackageStore.keySet, @@ -62,8 +63,11 @@ class PackageService( // `diff` but with interface-inherited choices resolved private[this] def resolveChoicesIn(diff: PackageStore): PackageStore = { def lookupIf(pkgId: Ref.PackageId) = (packageStore get pkgId) orElse (diff get pkgId) - val findIface = typesig.PackageSignature.findInterface(Function unlift lookupIf) - diff.transform((_, iface) => iface resolveChoicesAndIgnoreUnresolvedChoices findIface) + val findIface = + typesig.PackageSignature.findInterface((Function unlift lookupIf).andThen(_.typesig)) + diff.transform((_, iface) => + Signatures(iface.typesig.resolveChoicesAndIgnoreUnresolvedChoices(findIface), iface.pack) + ) } } @@ -378,8 +382,12 @@ object PackageService { import TemplateIds.{getInterfaceIds, getTemplateIds} val packageSigs = packageStore.values.toSet ( - buildTemplateIdMap(getTemplateIds(packageSigs) map ContractTypeId.Template.fromLedgerApi), - buildTemplateIdMap(getInterfaceIds(packageSigs) map ContractTypeId.Interface.fromLedgerApi), + buildTemplateIdMap( + getTemplateIds(packageSigs.map(_.typesig)) map ContractTypeId.Template.fromLedgerApi + ), + buildTemplateIdMap( + getInterfaceIds(packageSigs.map(_.typesig)) map ContractTypeId.Interface.fromLedgerApi + ), ) } @@ -448,7 +456,7 @@ object PackageService { b(pkgId, qn.module.dottedName, qn.name.dottedName) private def getChoiceTypeMap(packageStore: PackageStore): ChoiceTypeMap = - packageStore.values.view.flatMap(getChoices).toMap + packageStore.values.view.map(_.typesig).flatMap(getChoices).toMap private def getChoices( signature: typesig.PackageSignature @@ -507,7 +515,7 @@ object PackageService { } private def getKeyTypeMap(packageStore: PackageStore): KeyTypeMap = - packageStore.flatMap { case (_, interface) => getKeys(interface) } + packageStore.flatMap { case (_, interface) => getKeys(interface.typesig) } private def getKeys( interface: typesig.PackageSignature diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/Endpoints.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala similarity index 82% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/Endpoints.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala index a071cfcf38c6..d1d6c5d95243 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/Endpoints.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala @@ -1,11 +1,11 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 //TODO (i19539) repackage eventually import com.daml.error.utils.DecodedCantonError -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import com.digitalasset.canton.tracing.{TraceContext, W3CTraceContext} import io.circe.{Decoder, Encoder} import io.grpc.StatusRuntimeException @@ -16,7 +16,7 @@ import sttp.capabilities.WebSockets import sttp.capabilities.pekko.PekkoStreams import sttp.model.Header import sttp.tapir.generic.auto.* -import sttp.tapir.json.circe.{jsonBody, *} +import sttp.tapir.json.circe.* import sttp.tapir.server.ServerEndpoint.Full import sttp.tapir.* @@ -37,14 +37,28 @@ trait Endpoints { .securityIn( auth .bearer[Option[String]]() - .map(bearer => CallerContext(bearer.map(Jwt)))( - _.jwt.map(_.token) + .map(bearer => bearer.map(Jwt))( + _.map(_.token) ) .description("Ledger API standard JWT token") + .and( + auth + .apiKey(header[Option[String]]("Sec-WebSocket-Protocol")) + .map { bearer => + bearer.map(Jwt) + }(_.map(_.token)) + .description("Ledger API standard JWT token (websocket)") + ) + .map(tokens => CallerContext(tokens._1.orElse(tokens._2)))(cc => (cc.jwt, cc.jwt)) ) .errorOut(jsonBody[JsCantonError]) .in("v2") +// lazy val wsEndpoint: Endpoint[CallerContext, Unit, JsCantonError, Unit, Any] = endpoint +// +// .errorOut(jsonBody[JsCantonError]) +// .in("v2") + private val wsSubprotocol = sttp.model.Header("Sec-WebSocket-Protocol", "daml.ws.auth") protected def handleErrorResponse[R] @@ -101,26 +115,10 @@ trait Endpoints { .out(streamBinaryBody(PekkoStreams)(CodecFormat.OctetStream())) .serverLogic(jwt => i => - service(jwt)(i).toRight + service(jwt)(i).resultToRight .transform(handleErrorResponse)(ExecutionContext.parasitic) ) - def getWebSocket[HI, I: Decoder: Encoder: Schema, O: Decoder: Encoder: Schema]( - endpoint: Endpoint[CallerContext, HI, JsCantonError, Unit, Any], - service: CallerContext => HI => Flow[I, O, Any], - ): Full[CallerContext, CallerContext, HI, JsCantonError, Flow[ - I, - O, - Any, - ], Any with PekkoStreams with WebSockets, Future] = - endpoint.get - .in(header(wsSubprotocol)) - .out(header(wsSubprotocol)) - .serverSecurityLogicSuccess(Future.successful) - .out(webSocketBody[I, CodecFormat.Json, O, CodecFormat.Json](PekkoStreams)) - // TODO(i19398): Handle error result - .serverLogicSuccess(jwt => i => Future.successful(service(jwt)(i))) - def jsonWithBody[I: Decoder: Encoder: Schema, R: Decoder: Encoder: Schema, P]( endpoint: Endpoint[CallerContext, P, JsCantonError, Unit, Any], service: CallerContext => (TracedInput[P], I) => Future[Either[JsCantonError, R]], @@ -131,11 +129,11 @@ trait Endpoints { .in(jsonBody[I]) .serverSecurityLogicSuccess(Future.successful) .out(jsonBody[R]) - .serverLogic(callerContext => { i => + .serverLogic { callerContext => i => service(callerContext) .tupled(i) .transform(handleErrorResponse)(ExecutionContext.parasitic) - }) + } def json[R: Decoder: Encoder: Schema, P]( endpoint: Endpoint[CallerContext, P, JsCantonError, Unit, Any], @@ -150,6 +148,24 @@ trait Endpoints { i => service(callerContext)(i).transform(handleErrorResponse)(ExecutionContext.parasitic) ) + protected def websocket[HI, I: Decoder: Encoder: Schema, O: Decoder: Encoder: Schema]( + endpoint: Endpoint[CallerContext, HI, JsCantonError, Unit, Any], + service: CallerContext => HI => Flow[I, O, Any], + ): Full[CallerContext, CallerContext, HI, JsCantonError, Flow[ + I, + O, + Any, + ], Any with PekkoStreams with WebSockets, Future] = + endpoint + // .in(header(wsSubprotocol)) We send wsSubprotocol header, but we do not enforce it + .out(header(wsSubprotocol)) + .serverSecurityLogicSuccess(Future.successful) + .out(webSocketBody[I, CodecFormat.Json, O, CodecFormat.Json](PekkoStreams)) + // TODO(19398): Handle error result + .serverLogicSuccess { jwt => i => + Future.successful(service(jwt)(i)) + } + def traceHeadersMapping[I]() = new Mapping[(I, List[sttp.model.Header]), TracedInput[I]] { override def rawDecode(input: (I, List[Header])): DecodeResult[TracedInput[I]] = @@ -163,18 +179,17 @@ trait Endpoints { ) ) - override def encode(h: TracedInput[I]): (I, List[Header]) = { + override def encode(h: TracedInput[I]): (I, List[Header]) = ( h.in, W3CTraceContext.extractHeaders(h.traceContext).map { case (k, v) => Header(k, v) }.toList, ) - } override def validator: Validator[TracedInput[I]] = Validator.pass } implicit class FutureOps[R](future: Future[R]) { - def toRight: Future[Either[JsCantonError, R]] = + def resultToRight: Future[Either[JsCantonError, R]] = future.map(Right(_))(ExecutionContext.parasitic) } diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala new file mode 100644 index 000000000000..5f3ff81de990 --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsCommandService.scala @@ -0,0 +1,306 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.ledger.api.v2.command_service.{CommandServiceGrpc, SubmitAndWaitRequest} +import com.google.protobuf +import com.daml.ledger.api.v2.command_submission_service +import com.daml.ledger.api.v2.commands.Commands.DeduplicationPeriod +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.{JsCantonError, JsTransaction, JsTransactionTree} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import com.digitalasset.canton.ledger.client.LedgerClient +import com.digitalasset.canton.tracing.TraceContext +import io.circe.* +import io.circe.generic.semiauto.deriveCodec + +import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future} + +class JsCommandService( + ledgerClient: LedgerClient, + protocolConverters: ProtocolConverters, + val loggerFactory: NamedLoggerFactory, +)(implicit + val executionContext: ExecutionContext +) extends Endpoints + with NamedLogging { + + import JsCommandServiceCodecs.* + + private lazy val commands = baseEndpoint.in("commands") + + private def commandServiceClient(token: Option[String] = None)(implicit + traceContext: TraceContext + ): CommandServiceGrpc.CommandServiceStub = + ledgerClient.serviceClient(CommandServiceGrpc.stub, token) + + private def commandSubmissionServiceClient(token: Option[String] = None)(implicit + traceContext: TraceContext + ): command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionServiceStub = + ledgerClient.serviceClient(command_submission_service.CommandSubmissionServiceGrpc.stub, token) + + def endpoints() = List( + jsonWithBody( + commands.post + .in("submit-and-wait-for-update-id") + .description("Submit a batch of commands and wait for the update id"), + submitAndWaitForUpdateId, + ), + jsonWithBody( + commands.post + .in("submit-and-wait-for-transaction") + .description("Submit a batch of commands and wait for the flat transactions response"), + submitAndWaitForTransaction, + ), + jsonWithBody( + commands.post + .in("submit-and-wait-for-transaction-tree") + .description("Submit a batch of commands and wait for the transaction trees response"), + submitAndWaitForTransactionTree, + ), + jsonWithBody( + commands.post + .in("async") + .in("submit") + .description("Submit a command asynchronously"), + submitAsync, + ), + jsonWithBody( + commands.post + .in("async") + .in("submit-reassignment") + .description("Submit reassignment command asynchronously"), + submitReassignmentAsync, + ), + ) + + def submitAndWaitForUpdateId(callerContext: CallerContext): ( + TracedInput[Unit], + JsCommands, + ) => Future[ + Either[JsCantonError, JsSubmitAndWaitForUpdateIdResponse] + ] = (req, body) => { + for { + commands <- protocolConverters.Commands.fromJson(body)(callerContext.token()) + submitAndWaitRequest = + SubmitAndWaitRequest(commands = Some(commands)) + result <- commandServiceClient(callerContext.token())(req.traceContext) + .submitAndWaitForUpdateId(submitAndWaitRequest) + .map(protocolConverters.SubmitAndWaitUpdateIdResponse.toJson)( + ExecutionContext.parasitic + ) + .resultToRight + } yield result + } + + def submitAndWaitForTransactionTree(callerContext: CallerContext): ( + TracedInput[Unit], + JsCommands, + ) => Future[ + Either[JsCantonError, JsSubmitAndWaitForTransactionTreeResponse] + ] = (req, body) => { + for { + commands <- protocolConverters.Commands.fromJson(body)(callerContext.token()) + submitAndWaitRequest = + SubmitAndWaitRequest(commands = Some(commands)) + result <- commandServiceClient(callerContext.token())(req.traceContext) + .submitAndWaitForTransactionTree(submitAndWaitRequest) + .flatMap(r => + protocolConverters.SubmitAndWaitTransactionTreeResponse.toJson(r)(callerContext.token()) + ) + .resultToRight + } yield result + } + + def submitAndWaitForTransaction(callerContext: CallerContext): ( + TracedInput[Unit], + JsCommands, + ) => Future[ + Either[JsCantonError, JsSubmitAndWaitForTransactionResponse] + ] = (req, body) => { + for { + commands <- protocolConverters.Commands.fromJson(body)(callerContext.token()) + submitAndWaitRequest = + SubmitAndWaitRequest(commands = Some(commands)) + result <- commandServiceClient(callerContext.token())(req.traceContext) + .submitAndWaitForTransaction(submitAndWaitRequest) + .flatMap(r => + protocolConverters.SubmitAndWaitTransactionResponse.toJson(r)(callerContext.token()) + ) + .resultToRight + } yield result + } + + private def submitAsync(callerContext: CallerContext): ( + TracedInput[Unit], + JsCommands, + ) => Future[ + Either[JsCantonError, command_submission_service.SubmitResponse] + ] = (req, body) => { + for { + commands <- protocolConverters.Commands.fromJson(body)(callerContext.token()) + submitRequest = + command_submission_service.SubmitRequest(commands = Some(commands)) + result <- commandSubmissionServiceClient(callerContext.token())(req.traceContext) + .submit(submitRequest) + .resultToRight + } yield result + } + + private def submitReassignmentAsync(callerContext: CallerContext): ( + TracedInput[Unit], + JsSubmitReassignmentRequest, + ) => Future[ + Either[JsCantonError, command_submission_service.SubmitReassignmentResponse] + ] = (req, body) => { + val submitRequest = protocolConverters.JsSubmitReassignmentRequest.fromJson(body) + commandSubmissionServiceClient(callerContext.token())(req.traceContext) + .submitReassignment(submitRequest) + .resultToRight + + } +} + +final case class JsSubmitAndWaitForTransactionTreeResponse( + transaction_tree: JsTransactionTree, + completion_offset: String, +) + +final case class JsSubmitAndWaitForTransactionResponse( + transaction: JsTransaction, + completion_offset: String, +) + +final case class JsSubmitAndWaitForUpdateIdResponse( + update_id: String, + completion_offset: String, +) + +case class JsReassignmentCommand( + workflow_id: String, + application_id: String, + command_id: String, + submitter: String, + command: JsReassignmentCommand.JsCommand, + submission_id: String, +) + +object JsReassignmentCommand { + sealed trait JsCommand + + case class JsUnassignCommand( + contract_id: String, + source: String, + target: String, + ) extends JsCommand + + case class JsAssignCommand( + unassign_id: String, + source: String, + target: String, + ) extends JsCommand +} + +case class JsSubmitReassignmentRequest( + reassignment_command: Option[JsReassignmentCommand] +) + +object JsCommand { + sealed trait Command + final case class CreateCommand( + template_id: String, + create_arguments: Json, + ) extends Command + + final case class ExerciseCommand( + template_id: String, + contract_id: String, + choice: String, + choice_argument: Json, + ) extends Command + + final case class CreateAndExerciseCommand( + template_id: String, + create_arguments: Json, + choice: String, + choice_argument: Json, + ) extends Command + + final case class ExerciseByKeyCommand( + template_id: String, + contract_key: Json, + choice: String, + choice_argument: Json, + ) extends Command +} + +final case class JsCommands( + commands: Seq[JsCommand.Command], + workflow_id: String, + application_id: String, + command_id: String, + deduplication_period: DeduplicationPeriod, + min_ledger_time_abs: Option[protobuf.timestamp.Timestamp], + min_ledger_time_rel: Option[protobuf.duration.Duration], + act_as: Seq[String], + read_as: Seq[String], + submission_id: String, + disclosed_contracts: Seq[JsDisclosedContract], + domain_id: String, + package_id_selection_preference: Seq[String], +) +final case class JsDisclosedContract( + template_id: String, + contract_id: String, + created_event_blob: com.google.protobuf.ByteString, +) + +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 +object JsCommandServiceCodecs { + + implicit val deduplicationPeriodRW: Codec[DeduplicationPeriod] = deriveCodec + implicit val deduplicationPeriodDeduplicationDurationRW + : Codec[DeduplicationPeriod.DeduplicationDuration] = deriveCodec + implicit val deduplicationPeriodDeduplicationOffsetRW + : Codec[DeduplicationPeriod.DeduplicationOffset] = deriveCodec + + implicit val durationRW: Codec[protobuf.duration.Duration] = deriveCodec + + implicit val jsTransactionRW: Codec[JsTransaction] = + deriveCodec + + implicit val jsSubmitAndWaitForTransactionResponseRW + : Codec[JsSubmitAndWaitForTransactionResponse] = + deriveCodec + + implicit val submitResponseRW: Codec[command_submission_service.SubmitResponse] = + deriveCodec + + implicit val submitReassignmentResponseRW + : Codec[command_submission_service.SubmitReassignmentResponse] = + deriveCodec + + implicit val jsReassignmentCommandCommandRW: Codec[JsReassignmentCommand.JsCommand] = + deriveCodec + + implicit val jsReassignmentCommandRW: Codec[JsReassignmentCommand] = + deriveCodec + + implicit val jsSubmitReassignmentRequestRW: Codec[JsSubmitReassignmentRequest] = + deriveCodec + + implicit val jsCommandsRW: Codec[JsCommands] = deriveCodec + + implicit val jsCommandCommandRW: Codec[JsCommand.Command] = deriveCodec + implicit val jsCommandCreateRW: Codec[JsCommand.CreateCommand] = deriveCodec + implicit val jsCommandExerciseRW: Codec[JsCommand.ExerciseCommand] = deriveCodec + + implicit val jsDisclosedContractRW: Codec[JsDisclosedContract] = deriveCodec + + implicit val jsSubmitAndWaitForUpdateIdResponseRW: Codec[JsSubmitAndWaitForUpdateIdResponse] = + deriveCodec +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsEventService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsEventService.scala new file mode 100644 index 000000000000..0553cd32f45a --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsEventService.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.ledger.api.v2.event_query_service +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.{JsCantonError, JsEvent} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import com.digitalasset.canton.ledger.client.LedgerClient +import com.digitalasset.canton.tracing.TraceContext +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec +import sttp.tapir.{path, query} + +import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future} + +class JsEventService( + ledgerClient: LedgerClient, + protocolConverters: ProtocolConverters, + val loggerFactory: NamedLoggerFactory, +)(implicit + val executionContext: ExecutionContext +) extends Endpoints + with NamedLogging { + + import JsEventServiceCodecs.* + + private lazy val events = baseEndpoint.in("events") + + private def eventServiceClient(token: Option[String] = None)(implicit + traceContext: TraceContext + ): event_query_service.EventQueryServiceGrpc.EventQueryServiceStub = + ledgerClient.serviceClient(event_query_service.EventQueryServiceGrpc.stub, token) + + def endpoints() = List( + json( + events.get + .in("events-by-contract-id") + .in(path[String]("contract-id")) + .in(query[List[String]]("parties")) + .description("Get events by contract Id"), + getEventsByContractId, + ) + ) + + def getEventsByContractId(callerContext: CallerContext): ( + TracedInput[(String, List[String])] + ) => Future[ + Either[JsCantonError, JsGetEventsByContractIdResponse] + ] = req => { + val parties = req.in._2 + eventServiceClient(callerContext.token())(req.traceContext) + .getEventsByContractId( + event_query_service + .GetEventsByContractIdRequest( + contractId = req.in._1, + requestingParties = parties.toIndexedSeq, + ) + ) + .flatMap(protocolConverters.GetEventsByContractIdRequest.toJson(_)(callerContext.token())) + .resultToRight + } +} + +case class JsCreated( + created_event: JsEvent.CreatedEvent, + domain_id: String, +) +case class JsArchived( + archived_event: JsEvent.ArchivedEvent, + domain_id: String, +) + +case class JsGetEventsByContractIdResponse( + created: Option[JsCreated], + archived: Option[JsArchived], +) + +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 +object JsEventServiceCodecs { + implicit val jsCreatedRW: Codec[JsCreated] = deriveCodec + implicit val jsArchivedRW: Codec[JsArchived] = deriveCodec + implicit val jsGetEventsByContractIdResponseRW: Codec[JsGetEventsByContractIdResponse] = + deriveCodec +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsIdentityProviderService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsIdentityProviderService.scala similarity index 93% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsIdentityProviderService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsIdentityProviderService.scala index 17a0a217935e..fcd60347feda 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsIdentityProviderService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsIdentityProviderService.scala @@ -1,12 +1,12 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.ledger.api.v2.admin.identity_provider_config_service import com.digitalasset.canton.ledger.client.services.admin.IdentityProviderConfigClient -import com.digitalasset.canton.http.json2.JsSchema.DirectScalaPbRwImplicits.* -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.InvalidArgument import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import io.circe.Codec @@ -14,6 +14,7 @@ import io.circe.generic.semiauto.deriveCodec import sttp.tapir.* import sttp.tapir.generic.auto.* +import scala.annotation.nowarn import scala.language.existentials import scala.concurrent.Future @@ -68,7 +69,7 @@ class JsIdentityProviderService( .listIdentityProviderConfigs( new identity_provider_config_service.ListIdentityProviderConfigsRequest() ) - .toRight + .resultToRight private def createIdps( callerContext: CallerContext @@ -82,7 +83,7 @@ class JsIdentityProviderService( identityProviderConfigClient .serviceStub(callerContext.token())(req.traceContext) .createIdentityProviderConfig(body) - .toRight + .resultToRight private def updateIdp( callerContext: CallerContext @@ -97,7 +98,7 @@ class JsIdentityProviderService( identityProviderConfigClient .serviceStub(callerContext.token())(req.traceContext) .updateIdentityProviderConfig(body) - .toRight + .resultToRight } else { implicit val traceContext = req.traceContext error( @@ -121,7 +122,7 @@ class JsIdentityProviderService( identity_provider_config_service .GetIdentityProviderConfigRequest(identityProviderId = req.in) ) - .toRight + .resultToRight private def deleteIdp( callerContext: CallerContext @@ -135,10 +136,10 @@ class JsIdentityProviderService( identity_provider_config_service .DeleteIdentityProviderConfigRequest(identityProviderId = req.in) ) - .toRight + .resultToRight } - +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsIdentityProviderCodecs { implicit val identityProviderConfig : Codec[identity_provider_config_service.IdentityProviderConfig] = diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsMeteringService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsMeteringService.scala similarity index 82% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsMeteringService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsMeteringService.scala index b1bf46e7bc3d..9a04bb4e39ed 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsMeteringService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsMeteringService.scala @@ -1,11 +1,11 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.ledger.api.v2.admin.metering_report_service -import com.digitalasset.canton.ledger.client.services.admin.{MeteringReportClient} -import com.digitalasset.canton.http.json2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.ledger.client.services.admin.MeteringReportClient +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* import io.circe.Codec import io.circe.generic.semiauto.deriveCodec import sttp.tapir.* @@ -13,6 +13,7 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import com.google.protobuf +import scala.annotation.nowarn import scala.concurrent.{ExecutionContext, Future} class JsMeteringService(meteringReportClient: MeteringReportClient) extends Endpoints { @@ -45,7 +46,7 @@ class JsMeteringService(meteringReportClient: MeteringReportClient) extends Endp val resp = meteringReportClient .getMeteringReport(req, caller.token())(input._1.traceContext) - resp.toRight.transform(handleErrorResponse)(ExecutionContext.parasitic) + resp.resultToRight.transform(handleErrorResponse)(ExecutionContext.parasitic) } } @@ -54,7 +55,7 @@ class JsMeteringService(meteringReportClient: MeteringReportClient) extends Endp ) } - +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsMeteringServiceCodecs { implicit val getMeteringReportRequest: Codec[metering_report_service.GetMeteringReportRequest] = deriveCodec diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPackageService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPackageService.scala similarity index 90% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPackageService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPackageService.scala index 1e51eaf4dc84..9c05046f681e 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPackageService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPackageService.scala @@ -1,9 +1,9 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 -import com.digitalasset.canton.http.json2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* import com.digitalasset.canton.ledger.client.services.admin.PackageManagementClient import com.digitalasset.canton.ledger.client.services.pkg.PackageClient import com.daml.ledger.api.v2.package_service @@ -16,11 +16,13 @@ import sttp.tapir.path import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.IteratorHasAsScala import JsPackageCodecs.* -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import io.circe.Codec import io.circe.generic.semiauto.deriveCodec import sttp.tapir.generic.auto.* +import scala.annotation.nowarn + class JsPackageService( packageClient: PackageClient, packageManagementClient: PackageManagementClient, @@ -59,14 +61,14 @@ class JsPackageService( caller: CallerContext ): TracedInput[Unit] => Future[Either[JsCantonError, package_service.ListPackagesResponse]] = { req => - packageClient.listPackages(caller.token())(req.traceContext).toRight + packageClient.listPackages(caller.token())(req.traceContext).resultToRight } private def status( caller: CallerContext ): TracedInput[String] => Future[ Either[JsCantonError, package_service.GetPackageStatusResponse] - ] = req => packageClient.getPackageStatus(req.in)(req.traceContext).toRight + ] = req => packageClient.getPackageStatus(req.in)(req.traceContext).resultToRight private def upload(caller: CallerContext) = { (tracedInput: TracedInput[Source[util.ByteString, Any]]) => @@ -92,7 +94,7 @@ class JsPackageService( ) } } - +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsPackageCodecs { implicit val listPackagesResponse: Codec[package_service.ListPackagesResponse] = deriveCodec implicit val getPackageStatusResponse: Codec[package_service.GetPackageStatusResponse] = diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPartyManagementService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPartyManagementService.scala similarity index 92% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPartyManagementService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPartyManagementService.scala index 9c45c1e4aed8..a3e27cfb88f7 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsPartyManagementService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsPartyManagementService.scala @@ -1,7 +1,7 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.ledger.api.v2.admin.party_management_service import com.digitalasset.canton.ledger.client.services.admin.PartyManagementClient @@ -10,11 +10,12 @@ import io.circe.generic.semiauto.deriveCodec import sttp.tapir.generic.auto.* import scala.concurrent.{ExecutionContext, Future} -import com.digitalasset.canton.http.json2.JsSchema.DirectScalaPbRwImplicits.* -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.InvalidArgument import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import sttp.tapir.{path, query} +import scala.annotation.nowarn class JsPartyManagementService( partyManagementClient: PartyManagementClient, @@ -69,7 +70,7 @@ class JsPartyManagementService( partyManagementClient .serviceStub(ctx.token())(req.traceContext) .listKnownParties(party_management_service.ListKnownPartiesRequest()) - .toRight + .resultToRight // TODO (i19538) paging private val getParty @@ -84,7 +85,7 @@ class JsPartyManagementService( partyManagementClient .serviceStub(ctx.token())(req.traceContext) .getParties(partyRequest) - .toRight + .resultToRight } private val getParticipantId: CallerContext => TracedInput[Unit] => Future[ @@ -93,7 +94,7 @@ class JsPartyManagementService( partyManagementClient .serviceStub(ctx.token())(req.traceContext) .getParticipantId(party_management_service.GetParticipantIdRequest()) - .toRight + .resultToRight } private val allocateParty: CallerContext => ( @@ -105,7 +106,7 @@ class JsPartyManagementService( partyManagementClient .serviceStub(caller.token())(req.traceContext) .allocateParty(body) - .toRight + .resultToRight private val updateParty: CallerContext => ( TracedInput[String], @@ -117,7 +118,7 @@ class JsPartyManagementService( partyManagementClient .serviceStub(caller.token())(req.traceContext) .updatePartyDetails(body) - .toRight + .resultToRight } else { implicit val traceContext = req.traceContext error( @@ -129,7 +130,7 @@ class JsPartyManagementService( ) } } - +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsPartyManagementCodecs { implicit val partyDetails: Codec[party_management_service.PartyDetails] = deriveCodec diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsSchema.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsSchema.scala similarity index 60% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsSchema.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsSchema.scala index e7322840be1e..1d42c9b41015 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsSchema.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsSchema.scala @@ -1,18 +1,19 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.error.* import com.daml.error.ErrorCategory.GenericErrorCategory import com.daml.error.utils.DecodedCantonError import com.daml.ledger.api.v2.admin.object_meta.ObjectMeta +import com.daml.ledger.api.v2.trace_context.TraceContext import com.google.protobuf import com.google.protobuf.field_mask.FieldMask import com.google.protobuf.struct.Struct import com.google.protobuf.util.JsonFormat import io.circe.generic.semiauto.deriveCodec -import io.circe.{Codec, Decoder, Encoder} +import io.circe.{Codec, Decoder, Encoder, Json} import io.grpc.Status import org.slf4j.event.Level import sttp.tapir.CodecFormat.TextPlain @@ -20,11 +21,95 @@ import sttp.tapir.{DecodeResult, Schema, SchemaType} import java.time.Instant import java.util.Base64 +import scala.annotation.nowarn import scala.concurrent.duration.Duration import scala.util.Try +/** JSON wrappers that do not belong to a particular service */ object JsSchema { + final case class JsTransaction( + update_id: String, + command_id: String, + workflow_id: String, + effective_at: com.google.protobuf.timestamp.Timestamp, + events: Seq[JsEvent.Event], + offset: String, + domain_id: String, + trace_context: Option[TraceContext], + record_time: com.google.protobuf.timestamp.Timestamp, + ) + + final case class JsTransactionTree( + update_id: String, + command_id: String, + workflow_id: String, + effective_at: Option[protobuf.timestamp.Timestamp], + offset: String, + events_by_id: Map[String, JsTreeEvent.TreeEvent], + root_event_ids: Seq[String], + domain_id: String, + trace_context: Option[TraceContext], + record_time: protobuf.timestamp.Timestamp, + ) + + final case class JsStatus( + code: Int, + message: String, + details: String, + ) + + final case class JsInterfaceView( + interface_id: String, + view_status: JsStatus, + view_value: Option[Json], + ) + + object JsEvent { + sealed trait Event + + final case class CreatedEvent( + event_id: String, + contract_id: String, + template_id: String, + contract_key: Option[Json], + create_argument: Option[Json], + created_event_blob: protobuf.ByteString, + interface_views: Seq[JsInterfaceView], + witness_parties: Seq[String], + signatories: Seq[String], + observers: Seq[String], + created_at: protobuf.timestamp.Timestamp, + package_name: String, + ) extends Event + + final case class ArchivedEvent( + event_id: String, + contract_id: String, + template_id: String, + witness_parties: Seq[String], + package_name: String, + ) extends Event + } + + object JsTreeEvent { + sealed trait TreeEvent + + final case class ExercisedTreeEvent( + contract_id: String, + template_id: String, + choice_argument: Json, + exercise_result: Json, + ) extends TreeEvent + + final case class CreatedTreeEvent( + contract_id: String, + template_id: String, + create_argument: Json, + ) extends TreeEvent + + } + final case class JsCantonError( code: String, cause: String, @@ -36,6 +121,7 @@ object JsSchema { grpcCodeValue: Option[Int], ) + @nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsCantonError { implicit val rw: Codec[JsCantonError] = deriveCodec @@ -92,27 +178,11 @@ object JsSchema { resources = jsCantonError.resources.map { case (k, v) => (ErrorResource(k), v) }, ) } - - implicit def eitherDecoder[A, B](implicit - a: Decoder[A], - b: Decoder[B], - ): Decoder[Either[A, B]] = { - val left: Decoder[Either[A, B]] = a.map(Left.apply) - val right: Decoder[Either[A, B]] = b.map(scala.util.Right.apply) - // Always try to decode first the happy path - right or left - } - - implicit def eitherEncoder[A, B](implicit - a: Encoder[A], - b: Encoder[B], - ): Encoder[Either[A, B]] = - Encoder.instance(_.fold(a.apply, b.apply)) - + @nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object DirectScalaPbRwImplicits { implicit val om: Codec[ObjectMeta] = deriveCodec - + implicit val traceContext: Codec[TraceContext] = deriveCodec implicit val encodeDuration: Encoder[Duration] = Encoder.encodeString.contramap[Duration](_.toString) @@ -151,7 +221,7 @@ object JsSchema { implicit val timestampCodec: sttp.tapir.Codec[String, protobuf.timestamp.Timestamp, TextPlain] = sttp.tapir.Codec.instant.mapDecode { (time: Instant) => DecodeResult.Value(protobuf.timestamp.Timestamp(time.getEpochSecond, time.getNano)) - } { timestamp => Instant.ofEpochSecond(timestamp.seconds, timestamp.nanos.toLong) } + }(timestamp => Instant.ofEpochSecond(timestamp.seconds, timestamp.nanos.toLong)) implicit val decodeStruct: Decoder[protobuf.struct.Struct] = Decoder.decodeJson.map { json => @@ -169,6 +239,26 @@ object JsSchema { case Left(error) => throw new IllegalStateException(s"Failed to parse JSON: $error") } } - } + implicit val encodeIdentifier: Encoder[com.daml.ledger.api.v2.value.Identifier] = + Encoder.encodeString.contramap { identifier => + IdentifierConverter.toJson(identifier) + } + + implicit val decodeIdentifier: Decoder[com.daml.ledger.api.v2.value.Identifier] = + Decoder.decodeString.map(IdentifierConverter.fromJson) + + implicit val jsEvent: Codec[JsEvent.Event] = deriveCodec + + implicit val jsStatus: Codec[JsStatus] = deriveCodec + implicit val jsInterfaceView: Codec[JsInterfaceView] = deriveCodec + implicit val jsCreatedEvent: Codec[JsEvent.CreatedEvent] = deriveCodec + implicit val jsArchivedEvent: Codec[JsEvent.ArchivedEvent] = deriveCodec + implicit val jsTransactionTree: Codec[JsTransactionTree] = deriveCodec + implicit val jsSubmitAndWaitForTransactionTreeResponse + : Codec[JsSubmitAndWaitForTransactionTreeResponse] = deriveCodec + implicit val jsTreeEvent: Codec[JsTreeEvent.TreeEvent] = deriveCodec + implicit val jsExercisedTreeEvent: Codec[JsTreeEvent.ExercisedTreeEvent] = deriveCodec + implicit val jsCreatedTreeEvent: Codec[JsTreeEvent.CreatedTreeEvent] = deriveCodec + } } diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsStateService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsStateService.scala new file mode 100644 index 000000000000..80bcabfc2f63 --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsStateService.scala @@ -0,0 +1,234 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.grpc.adapter.ExecutionSequencerFactory +import com.daml.grpc.adapter.client.pekko.ClientAdapter +import com.daml.ledger.api.v2.state_service +import com.daml.ledger.api.v2.transaction_filter +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.{JsCantonError, JsEvent} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import com.digitalasset.canton.http.json.v2.JsContractEntry.{ + JsActiveContract, + JsContractEntry, + JsIncompleteAssigned, + JsIncompleteUnassigned, +} +import com.digitalasset.canton.ledger.client.LedgerClient +import com.digitalasset.canton.tracing.TraceContext +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec +import org.apache.pekko.NotUsed +import org.apache.pekko.stream.scaladsl.Flow +import sttp.tapir.query + +import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future} + +class JsStateService( + ledgerClient: LedgerClient, + protocolConverters: ProtocolConverters, + val loggerFactory: NamedLoggerFactory, +)(implicit + val executionContext: ExecutionContext, + esf: ExecutionSequencerFactory, +) extends Endpoints + with NamedLogging { + + import JsStateServiceCodecs.* + + private lazy val state = baseEndpoint.in("state") +// private lazy val wsState = wsEndpoint.in("state") + + private def stateServiceClient(token: Option[String] = None)(implicit + traceContext: TraceContext + ): state_service.StateServiceGrpc.StateServiceStub = + ledgerClient.serviceClient(state_service.StateServiceGrpc.stub, token) + + def endpoints() = List( + websocket( + state.get + .in("active-contracts") + .description("Get active contracts stream"), + getActiveContractsStream, + ), + json( + state.get + .in("connected-domains") + .in(query[String]("party")) + .description("Get connected domains"), + getConnectedDomains, + ), + json( + state.get + .in("ledger-end") + .description("Get ledger end"), + getLedgerEnd, + ), + json( + state.get + .in("latest-pruned-offsets") + .description("Get latest pruned offsets"), + getLatestPrunedOffsets, + ), + ) + + private def getConnectedDomains( + callerContext: CallerContext + ): TracedInput[String] => Future[ + Either[JsCantonError, state_service.GetConnectedDomainsResponse] + ] = req => + stateServiceClient(callerContext.token())(req.traceContext) + .getConnectedDomains(state_service.GetConnectedDomainsRequest(party = req.in)) + .resultToRight + + private def getLedgerEnd( + callerContext: CallerContext + ): TracedInput[Unit] => Future[ + Either[JsCantonError, state_service.GetLedgerEndResponse] + ] = req => + stateServiceClient(callerContext.token())(req.traceContext) + .getLedgerEnd(state_service.GetLedgerEndRequest()) + .resultToRight + + private def getLatestPrunedOffsets( + callerContext: CallerContext + ): TracedInput[Unit] => Future[ + Either[JsCantonError, state_service.GetLatestPrunedOffsetsResponse] + ] = req => + stateServiceClient(callerContext.token())(req.traceContext) + .getLatestPrunedOffsets(state_service.GetLatestPrunedOffsetsRequest()) + .resultToRight + + private def getActiveContractsStream( + caller: CallerContext + ): Unit => Flow[state_service.GetActiveContractsRequest, JsGetActiveContractsResponse, NotUsed] = + _ => { + Flow[state_service.GetActiveContractsRequest] + .flatMapConcat { req => + ClientAdapter + .serverStreaming( + req, + stateServiceClient(caller.token())(TraceContext.empty).getActiveContracts, + ) + .mapAsync(1)(r => protocolConverters.GetActiveContractsResponse.toJson(r)(caller.token())) + } + } +} + +object JsContractEntry { + sealed trait JsContractEntry + + case object JsEmpty extends JsContractEntry + case class JsIncompleteAssigned(assigned_event: JsAssignedEvent) extends JsContractEntry + case class JsIncompleteUnassigned( + created_event: JsEvent.CreatedEvent, + unassigned_event: JsUnassignedEvent, + ) extends JsContractEntry + + case class JsActiveContract( + created_event: JsEvent.CreatedEvent, + domain_id: String, + reassignment_counter: Long, + ) extends JsContractEntry +} + +final case class JsAssignedEvent( + source: String, + target: String, + unassign_id: String, + submitter: String, + reassignment_counter: Long, + created_event: JsEvent.CreatedEvent, +) + +final case class JsUnassignedEvent( + unassign_id: String, + contract_id: String, + template_id: String, + source: String, + target: String, + submitter: String, + reassignment_counter: Long, + assignment_exclusivity: Option[com.google.protobuf.timestamp.Timestamp], + witness_parties: Seq[String], + package_name: String, +) + +final case class JsGetActiveContractsResponse( + offset: String, + workflow_id: String, + contract_entry: JsContractEntry, +) + +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 +object JsStateServiceCodecs { + + implicit val filtersRW: Codec[transaction_filter.Filters] = deriveCodec + implicit val cumulativeFilterRW: Codec[transaction_filter.CumulativeFilter] = deriveCodec + implicit val identifierFilterRW: Codec[transaction_filter.CumulativeFilter.IdentifierFilter] = + deriveCodec + + implicit val wildcardFilterRW: Codec[transaction_filter.WildcardFilter] = + deriveCodec + implicit val templateFilterRW: Codec[transaction_filter.TemplateFilter] = + deriveCodec + + implicit val interfaceFilterRW: Codec[transaction_filter.InterfaceFilter] = + deriveCodec + implicit val iwildcardFilterRW + : Codec[transaction_filter.CumulativeFilter.IdentifierFilter.WildcardFilter] = + deriveCodec + + implicit val itemplateFilterRW + : Codec[transaction_filter.CumulativeFilter.IdentifierFilter.TemplateFilter] = + deriveCodec + implicit val iinterfaceFilterRW + : Codec[transaction_filter.CumulativeFilter.IdentifierFilter.InterfaceFilter] = + deriveCodec + implicit val transactionFilterRW: Codec[transaction_filter.TransactionFilter] = deriveCodec + implicit val getActiveContractsRequestRW: Codec[state_service.GetActiveContractsRequest] = + deriveCodec + + implicit val jsGetActiveContractsResponseRW: Codec[JsGetActiveContractsResponse] = deriveCodec + + implicit val jsContractEntryRW: Codec[JsContractEntry] = deriveCodec + implicit val jsIncompleteUnassignedRW: Codec[JsIncompleteUnassigned] = deriveCodec + implicit val jsIncompleteAssignedRW: Codec[JsIncompleteAssigned] = deriveCodec + implicit val jsActiveContractRW: Codec[JsActiveContract] = deriveCodec + implicit val jsUnassignedEventRW: Codec[JsUnassignedEvent] = deriveCodec + implicit val jsAssignedEventRW: Codec[JsAssignedEvent] = deriveCodec + + implicit val getConnectedDomainsRequestRW: Codec[state_service.GetConnectedDomainsRequest] = + deriveCodec + implicit val getConnectedDomainsResponseRW: Codec[state_service.GetConnectedDomainsResponse] = + deriveCodec + implicit val connectedDomainRW: Codec[state_service.GetConnectedDomainsResponse.ConnectedDomain] = + deriveCodec + implicit val participantPermissionRW: Codec[state_service.ParticipantPermission] = deriveCodec + + implicit val getLedgerEndRequestRW: Codec[state_service.GetLedgerEndRequest] = deriveCodec + + implicit val participantOffsetRW + : Codec[com.daml.ledger.api.v2.participant_offset.ParticipantOffset] = deriveCodec + implicit val participantOffsetValueRW + : Codec[com.daml.ledger.api.v2.participant_offset.ParticipantOffset.Value] = deriveCodec + implicit val participantOffsetValueBoundaryRW + : Codec[com.daml.ledger.api.v2.participant_offset.ParticipantOffset.Value.Boundary] = + deriveCodec + implicit val participantOffsetParticipantBoundaryRW: Codec[ + com.daml.ledger.api.v2.participant_offset.ParticipantOffset.ParticipantBoundary + ] = deriveCodec + implicit val participantOffsetValueAbsoluteRW + : Codec[com.daml.ledger.api.v2.participant_offset.ParticipantOffset.Value.Absolute] = + deriveCodec + implicit val getLedgerEndResponseRW: Codec[state_service.GetLedgerEndResponse] = deriveCodec + implicit val getLatestPrunedOffsetsResponseRW + : Codec[state_service.GetLatestPrunedOffsetsResponse] = + deriveCodec + +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala new file mode 100644 index 000000000000..b4c8774118a2 --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUpdateService.scala @@ -0,0 +1,275 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.grpc.adapter.ExecutionSequencerFactory +import com.daml.grpc.adapter.client.pekko.ClientAdapter +import com.daml.ledger.api.v2.update_service +import com.daml.ledger.api.v2.offset_checkpoint +import com.daml.ledger.api.v2.reassignment +import com.digitalasset.canton.http.json.v2.JsSchema.JsEvent.CreatedEvent +import com.digitalasset.canton.http.json.v2.JsSchema.{JsTransaction, JsTransactionTree} +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import com.digitalasset.canton.ledger.client.LedgerClient +import com.digitalasset.canton.tracing.TraceContext +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec +import org.apache.pekko.NotUsed +import org.apache.pekko.stream.scaladsl.Flow +import sttp.tapir.{path, query} + +import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future} + +class JsUpdateService( + ledgerClient: LedgerClient, + protocolConverters: ProtocolConverters, + val loggerFactory: NamedLoggerFactory, +)(implicit + val executionContext: ExecutionContext, + esf: ExecutionSequencerFactory, +) extends Endpoints + with NamedLogging { + + import JsUpdateServiceCodecs.* + + private lazy val updates = baseEndpoint.in("updates") + + private def updateServiceClient(token: Option[String] = None)(implicit + traceContext: TraceContext + ): update_service.UpdateServiceGrpc.UpdateServiceStub = + ledgerClient.serviceClient(update_service.UpdateServiceGrpc.stub, token) + + def endpoints() = List( + websocket( + updates.get + .in("flats") + .description("Get flat transactions update stream"), + getFlats, + ), + websocket( + updates.get + .in("trees") + .description("Get update transactions tree stream"), + getTrees, + ), + json( + updates.get + .in("transaction-tree-by-event-id") + .in(path[String]("event-id")) + .in(query[List[String]]("parties")) + .description("Get transaction tree by event id"), + getTreeByEventId, + ), + json( + updates.get + .in("transaction-by-event-id") + .in(path[String]("event-id")) + .in(query[List[String]]("parties")) + .description("Get transaction by event id"), + getTransactionByEventId, + ), + json( + updates.get + .in("transaction-by-id") + .in(path[String]("update-id")) + .in(query[List[String]]("parties")) + .description("Get transaction by id"), + getTransactionById, + ), + json( + updates.get + .in("transaction-tree-by-id") + .in(path[String]("update-id")) + .in(query[List[String]]("parties")) + .description("Get transaction tree by id"), + getTransactionTreeById, + ), + ) + + private def getTreeByEventId( + caller: CallerContext + ): TracedInput[(String, List[String])] => Future[ + Either[JsCantonError, JsGetTransactionTreeResponse] + ] = + req => + updateServiceClient(caller.token())(req.traceContext) + .getTransactionTreeByEventId( + update_service.GetTransactionByEventIdRequest( + eventId = req.in._1, + requestingParties = req.in._2, + ) + ) + .flatMap(protocolConverters.GetTransactionTreeResponse.toJson(_)(caller.token())) + .resultToRight + + private def getTransactionByEventId( + caller: CallerContext + ): TracedInput[(String, List[String])] => Future[ + Either[JsCantonError, JsGetTransactionResponse] + ] = + req => + updateServiceClient(caller.token())(req.traceContext) + .getTransactionByEventId( + update_service.GetTransactionByEventIdRequest( + eventId = req.in._1, + requestingParties = req.in._2, + ) + ) + .flatMap(protocolConverters.GetTransactionResponse.toJson(_)(caller.token())) + .resultToRight + + private def getTransactionById( + caller: CallerContext + ): TracedInput[(String, List[String])] => Future[ + Either[JsCantonError, JsGetTransactionResponse] + ] = { req => + updateServiceClient(caller.token())(req.traceContext) + .getTransactionById( + update_service.GetTransactionByIdRequest( + updateId = req.in._1, + requestingParties = req.in._2, + ) + ) + .flatMap(protocolConverters.GetTransactionResponse.toJson(_)(caller.token())) + .resultToRight + } + + private def getTransactionTreeById( + caller: CallerContext + ): TracedInput[(String, List[String])] => Future[ + Either[JsCantonError, JsGetTransactionTreeResponse] + ] = + req => + updateServiceClient(caller.token())(req.traceContext) + .getTransactionTreeById( + update_service.GetTransactionByIdRequest( + updateId = req.in._1, + requestingParties = req.in._2, + ) + ) + .flatMap(protocolConverters.GetTransactionTreeResponse.toJson(_)(caller.token())) + .resultToRight + + private def getFlats( + caller: CallerContext + ): Unit => Flow[update_service.GetUpdatesRequest, JsGetUpdatesResponse, NotUsed] = + _ => { + Flow[update_service.GetUpdatesRequest] + .flatMapConcat { req => + ClientAdapter + .serverStreaming( + req, + updateServiceClient(caller.token())(TraceContext.empty).getUpdates, + ) + .mapAsync(1)(r => protocolConverters.GetUpdatesResponse.toJson(r)(caller.token())) + } + } + + private def getTrees( + caller: CallerContext + ): Unit => Flow[update_service.GetUpdatesRequest, JsGetUpdateTreesResponse, NotUsed] = + _ => { + Flow[update_service.GetUpdatesRequest] + .flatMapConcat { req => + ClientAdapter + .serverStreaming( + req, + updateServiceClient(caller.token())(TraceContext.empty).getUpdateTrees, + ) + .mapAsync(1)(r => protocolConverters.GetUpdateTreesResponse.toJson(r)(caller.token())) + } + } + +} + +object JsReassignmentEvent { + sealed trait JsReassignmentEvent + + case class JsAssignmentEvent( + source: String, + target: String, + unassign_id: String, + submitter: String, + reassignment_counter: Long, + created_event: CreatedEvent, + ) extends JsReassignmentEvent + + case class JsUnassignedEvent(value: reassignment.UnassignedEvent) extends JsReassignmentEvent + +} + +case class JsReassignment( + update_id: String, + command_id: String, + workflow_id: String, + offset: String, + event: JsReassignmentEvent.JsReassignmentEvent, + trace_context: Option[com.daml.ledger.api.v2.trace_context.TraceContext], + record_time: com.google.protobuf.timestamp.Timestamp, +) + +object JsUpdate { + sealed trait Update + case class OffsetCheckpoint(value: offset_checkpoint.OffsetCheckpoint) extends Update + case class Reassignment(value: JsReassignment) extends Update + case class Transaction(value: JsTransaction) extends Update +} + +case class JsGetTransactionTreeResponse(transaction: JsTransactionTree) + +case class JsGetTransactionResponse(transaction: JsTransaction) + +case class JsGetUpdatesResponse( + update: JsUpdate.Update +) + +object JsUpdateTree { + sealed trait Update + case class OffsetCheckpoint(value: offset_checkpoint.OffsetCheckpoint) extends Update + case class Reassignment(value: JsReassignment) extends Update + case class TransactionTree(value: JsTransactionTree) extends Update +} + +case class JsGetUpdateTreesResponse( + update: JsUpdateTree.Update +) + +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 +object JsUpdateServiceCodecs { + import JsCommandServiceCodecs.* + import JsStateServiceCodecs.* + + implicit val getUpdatesRequest: Codec[update_service.GetUpdatesRequest] = deriveCodec + + implicit val jsGetUpdatesResponse: Codec[JsGetUpdatesResponse] = deriveCodec + + implicit val offsetCheckpoint: Codec[offset_checkpoint.OffsetCheckpoint] = deriveCodec + implicit val offsetCheckpointDomainTime: Codec[offset_checkpoint.DomainTime] = deriveCodec + implicit val jsUpdate: Codec[JsUpdate.Update] = deriveCodec + implicit val jsUpdateOffsetCheckpoint: Codec[JsUpdate.OffsetCheckpoint] = deriveCodec + implicit val jsUpdateReassignment: Codec[JsUpdate.Reassignment] = deriveCodec + implicit val jsUpdateTransaction: Codec[JsUpdate.Transaction] = deriveCodec + implicit val jsReassignment: Codec[JsReassignment] = deriveCodec + implicit val jsReassignmentEvent: Codec[JsReassignmentEvent.JsReassignmentEvent] = deriveCodec + + implicit val jsReassignmentEventJsUnassignedEvent: Codec[JsReassignmentEvent.JsUnassignedEvent] = + deriveCodec + + implicit val unassignedEvent: Codec[reassignment.UnassignedEvent] = + deriveCodec + + implicit val jsGetUpdateTreesResponse: Codec[JsGetUpdateTreesResponse] = deriveCodec + + implicit val jsGetTransactionTreeResponse: Codec[JsGetTransactionTreeResponse] = deriveCodec + implicit val jsGetTransactionResponse: Codec[JsGetTransactionResponse] = deriveCodec + + implicit val jsUpdateTree: Codec[JsUpdateTree.Update] = deriveCodec + implicit val jsUpdateTreeReassignment: Codec[JsUpdateTree.Reassignment] = deriveCodec + implicit val jsUpdateTreeTransaction: Codec[JsUpdateTree.TransactionTree] = deriveCodec +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsUserManagementService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUserManagementService.scala similarity index 94% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsUserManagementService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUserManagementService.scala index 35967f2e1cb0..92d705d01662 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsUserManagementService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsUserManagementService.scala @@ -1,13 +1,13 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.ledger.api.v2.admin.user_management_service import com.digitalasset.daml.lf.data.Ref.UserId import com.digitalasset.canton.ledger.client.services.admin.UserManagementClient -import com.digitalasset.canton.http.json2.JsSchema.DirectScalaPbRwImplicits.* -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.DirectScalaPbRwImplicits.* +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.InvalidArgument import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext @@ -17,6 +17,7 @@ import sttp.model.QueryParams import sttp.tapir.generic.auto.* import sttp.tapir.* +import scala.annotation.nowarn import scala.concurrent.Future class JsUserManagementService( @@ -95,7 +96,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .createUser(body) - .toRight + .resultToRight private def listUsers( callerContext: CallerContext @@ -105,7 +106,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .listUsers(user_management_service.ListUsersRequest()) - .toRight + .resultToRight // TODO (i19538) paging private def getUser( @@ -117,7 +118,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .getUser(user_management_service.GetUserRequest(userId = userId)) - .toRight + .resultToRight case Left(error) => malformedUserId(error)(req.traceContext) } @@ -131,7 +132,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .updateUser(body) - .toRight + .resultToRight } else { unmatchedUserId(req.traceContext, req.in, body.user.map(_.id)) } @@ -143,7 +144,7 @@ class JsUserManagementService( case Right(userId) => userManagementClient .deleteUser(userId, callerContext.jwt.map(_.token))(req.traceContext) - .toRight + .resultToRight case Left(errorMsg) => malformedUserId(errorMsg)(req.traceContext) } @@ -158,7 +159,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .listUserRights(new user_management_service.ListUserRightsRequest(userId = userId)) - .toRight + .resultToRight case Left(error) => malformedUserId(error)(req.traceContext) } @@ -172,7 +173,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .grantUserRights(body) - .toRight + .resultToRight } else { unmatchedUserId(req.traceContext, req.in, Some(body.userId)) } @@ -187,7 +188,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .revokeUserRights(body) - .toRight + .resultToRight } else { unmatchedUserId(req.traceContext, req.in, Some(body.userId)) } @@ -201,7 +202,7 @@ class JsUserManagementService( userManagementClient .serviceStub(callerContext.token())(req.traceContext) .updateUserIdentityProviderId(body) - .toRight + .resultToRight } else { unmatchedUserId(req.traceContext, req.in, Some(body.userId)) } @@ -223,6 +224,7 @@ class JsUserManagementService( ) } +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsUserManagementCodecs { implicit val user: Codec[user_management_service.User] = deriveCodec diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsVersionService.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsVersionService.scala similarity index 87% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsVersionService.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsVersionService.scala index bc51d74b675f..8de7170e6833 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/JsVersionService.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/JsVersionService.scala @@ -1,16 +1,17 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 import com.daml.ledger.api.v2.experimental_features import com.daml.ledger.api.v2.version_service -import com.digitalasset.canton.http.json2.JsSchema.JsCantonError +import com.digitalasset.canton.http.json.v2.JsSchema.JsCantonError import com.digitalasset.canton.ledger.client.services.version.VersionClient import io.circe.Codec import io.circe.generic.semiauto.deriveCodec import sttp.tapir.generic.auto.* +import scala.annotation.nowarn import scala.concurrent.{ExecutionContext, Future} @@ -33,9 +34,10 @@ class JsVersionService(versionClient: VersionClient)(implicit tracedInput => versionClient .serviceStub(caller.token())(tracedInput.traceContext) - .getLedgerApiVersion(version_service.GetLedgerApiVersionRequest()).toRight + .getLedgerApiVersion(version_service.GetLedgerApiVersionRequest()).resultToRight } +@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072 object JsVersionServiceCodecs { implicit val est: Codec[experimental_features.ExperimentalStaticTime] = deriveCodec implicit val ecis: Codec[experimental_features.ExperimentalCommandInspectionService] = deriveCodec diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/ProtocolConverters.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/ProtocolConverters.scala new file mode 100644 index 000000000000..3ea4632b4bdc --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/ProtocolConverters.scala @@ -0,0 +1,921 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.ledger.api.v2 as lapi +import com.digitalasset.daml.lf.data.Ref +import com.digitalasset.canton.fetchcontracts.util.IdentifierConverters +import com.digitalasset.canton.http.json.v2.JsContractEntry.JsContractEntry +import com.digitalasset.canton.http.json.v2.JsReassignmentEvent.JsReassignmentEvent +import com.digitalasset.canton.http.json.v2.JsSchema.{JsEvent, JsInterfaceView, JsStatus, JsTransaction, JsTransactionTree, JsTreeEvent} +import com.google.rpc.status.Status +import io.circe.Json +import ujson.StringRenderer +import ujson.circe.CirceJson + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.implicitConversions + +trait ProtocolConverter[LAPI, JS] { + def jsFail(err: String): Nothing = throw new IllegalArgumentException( + err + ) // TODO (i19398) improve error handling +} + +class ProtocolConverters(schemaProcessors: SchemaProcessors)(implicit + val executionContext: ExecutionContext +) { + + implicit def fromCirce(js: io.circe.Json): ujson.Value = + ujson.read(CirceJson.transform(js, StringRenderer()).toString) + + implicit def toCirce(js: ujson.Value): io.circe.Json = CirceJson(js) + + object JsReassignmentCommandConverter + extends ProtocolConverter[ + lapi.reassignment_command.ReassignmentCommand.Command, + JsReassignmentCommand.JsCommand, + ] { + def fromJson( + command: JsReassignmentCommand.JsCommand + ): lapi.reassignment_command.ReassignmentCommand.Command = + command match { + case cmd: JsReassignmentCommand.JsUnassignCommand => + lapi.reassignment_command.ReassignmentCommand.Command.UnassignCommand( + lapi.reassignment_command.UnassignCommand( + contractId = cmd.contract_id, + source = cmd.source, + target = cmd.target, + ) + ) + case cmd: JsReassignmentCommand.JsAssignCommand => + lapi.reassignment_command.ReassignmentCommand.Command.AssignCommand( + lapi.reassignment_command.AssignCommand( + unassignId = cmd.unassign_id, + source = cmd.source, + target = cmd.target, + ) + ) + } + + } + + object JsSubmitReassignmentRequest + extends ProtocolConverter[ + lapi.command_submission_service.SubmitReassignmentRequest, + JsSubmitReassignmentRequest, + ] { + def fromJson( + jsSubmission: JsSubmitReassignmentRequest + ): lapi.command_submission_service.SubmitReassignmentRequest = + lapi.command_submission_service.SubmitReassignmentRequest(reassignmentCommand = + jsSubmission.reassignment_command.map { reassignmentCommands => + com.daml.ledger.api.v2.reassignment_command.ReassignmentCommand( + workflowId = reassignmentCommands.workflow_id, + applicationId = reassignmentCommands.application_id, + commandId = reassignmentCommands.command_id, + submitter = reassignmentCommands.submitter, + command = JsReassignmentCommandConverter.fromJson(reassignmentCommands.command), + submissionId = reassignmentCommands.submission_id, + ) + } + ) + } + + object Commands extends ProtocolConverter[lapi.commands.Commands, JsCommands] { + + private def optionToJson(v: Option[ujson.Value]): Json = { + val opt: ujson.Value = v.getOrElse(ujson.Null) + opt + } + + def fromJson(jsCommands: JsCommands)(token: Option[String]): Future[lapi.commands.Commands] = { + import jsCommands.* + + val convertedCommands: Seq[Future[lapi.commands.Command.Command]] = commands.map { + case JsCommand.CreateCommand(template_id, create_arguments) => + for { + protoCreateArgsRecord <- + schemaProcessors + .contractArgFromJsonToProto( + templateId = + IdentifierConverters.lfIdentifier(IdentifierConverter.fromJson(template_id)), + jsonArgsValue = create_arguments, + )(token) + + } yield lapi.commands.Command.Command.Create( + lapi.commands.CreateCommand( + templateId = Some(IdentifierConverter.fromJson(template_id)), + createArguments = Some(protoCreateArgsRecord.getRecord), + ) + ) + case JsCommand.ExerciseCommand(template_id, contract_id, choice, choice_argument) => + val lfChoiceName = Ref.ChoiceName.assertFromString(choice) + for { + choiceArgs <- + schemaProcessors.choiceArgsFromJsonToProto( + templateId = + IdentifierConverters.lfIdentifier(IdentifierConverter.fromJson(template_id)), + choiceName = lfChoiceName, + jsonArgsValue = choice_argument, + )(token) + } yield lapi.commands.Command.Command.Exercise( + lapi.commands.ExerciseCommand( + templateId = Some(IdentifierConverter.fromJson(template_id)), + contractId = contract_id, + choiceArgument = Some(choiceArgs), + choice = choice, + ) + ) + + case cmd: JsCommand.ExerciseByKeyCommand => + for { + choiceArgs <- + schemaProcessors.choiceArgsFromJsonToProto( + templateId = + IdentifierConverters.lfIdentifier(IdentifierConverter.fromJson(cmd.template_id)), + choiceName = Ref.ChoiceName.assertFromString(cmd.choice), + jsonArgsValue = cmd.choice_argument, + )(token) + contractKey <- + schemaProcessors.contractArgFromJsonToProto( + templateId = + IdentifierConverters.lfIdentifier(IdentifierConverter.fromJson(cmd.template_id)), + jsonArgsValue = cmd.contract_key, + )(token) + } yield lapi.commands.Command.Command.ExerciseByKey( + lapi.commands.ExerciseByKeyCommand( + templateId = Some(IdentifierConverter.fromJson(cmd.template_id)), + contractKey = Some(contractKey), + choice = cmd.choice, + choiceArgument = Some(choiceArgs), + ) + ) + case cmd: JsCommand.CreateAndExerciseCommand => + for { + createArgs <- + schemaProcessors + .contractArgFromJsonToProto( + templateId = IdentifierConverters.lfIdentifier( + IdentifierConverter.fromJson(cmd.template_id) + ), + jsonArgsValue = cmd.create_arguments, + )(token) + choiceArgs <- + schemaProcessors.choiceArgsFromJsonToProto( + templateId = + IdentifierConverters.lfIdentifier(IdentifierConverter.fromJson(cmd.template_id)), + choiceName = Ref.ChoiceName.assertFromString(cmd.choice), + jsonArgsValue = cmd.choice_argument, + )(token) + } yield lapi.commands.Command.Command.CreateAndExercise( + lapi.commands.CreateAndExerciseCommand( + templateId = Some(IdentifierConverter.fromJson(cmd.template_id)), + createArguments = Some(createArgs.getRecord), + choice = cmd.choice, + choiceArgument = Some(choiceArgs), + ) + ) + } + Future + .sequence(convertedCommands) + .map(cc => + lapi.commands.Commands( + workflowId = workflow_id, + applicationId = application_id, + commandId = command_id, + commands = cc.map(lapi.commands.Command(_)), + deduplicationPeriod = deduplication_period, + minLedgerTimeAbs = min_ledger_time_abs, + minLedgerTimeRel = min_ledger_time_rel, + actAs = act_as, + readAs = read_as, + submissionId = submission_id, + disclosedContracts = disclosed_contracts.map(js => + lapi.commands.DisclosedContract( + templateId = Some(IdentifierConverter.fromJson(js.template_id)), + contractId = js.contract_id, + createdEventBlob = js.created_event_blob, + ) + ), + domainId = domain_id, + packageIdSelectionPreference = package_id_selection_preference, + ) + ) + } + + def toJson(lapiCommands: lapi.commands.Commands)(token: Option[String]): Future[JsCommands] = { + val jsCommands: Seq[Future[JsCommand.Command]] = lapiCommands.commands + .map(_.command) + .map { + case lapi.commands.Command.Command.Empty => jsFail("Invalid value") + case lapi.commands.Command.Command.Create(createCommand) => + for { + contractArgs <- schemaProcessors.contractArgFromProtoToJson( + IdentifierConverters.lfIdentifier(createCommand.getTemplateId), + createCommand.getCreateArguments, + )(token) + } yield JsCommand.CreateCommand( + IdentifierConverter.toJson(createCommand.getTemplateId), + contractArgs, + ) + + case lapi.commands.Command.Command.Exercise(exerciseCommand) => + for { + choiceArgs <- schemaProcessors.choiceArgsFromProtoToJson( + templateId = IdentifierConverters.lfIdentifier(exerciseCommand.getTemplateId), + choiceName = Ref.ChoiceName.assertFromString(exerciseCommand.choice), + protoArgs = exerciseCommand.getChoiceArgument, + )(token) + } yield JsCommand.ExerciseCommand( + IdentifierConverter.toJson(exerciseCommand.getTemplateId), + exerciseCommand.contractId, + exerciseCommand.choice, + choiceArgs, + ) + + case lapi.commands.Command.Command.CreateAndExercise(cmd) => + for { + createArgs <- schemaProcessors.contractArgFromProtoToJson( + templateId = IdentifierConverters.lfIdentifier(cmd.getTemplateId), + protoArgs = cmd.getCreateArguments, + )(token) + choiceArgs <- schemaProcessors.choiceArgsFromProtoToJson( + templateId = IdentifierConverters.lfIdentifier(cmd.getTemplateId), + choiceName = Ref.ChoiceName.assertFromString(cmd.choice), + protoArgs = cmd.getChoiceArgument, + )(token) + } yield JsCommand.CreateAndExerciseCommand( + template_id = IdentifierConverter.toJson(cmd.getTemplateId), + create_arguments = createArgs, + choice = cmd.choice, + choice_argument = choiceArgs, + ) + case lapi.commands.Command.Command.ExerciseByKey(cmd) => + for { + contractKey <- schemaProcessors.keyArgFromProtoToJson( + templateId = IdentifierConverters.lfIdentifier(cmd.getTemplateId), + protoArgs = cmd.getContractKey, + )(token) + choiceArgs <- schemaProcessors.choiceArgsFromProtoToJson( + templateId = IdentifierConverters.lfIdentifier(cmd.getTemplateId), + choiceName = Ref.ChoiceName.assertFromString(cmd.choice), + protoArgs = cmd.getChoiceArgument, + )(token) + } yield JsCommand.ExerciseByKeyCommand( + template_id = IdentifierConverter.toJson(cmd.getTemplateId), + contract_key = contractKey, + choice = cmd.choice, + choice_argument = choiceArgs, + ) + } + Future + .sequence(jsCommands) + .map(cmds => + JsCommands( + commands = cmds, + workflow_id = lapiCommands.workflowId, + application_id = lapiCommands.applicationId, + command_id = lapiCommands.commandId, + deduplication_period = lapiCommands.deduplicationPeriod, + disclosed_contracts = lapiCommands.disclosedContracts.map { disclosedContract => + JsDisclosedContract( + template_id = IdentifierConverter.toJson(disclosedContract.getTemplateId), + contract_id = disclosedContract.contractId, + created_event_blob = disclosedContract.createdEventBlob, + ) + }, + act_as = lapiCommands.actAs, + read_as = lapiCommands.readAs, + submission_id = lapiCommands.submissionId, + domain_id = lapiCommands.domainId, + min_ledger_time_abs = lapiCommands.minLedgerTimeAbs, + min_ledger_time_rel = lapiCommands.minLedgerTimeRel, + package_id_selection_preference = lapiCommands.packageIdSelectionPreference, + ) + ) + } + } + + object InterfaceView + extends ProtocolConverter[com.daml.ledger.api.v2.event.InterfaceView, JsInterfaceView] { + + def toJson( + iview: com.daml.ledger.api.v2.event.InterfaceView + )(implicit token: Option[String]): Future[JsInterfaceView] = + for { + record <- schemaProcessors.contractArgFromProtoToJson( + IdentifierConverters.lfIdentifier( + iview.getInterfaceId + ), + iview.getViewValue, + )(token) + } yield JsInterfaceView( + interface_id = IdentifierConverter.toJson(iview.getInterfaceId), + view_status = JsStatusConverter.toJson(iview.getViewStatus), + view_value = iview.viewValue.map(_ => record), + ) + } + + object Event extends ProtocolConverter[com.daml.ledger.api.v2.event.Event.Event, JsEvent.Event] { + def toJson(event: com.daml.ledger.api.v2.event.Event.Event)(implicit + token: Option[String] + ): Future[JsEvent.Event] = + event match { + case lapi.event.Event.Event.Empty => jsFail("Invalid value") + case lapi.event.Event.Event.Created(value) => + CreatedEvent.toJson(value) + case lapi.event.Event.Event.Archived(value) => + Future(ArchivedEvent.toJson(value)) + } + + } + + object Transaction extends ProtocolConverter[lapi.transaction.Transaction, JsTransaction] { + + def toJson(v: lapi.transaction.Transaction)(implicit + token: Option[String] + ): Future[JsTransaction] = + Future + .sequence(v.events.map(e => Event.toJson(e.event))) + .map(ev => + JsTransaction( + update_id = v.updateId, + command_id = v.commandId, + workflow_id = v.workflowId, + effective_at = v.getEffectiveAt, + events = ev, + offset = v.offset, + domain_id = v.domainId, + trace_context = v.traceContext, + record_time = v.getRecordTime, + ) + ) + } + + object TransactionTree + extends ProtocolConverter[lapi.transaction.TransactionTree, JsTransactionTree] { + def toJson( + lapiTransactionTree: lapi.transaction.TransactionTree + )(implicit token: Option[String]): Future[JsTransactionTree] = { + val jsEventsById = lapiTransactionTree.eventsById.view + .mapValues(_.kind) + .mapValues { + case lapi.transaction.TreeEvent.Kind.Empty => jsFail("Empty event") + case lapi.transaction.TreeEvent.Kind.Created(created) => + val apiTemplateId = created.getTemplateId + val lfIdentifier = Ref.Identifier.assertFromString( + s"${apiTemplateId.packageId}:${apiTemplateId.moduleName}:${apiTemplateId.entityName}" + ) + for { + contractArgs <- created.createArguments + .map(args => + schemaProcessors.contractArgFromProtoToJson( + lfIdentifier, + args, + )(token) + ) + .getOrElse(Future(ujson.Null)) + } yield JsTreeEvent.CreatedTreeEvent( + created.contractId, + IdentifierConverter.toJson(apiTemplateId), + contractArgs, + ) + case lapi.transaction.TreeEvent.Kind.Exercised(exercised) => + val apiTemplateId = exercised.getTemplateId + val lfIdentifier = Ref.Identifier.assertFromString( + s"${apiTemplateId.packageId}:${apiTemplateId.moduleName}:${apiTemplateId.entityName}" + ) + val choiceName = Ref.ChoiceName.assertFromString(exercised.choice) + for { + choiceArgs <- schemaProcessors.choiceArgsFromProtoToJson( + templateId = lfIdentifier, + choiceName = choiceName, + protoArgs = exercised.getChoiceArgument, + )(token) + exerciseResult <- schemaProcessors.exerciseResultFromProtoToJson( + lfIdentifier, + choiceName, + exercised.getExerciseResult, + )(token) + } yield JsTreeEvent.ExercisedTreeEvent( + exercised.contractId, + IdentifierConverter.toJson(apiTemplateId), + choiceArgs, + exerciseResult, + ) + } + Future + .traverse(jsEventsById.toSeq) { case (key, fv) => + fv.map(key -> _) + } + .map(_.toMap) + .map(jsEvents => + JsTransactionTree( + update_id = lapiTransactionTree.updateId, + command_id = lapiTransactionTree.commandId, + workflow_id = lapiTransactionTree.workflowId, + effective_at = lapiTransactionTree.effectiveAt, + offset = lapiTransactionTree.offset, + events_by_id = jsEvents, + root_event_ids = lapiTransactionTree.rootEventIds, + domain_id = lapiTransactionTree.domainId, + trace_context = lapiTransactionTree.traceContext, + record_time = lapiTransactionTree.getRecordTime, + ) + ) + } + + def fromJson( + jsTransactionTree: JsTransactionTree + )(implicit token: Option[String]): Future[lapi.transaction.TransactionTree] = { + val lapiEventsById = jsTransactionTree.events_by_id.view + .mapValues { + case JsTreeEvent.CreatedTreeEvent(contractId, templateId, createArguments) => + val apiTemplateId = IdentifierConverter.fromJson(templateId) + for { + protoCreateArgsRecord <- schemaProcessors.contractArgFromJsonToProto( + templateId = Ref.Identifier.assertFromString(templateId), + jsonArgsValue = + ujson.read(CirceJson.transform(createArguments, StringRenderer()).toString), + )(token) + } yield lapi.transaction.TreeEvent( + kind = lapi.transaction.TreeEvent.Kind.Created( + lapi.event.CreatedEvent( + contractId = contractId, + templateId = Some(apiTemplateId), + createArguments = Some( + protoCreateArgsRecord.getRecord + ), + ) + ) + ) + case JsTreeEvent.ExercisedTreeEvent( + contractId, + templateId, + choiceArgument, + exerciseResult, + ) => + val apiTemplateId = IdentifierConverter.fromJson(templateId) + val lfIdentifier = Ref.Identifier.assertFromString( + s"${apiTemplateId.packageId}:${apiTemplateId.moduleName}:${apiTemplateId.entityName}" + ) + val choiceName = Ref.ChoiceName.assertFromString("notThere") + for { + choiceArgs <- schemaProcessors.choiceArgsFromJsonToProto( + templateId = lfIdentifier, + choiceName = choiceName, + jsonArgsValue = + ujson.read(CirceJson.transform(choiceArgument, StringRenderer()).toString), + )(token) + lapiExerciseResult <- schemaProcessors.exerciseResultFromJsonToProto( + lfIdentifier, + choiceName, + ujson.read(CirceJson.transform(exerciseResult, StringRenderer()).toString), + )(token) + } yield lapi.transaction.TreeEvent( + kind = lapi.transaction.TreeEvent.Kind.Exercised( + lapi.event.ExercisedEvent( + contractId = contractId, + templateId = Some(apiTemplateId), + choice = choiceName, + choiceArgument = Some(choiceArgs), + exerciseResult = lapiExerciseResult, + ) + ) + ) + } + Future + .traverse(lapiEventsById.toSeq) { case (key, fv) => + fv.map(key -> _) + } + .map(events => + lapi.transaction.TransactionTree( + eventsById = events.toMap, + rootEventIds = jsTransactionTree.root_event_ids, + offset = jsTransactionTree.offset, + updateId = jsTransactionTree.update_id, + commandId = jsTransactionTree.command_id, + workflowId = jsTransactionTree.workflow_id, + effectiveAt = jsTransactionTree.effective_at, + domainId = jsTransactionTree.domain_id, + traceContext = jsTransactionTree.trace_context, + recordTime = Some(jsTransactionTree.record_time), + ) + ) + } + } + + object SubmitAndWaitTransactionTreeResponse + extends ProtocolConverter[ + lapi.command_service.SubmitAndWaitForTransactionTreeResponse, + JsSubmitAndWaitForTransactionTreeResponse, + ] { + + def toJson( + response: lapi.command_service.SubmitAndWaitForTransactionTreeResponse + )(implicit token: Option[String]): Future[JsSubmitAndWaitForTransactionTreeResponse] = + TransactionTree + .toJson(response.getTransaction) + .map(tree => + JsSubmitAndWaitForTransactionTreeResponse( + transaction_tree = tree, + completion_offset = response.completionOffset, + ) + ) + + def fromJson( + response: JsSubmitAndWaitForTransactionTreeResponse + )(implicit + token: Option[String] + ): Future[lapi.command_service.SubmitAndWaitForTransactionTreeResponse] = + TransactionTree + .fromJson(response.transaction_tree) + .map(tree => + lapi.command_service.SubmitAndWaitForTransactionTreeResponse( + transaction = Some(tree), + completionOffset = response.completion_offset, + ) + ) + } + + object SubmitAndWaitTransactionResponse + extends ProtocolConverter[ + lapi.command_service.SubmitAndWaitForTransactionResponse, + JsSubmitAndWaitForTransactionResponse, + ] { + + def toJson( + response: lapi.command_service.SubmitAndWaitForTransactionResponse + )(implicit + token: Option[String] + ): Future[JsSubmitAndWaitForTransactionResponse] = + Transaction + .toJson(response.getTransaction) + .map(tx => + JsSubmitAndWaitForTransactionResponse( + transaction = tx, + completion_offset = response.completionOffset, + ) + ) + } + + object SubmitAndWaitUpdateIdResponse + extends ProtocolConverter[ + lapi.command_service.SubmitAndWaitForUpdateIdResponse, + JsSubmitAndWaitForUpdateIdResponse, + ] { + + def toJson( + response: lapi.command_service.SubmitAndWaitForUpdateIdResponse + ): JsSubmitAndWaitForUpdateIdResponse = + JsSubmitAndWaitForUpdateIdResponse( + update_id = response.updateId, + completion_offset = response.completionOffset, + ) + + def fromJson( + response: JsSubmitAndWaitForUpdateIdResponse + ): lapi.command_service.SubmitAndWaitForUpdateIdResponse = + lapi.command_service.SubmitAndWaitForUpdateIdResponse( + updateId = response.update_id, + completionOffset = response.completion_offset, + ) + } + + object GetEventsByContractIdRequest + extends ProtocolConverter[ + lapi.event_query_service.GetEventsByContractIdRequest, + JsGetEventsByContractIdResponse, + ] { + def toJson( + response: lapi.event_query_service.GetEventsByContractIdResponse + )(implicit + token: Option[String] + ): Future[JsGetEventsByContractIdResponse] = + for { + createdEvents <- response.created + .map(c => CreatedEvent.toJson(c.getCreatedEvent).map(Some(_))) + .getOrElse(Future(None)) + } yield JsGetEventsByContractIdResponse( + created = response.created.flatMap(c => + createdEvents.map(ce => + JsCreated( + created_event = ce, + domain_id = c.domainId, + ) + ) + ), + archived = response.archived.map(a => + JsArchived( + archived_event = ArchivedEvent.toJson(a.getArchivedEvent), + domain_id = a.domainId, + ) + ), + ) + } + + object ArchivedEvent extends ProtocolConverter[lapi.event.ArchivedEvent, JsEvent.ArchivedEvent] { + def toJson(e: lapi.event.ArchivedEvent): JsEvent.ArchivedEvent = JsEvent.ArchivedEvent( + event_id = e.eventId, + contract_id = e.contractId, + template_id = IdentifierConverter.toJson(e.getTemplateId), + witness_parties = e.witnessParties, + package_name = e.packageName, + ) + + def fromJson(ev: JsEvent.ArchivedEvent): lapi.event.ArchivedEvent = lapi.event.ArchivedEvent( + eventId = ev.event_id, + contractId = ev.contract_id, + templateId = Some(IdentifierConverter.fromJson(ev.template_id)), + witnessParties = ev.witness_parties, + packageName = ev.package_name, + ) + } + + object CreatedEvent extends ProtocolConverter[lapi.event.CreatedEvent, JsEvent.CreatedEvent] { + def toJson(created: lapi.event.CreatedEvent)(implicit + token: Option[String] + ): Future[JsEvent.CreatedEvent] = { + val apiTemplateId = created.getTemplateId + val lfIdentifier = Ref.Identifier.assertFromString( + s"${apiTemplateId.packageId}:${apiTemplateId.moduleName}:${apiTemplateId.entityName}" + ) + for { + contractKey <- created.contractKey + .map(ck => + schemaProcessors + .keyArgFromProtoToJson( + lfIdentifier, + ck, + )(token) + .map(Some(_)) + ) + .getOrElse(Future(None)) + createdArgs <- created.createArguments + .map(ca => + schemaProcessors + .contractArgFromProtoToJson( + lfIdentifier, + ca, + )(token) + .map(Some(_)) + ) + .getOrElse(Future(None)) + interfaceViews <- Future.sequence(created.interfaceViews.map(InterfaceView.toJson)) + } yield JsEvent.CreatedEvent( + event_id = created.eventId, + contract_id = created.contractId, + template_id = IdentifierConverter.toJson(created.getTemplateId), + contract_key = contractKey.map(toCirce(_)), + create_argument = createdArgs.map(toCirce(_)), + created_event_blob = created.createdEventBlob, + interface_views = interfaceViews, + witness_parties = created.witnessParties, + signatories = created.signatories, + observers = created.observers, + created_at = created.getCreatedAt, + package_name = created.packageName, + ) + } + + } + + object AssignedEvent + extends ProtocolConverter[ + lapi.reassignment.AssignedEvent, + JsAssignedEvent, + ] { + + def toJson(v: lapi.reassignment.AssignedEvent)(implicit + token: Option[String] + ): Future[JsAssignedEvent] = + CreatedEvent + .toJson(v.getCreatedEvent) + .map(ev => + JsAssignedEvent( + source = v.source, + target = v.target, + unassign_id = v.unassignId, + submitter = v.submitter, + reassignment_counter = v.reassignmentCounter, + created_event = ev, + ) + ) + } + + object UnassignedEvent + extends ProtocolConverter[ + lapi.reassignment.UnassignedEvent, + JsUnassignedEvent, + ] { + def toJson(v: lapi.reassignment.UnassignedEvent): JsUnassignedEvent = + JsUnassignedEvent( + unassign_id = v.unassignId, + contract_id = v.contractId, + template_id = IdentifierConverter.toJson(v.getTemplateId), + source = v.source, + target = v.target, + submitter = v.submitter, + reassignment_counter = v.reassignmentCounter, + assignment_exclusivity = v.assignmentExclusivity, + witness_parties = v.witnessParties, + package_name = v.packageName, + ) + } + + object ContractEntry + extends ProtocolConverter[ + lapi.state_service.GetActiveContractsResponse.ContractEntry, + JsContractEntry, + ] { + def toJson( + v: lapi.state_service.GetActiveContractsResponse.ContractEntry + )(implicit + token: Option[String] + ): Future[JsContractEntry] = + v match { + case lapi.state_service.GetActiveContractsResponse.ContractEntry.Empty => + Future(JsContractEntry.JsEmpty) + case lapi.state_service.GetActiveContractsResponse.ContractEntry.ActiveContract(value) => + CreatedEvent + .toJson(value.getCreatedEvent) + .map(ce => + JsContractEntry.JsActiveContract( + created_event = ce, + domain_id = value.domainId, + reassignment_counter = value.reassignmentCounter, + ) + ) + case lapi.state_service.GetActiveContractsResponse.ContractEntry + .IncompleteUnassigned(value) => + CreatedEvent + .toJson(value.getCreatedEvent) + .map(ce => + JsContractEntry.JsIncompleteUnassigned( + created_event = ce, + unassigned_event = UnassignedEvent.toJson(value.getUnassignedEvent), + ) + ) + case lapi.state_service.GetActiveContractsResponse.ContractEntry + .IncompleteAssigned(value) => + AssignedEvent + .toJson(value.getAssignedEvent) + .map(ae => + JsContractEntry.JsIncompleteAssigned( + ae + ) + ) + } + + } + + object GetActiveContractsResponse + extends ProtocolConverter[ + lapi.state_service.GetActiveContractsResponse, + JsGetActiveContractsResponse, + ] { + def toJson(v: lapi.state_service.GetActiveContractsResponse)(implicit + token: Option[String] + ): Future[JsGetActiveContractsResponse] = + ContractEntry + .toJson(v.contractEntry) + .map(ce => + JsGetActiveContractsResponse( + offset = v.offset, + workflow_id = v.workflowId, + contract_entry = ce, + ) + ) + } + + object ReassignmentEvent + extends ProtocolConverter[lapi.reassignment.Reassignment.Event, JsReassignmentEvent] { + def toJson(v: lapi.reassignment.Reassignment.Event)(implicit + token: Option[String] + ): Future[JsReassignmentEvent] = + v match { + case lapi.reassignment.Reassignment.Event.Empty => jsFail("Invalid value") + case lapi.reassignment.Reassignment.Event.UnassignedEvent(value) => + Future(JsReassignmentEvent.JsUnassignedEvent(value)) + case lapi.reassignment.Reassignment.Event.AssignedEvent(value) => + CreatedEvent + .toJson(value.getCreatedEvent) + .map(ce => + JsReassignmentEvent.JsAssignmentEvent( + source = value.source, + target = value.target, + unassign_id = value.unassignId, + submitter = value.submitter, + reassignment_counter = value.reassignmentCounter, + created_event = ce, + ) + ) + } + + } + + object Reassignment extends ProtocolConverter[lapi.reassignment.Reassignment, JsReassignment] { + def toJson(v: lapi.reassignment.Reassignment)(implicit + token: Option[String] + ): Future[JsReassignment] = ReassignmentEvent + .toJson(v.event) + .map(e => + JsReassignment( + update_id = v.updateId, + command_id = v.commandId, + workflow_id = v.workflowId, + offset = v.offset, + event = e, + trace_context = v.traceContext, + record_time = v.getRecordTime, + ) + ) + } + + object GetUpdatesResponse + extends ProtocolConverter[lapi.update_service.GetUpdatesResponse, JsGetUpdatesResponse] { + def toJson(obj: lapi.update_service.GetUpdatesResponse)(implicit + token: Option[String] + ): Future[JsGetUpdatesResponse] = + (obj.update match { + case lapi.update_service.GetUpdatesResponse.Update.Empty => jsFail("Invalid value") + case lapi.update_service.GetUpdatesResponse.Update.Transaction(value) => + Transaction.toJson(value).map(JsUpdate.Transaction) + case lapi.update_service.GetUpdatesResponse.Update.Reassignment(value) => + Reassignment.toJson(value).map(JsUpdate.Reassignment) + case lapi.update_service.GetUpdatesResponse.Update.OffsetCheckpoint(value) => + Future(JsUpdate.OffsetCheckpoint(value)) + }).map(update => JsGetUpdatesResponse(update)) + } + + object GetUpdateTreesResponse + extends ProtocolConverter[ + lapi.update_service.GetUpdateTreesResponse, + JsGetUpdateTreesResponse, + ] { + def toJson( + value: lapi.update_service.GetUpdateTreesResponse + )(implicit + token: Option[String] + ): Future[JsGetUpdateTreesResponse] = + (value.update match { + case lapi.update_service.GetUpdateTreesResponse.Update.Empty => jsFail("Invalid value") + case lapi.update_service.GetUpdateTreesResponse.Update.OffsetCheckpoint(value) => + Future(JsUpdateTree.OffsetCheckpoint(value)) + case lapi.update_service.GetUpdateTreesResponse.Update.TransactionTree(value) => + TransactionTree.toJson(value).map(JsUpdateTree.TransactionTree) + case lapi.update_service.GetUpdateTreesResponse.Update.Reassignment(value) => + Reassignment.toJson(value).map(JsUpdateTree.Reassignment) + }).map(update => JsGetUpdateTreesResponse(update)) + } + + object GetTransactionTreeResponse + extends ProtocolConverter[ + lapi.update_service.GetTransactionTreeResponse, + JsGetTransactionTreeResponse, + ] { + def toJson( + obj: lapi.update_service.GetTransactionTreeResponse + )(implicit + token: Option[String] + ): Future[JsGetTransactionTreeResponse] = + TransactionTree.toJson(obj.getTransaction).map(JsGetTransactionTreeResponse) + } + + object GetTransactionResponse + extends ProtocolConverter[ + lapi.update_service.GetTransactionResponse, + JsGetTransactionResponse, + ] { + def toJson(obj: lapi.update_service.GetTransactionResponse)(implicit + token: Option[String] + ): Future[JsGetTransactionResponse] = + Transaction.toJson(obj.getTransaction).map(JsGetTransactionResponse) + } +} + +object IdentifierConverter extends ProtocolConverter[lapi.value.Identifier, String] { + def fromJson(jsIdentifier: String): lapi.value.Identifier = + jsIdentifier.split(":").toSeq match { + case Seq(packageId, moduleName, entityName) => + lapi.value.Identifier( + packageId = packageId, + moduleName = moduleName, + entityName = entityName, + ) + case _ => jsFail(s"Invalid identifier: $jsIdentifier") + } + + def toJson(lapiIdentifier: lapi.value.Identifier): String = + s"${lapiIdentifier.packageId}:${lapiIdentifier.moduleName}:${lapiIdentifier.entityName}" +} + +object JsStatusConverter extends ProtocolConverter[com.google.rpc.status.Status, JsStatus] { + def toJson(lapi: Status): JsStatus = JsStatus( + code = lapi.code, + message = lapi.message, + details = lapi.details.map(_.toString).mkString, + ) +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/SchemaProcessors.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/SchemaProcessors.scala new file mode 100644 index 000000000000..d7b232bd5330 --- /dev/null +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/SchemaProcessors.scala @@ -0,0 +1,164 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.http.json.v2 + +import com.daml.ledger.api.v2.value +import com.daml.ledger.api.v2.value.Value +import com.digitalasset.canton.caching.CaffeineCache +import com.digitalasset.daml.lf.data.Ref +import com.digitalasset.daml.lf.data.Ref.IdString +import com.digitalasset.daml.lf.language.Ast +import com.digitalasset.transcode.codec.json.JsonCodec +import com.digitalasset.transcode.codec.proto.GrpcValueCodec +import com.digitalasset.transcode.daml_lf.{Dictionary, SchemaEntity, SchemaProcessor} +import com.digitalasset.transcode.schema.SchemaVisitor +import com.digitalasset.transcode.{Codec, Converter} +import com.github.benmanes.caffeine.cache.Caffeine + +import scala.concurrent.{ExecutionContext, Future} + +class SchemaProcessors( + val fetchSignatures: Option[String] => Future[Map[Ref.PackageId, Ast.PackageSignature]] +)(implicit + val executionContext: ExecutionContext +) { + + private val cache = CaffeineCache[String, Map[Ref.PackageId, Ast.PackageSignature]]( + Caffeine + .newBuilder() + .maximumSize(SchemaProcessorsCache.MaxCacheSize), + None, + ) + + private def ensurePackage( + packageId: Ref.PackageId, + token: Option[String], + ): Future[Map[Ref.PackageId, Ast.PackageSignature]] = { + val tokenKey = token.getOrElse("") + val signatures = cache.getIfPresent(tokenKey) + signatures.fold { + // TODO(i20707) use the new Ledger API's package metadata view + fetchSignatures(token).map { fetched => + cache.put(tokenKey, fetched) + fetched + } + } { signatures => + if (signatures.contains(packageId)) { + Future(signatures) + } else { + fetchSignatures(token).map { newSignatures => + cache.put(tokenKey, newSignatures) + newSignatures + } + } + } + } + + private def prepareToProto( + packageId: Ref.PackageId, + token: Option[String], + ): Future[Dictionary[Converter[ujson.Value, Value]]] = + ensurePackage(packageId, token).map { signatures => + val visitor: SchemaVisitor { type Type = (Codec[ujson.Value], Codec[value.Value]) } = + SchemaVisitor.compose(new JsonCodec(), GrpcValueCodec) + val collector = + Dictionary.collect[Converter[ujson.Value, value.Value]] compose SchemaEntity + .map((v: visitor.Type) => Converter(v._1, v._2)) + + SchemaProcessor + .process(packages = signatures)(visitor)( + collector + ) + .fold(error => throw new IllegalStateException(error), identity) + } + + private def prepareToJson( + packageId: Ref.PackageId, + token: Option[String], + ): Future[Dictionary[Converter[Value, ujson.Value]]] = + ensurePackage(packageId, token).map { signatures => + val visitor: SchemaVisitor { type Type = (Codec[value.Value], Codec[ujson.Value]) } = + SchemaVisitor.compose(GrpcValueCodec, new JsonCodec()) + val collector = + Dictionary.collect[Converter[value.Value, ujson.Value]] compose SchemaEntity + .map((v: visitor.Type) => Converter(v._1, v._2)) + + SchemaProcessor + .process(packages = signatures)(visitor)( + collector + ) + .fold(error => throw new IllegalStateException(error), identity) + } + + def convertGrpcToJson(templateId: Ref.Identifier, proto: value.Value)( + token: Option[String] + ): Future[ujson.Value] = + prepareToJson(templateId.packageId, token).map(_.templates(templateId).convert(proto)) + + def contractArgFromJsonToProto( + templateId: Ref.Identifier, + jsonArgsValue: ujson.Value, + )(token: Option[String]): Future[value.Value] = + prepareToProto(templateId.packageId, token).map(_.templates(templateId).convert(jsonArgsValue)) + + def choiceArgsFromJsonToProto( + templateId: Ref.Identifier, + choiceName: IdString.Name, + jsonArgsValue: ujson.Value, + )(token: Option[String]): Future[value.Value] = + prepareToProto(templateId.packageId, token).map( + _.choiceArguments((templateId, choiceName)).convert(jsonArgsValue) + ) + + def contractArgFromProtoToJson( + templateId: Ref.Identifier, + protoArgs: value.Record, + )(token: Option[String]): Future[ujson.Value] = + convertGrpcToJson(templateId, value.Value(value.Value.Sum.Record(protoArgs)))(token) + + def choiceArgsFromProtoToJson( + templateId: Ref.Identifier, + choiceName: IdString.Name, + protoArgs: value.Value, + )(token: Option[String]) = prepareToJson(templateId.packageId, token).map( + _.choiceArguments((templateId, choiceName)).convert(protoArgs) + ) + + def keyArgFromProtoToJson( + templateId: Ref.Identifier, + protoArgs: value.Value, + )(token: Option[String]): Future[ujson.Value] = + prepareToJson(templateId.packageId, token).map(_.templates(templateId).convert(protoArgs)) + + def keyArgFromJsonToProto( + templateId: Ref.Identifier, + protoArgs: ujson.Value, + )(token: Option[String]): Future[value.Value] = + prepareToProto(templateId.packageId, token).map(_.templates(templateId).convert(protoArgs)) + + def exerciseResultFromProtoToJson( + lfIdentifier: Ref.Identifier, + choiceName: IdString.Name, + v: value.Value, + )(token: Option[String]) = prepareToJson(lfIdentifier.packageId, token).map( + _.choiceArguments((lfIdentifier, choiceName)).convert(v) + ) + + def exerciseResultFromJsonToProto( + lfIdentifier: Ref.Identifier, + choiceName: IdString.Name, + value: ujson.Value, + )(token: Option[String]): Future[Option[Value]] = value match { + case ujson.Null => Future(None) + case _ => + prepareToProto(lfIdentifier.packageId, token) + .map(_.choiceArguments((lfIdentifier, choiceName)).convert(value)) + .map(Some(_)) + } + +} + +object SchemaProcessorsCache { + val MaxCacheSize: Long = 100 +} diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/V2Routes.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala similarity index 54% rename from sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/V2Routes.scala rename to sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala index 612fe07e9364..191a0b552e99 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json2/V2Routes.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala @@ -1,21 +1,30 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.http.json2 +package com.digitalasset.canton.http.json.v2 +import com.daml.grpc.adapter.ExecutionSequencerFactory +import com.daml.jwt.Jwt +import com.digitalasset.canton.http.PackageService +import com.digitalasset.canton.http.util.Logging.instanceUUIDLogCtx import com.digitalasset.canton.ledger.client.LedgerClient import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import org.apache.pekko.http.scaladsl.server.Route import org.apache.pekko.stream.Materializer import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter +import com.digitalasset.daml.lf.data.Ref.IdString import scala.concurrent.ExecutionContext class V2Routes( + commandService: JsCommandService, + eventService: JsEventService, identityProviderService: JsIdentityProviderService, meteringService: JsMeteringService, packageService: JsPackageService, partyManagementService: JsPartyManagementService, + stateService: JsStateService, + updateService: JsUpdateService, userManagementService: JsUserManagementService, versionService: JsVersionService, val loggerFactory: NamedLoggerFactory, @@ -25,9 +34,13 @@ class V2Routes( val v2Routes: Route = PekkoHttpServerInterpreter()(ec).toRoute( - versionService.endpoints() + commandService.endpoints() + ++ eventService.endpoints() + ++ versionService.endpoints() ++ packageService.endpoints() ++ partyManagementService.endpoints() + ++ stateService.endpoints() + ++ updateService.endpoints() ++ userManagementService.endpoints() ++ identityProviderService.endpoints() ++ meteringService.endpoints() @@ -37,22 +50,50 @@ class V2Routes( object V2Routes { def apply( ledgerClient: LedgerClient, + packageService: PackageService, executionContext: ExecutionContext, materializer: Materializer, loggerFactory: NamedLoggerFactory, + )(implicit + esf: ExecutionSequencerFactory ): V2Routes = { implicit val ec: ExecutionContext = executionContext + def fetchSignatures(token: Option[String]) = for { + _ <- instanceUUIDLogCtx { implicit lc => + packageService.reload( + Jwt(token.getOrElse("")) + ) + } + } yield packageService.packageStore + .map { case (k, v) => + (IdString.PackageId.assertFromString(k), v.pack) + } + + val schemaProcessors = new SchemaProcessors(fetchSignatures) + val protocolConverters = new ProtocolConverters(schemaProcessors) + val commandService = + new JsCommandService(ledgerClient, protocolConverters, loggerFactory) + + val eventService = + new JsEventService(ledgerClient, protocolConverters, loggerFactory) val versionService = new JsVersionService(ledgerClient.versionClient) - val partyManagementService = new JsPartyManagementService(ledgerClient.partyManagementClient, loggerFactory) + val stateService = + new JsStateService(ledgerClient, protocolConverters, loggerFactory) - val packageService = + val partyManagementService = + new JsPartyManagementService(ledgerClient.partyManagementClient, loggerFactory) + + val jsPackageService = new JsPackageService(ledgerClient.packageService, ledgerClient.packageManagementClient)( executionContext, materializer, ) + val updateService = + new JsUpdateService(ledgerClient, protocolConverters, loggerFactory) + val userManagementService = new JsUserManagementService(ledgerClient.userManagementClient, loggerFactory) val meteringService = new JsMeteringService(ledgerClient.meteringReportClient) @@ -63,10 +104,14 @@ object V2Routes { ) new V2Routes( + commandService, + eventService, identityProviderService, meteringService, - packageService, + jsPackageService, partyManagementService, + stateService, + updateService, userManagementService, versionService, loggerFactory, diff --git a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/ledger/service/LedgerReader.scala b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/ledger/service/LedgerReader.scala index 83591039217f..dcb24408f78e 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/ledger/service/LedgerReader.scala +++ b/sdk/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/ledger/service/LedgerReader.scala @@ -7,7 +7,7 @@ import com.daml.ledger.api.v2.package_service.GetPackageResponse import com.digitalasset.daml.lf.archive import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageId} -import com.digitalasset.daml.lf.typesig.reader.SignatureReader +import com.digitalasset.daml.lf.typesig.reader.{DamlLfArchiveReader, SignatureReader} import com.digitalasset.daml.lf.typesig.{DefDataType, PackageSignature} import com.daml.logging.LoggingContextOf import com.daml.scalautil.TraverseFMSyntax.* @@ -15,6 +15,8 @@ import com.daml.timer.RetryStrategy import com.digitalasset.canton.ledger.client.services.pkg.PackageClient import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.NoTracing +import com.digitalasset.daml.lf.archive.DamlLf +import com.digitalasset.daml.lf.language.{Ast, Util} import scalaz.* import scalaz.Scalaz.* @@ -65,7 +67,7 @@ final case class LedgerReader(loggerFactory: NamedLoggerFactory) token: Option[String], )( pkid: String - )(implicit ec: ExecutionContext, lc: LoggingContextOf[Any]): Future[Error \/ PackageSignature] = { + )(implicit ec: ExecutionContext, lc: LoggingContextOf[Any]): Future[Error \/ Signatures] = { import loadCache.cache retryLoop { cache @@ -126,8 +128,9 @@ object LedgerReader { type Error = String + case class Signatures(typesig: PackageSignature, pack: Ast.PackageSignature) // PackageId -> PackageSignature - type PackageStore = Map[String, PackageSignature] + type PackageStore = Map[String, Signatures] val UpToDate: Future[Error \/ Option[PackageStore]] = Future.successful(\/-(None)) @@ -148,7 +151,7 @@ object LedgerReader { // request pattern, so there isn't anything you can really do about it on // the server configuration. 100% miss rate means no redundant work is // happening; it does not mean the server is being slower. - private[LedgerReader] val cache = CaffeineCache[String, Error \/ PackageSignature]( + private[LedgerReader] val cache = CaffeineCache[String, Error \/ Signatures]( Caffeine .newBuilder() .softValues() @@ -164,22 +167,29 @@ object LedgerReader { } private def createPackageStoreFromArchives( - packageResponses: List[Error \/ PackageSignature] + packageResponses: List[Error \/ Signatures] ): Error \/ PackageStore = { packageResponses.sequence - .map(_.groupMapReduce(_.packageId: String)(identity)((_, sig) => sig)) + .map(_.groupMapReduce(_.typesig.packageId: String)(identity)((_, sig) => sig)) } private def decodeInterfaceFromPackageResponse( packageResponse: GetPackageResponse - ): Error \/ PackageSignature = { + ): Error \/ Signatures = { import packageResponse.* \/.attempt { - val payload = archive.ArchivePayloadParser.assertFromByteString(archivePayload) + val payload: DamlLf.ArchivePayload = + archive.ArchivePayloadParser.assertFromByteString(archivePayload) + val pck = DamlLfArchiveReader.readPackage(PackageId.assertFromString(hash), payload) + val (errors, out) = - SignatureReader.readPackageSignature(PackageId.assertFromString(hash), payload) + SignatureReader.readPackageSignature(() => pck) + (if (!errors.empty) -\/("Errors reading LF archive:\n" + errors.toString) - else \/-(out)): Error \/ PackageSignature + else + \/-(out).flatMap(x => + pck.map(p => Signatures(x, Util.toSignature(p._2))) + )): Error \/ Signatures }(_.getLocalizedMessage).join } @@ -189,14 +199,13 @@ object LedgerReader { val store = packageStore() store.get(id.packageId).flatMap { packageSignature => - packageSignature.typeDecls.get(id.qualifiedName).map(_.`type`).orElse { + packageSignature.typesig.typeDecls.get(id.qualifiedName).map(_.`type`).orElse { for { - interface <- packageSignature.interfaces.get(id.qualifiedName) + interface <- packageSignature.typesig.interfaces.get(id.qualifiedName) viewTypeId <- interface.viewType - viewType <- PackageSignature.resolveInterfaceViewType(store).lift(viewTypeId) + viewType <- PackageSignature.resolveInterfaceViewType(store.view.mapValues(_.typesig)).lift(viewTypeId) } yield DefDataType(ImmArraySeq(), viewType) } } } - } diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml index 0d4460ee632f..ba0afb2230ef 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_1/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: JsonEncodingTest diff --git a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml index 5edc1efa0540..a60cd65dab8e 100644 --- a/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml +++ b/sdk/canton/community/ledger/ledger-json-api/src/test/daml/v2_dev/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.dev name: JsonEncodingTestDev diff --git a/sdk/canton/community/ledger/transcode/.gitignore b/sdk/canton/community/ledger/transcode/.gitignore new file mode 100644 index 000000000000..294700c4fcdc --- /dev/null +++ b/sdk/canton/community/ledger/transcode/.gitignore @@ -0,0 +1,5 @@ +/out/ +/.idea/ +/.bsp/ +/.daml-sdk/ +/target/ diff --git a/sdk/canton/community/ledger/transcode/README.md b/sdk/canton/community/ledger/transcode/README.md new file mode 100644 index 000000000000..dd84305da529 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/README.md @@ -0,0 +1,4 @@ +Code in this folder is a copy of https://github.com/DACH-NY/transcode project + +This is a temporary solution till we find a better way. + diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Codec.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Codec.scala new file mode 100644 index 000000000000..32b1892cc03b --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Codec.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode + +import com.digitalasset.transcode.schema.DynamicValue + +/** Codec encodes and decodes target protocol to and from an intermediary + * [[com.digitalasset.transcode.schema.DynamicValue]] representation. Arbitrary codecs can be composed together to + * create direct interoperability [[Converter]]s. + */ +trait Codec[A] extends Decoder[A] with Encoder[A] + +/** Decodes target protocol representation into intermediary DynamicValue representation. */ +trait Decoder[A] { def toDynamicValue(v: A): DynamicValue } + +/** Encodes intermediary DynamicValue representation into target protocol representation. */ +trait Encoder[A] { def fromDynamicValue(dv: DynamicValue): A } diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Converter.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Converter.scala new file mode 100644 index 000000000000..984681ad88ed --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/Converter.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode + +trait Converter[A, B] { def convert(a: A): B } +object Converter { + def apply[A, B](decoder: Decoder[A], encoder: Encoder[B]): Converter[A, B] = + (a: A) => encoder.fromDynamicValue(decoder.toDynamicValue(a)) +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/json/JsonCodec.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/json/JsonCodec.scala new file mode 100644 index 000000000000..8d899ccce8b8 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/json/JsonCodec.scala @@ -0,0 +1,238 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.codec.json + +import com.digitalasset.transcode.Codec +import com.digitalasset.transcode.schema.DynamicValue.* +import com.digitalasset.transcode.schema.* +import ujson.* + +import java.time.* +import java.time.format.DateTimeFormatter +import scala.collection.mutable.ArrayBuffer + +/** Json Codec. + * + * @param encodeNumericAsString + * encode numeric as string (true) or as a json number (false). The latter might be useful for querying and + * mathematical operations, but can loose precision due to float point errors. + * @param encodeInt64AsString + * encode int64 as a string (true) or as a json number (false). The latter might be useful for querying and + * mathematical operations, but can loose precision, as numbers in some json implementations are backed by Double + */ + +//TODO (i20144) remove or convert to scala2 +class JsonCodec( + encodeNumericAsString: Boolean = true, + encodeInt64AsString: Boolean = true, +) extends SchemaVisitor { + override type Type = Codec[Value] + + override def record( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + fields0: => Seq[(FieldName, Type)], + ): Type = new Type { + private lazy val fields = fields0.map { case (n, c) => n.fieldName -> c } + + override def toDynamicValue(v: Value): DynamicValue = { + val valueMap = v.obj.value + DynamicValue.Record(fields map { case (name, c) => c.toDynamicValue(valueMap(name)) }) + } + + override def fromDynamicValue(dv: DynamicValue): Value = + Obj.from(dv.record.iterator zip fields map { case (f, (name, c)) => + name -> c.fromDynamicValue(f) + }) + } + + override def variant( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + cases: => Seq[(VariantConName, Type)], + ): Type = new Type { + private lazy val caseList = cases.map { case (name, codec) => + name.variantConName -> codec + }.toArray + private lazy val caseMap = cases.zipWithIndex.map { case ((n, c), ix) => + n.variantConName -> (ix, c) + }.toMap + + override def toDynamicValue(v: Value): DynamicValue = { + val (ctorIx, valueCodec) = caseMap(v.obj("tag").str) + DynamicValue.Variant(ctorIx, valueCodec.toDynamicValue(v.obj("value"))) + } + + override def fromDynamicValue(dv: DynamicValue): Value = { + val (ctorName, valueCodec) = caseList(dv.variant.ctorIx) + Obj("tag" -> Str(ctorName), "value" -> valueCodec.fromDynamicValue(dv.variant.value)) + } + } + + override def `enum`(id: Identifier, cases: Seq[EnumConName]): Type = new Type { + private val caseList = cases.map(_.enumConName).toArray + private val caseMap = cases.map(_.enumConName).zipWithIndex.toMap + + override def toDynamicValue(v: Value): DynamicValue = + DynamicValue.Enum(caseMap(v.str)) + + override def fromDynamicValue(dv: DynamicValue): Value = + Str(caseList(dv.`enum`)) + } + + override def list(elem: Type): Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = + DynamicValue.List(v.arr.view.map(elem.toDynamicValue)) + + override def fromDynamicValue(dv: DynamicValue): Value = + Arr(ArrayBuffer.from(dv.list.iterator.map(elem.fromDynamicValue))) + } + + override def optional(elem: Type): Type = new OptionalProcessor(elem) + private class OptionalProcessor(elem: Type) extends Type { + def toDynamicValue(v: Value): DynamicValue = elem match { + case _: OptionalProcessor => toDynamicValueNested(v) + case _ => toDynamicValueSimple(v) + } + def fromDynamicValue(dv: DynamicValue): Value = elem match { + case _: OptionalProcessor => fromDynamicValueNested(dv) + case _ => fromDynamicValueSimple(dv) + } + + private def toDynamicValueSimple(v: Value): DynamicValue = + DynamicValue.Optional(if (v.isNull) None else Some(elem.toDynamicValue(v))) + private def fromDynamicValueSimple(dv: DynamicValue): Value = + dv.optional.fold[ujson.Value](ujson.Null)(v => elem.fromDynamicValue(v)) + + private def toDynamicValueNested(v: Value): DynamicValue = + DynamicValue.Optional(if (v.arr.isEmpty) None else Some(toDynamicValueNext(elem)(v.arr(0)))) + private def fromDynamicValueNested(dv: DynamicValue): Value = + dv.optional.fold(Arr())(v => Arr(fromDynamicValueNext(elem)(v))) + + private def toDynamicValueNext(e: Type) = e match { + case elem: OptionalProcessor => elem.toDynamicValueNested + case elem => elem.toDynamicValue + } + + private def fromDynamicValueNext(e: Type) = e match { + case elem: OptionalProcessor => elem.fromDynamicValueNested + case elem => elem.fromDynamicValue + } + } + + override def textMap(value: Type): Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = + DynamicValue.TextMap(v.obj.value.view.mapValues(value.toDynamicValue)) + + override def fromDynamicValue(dv: DynamicValue): Value = + Obj.from(dv.textMap.iterator.map { case (k, v) => k -> value.fromDynamicValue(v) }) + } + + override def genMap(key: Type, value: Type): Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = + DynamicValue.GenMap( + v.arr.view.map(e => key.toDynamicValue(e.arr(0)) -> value.toDynamicValue(e.arr(1))) + ) + + override def fromDynamicValue(dv: DynamicValue): Value = + Arr.from( + dv.genMap.iterator.map { case (k, v) => + Arr(key.fromDynamicValue(k), value.fromDynamicValue(v)) + } + ) + } + + override def unit: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Unit + + override def fromDynamicValue(dv: DynamicValue): Value = Obj() + } + + override def bool: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Bool(v.bool) + + override def fromDynamicValue(dv: DynamicValue): Value = ujson.Bool(dv.bool) + } + + override def text: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Text(v.str) + + override def fromDynamicValue(dv: DynamicValue): Value = Str(dv.text) + } + + override def int64: Type = + if (encodeInt64AsString) { + new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Int64(v.str.toLong) + + override def fromDynamicValue(dv: DynamicValue): Value = Str(String.valueOf(dv.int64)) + } + } else { + new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Int64(v.num.longValue) + + override def fromDynamicValue(dv: DynamicValue): Value = Num(dv.int64.toDouble) + } + } + + override def numeric(scale: Int): Type = + if (encodeNumericAsString) { + new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Numeric(v.str) + + override def fromDynamicValue(dv: DynamicValue): Value = Str(dv.numeric) + } + } else { + new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Numeric(v.num.toString) + + override def fromDynamicValue(dv: DynamicValue): Value = Num(dv.numeric.toDouble) + } + } + + override def party: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.Party(v.str) + + override def fromDynamicValue(dv: DynamicValue): Value = Str(dv.party) + } + + override def timestamp: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = { + val time = DateTimeFormatter.ISO_DATE_TIME.parse(v.str, ZonedDateTime.from) + DynamicValue.Timestamp(time.toEpochSecond * 1000000 + time.getNano / 1000) + } + + override def fromDynamicValue(dv: DynamicValue): Value = { + val time = LocalDateTime + .ofEpochSecond( + dv.timestamp / 1000000, + (dv.timestamp % 1000000 * 1000).intValue, + ZoneOffset.UTC, + ) + .atZone(ZoneOffset.UTC) + Str(DateTimeFormatter.ISO_DATE_TIME.format(time)) + } + } + + override def date: Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = + DynamicValue.Date( + DateTimeFormatter.ISO_LOCAL_DATE.parse(v.str, LocalDate.from).toEpochDay.intValue + ) + + override def fromDynamicValue(dv: DynamicValue): Value = + Str(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.ofEpochDay(dv.date.longValue))) + } + + override def contractId(template: Type): Type = new Type { + override def toDynamicValue(v: Value): DynamicValue = DynamicValue.ContractId(v.str) + + override def fromDynamicValue(dv: DynamicValue): Value = Str(dv.contractId) + } + + override def interface(id: Identifier): Type = unit + + override def variable(name: TypeVarName, value: Codec[Value]): Codec[Value] = value + +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/proto/GrpcValueCodec.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/proto/GrpcValueCodec.scala new file mode 100644 index 000000000000..758dd1632bc1 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/codec/proto/GrpcValueCodec.scala @@ -0,0 +1,167 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Copyright (c) 2023, Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.codec.proto + +import com.daml.ledger.api.v2.value +import com.daml.ledger.api.v2.value.Value.Sum +import com.digitalasset.transcode.Codec +import com.digitalasset.transcode.schema.DynamicValue.* +import com.digitalasset.transcode.schema.* +import com.google.protobuf.empty.Empty + +/** This codec converts Ledger API GRPC values. */ +object GrpcValueCodec extends SchemaVisitor { + type Type = Codec[value.Value] + + override def record( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + fields: => Seq[(FieldName, Type)], + ): Type = new Type { + private lazy val codecs = fields.map(_._2) + + override def fromDynamicValue(dv: DynamicValue): value.Value = { + val fs = dv.record.iterator zip codecs map { case (f, c) => + value.RecordField(value = Some(c.fromDynamicValue(f))) + } + value.Value(Sum.Record(value.Record(fields = fs.toSeq))) + } + + override def toDynamicValue(a: value.Value): DynamicValue = + DynamicValue.Record( + a.getRecord.fields.view zip codecs map { case (f, c) => c.toDynamicValue(f.getValue) } + ) + } + + override def variant( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + cases: => Seq[(VariantConName, Type)], + ): Type = new Type { + private lazy val caseList = cases.map { case (n, c) => (n.variantConName, c) }.toArray + private lazy val caseMap = cases.zipWithIndex.map { case ((n, c), ix) => + n.variantConName -> (ix, c) + }.toMap + + override def fromDynamicValue(dv: DynamicValue): value.Value = { + val (ctor, valueCodec) = caseList(dv.variant.ctorIx) + value.Value( + Sum.Variant( + value.Variant( + constructor = ctor, + value = Some(valueCodec.fromDynamicValue(dv.variant.value)), + ) + ) + ) + } + + override def toDynamicValue(a: value.Value): DynamicValue = { + val (ctor, valueCodec) = caseMap(a.getVariant.constructor) + DynamicValue.Variant(ctor, valueCodec.toDynamicValue(a.getVariant.getValue)) + } + } + + override def `enum`(id: Identifier, cases: Seq[EnumConName]): Type = new Type { + private val caseList = cases.map(_.enumConName).toArray + private val caseMap = cases.map(_.enumConName).zipWithIndex.toMap + + override def toDynamicValue(a: value.Value): DynamicValue = + DynamicValue.Enum(caseMap(a.getEnum.constructor)) + + override def fromDynamicValue(dv: DynamicValue): value.Value = + value.Value(Sum.Enum(value.Enum(constructor = caseList(dv.`enum`)))) + } + + override def list(elem: Type): Type = new Type { + override def fromDynamicValue(dv: DynamicValue): value.Value = + value.Value(Sum.List(value.List(dv.list.iterator.map(elem.fromDynamicValue).toSeq))) + + override def toDynamicValue(a: value.Value): DynamicValue = + DynamicValue.List(a.getList.elements.view.map(elem.toDynamicValue)) + } + + override def optional(elem: Type): Type = new Type { + override def fromDynamicValue(dv: DynamicValue): value.Value = + value.Value(Sum.Optional(value.Optional(value = dv.optional.map(elem.fromDynamicValue)))) + + override def toDynamicValue(a: value.Value): DynamicValue = + DynamicValue.Optional(a.getOptional.value.map(elem.toDynamicValue)) + } + + override def textMap(valueCodec: Type): Type = new Type { + override def fromDynamicValue(dv: DynamicValue): value.Value = { + val entries = dv.textMap.iterator.map { case (k, v) => + value.TextMap.Entry(k, Some(valueCodec.fromDynamicValue(v))) + } + value.Value(Sum.TextMap(value.TextMap(entries = entries.toSeq))) + } + + override def toDynamicValue(a: value.Value): DynamicValue = + DynamicValue.TextMap( + a.getTextMap.entries.view.map(e => e.key -> valueCodec.toDynamicValue(e.getValue)) + ) + } + + override def genMap(keyCodec: Type, valueCodec: Type): Type = new Type { + override def fromDynamicValue(dv: DynamicValue): value.Value = { + val entries = dv.genMap.iterator.map { case (k, v) => + value.GenMap.Entry(Some(keyCodec.fromDynamicValue(k)), Some(valueCodec.fromDynamicValue(v))) + } + value.Value(Sum.GenMap(value.GenMap(entries = entries.toSeq))) + } + + override def toDynamicValue(a: value.Value): DynamicValue = { + val entries = + a.getGenMap.entries.view.map(e => + keyCodec.toDynamicValue(e.getKey) -> valueCodec.toDynamicValue(e.getValue) + ) + DynamicValue.GenMap(entries) + } + } + + override def unit: Type = + primitive(_ => Sum.Unit(Empty()), _ => DynamicValue.Unit) + + override def int64: Type = + primitive(v => Sum.Int64(v.int64), v => DynamicValue.Int64(v.getInt64)) + + override def numeric(scale: Int): Type = + primitive(v => Sum.Numeric(v.numeric), v => DynamicValue.Numeric(v.getNumeric)) + + override def text: Type = + primitive(v => Sum.Text(v.text), v => DynamicValue.Text(v.getText)) + + override def timestamp: Type = + primitive(v => Sum.Timestamp(v.timestamp), v => DynamicValue.Timestamp(v.getTimestamp)) + + override def party: Type = + primitive(v => Sum.Party(v.party), v => DynamicValue.Party(v.getParty)) + + override def bool: Type = + primitive(v => Sum.Bool(v.bool), v => DynamicValue.Bool(v.getBool)) + + override def date: Type = + primitive(v => Sum.Date(v.date), v => DynamicValue.Date(v.getDate)) + + override def contractId(template: Type): Type = + primitive(v => Sum.ContractId(v.contractId), v => DynamicValue.ContractId(v.getContractId)) + + override def interface(id: Identifier): Type = unit + + override def variable(name: TypeVarName, value: Type): Type = value + + private def primitive(f: DynamicValue => Sum, g: value.Value => DynamicValue): Type = new Type { + override def fromDynamicValue(dv: DynamicValue): value.Value = value.Value(f(dv)) + + override def toDynamicValue(a: value.Value): DynamicValue = g(a) + } +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/Dictionary.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/Dictionary.scala new file mode 100644 index 000000000000..a2049f628565 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/Dictionary.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.daml_lf + +import com.digitalasset.daml.lf.data.Ref + +/** Dictionary of templates, keys, choice arguments and choice results. */ +final case class Dictionary[T]( + templates: Map[Ref.Identifier, T], + templateKeys: Map[Ref.Identifier, T], + choiceArguments: Map[(Ref.Identifier, Ref.ChoiceName), T], + choiceResults: Map[(Ref.Identifier, Ref.ChoiceName), T], +) + +object Dictionary { + + /** Collect [[SchemaEntity]] instances into a [[com.digitalasset.daml.lf.data.Ref.Identifier]]-keyed dictionary. Usable in codecs. + */ + def collect[T]: CollectResult[T, Dictionary[T]] = (entities: Seq[SchemaEntity[T]]) => + Dictionary( + entities.collect { case SchemaEntity.Template(id, pkgInfo, payload, key, kind, implements) => + id -> payload + }.toMap, + entities.collect { + case SchemaEntity.Template(id, pkgInfo, payload, Some(key), kind, implements) => id -> key + }.toMap, + entities.collect { + case SchemaEntity.Choice(entityId, pkgInfo, choiceName, argument, result, consuming) => + (entityId, choiceName) -> argument + }.toMap, + entities.collect { + case SchemaEntity.Choice(entityId, pkgInfo, choiceName, argument, result, consuming) => + (entityId, choiceName) -> result + }.toMap, + ) + +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaEntity.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaEntity.scala new file mode 100644 index 000000000000..56b47f66b45a --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaEntity.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.daml_lf + +import com.digitalasset.daml.lf.data.Ref + +/** Top-level Ledger Schema entity. This can either be a template/interface or a choice definition. */ +sealed trait SchemaEntity[P] { def map[Q](f: P => Q): SchemaEntity[Q] } +object SchemaEntity { + final case class PackageInfo( + name: Ref.PackageName, + version: Ref.PackageVersion, + ) + + final case class Template[P]( + id: Ref.Identifier, + packageInfo: PackageInfo, + payload: P, + key: Option[P], + kind: Template.DataKind, + implements: Seq[Ref.Identifier], + ) extends SchemaEntity[P] { + def map[Q](f: P => Q): SchemaEntity[Q] = copy(payload = f(payload), key = key.map(f)) + } + + object Template { + sealed trait DataKind + object DataKind { + case object Template extends DataKind + case object Interface extends DataKind + } + } + + final case class Choice[P]( + entityId: Ref.Identifier, + packageInfo: PackageInfo, + choiceName: Ref.ChoiceName, + argument: P, + result: P, + consuming: Boolean, + ) extends SchemaEntity[P] { + def map[Q](f: P => Q): SchemaEntity[Q] = copy(argument = f(argument), result = f(result)) + } + + /** Returns sequence of [[SchemaEntity]] instances for further processing. */ + def collect[A]: CollectResult[A, Seq[SchemaEntity[A]]] = identity + + def map[A, B](f: A => B): CollectResult[A, Seq[SchemaEntity[B]]] = entities => + entities.map(_.map(f)) +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaProcessor.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaProcessor.scala new file mode 100644 index 000000000000..712997a8e0e0 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/SchemaProcessor.scala @@ -0,0 +1,249 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.daml_lf + +import com.digitalasset.daml.lf.data.Ref +import com.digitalasset.daml.lf.language.Ast.GenDefInterface +import com.digitalasset.daml.lf.language.{Ast, Util} +import com.digitalasset.transcode.daml_lf.SchemaEntity.PackageInfo +import com.digitalasset.transcode.daml_lf.SchemaEntity.Template.DataKind +import com.digitalasset.transcode.schema.* + +import scala.Function.const +import scala.collection.immutable.Seq +import scala.collection.mutable +import scala.util.Try + +/** [[SchemaProcessor]] traverses Daml Schema and produces results by combining + * [[com.digitalasset.transcode.schema.SchemaVisitor]] and Entity Collector ([[CollectResult]]). + */ +object SchemaProcessor { + + /** Process Daml Packages. Provide visitor (codec/codegen) and result collector ([[Dictionary.collect]] etc). + */ + def process[T]( + packages: Map[Ref.PackageId, Ast.PackageSignature], + filter: ((Ref.PackageName, Ref.QualifiedName)) => Boolean = const(true), + )(visitor: SchemaVisitor)( + collect: CollectResult[visitor.Type, T] + ): Either[String, T] = + process(packages.keys, packages.apply, filter)(visitor)(collect) + + /** Process Daml Packages. Package signatures are computed lazily on demand. Provide visitor (codec/codegen) and + * result collector ([[Dictionary.collect]] etc). + */ + def process[T]( + rootPackages: IterableOnce[Ref.PackageId], + getPackageSignature: Ref.PackageId => Ast.PackageSignature, + filter: ((Ref.PackageName, Ref.QualifiedName)) => Boolean, + )(visitor: SchemaVisitor)( + collect: CollectResult[visitor.Type, T] + ): Either[String, T] = + Try { + collect( + new SchemaProcessor[visitor.Type]( + rootPackages, + getPackageSignature, + filter, + visitor, + ).getEntities + ) + }.toEither.left.map { error => + error.printStackTrace() + error.getMessage + } +} + +// warning reason: code is translated from scala3 +@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable")) +private class SchemaProcessor[T]( + rootPackages: IterableOnce[Ref.PackageId], + getPackageSignature: Ref.PackageId => Ast.PackageSignature, + filter: ((Ref.PackageName, Ref.QualifiedName)) => Boolean, + visitor: SchemaVisitor { type Type = T }, +) { + + def getEntities: Seq[SchemaEntity[T]] = { + require( + apiDefinitions.nonEmpty, + "No user-supplied Daml models found on connected ledger. Please, deploy your application's DAR to the ledger.", + ) + apiDefinitions + } + + private type Args = Seq[visitor.Type] + private type VarMap = Seq[(TypeVarName, visitor.Type)] + + private def err(msg: String) = throw new RuntimeException(msg) + + private val getPackage = cached { (pkgId: Ref.PackageId) => + getPackageSignature(pkgId) + } + + private val getInterface = cached { (id: Ref.Identifier) => + val interface = + getPackage(id.packageId).modules(id.qualifiedName.module).interfaces(id.qualifiedName.name) + require( + interface.coImplements.isEmpty, + s"$id contains coImplements definition, which is not supported. Please remove it.", + ) + interface + } + + private val getPackageInfo = cached { (id: Ref.PackageId) => + val metadata = getPackage(id).metadata + PackageInfo(metadata.name, metadata.version) + } + + private val getIdentifier = { (id: Ref.Identifier) => + Identifier(id.packageId, id.qualifiedName.module.dottedName, id.qualifiedName.name.dottedName) + } + + private val getDefinition = cached { (id: Ref.Identifier) => + val module = getPackage(id.packageId).modules(id.qualifiedName.module) + if (module.definitions.contains(id.qualifiedName.name)) { + module.definitions(id.qualifiedName.name) + } else { + module.interfaces(id.qualifiedName.name) + } + } + + // All top-level API definitions (Templates, Choices, Interfaces) + private lazy val apiDefinitions: Seq[SchemaEntity[visitor.Type]] = for { + pkgId <- rootPackages.iterator.toSeq + pkg = getPackage(pkgId) + (moduleName, module) <- pkg.modules + (templateName, template) <- module.templates + + templateId = Ref.Identifier(pkgId, Ref.QualifiedName(moduleName, templateName)) + interfaceIds = template.implements.keys + (included, excluded) = (templateId +: interfaceIds) + .map(id => getPackageInfo(id.packageId).name -> id.qualifiedName) + .partition(filter) + + // if any of interfaces or base classes are included, then the whole hierarchy is included + // orphan interfaces or interfaces with coImplement definitions are not included + pretty = (names: Seq[(Ref.PackageName, Ref.QualifiedName)]) => + names.map { case (p, n) => s"$p:$n" }.mkString(", ") + _ = require( + included.nonEmpty && excluded.isEmpty || included.isEmpty, + s"Expected [${pretty(excluded)}] to be included along with [${pretty(included)}], but former were excluded", + ) if included.nonEmpty + + templateEntity = SchemaEntity.Template( + templateId, + getPackageInfo(templateId.packageId), + fromDef((templateId, Seq.empty)), + template.key.map(x => fromType(x.typ, Seq.empty)), + DataKind.Template, + template.implements.values.map(_.interfaceId).toSeq, + ) + + choiceEntities = template.choices.map { case (name, choice) => + SchemaEntity.Choice( + templateId, + getPackageInfo(templateId.packageId), + choice.name, + fromType(choice.argBinder._2, Seq.empty), + fromType(choice.returnType, Seq.empty), + choice.consuming, + ) + } + + interfaceEntities = template.implements.keys + .map(id => id -> getInterface(id)) + .flatMap { case (id, i) => + val interfaceEntity = SchemaEntity.Template( + id, + getPackageInfo(id.packageId), + fromType(i.view, Seq.empty), + None, + DataKind.Interface, + Seq.empty, + ) + val choiceEntities = i.choices.map { case (name, choice) => + SchemaEntity.Choice( + id, + getPackageInfo(id.packageId), + choice.name, + fromType(choice.argBinder._2, Seq.empty), + fromType(choice.returnType, Seq.empty), + choice.consuming, + ) + }.toSeq + Seq(interfaceEntity) ++ choiceEntities + } + entity <- Seq(templateEntity) ++ choiceEntities ++ interfaceEntities + } yield entity + + // Addressable types that have FQNames + private val fromDef = cached[(Ref.Identifier, Args), visitor.Type] { case (id, args) => + getDefinition(id) match { + case Ast.DDataType(_, params, cons) => + fromCons(id, cons, params.toSeq.map { case (n, _) => TypeVarName(n) } zip args) + case Ast.DTypeSyn(params, typ) => + fromType(typ, params.toSeq.map { case (n, _) => TypeVarName(n) } zip args) + case iface: GenDefInterface[_] => fromType(iface.view, Seq.empty) + case other => err(s"Data type $other is not supported") + } + } + + // Records, Variants and Enums + private def fromCons(id: Ref.Identifier, cons: Ast.DataCons, varMap: VarMap): visitor.Type = + cons match { + case Ast.DataRecord(fields) => + lazy val fieldProcessors = fields.toSeq.map { case (name, typ) => + FieldName(name) -> fromType(typ, varMap) + } + visitor.record(getIdentifier(id), varMap, fieldProcessors) + + case Ast.DataVariant(variants) => + lazy val cases = variants.toSeq.map { case (name, typ) => + VariantConName(name) -> fromType(typ, varMap) + } + visitor.variant(getIdentifier(id), varMap, cases) + + case Ast.DataEnum(constructors) => + visitor.`enum`(getIdentifier(id), constructors.map(EnumConName).toSeq) + + case Ast.DataInterface => + visitor.interface(getIdentifier(id)) + } + + // Simple types and type applications + private def fromType(typ: Ast.Type, varMap: VarMap): visitor.Type = typ match { + case Util.TTyConApp(id, args) => fromDef((id, args.toSeq.map(fromType(_, varMap)))) + case Ast.TSynApp(id, args) => fromDef((id, args.toSeq.map(fromType(_, varMap)))) + + case Ast.TVar(name) => + visitor.variable( + TypeVarName(name), + varMap + .collectFirst { case (n, p) if n == TypeVarName(name) => p } + .getOrElse(err(s"Variable $name not found")), + ) + + case Util.TList(typ) => visitor.list(fromType(typ, varMap)) + case Util.TOptional(typ) => visitor.optional(fromType(typ, varMap)) + case Util.TTextMap(typ) => visitor.textMap(fromType(typ, varMap)) + case Util.TGenMap(kTyp, vTyp) => visitor.genMap(fromType(kTyp, varMap), fromType(vTyp, varMap)) + + case Util.TUnit => visitor.unit + case Util.TBool => visitor.bool + case Util.TText => visitor.text + case Util.TInt64 => visitor.int64 + case Util.TNumeric(Ast.TNat(scale)) => visitor.numeric(scale) + case Util.TTimestamp => visitor.timestamp + case Util.TDate => visitor.date + case Util.TParty => visitor.party + case Util.TContractId(typ) => visitor.contractId(fromType(typ, varMap)) + + case other => err(s"Type $other not supported") + } + + private def cached[K, V](compute: K => V): K => V = { + val cache = mutable.HashMap.empty[K, V] + (k: K) => cache.getOrElseUpdate(k, compute(k)) + } +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/package.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/package.scala new file mode 100644 index 000000000000..48715790f33b --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/daml_lf/package.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode + +/** Note: + * code in this package is a translation to scala2 of code from https://github.com/DACH-NY/transcode + */ +package object daml_lf { + + /** Collect sequence of [[SchemaEntity]] instances into usable result. See [[Dictionary.collect]] and + * [[SchemaEntity.collect]] for several examples + */ + type CollectResult[A, B] = Seq[SchemaEntity[A]] => B +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/DynamicValue.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/DynamicValue.scala new file mode 100644 index 000000000000..ae6550ff3fae --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/DynamicValue.scala @@ -0,0 +1,191 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.schema + +import scala.annotation.nowarn + +/** There are 3 kinds of possible dynamic values: + * - ADTs: records or variants. These are fixed in size, and are containers to other types. + * - Traversables: lists, maps, optionals. These are variable in size and are containers to other types. + * - Primitives: primitive scalar types. + * + * Codecs and code-generations should be constructed in such a way that ADTs and Traversables expecting other + * underlying dynamic values in processing routines should know exactly what underlying values they expect. The + * unwrapping of dynamic value and casting should be safe then. SchemaProcessor takes care of constructing a tree of + * type processors and injects correct underlying processors where needed. Also see examples of how this is used in + * JSON and Grpc codecs. + */ + +sealed trait DynamicValue { + def inner: Any +} + +//suppression reason: code translated from scala3 (opaque type) +@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) +object DynamicValue { + + /////////// + // Kinds // + /////////// + trait Adt extends DynamicValue + trait Traversable extends DynamicValue + trait Primitive extends DynamicValue + + ////////////////////////// + // Algebraic data types // + ////////////////////////// + + /** ADT, Product type. Field values have to be in the same order os in the defining type. Field names here are not + * necessary, as codecs knows about field names at the time of construction + */ + + final case class Record(fields: IterableOnce[DynamicValue]) extends Adt { + override def inner: Any = fields + } + + implicit class RecordExtension(value: DynamicValue) { + def record: IterableOnce[DynamicValue] = value.inner.asInstanceOf[IterableOnce[DynamicValue]] + } + + /** ADT, Sum type. Contains constructor's ordinal index and a wrapped value. */ + + final case class Variant(ctorIx: Int, value: DynamicValue) extends Adt { + override def inner: Any = value + } + + implicit class VariantExtension(value: DynamicValue) { + def variant: Variant = value.asInstanceOf[Variant] + } + + /** ADT, Sum type - special case of sum type with only constructors. */ + + final case class Enum(value: Int) extends Adt { + override def inner: Any = value + } + + implicit class EnumExtension(value: DynamicValue) { + def `enum`: Int = value.asInstanceOf[Enum].value + } + + /////////////////////// + // Traversable types // + /////////////////////// + + /** Sequence of elements */ + final case class List(value: IterableOnce[DynamicValue]) extends Traversable { + override def inner: Any = value + } + implicit class ListExtension(value: DynamicValue) { + def list: IterableOnce[DynamicValue] = value.inner.asInstanceOf[IterableOnce[DynamicValue]] + } + + /** Optional element */ + final case class Optional(value: Option[DynamicValue]) extends Traversable { + override def inner: Any = value + } + + implicit class OptionalExtension(value: DynamicValue) { + def optional: Option[DynamicValue] = value.inner.asInstanceOf[Option[DynamicValue]] + } + + /** Map with String keys. Codecs should maintain stable order of key-value entries if possible. */ + final case class TextMap(value: IterableOnce[(String, DynamicValue)]) extends Traversable { + override def inner: Any = value + } + implicit class TextMapExtension(value: DynamicValue) { + def textMap: IterableOnce[(String, DynamicValue)] = + value.asInstanceOf[IterableOnce[(String, DynamicValue)]] + } + + /** Map with arbitrarily-typed keys and values. Codecs should maintain stable order of key-value entries if possible. + */ + final case class GenMap(value: IterableOnce[(DynamicValue, DynamicValue)]) extends Traversable { + override def inner: Any = value + } + + implicit class GenMapExtension(value: DynamicValue) { + def genMap: IterableOnce[(DynamicValue, DynamicValue)] = + value.inner.asInstanceOf[IterableOnce[(DynamicValue, DynamicValue)]] + } + + ///////////////////// + // Primitive Types // + ///////////////////// + + /** Unit */ + case object Unit extends Primitive { + override def inner: Any = Unit + } + implicit class UnitExtension(value: DynamicValue) { + @nowarn def unit: scala.Unit = value.inner.asInstanceOf[Unit] + } + + /** Boolean */ + final case class Bool(value: Boolean) extends Primitive { + override def inner: Any = value + } + implicit class BoolExtension(value: DynamicValue) { + def bool: Boolean = value.inner.asInstanceOf[Boolean] + } + + /** Text */ + final case class Text(value: String) extends Primitive { + override def inner: Any = value + } + implicit class TextExtension(value: DynamicValue) { + def text: String = value.inner.asInstanceOf[String] + } + + /** 8-byte integer */ + final case class Int64(value: Long) extends Primitive { + override def inner: Any = value + } + + implicit class Int64Extension(value: DynamicValue) { + def int64: Long = value.inner.asInstanceOf[Long] + } + + /** Numeric type with precision. Represented as a String */ + final case class Numeric(value: String) extends Primitive { + override def inner: Any = value + } + + implicit class NumericExtension(value: DynamicValue) { + def numeric: String = value.inner.asInstanceOf[String] + } + + /** Timestamp. Number of microseconds (10^-6^) since epoch (midnight of 1 Jan 1970) in UTC timezone. */ + final case class Timestamp(value: Long) extends Primitive { + override def inner: Any = value + } + + implicit class TimestampExtension(value: DynamicValue) { + def timestamp: Long = value.inner.asInstanceOf[Long] + } + + /** Local date. Number of dates since epoch (1 Jan 1970). */ + final case class Date(value: Int) extends Primitive { + override def inner: Any = Int + } + + implicit class DateExtension(value: DynamicValue) { + def date: Int = value.inner.asInstanceOf[Int] + } + + final case class Party(value: String) extends Primitive { + override def inner: Any = value + } + + implicit class PartyExtension(value: DynamicValue) { + def party: String = value.inner.asInstanceOf[String] + } + + final case class ContractId(value: String) extends Primitive { + override def inner: Any = value + } + + implicit class ContractIdExtension(value: DynamicValue) { + def contractId: String = value.inner.asInstanceOf[String] + } +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala new file mode 100644 index 000000000000..9883f0a2ff38 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/SchemaVisitor.scala @@ -0,0 +1,242 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode.schema + +/** This trait describes various types that a Daml package can contain. `SchemaProcessor` will use an implementation of + * this trait to feed types into the visitor. The visitor implementation should define [[SchemaVisitor.Type]], which + * can be a Codec, or a Code-generator along with instructions how to process daml types. There are several use cases: + * + * ==Codecs== + * + * To achieve the best performance, a codec should create a tree-like structure copying the structure of Daml types + * with each node processing corresponding daml type and delegating processing to the next node if it's a type + * container. + * + * A codec should convert to and from [[DynamicValue]] instances. This allows to compose codecs from various protocols + * by combining them in [[com.digitalasset.transcode.Converter]]. For example, one code combine `JsonCodec` and + * `GrpcValueCodec` to get direct conversions from json to Ledger API proto values and vice versa. Or one can compose + * `JsonCodec` and `ScalaCodec`, etc. + * + * ==Code generators== + * + * Code generators can produce code snippets at each handler and combine them into a file or a set of files that can be + * used as generated source in the target language. + * + * It is advisable to also generate a codec along with DTOs (Data Transfer Object) to allow for direct interoperability + * with other existing protocols (Json or Protobuf). + */ +trait SchemaVisitor { + + /** Visitor handler type for various DAML schema cases. + */ + type Type + + ////////////////////////// + // Algebraic data types // + ////////////////////////// + + // NB: Be aware of potential recursive definitions. Don't instantiate call-by-name parameters in the constructor. + // Alternatively, protect against the loops + + /** ADT, Product type */ + def record( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + fields: => Seq[(FieldName, Type)], + ): Type + + /** ADT, Sum type */ + def variant( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + cases: => Seq[(VariantConName, Type)], + ): Type + + /** ADT, Sum type - special case, where there are only named constructors without further payloads */ + def `enum`(id: Identifier, cases: Seq[EnumConName]): Type + + ////////////////// + // traversables // + ////////////////// + + /** Sequence of elements */ + def list(elem: Type): Type + + /** Optional element */ + def optional(elem: Type): Type + + /** Map with keys of String/Text type */ + def textMap(value: Type): Type + + /** Map with keys and values of any type */ + def genMap(key: Type, value: Type): Type + + //////////////// + // primitives // + //////////////// + + /** Unit */ + def unit: Type + + /** Boolean */ + def bool: Type + + /** Text */ + def text: Type + + /** 8-byte Integer */ + def int64: Type + + /** Numeric with scale */ + def numeric(scale: Int): Type + + /** Timestamp */ + def timestamp: Type + + /** Date */ + def date: Type + + /** Party */ + def party: Type + + /** Contract Id, parametrized with the processor for corresponding template */ + def contractId(template: Type): Type + + /////////// + // other // + /////////// + + /** Interface. Used in code-gens. There is no representation of interface in Dynamic Value */ + def interface(name: Identifier): Type + + /** Type Variable. + * + * Codecs might want to use `value` substitution, effectively replacing type variables with concrete types, while + * code generators might want to use type variable names. + */ + def variable(name: TypeVarName, value: Type): Type + +} + +object SchemaVisitor { + + def compose[T](left: SchemaVisitor, right: SchemaVisitor) = new SchemaVisitor { + + type Type = (left.Type, right.Type) + + private def lefts[A](seq: Seq[(A, Type)]): Seq[(A, left.Type)] = seq.map { case (n, t) => + (n, t._1) + } + + private def rights[A](seq: Seq[(A, Type)]): Seq[(A, right.Type)] = seq.map { case (n, t) => + (n, t._2) + } + + override def record( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + fields: => Seq[(FieldName, Type)], + ): Type = + ( + left.record(id, lefts(appliedArgs), lefts(fields)), + right.record(id, rights(appliedArgs), rights(fields)), + ) + + override def variant( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + cases: => Seq[(VariantConName, Type)], + ): Type = + ( + left.variant(id, lefts(appliedArgs), lefts(cases)), + right.variant(id, rights(appliedArgs), rights(cases)), + ) + + override def `enum`(id: Identifier, cases: Seq[EnumConName]): Type = + (left.`enum`(id, cases), right.`enum`(id, cases)) + + override def list(elem: Type): Type = (left.list(elem._1), right.list(elem._2)) + + override def optional(elem: Type): Type = (left.optional(elem._1), right.optional(elem._2)) + + override def textMap(value: Type): Type = (left.textMap(value._1), right.textMap(value._2)) + + override def genMap(key: Type, value: Type): Type = + (left.genMap(key._1, value._1), right.genMap(key._2, value._2)) + + override def unit: Type = (left.unit, right.unit) + + override def bool: Type = (left.bool, right.bool) + + override def text: Type = (left.text, right.text) + + override def int64: Type = (left.int64, right.int64) + + override def numeric(scale: Int): Type = (left.numeric(scale), right.numeric(scale)) + + override def timestamp: Type = (left.timestamp, right.timestamp) + + override def date: Type = (left.date, right.date) + + override def party: Type = (left.party, right.party) + + override def contractId(template: Type): Type = + (left.contractId(template._1), right.contractId(template._2)) + + override def interface(id: Identifier): Type = (left.interface(id), right.interface(id)) + + override def variable(name: TypeVarName, value: Type): Type = + (left.variable(name, value._1), right.variable(name, value._2)) + + } + + /** Trait implementing all the cases and allowing to override only partially what's needed */ + trait Unit extends SchemaVisitor { + type Type = _root_.scala.Unit + + override def record( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + fields: => Seq[(FieldName, Type)], + ): Type = {} + + override def variant( + id: Identifier, + appliedArgs: => Seq[(TypeVarName, Type)], + cases: => Seq[(VariantConName, Type)], + ): Type = {} + + override def `enum`(id: Identifier, cases: Seq[EnumConName]): Type = {} + + override def list(elem: Type): Type = {} + + override def optional(elem: Type): Type = {} + + override def textMap(value: Type): Type = {} + + override def genMap(key: Type, value: Type): Type = {} + + override def unit: Type = {} + + override def bool: Type = {} + + override def text: Type = {} + + override def int64: Type = {} + + override def numeric(scale: Int): Type = {} + + override def timestamp: Type = {} + + override def date: Type = {} + + override def party: Type = {} + + override def contractId(template: Type): Type = {} + + override def interface(id: Identifier): Type = {} + + override def variable(name: TypeVarName, value: Type): Type = {} + } +} diff --git a/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/package.scala b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/package.scala new file mode 100644 index 000000000000..9da6e8df6978 --- /dev/null +++ b/sdk/canton/community/ledger/transcode/src/main/scala/com/digitalasset/transcode/schema/package.scala @@ -0,0 +1,61 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.transcode + +/** Note: + * code in this package is a translation to scala2 of code from https://github.com/DACH-NY/transcode + */ +package schema { + final case class Identifier(packageId: String, moduleName: String, entityName: String) { + lazy val qualifiedName = s"$moduleName:$entityName" + } + + object Identifier { + private[transcode] def fromString(str: String): Identifier = str.split(':') match { + case Array(pkgId, module, name) => Identifier(pkgId, module, name) + case other => throw new Exception(s"Unsupported identifier format: $str") + } + } +} +package object schema { + + type TypeVarName = String + + def TypeVarName(value: String): TypeVarName = value + + implicit class TypeVarNameExtension(value: TypeVarName) { + def typeVarName: String = value + } + + type FieldName = String + + def FieldName(value: String): FieldName = value + + implicit class FieldNameExtensiond(value: FieldName) { + def fieldName: String = value + } + type EnumConName = String + + def EnumConName(value: String): EnumConName = value + + implicit class EnumConNameExtension(value: EnumConName) { + def enumConName: String = value + } + + type ChoiceName = String + + def ChoiceName(value: String): ChoiceName = value + + implicit class ChoiceNameExtension(value: ChoiceName) { + def choiceName: String = value + } + + type VariantConName = String + + def VariantConName(value: String): VariantConName = value + + implicit class VariantConNameExtension(value: VariantConName) { + def variantConName: String = value + } +} diff --git a/sdk/canton/community/participant/src/main/daml/daml.yaml b/sdk/canton/community/participant/src/main/daml/daml.yaml index d01a2c26c1e5..2522c4f17aab 100644 --- a/sdk/canton/community/participant/src/main/daml/daml.yaml +++ b/sdk/canton/community/participant/src/main/daml/daml.yaml @@ -1,4 +1,4 @@ -sdk-version: 3.2.0-snapshot.20240809.13222.0.vbff35dd8 +sdk-version: 3.2.0-snapshot.20240814.13230.0.vc62dc41c build-options: - --target=2.1 name: AdminWorkflows diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala index ebbdb9ceefdd..aac5f830aa57 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala @@ -37,9 +37,9 @@ import com.digitalasset.canton.networking.grpc.{ CantonMutableHandlerRegistry, StaticGrpcServices, } -import com.digitalasset.canton.participant.admin.* import com.digitalasset.canton.participant.admin.grpc.* import com.digitalasset.canton.participant.admin.workflows.java.canton +import com.digitalasset.canton.participant.admin.{PackageDependencyResolver, *} import com.digitalasset.canton.participant.config.* import com.digitalasset.canton.participant.domain.grpc.GrpcDomainRegistry import com.digitalasset.canton.participant.domain.{DomainAliasManager, DomainAliasResolution} @@ -66,6 +66,7 @@ import com.digitalasset.canton.participant.sync.* import com.digitalasset.canton.participant.topology.ParticipantTopologyManagerError.IdentityManagerParentError import com.digitalasset.canton.participant.topology.{ LedgerServerPartyNotifier, + ParticipantPackageVettingValidation, ParticipantTopologyDispatcher, ParticipantTopologyManagerError, ParticipantTopologyManagerOps, @@ -89,7 +90,7 @@ import com.digitalasset.canton.topology.client.{ StoreBasedDomainTopologyClient, StoreBasedTopologySnapshot, } -import com.digitalasset.canton.topology.store.TopologyStoreId.DomainStore +import com.digitalasset.canton.topology.store.TopologyStoreId.{AuthorizedStore, DomainStore} import com.digitalasset.canton.topology.store.{PartyMetadataStore, TopologyStore, TopologyStoreId} import com.digitalasset.canton.topology.transaction.{ HostingParticipant, @@ -144,9 +145,13 @@ class ParticipantNodeBootstrap( ParticipantMetrics, ](arguments) { - // TODO(#12946) clean up to remove SingleUseCell private val cantonSyncService = new SingleUseCell[CantonSyncService] + private val packageDependencyResolver = new SingleUseCell[PackageDependencyResolver] + private def tryGetPackageDependencyResolver(): PackageDependencyResolver = + packageDependencyResolver.getOrElse( + sys.error("packageDependencyResolver should be defined") + ) override protected def sequencedTopologyStores: Seq[TopologyStore[DomainStore]] = cantonSyncService.get.toList.flatMap(_.syncDomainPersistentStateManager.getAll.values).collect { case s: SyncDomainPersistentState => s.topologyStore @@ -177,6 +182,53 @@ class ParticipantNodeBootstrap( ): BootstrapStageOrLeaf[ParticipantNode] = new StartupNode(storage, crypto, adminServerRegistry, nodeId, manager, healthService) + override protected def createAuthorizedTopologyManager( + nodeId: UniqueIdentifier, + crypto: Crypto, + authorizedStore: TopologyStore[AuthorizedStore], + storage: Storage, + ): AuthorizedTopologyManager = { + val resolver = new PackageDependencyResolver( + DamlPackageStore( + storage, + arguments.futureSupervisor, + arguments.parameterConfig, + exitOnFatalFailures = parameters.exitOnFatalFailures, + loggerFactory, + ), + arguments.parameterConfig.processingTimeouts, + loggerFactory, + ) + val _ = packageDependencyResolver.putIfAbsent(resolver) + + val topologyManager = new AuthorizedTopologyManager( + nodeId, + clock, + crypto, + authorizedStore, + exitOnFatalFailures = parameters.exitOnFatalFailures, + bootstrapStageCallback.timeouts, + futureSupervisor, + bootstrapStageCallback.loggerFactory, + ) with ParticipantPackageVettingValidation { + + override def validatePackages( + currentlyVettedPackages: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], + forceFlags: ForceFlags, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = + checkPackageDependencies( + currentlyVettedPackages, + nextPackageIds, + resolver, + forceFlags, + ) + } + topologyManager + } + private class StartupNode( storage: Storage, crypto: Crypto, @@ -192,18 +244,6 @@ class ParticipantNodeBootstrap( private val participantId = ParticipantId(nodeId) - private val packageDependencyResolver = new PackageDependencyResolver( - DamlPackageStore( - storage, - arguments.futureSupervisor, - arguments.parameterConfig, - exitOnFatalFailures = parameters.exitOnFatalFailures, - loggerFactory, - ), - arguments.parameterConfig.processingTimeouts, - loggerFactory, - ) - private def createSyncDomainAndTopologyDispatcher( aliasResolution: DomainAliasResolution, indexedStringStore: IndexedStringStore, @@ -214,9 +254,9 @@ class ParticipantNodeBootstrap( storage, indexedStringStore, parameters, - config.topology, crypto, clock, + tryGetPackageDependencyResolver(), futureSupervisor, loggerFactory, ) @@ -430,7 +470,7 @@ class ParticipantNodeBootstrap( partyNotifierFactory, adminToken, participantOps, - packageDependencyResolver, + tryGetPackageDependencyResolver(), createSyncDomainAndTopologyDispatcher, createPackageOps, ).map { @@ -453,7 +493,7 @@ class ParticipantNodeBootstrap( addCloseable(sync) addCloseable(ledgerApiServer) addCloseable(ledgerApiDependentServices) - addCloseable(packageDependencyResolver) + addCloseable(tryGetPackageDependencyResolver()) val node = new ParticipantNode( participantId, arguments.metrics, @@ -704,7 +744,7 @@ class ParticipantNodeBootstrap( sequencerInfoLoader = new SequencerInfoLoader( parameterConfig.processingTimeouts, parameterConfig.tracing.propagation, - ProtocolVersionCompatibility.supportedProtocolsParticipant(parameterConfig), + ProtocolVersionCompatibility.supportedProtocols(parameterConfig), parameterConfig.protocolConfig.minimumProtocolVersion, parameterConfig.protocolConfig.dontWarnOnDeprecatedPV, loggerFactory, @@ -1105,21 +1145,21 @@ class ParticipantNode( def readyDomains: Map[DomainId, Boolean] = sync.readyDomains.values.toMap - override def status: Future[ParticipantStatus] = { + override def status: ParticipantStatus = { val ports = Map("ledger" -> config.ledgerApi.port, "admin" -> config.adminApi.port) val domains = readyDomains val topologyQueues = identityPusher.queueStatus - Future.successful( - ParticipantStatus( - id.uid, - uptime(), - ports, - domains, - sync.isActive(), - topologyQueues, - healthData, - ) + + ParticipantStatus( + id.uid, + uptime(), + ports, + domains, + sync.isActive(), + topologyQueues, + healthData, ) + } override def isActive: Boolean = storage.isActive diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala index 3b90bebce0fa..b5e89bcdd131 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageDependencyResolver.scala @@ -43,6 +43,13 @@ class PackageDependencyResolver( ): EitherT[FutureUnlessShutdown, PackageId, Set[PackageId]] = EitherT(dependencyCache.getUS(packageId).map(_.map(_ - packageId))) + def packageDependencies(packages: List[PackageId])(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, PackageId, Set[PackageId]] = + packages + .parTraverse(packageDependencies) + .map(_.flatten.toSet -- packages) + def getPackageDescription(packageId: PackageId)(implicit traceContext: TraceContext ): Future[Option[PackageDescription]] = damlPackageStore.getPackageDescription(packageId) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageOps.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageOps.scala index a0b9488f1dc5..517d20f7cebf 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageOps.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/PackageOps.scala @@ -213,7 +213,7 @@ class PackageOpsImpl( signingKeys = Seq(participantId.fingerprint), protocolVersion = initialProtocolVersion, expectFullAuthorization = true, - forceChanges = ForceFlags(ForceFlag.PackageVettingRevocation), + forceChanges = ForceFlags(ForceFlag.AllowUnvetPackage), ) .leftMap(IdentityManagerParentError(_): ParticipantTopologyManagerError) .map(_ => ()) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcDomainConnectivityService.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcDomainConnectivityService.scala index cf3420745c58..ebbbaa5092b9 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcDomainConnectivityService.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcDomainConnectivityService.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.error.BaseCantonError import com.digitalasset.canton.lifecycle.{CloseContext, FutureUnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil +import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.* import com.digitalasset.canton.participant.domain.{ DomainAliasManager, @@ -31,6 +32,7 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.EitherTUtil import com.digitalasset.canton.util.ShowUtil.* +import io.grpc.Status import scala.concurrent.{ExecutionContext, Future} @@ -128,6 +130,27 @@ class GrpcDomainConnectivityService( CantonGrpcUtil.mapErrNewEUS(ret) } + override def logout(request: v30.LogoutRequest): Future[v30.LogoutResponse] = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + val v30.LogoutRequest(domainAliasP) = request + + val ret = for { + domainAlias <- EitherT + .fromEither[Future](DomainAlias.create(domainAliasP)) + .leftMap(err => + Status.INVALID_ARGUMENT + .withDescription(s"Failed to parse domain alias: $err") + .asRuntimeException() + ) + _ <- sync + .logout(domainAlias) + .leftMap(err => err.asRuntimeException()) + .onShutdown(Left(AbortedDueToShutdown.Error().asGrpcError)) + } yield v30.LogoutResponse() + + EitherTUtil.toFuture(ret) + } + override def listConnectedDomains( request: v30.ListConnectedDomainsRequest ): Future[v30.ListConnectedDomainsResponse] = diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/domain/DomainRegistryHelpers.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/domain/DomainRegistryHelpers.scala index 9e2efa18d777..f53a12fc5a91 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/domain/DomainRegistryHelpers.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/domain/DomainRegistryHelpers.scala @@ -184,7 +184,7 @@ trait DomainRegistryHelpers extends FlagCloseable with NamedLogging { this: HasF metrics(config.domain).sequencerClient, participantNodeParameters.loggingConfig, domainLoggerFactory, - ProtocolVersionCompatibility.supportedProtocolsParticipant(participantNodeParameters), + ProtocolVersionCompatibility.supportedProtocols(participantNodeParameters), participantNodeParameters.protocolConfig.minimumProtocolVersion, ) } diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SyncDomainPersistentState.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SyncDomainPersistentState.scala index 2901c4f9b17d..efd02c4503e3 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SyncDomainPersistentState.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/SyncDomainPersistentState.scala @@ -7,6 +7,7 @@ import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.crypto.{Crypto, CryptoPureApi} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.participant.ParticipantNodeParameters +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.store.EventLogId.DomainEventLogId import com.digitalasset.canton.participant.store.db.DbSyncDomainPersistentState import com.digitalasset.canton.participant.store.memory.InMemorySyncDomainPersistentState @@ -59,6 +60,7 @@ object SyncDomainPersistentState { crypto: Crypto, parameters: ParticipantNodeParameters, indexedStringStore: IndexedStringStore, + packageDependencyResolver: PackageDependencyResolver, loggerFactory: NamedLoggerFactory, futureSupervisor: FutureSupervisor, )(implicit ec: ExecutionContext): SyncDomainPersistentState = { @@ -74,6 +76,7 @@ object SyncDomainPersistentState { parameters.enableAdditionalConsistencyChecks, indexedStringStore, exitOnFatalFailures = parameters.exitOnFatalFailures, + packageDependencyResolver, domainLoggerFactory, parameters.processingTimeouts, futureSupervisor, @@ -88,6 +91,7 @@ object SyncDomainPersistentState { crypto, parameters, indexedStringStore, + packageDependencyResolver, domainLoggerFactory, futureSupervisor, ) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSyncDomainPersistentState.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSyncDomainPersistentState.scala index decac9ea2378..afe221ab1df6 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSyncDomainPersistentState.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbSyncDomainPersistentState.scala @@ -3,13 +3,17 @@ package com.digitalasset.canton.participant.store.db +import cats.data.EitherT +import com.digitalasset.canton.LfPackageId import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.crypto.{Crypto, CryptoPureApi} -import com.digitalasset.canton.lifecycle.Lifecycle +import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, Lifecycle} import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.participant.ParticipantNodeParameters +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.store.EventLogId.DomainEventLogId import com.digitalasset.canton.participant.store.SyncDomainPersistentState +import com.digitalasset.canton.participant.topology.ParticipantPackageVettingValidation import com.digitalasset.canton.protocol.TargetDomainId import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.store.db.{DbSequencedEventStore, DbSequencerCounterTrackerStore} @@ -18,8 +22,14 @@ import com.digitalasset.canton.store.{IndexedDomain, IndexedStringStore} import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.store.TopologyStoreId.DomainStore import com.digitalasset.canton.topology.store.db.DbTopologyStore -import com.digitalasset.canton.topology.{DomainOutboxQueue, DomainTopologyManager, ParticipantId} -import com.digitalasset.canton.tracing.NoTracing +import com.digitalasset.canton.topology.{ + DomainOutboxQueue, + DomainTopologyManager, + ForceFlags, + ParticipantId, + TopologyManagerError, +} +import com.digitalasset.canton.tracing.{NoTracing, TraceContext} import com.digitalasset.canton.version.Transfer.TargetProtocolVersion import com.digitalasset.canton.version.{ProtocolVersion, ReleaseProtocolVersion} @@ -34,6 +44,7 @@ class DbSyncDomainPersistentState( crypto: Crypto, parameters: ParticipantNodeParameters, indexedStringStore: IndexedStringStore, + packageDependencyResolver: PackageDependencyResolver, val loggerFactory: NamedLoggerFactory, val futureSupervisor: FutureSupervisor, )(implicit ec: ExecutionContext) @@ -156,7 +167,22 @@ class DbSyncDomainPersistentState( timeouts = timeouts, futureSupervisor = futureSupervisor, loggerFactory = loggerFactory, - ) + ) with ParticipantPackageVettingValidation { + + override def validatePackages( + currentlyVettedPackages: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], + forceFlags: ForceFlags, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = + checkPackageDependencies( + currentlyVettedPackages, + nextPackageIds, + packageDependencyResolver, + forceFlags, + ) + } override def close(): Unit = Lifecycle.close( diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncDomainPersistentState.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncDomainPersistentState.scala index 3494054fb6b8..33c4ad040e81 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncDomainPersistentState.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/memory/InMemorySyncDomainPersistentState.scala @@ -3,12 +3,17 @@ package com.digitalasset.canton.participant.store.memory +import cats.data.EitherT +import com.digitalasset.canton.LfPackageId import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.crypto.{Crypto, CryptoPureApi} +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.store.EventLogId.DomainEventLogId import com.digitalasset.canton.participant.store.SyncDomainPersistentState +import com.digitalasset.canton.participant.topology.ParticipantPackageVettingValidation import com.digitalasset.canton.protocol.TargetDomainId import com.digitalasset.canton.store.memory.{ InMemorySendTrackerStore, @@ -19,7 +24,14 @@ import com.digitalasset.canton.store.{IndexedDomain, IndexedStringStore} import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.store.TopologyStoreId.DomainStore import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStore -import com.digitalasset.canton.topology.{DomainOutboxQueue, DomainTopologyManager, ParticipantId} +import com.digitalasset.canton.topology.{ + DomainOutboxQueue, + DomainTopologyManager, + ForceFlags, + ParticipantId, + TopologyManagerError, +} +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.version.ProtocolVersion import scala.concurrent.ExecutionContext @@ -33,6 +45,7 @@ class InMemorySyncDomainPersistentState( override val enableAdditionalConsistencyChecks: Boolean, indexedStringStore: IndexedStringStore, exitOnFatalFailures: Boolean, + packageDependencyResolver: PackageDependencyResolver, val loggerFactory: NamedLoggerFactory, val timeouts: ProcessingTimeout, val futureSupervisor: FutureSupervisor, @@ -74,7 +87,22 @@ class InMemorySyncDomainPersistentState( timeouts, futureSupervisor, loggerFactory, - ) + ) with ParticipantPackageVettingValidation { + + override def validatePackages( + currentlyVettedPackages: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], + forceFlags: ForceFlags, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = + checkPackageDependencies( + currentlyVettedPackages, + nextPackageIds, + packageDependencyResolver, + forceFlags, + ) + } override def isMemory: Boolean = true diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala index a18bc8984b9d..96ce14470829 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/CantonSyncService.scala @@ -110,6 +110,7 @@ import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, SubmissionId} import com.digitalasset.daml.lf.data.{ImmArray, Ref} import com.digitalasset.daml.lf.engine.Engine import com.google.protobuf.ByteString +import io.grpc.Status import io.opentelemetry.api.trace.Tracer import org.apache.pekko.NotUsed import org.apache.pekko.stream.Materializer @@ -1518,6 +1519,24 @@ class CantonSyncService( ) } + def logout(domainAlias: DomainAlias)(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, Status, Unit] = + for { + domainId <- EitherT.fromOption[FutureUnlessShutdown]( + aliasManager.domainIdForAlias(domainAlias), + Status.INVALID_ARGUMENT.withDescription( + s"The domain with alias ${domainAlias.unwrap} is unknown." + ), + ) + _ <- connectedDomainsMap + .get(domainId) + .fold(EitherT.pure[FutureUnlessShutdown, Status] { + logger.info(show"Nothing to do, as we are not connected to $domainAlias") + () + })(syncDomain => syncDomain.logout()) + } yield () + private def performDomainDisconnect( domain: DomainAlias )(implicit traceContext: TraceContext): Either[SyncServiceError, Unit] = { diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala index 4f0d04e2d89d..da43ebd58c53 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomain.scala @@ -90,6 +90,7 @@ import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.{EitherUtil, ErrorUtil, FutureUtil, MonadUtil} import com.digitalasset.canton.version.Transfer.{SourceProtocolVersion, TargetProtocolVersion} import com.digitalasset.daml.lf.engine.Engine +import io.grpc.Status import io.opentelemetry.api.trace.Tracer import org.apache.pekko.stream.Materializer @@ -657,7 +658,7 @@ class SyncDomain( )(implicit traceContext: TraceContext): FutureUnlessShutdown[Unit] = Seq( topologyProcessor.subscriptionStartsAt(start, domainTimeTracker)(traceContext), - trafficProcessor.subscriptionStartsAt(start)(traceContext), + trafficProcessor.subscriptionStartsAt(start, domainTimeTracker)(traceContext), ).parSequence_ override def apply( @@ -901,6 +902,9 @@ class SyncDomain( def numberOfDirtyRequests(): Int = ephemeral.requestJournal.numberOfDirtyRequests + def logout(): EitherT[FutureUnlessShutdown, Status, Unit] = + sequencerClient.logout() + override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = // As the commitment and protocol processors use the sequencer client to send messages, close // them before closing the domainHandle. Both of them will ignore the requests from the message dispatcher diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainPersistentStateManager.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainPersistentStateManager.scala index 95b1d0ceb1af..ead4ae63737c 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainPersistentStateManager.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/sync/SyncDomainPersistentStateManager.scala @@ -7,7 +7,6 @@ import cats.data.EitherT import cats.syntax.parallel.* import com.digitalasset.canton.DomainAlias import com.digitalasset.canton.concurrent.FutureSupervisor -import com.digitalasset.canton.config.TopologyConfig import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps @@ -18,6 +17,7 @@ import com.digitalasset.canton.environment.{ import com.digitalasset.canton.lifecycle.Lifecycle import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.participant.ParticipantNodeParameters +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.domain.{DomainAliasResolution, DomainRegistryError} import com.digitalasset.canton.participant.store.* import com.digitalasset.canton.participant.topology.TopologyComponentFactory @@ -50,9 +50,9 @@ class SyncDomainPersistentStateManager( storage: Storage, val indexedStringStore: IndexedStringStore, parameters: ParticipantNodeParameters, - topologyConfig: TopologyConfig, crypto: Crypto, clock: Clock, + packageDependencyResolver: PackageDependencyResolver, futureSupervisor: FutureSupervisor, protected val loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext) @@ -206,6 +206,7 @@ class SyncDomainPersistentStateManager( crypto, parameters, indexedStringStore, + packageDependencyResolver, loggerFactory, futureSupervisor, ) diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantPackageVettingValidation.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantPackageVettingValidation.scala new file mode 100644 index 000000000000..57040d96908d --- /dev/null +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantPackageVettingValidation.scala @@ -0,0 +1,53 @@ +// 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.topology + +import cats.data.EitherT +import com.digitalasset.canton.LfPackageId +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.logging.NamedLogging +import com.digitalasset.canton.participant.admin.PackageDependencyResolver +import com.digitalasset.canton.topology.TopologyManagerError.ParticipantTopologyManagerError +import com.digitalasset.canton.topology.{ForceFlag, ForceFlags, TopologyManagerError} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.daml.lf.data.Ref.PackageId + +import scala.concurrent.ExecutionContext + +trait ParticipantPackageVettingValidation extends NamedLogging { + def checkPackageDependencies( + headPackageIds: Set[LfPackageId], + nextPackageIds: Set[LfPackageId], + packageDependencyResolver: PackageDependencyResolver, + forceFlags: ForceFlags, + )(implicit + traceContext: TraceContext, + ec: ExecutionContext, + ): EitherT[FutureUnlessShutdown, TopologyManagerError, Unit] = { + val toBeAdded = nextPackageIds -- headPackageIds + for { + dependencies <- packageDependencyResolver + .packageDependencies(toBeAdded.toList) + .leftFlatMap[Set[PackageId], TopologyManagerError] { missing => + if (forceFlags.permits(ForceFlag.AllowUnknownPackage)) + EitherT.rightT(Set.empty) + else + EitherT.leftT( + ParticipantTopologyManagerError.CannotVetDueToMissingPackages + .Missing(missing): TopologyManagerError + ) + } + + // check that all dependencies are vetted. + unvetted = dependencies -- headPackageIds + _ <- EitherT + .cond[FutureUnlessShutdown]( + unvetted.isEmpty || forceFlags.permits(ForceFlag.AllowUnvettedDependencies), + (), + ParticipantTopologyManagerError.DependenciesNotVetted + .Reject(unvetted): TopologyManagerError, + ) + } yield () + } +} diff --git a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyManager.scala b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyManager.scala index 4004f624f9df..7db57b8a5fa8 100644 --- a/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyManager.scala +++ b/sdk/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/ParticipantTopologyManager.scala @@ -14,7 +14,6 @@ import com.digitalasset.canton.topology.* import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.version.ProtocolVersion -import com.digitalasset.daml.lf.data.Ref.PackageId trait ParticipantTopologyManagerOps { def allocateParty( @@ -38,44 +37,6 @@ object ParticipantTopologyManagerError extends ParticipantErrorGroup { override def logOnCreation: Boolean = false } - @Explanation( - """This error indicates a vetting request failed due to dependencies not being vetted. - |On every vetting request, the set supplied packages is analysed for dependencies. The - |system requires that not only the main packages are vetted explicitly but also all dependencies. - |This is necessary as not all participants are required to have the same packages installed and therefore - |not every participant can resolve the dependencies implicitly.""" - ) - @Resolution("Vet the dependencies first and then repeat your attempt.") - object DependenciesNotVetted - extends ErrorCode( - id = "DEPENDENCIES_NOT_VETTED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(unvetted: Set[PackageId])(implicit - val loggingContext: ErrorLoggingContext - ) extends CantonError.Impl( - cause = "Package vetting failed due to dependencies not being vetted" - ) - with ParticipantTopologyManagerError - } - - @Explanation( - """This error indicates that a request involving topology management was attempted on a participant that is not yet initialised. - |During initialisation, only namespace and identifier delegations can be managed.""" - ) - @Resolution("Initialise the participant and retry.") - object UninitializedParticipant - extends ErrorCode( - id = "UNINITIALIZED_PARTICIPANT", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Reject(_cause: String)(implicit val loggingContext: ErrorLoggingContext) - extends CantonError.Impl( - cause = _cause - ) - with ParticipantTopologyManagerError - } - @Explanation( """This error indicates that a dangerous PartyToParticipant mapping deletion was rejected. |If the command is run and there are active contracts where the party is a stakeholder these contracts diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala index 5238a6ea6c63..bdb48a1cc55c 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/admin/PackageOpsTest.scala @@ -327,7 +327,7 @@ class PackageOpsTest extends PackageOpsTestBase { eqTo(Seq(participantId.fingerprint)), eqTo(testedProtocolVersion), eqTo(true), - eqTo(ForceFlags(ForceFlag.PackageVettingRevocation)), + eqTo(ForceFlags(ForceFlag.AllowUnvetPackage)), )(anyTraceContext) ).thenReturn(EitherT.rightT(signedTopologyTransaction(List(pkgId2)))) diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala index 10cf93950da5..7931adc4ad3d 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/ProtocolProcessorTest.scala @@ -28,6 +28,7 @@ import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.ledger.participant.state.CompletionInfo import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.pretty.Pretty +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.config.LedgerApiServerConfig import com.digitalasset.canton.participant.metrics.ParticipantTestMetrics import com.digitalasset.canton.participant.protocol.EngineController.EngineAbortStatus @@ -243,6 +244,7 @@ class ProtocolProcessorTest ) = { val multiDomainEventLog = mock[MultiDomainEventLog] + val packageDependencyResolver = mock[PackageDependencyResolver] val clock = new WallClock(timeouts, loggerFactory) val persistentState = new InMemorySyncDomainPersistentState( @@ -254,6 +256,7 @@ class ProtocolProcessorTest enableAdditionalConsistencyChecks = true, new InMemoryIndexedStringStore(minIndex = 1, maxIndex = 1), // only one domain needed exitOnFatalFailures = true, + packageDependencyResolver, loggerFactory, timeouts, futureSupervisor, diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferInProcessingStepsTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferInProcessingStepsTest.scala index 2260677de080..36d920b4930c 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferInProcessingStepsTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferInProcessingStepsTest.scala @@ -14,6 +14,7 @@ import com.digitalasset.canton.crypto.provider.symbolic.{SymbolicCrypto, Symboli import com.digitalasset.canton.data.ViewType.TransferInViewType import com.digitalasset.canton.data.{CantonTimestamp, FullTransferInTree, TransferSubmitterMetadata} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.metrics.ParticipantTestMetrics import com.digitalasset.canton.participant.protocol.EngineController.EngineAbortStatus import com.digitalasset.canton.participant.protocol.conflictdetection.ConflictDetectionHelpers.{ @@ -136,6 +137,7 @@ class TransferInProcessingStepsTest testedProtocolVersion, enableAdditionalConsistencyChecks = true, indexedStringStore = indexedStringStore, + packageDependencyResolver = mock[PackageDependencyResolver], loggerFactory = loggerFactory, exitOnFatalFailures = true, timeouts = timeouts, diff --git a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferOutProcessingStepsTest.scala b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferOutProcessingStepsTest.scala index 2e1274b0bb15..56eba4094e4e 100644 --- a/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferOutProcessingStepsTest.scala +++ b/sdk/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/protocol/transfer/TransferOutProcessingStepsTest.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.data.{ TransferSubmitterMetadata, } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.participant.admin.PackageDependencyResolver import com.digitalasset.canton.participant.metrics.ParticipantTestMetrics import com.digitalasset.canton.participant.protocol.EngineController.EngineAbortStatus import com.digitalasset.canton.participant.protocol.conflictdetection.ConflictDetectionHelpers.{ @@ -147,6 +148,7 @@ final class TransferOutProcessingStepsTest enableAdditionalConsistencyChecks = true, indexedStringStore = indexedStringStore, exitOnFatalFailures = true, + packageDependencyResolver = mock[PackageDependencyResolver], loggerFactory, timeouts, futureSupervisor, diff --git a/sdk/canton/daml-common-staging/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala b/sdk/canton/daml-common-staging/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala index b30d9d4294f4..6acd397cc869 100644 --- a/sdk/canton/daml-common-staging/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala +++ b/sdk/canton/daml-common-staging/util-external/src/main/scala/com/digitalasset/canton/config/RequireTypes.scala @@ -185,6 +185,7 @@ object RequireTypes { object NonNegativeInt { lazy val zero: NonNegativeInt = NonNegativeInt.tryCreate(0) lazy val one: NonNegativeInt = NonNegativeInt.tryCreate(1) + lazy val two: NonNegativeInt = NonNegativeInt.tryCreate(2) lazy val maxValue: NonNegativeInt = NonNegativeInt.tryCreate(Int.MaxValue) def create(n: Int): Either[InvariantViolation, NonNegativeInt] = NonNegativeNumeric.create(n) diff --git a/sdk/canton/ref b/sdk/canton/ref index 3e92ff0cf2c1..8b79fc8819e4 100644 --- a/sdk/canton/ref +++ b/sdk/canton/ref @@ -1 +1 @@ -20240812.13862.v8f7e949f +20240817.13886.vfc183f82 diff --git a/sdk/maven_install_2.13.json b/sdk/maven_install_2.13.json index 97cf01ed4ede..2686073e729c 100644 --- a/sdk/maven_install_2.13.json +++ b/sdk/maven_install_2.13.json @@ -1,8 +1,8 @@ { "dependency_tree": { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": -1400919288, - "__RESOLVED_ARTIFACTS_HASH": -715849621, + "__INPUT_ARTIFACTS_HASH": 2072636804, + "__RESOLVED_ARTIFACTS_HASH": -133603466, "conflict_resolution": {}, "dependencies": [ { @@ -1920,7 +1920,7 @@ "dependencies": [ "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", "com.lihaoyi:sourcecode_2.13:0.3.0", @@ -1946,7 +1946,7 @@ "dependencies": [ "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0", @@ -1975,7 +1975,7 @@ "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", "com.lihaoyi:fastparse_2.13:2.3.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2026,7 +2026,7 @@ "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2068,7 +2068,7 @@ "com.lihaoyi:ammonite-compiler-interface_2.13.11:2.5.9", "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", "com.lihaoyi:sourcecode_2.13:0.3.0", @@ -2109,7 +2109,7 @@ "com.lihaoyi:ammonite-compiler-interface_2.13.11:jar:sources:2.5.9", "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0", @@ -2146,7 +2146,7 @@ "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", "com.lihaoyi:fastparse_2.13:2.3.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2208,7 +2208,7 @@ "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2260,7 +2260,7 @@ "com.lihaoyi:ammonite-interp-api_2.13.11:2.5.9", "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2298,7 +2298,7 @@ "com.lihaoyi:ammonite-interp-api_2.13.11:jar:sources:2.5.9", "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2337,7 +2337,7 @@ "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", "com.lihaoyi:fastparse_2.13:2.3.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2405,7 +2405,7 @@ "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2463,7 +2463,7 @@ "com.lihaoyi:ammonite-repl-api_2.13.11:2.5.9", "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2506,7 +2506,7 @@ "com.lihaoyi:ammonite-repl-api_2.13.11:jar:sources:2.5.9", "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2586,7 +2586,7 @@ "coord": "com.lihaoyi:ammonite-util_2.13:2.5.9", "dependencies": [ "com.lihaoyi:fansi_2.13:0.4.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", "com.lihaoyi:sourcecode_2.13:0.3.0", @@ -2615,7 +2615,7 @@ "coord": "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "dependencies": [ "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0", @@ -2655,7 +2655,7 @@ "com.lihaoyi:ammonite-util_2.13:2.5.9", "com.lihaoyi:fansi_2.13:0.4.0", "com.lihaoyi:fastparse_2.13:2.3.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:mainargs_2.13:0.3.0", "com.lihaoyi:os-lib_2.13:0.8.0", "com.lihaoyi:pprint_2.13:0.8.1", @@ -2730,7 +2730,7 @@ "com.lihaoyi:ammonite-util_2.13:jar:sources:2.5.9", "com.lihaoyi:fansi_2.13:jar:sources:0.4.0", "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:mainargs_2.13:jar:sources:0.3.0", "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "com.lihaoyi:pprint_2.13:jar:sources:0.8.1", @@ -2825,11 +2825,11 @@ { "coord": "com.lihaoyi:fastparse_2.13:2.3.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/fastparse_2.13/2.3.0/fastparse_2.13-2.3.0.jar", @@ -2846,11 +2846,11 @@ { "coord": "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/fastparse_2.13/2.3.0/fastparse_2.13-2.3.0-sources.jar", @@ -2862,38 +2862,38 @@ "url": "https://repo1.maven.org/maven2/com/lihaoyi/fastparse_2.13/2.3.0/fastparse_2.13-2.3.0-sources.jar" }, { - "coord": "com.lihaoyi:geny_2.13:0.7.1", + "coord": "com.lihaoyi:geny_2.13:1.1.1", "dependencies": [ "org.scala-lang:scala-library:2.13.11" ], "directDependencies": [ "org.scala-lang:scala-library:2.13.11" ], - "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1.jar", + "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1.jar", "mirror_urls": [ - "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1.jar" + "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1.jar" ], "packages": [ "geny" ], - "sha256": "fc01dab696f7b84ba5ac28bbf2e60d8bcbd9ab96717e50fdbeb4e8acf3452a56", - "url": "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1.jar" + "sha256": "20af231c222fc71c29e06b3cd8d9190a16b412da83cc49fb0b778cf2dc6f94d2", + "url": "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1.jar" }, { - "coord": "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "coord": "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "dependencies": [ "org.scala-lang:scala-library:jar:sources:2.13.11" ], "directDependencies": [ "org.scala-lang:scala-library:jar:sources:2.13.11" ], - "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1-sources.jar", + "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1-sources.jar", "mirror_urls": [ - "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1-sources.jar" + "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1-sources.jar" ], "packages": [], - "sha256": "8726bfd65672e238c745e9977e853fea45aae08b22ce6139d8d37f478100ebf0", - "url": "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/0.7.1/geny_2.13-0.7.1-sources.jar" + "sha256": "bd850d83f722194fc9160241210d4fc2bc613ecdcc9a11bfd6930a7b776d554f", + "url": "https://repo1.maven.org/maven2/com/lihaoyi/geny_2.13/1.1.1/geny_2.13-1.1.1-sources.jar" }, { "coord": "com.lihaoyi:mainargs_2.13:0.3.0", @@ -2940,10 +2940,10 @@ { "coord": "com.lihaoyi:os-lib_2.13:0.8.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1" + "com.lihaoyi:geny_2.13:1.1.1" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:0.7.1" + "com.lihaoyi:geny_2.13:1.1.1" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/os-lib_2.13/0.8.0/os-lib_2.13-0.8.0.jar", "mirror_urls": [ @@ -2958,10 +2958,10 @@ { "coord": "com.lihaoyi:os-lib_2.13:jar:sources:0.8.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1" + "com.lihaoyi:geny_2.13:jar:sources:1.1.1" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1" + "com.lihaoyi:geny_2.13:jar:sources:1.1.1" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/os-lib_2.13/0.8.0/os-lib_2.13-0.8.0-sources.jar", "mirror_urls": [ @@ -3017,10 +3017,10 @@ { "coord": "com.lihaoyi:requests_2.13:0.7.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1" + "com.lihaoyi:geny_2.13:1.1.1" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:0.7.1" + "com.lihaoyi:geny_2.13:1.1.1" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/requests_2.13/0.7.0/requests_2.13-0.7.0.jar", "mirror_urls": [ @@ -3035,10 +3035,10 @@ { "coord": "com.lihaoyi:requests_2.13:jar:sources:0.7.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1" + "com.lihaoyi:geny_2.13:jar:sources:1.1.1" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1" + "com.lihaoyi:geny_2.13:jar:sources:1.1.1" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/requests_2.13/0.7.0/requests_2.13-0.7.0-sources.jar", "mirror_urls": [ @@ -3052,7 +3052,7 @@ "coord": "com.lihaoyi:scalaparse_2.13:2.3.0", "dependencies": [ "com.lihaoyi:fastparse_2.13:2.3.0", - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0" ], "directDependencies": [ @@ -3073,7 +3073,7 @@ "coord": "com.lihaoyi:scalaparse_2.13:jar:sources:2.3.0", "dependencies": [ "com.lihaoyi:fastparse_2.13:jar:sources:2.3.0", - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0" ], "directDependencies": [ @@ -3121,10 +3121,56 @@ "sha256": "cc84a3a8bff5412e444131014cc0e23428b6fb65d2a5791d339f4be808f230da", "url": "https://repo1.maven.org/maven2/com/lihaoyi/sourcecode_2.13/0.3.0/sourcecode_2.13-0.3.0-sources.jar" }, + { + "coord": "com.lihaoyi:ujson-circe_2.13:2.0.0", + "dependencies": [ + "com.lihaoyi:geny_2.13:1.1.1", + "com.lihaoyi:ujson_2.13:2.0.0", + "com.lihaoyi:upickle-core_2.13:2.0.0", + "io.circe:circe-parser_2.13:0.14.2", + "org.scala-lang:scala-library:2.13.11" + ], + "directDependencies": [ + "com.lihaoyi:ujson_2.13:2.0.0", + "io.circe:circe-parser_2.13:0.14.2", + "org.scala-lang:scala-library:2.13.11" + ], + "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0.jar" + ], + "packages": [ + "ujson.circe" + ], + "sha256": "279be8b32bb7a31d66ff661c7bcbf86f26e3a3522e9d9fad00b43dc71071dcf0", + "url": "https://repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0.jar" + }, + { + "coord": "com.lihaoyi:ujson-circe_2.13:jar:sources:2.0.0", + "dependencies": [ + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", + "com.lihaoyi:ujson_2.13:jar:sources:2.0.0", + "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", + "io.circe:circe-parser_2.13:jar:sources:0.14.2", + "org.scala-lang:scala-library:jar:sources:2.13.11" + ], + "directDependencies": [ + "com.lihaoyi:ujson_2.13:jar:sources:2.0.0", + "io.circe:circe-parser_2.13:jar:sources:0.14.2", + "org.scala-lang:scala-library:jar:sources:2.13.11" + ], + "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0-sources.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0-sources.jar" + ], + "packages": [], + "sha256": "9974078545385f956a241412a3b8692ce8cefe97f6eb9d8aef77d19ab2379e68", + "url": "https://repo1.maven.org/maven2/com/lihaoyi/ujson-circe_2.13/2.0.0/ujson-circe_2.13-2.0.0-sources.jar" + }, { "coord": "com.lihaoyi:ujson_2.13:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:upickle-core_2.13:2.0.0", "org.scala-lang:scala-library:2.13.11" ], @@ -3145,7 +3191,7 @@ { "coord": "com.lihaoyi:ujson_2.13:jar:sources:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", "org.scala-lang:scala-library:jar:sources:2.13.11" ], @@ -3164,7 +3210,7 @@ { "coord": "com.lihaoyi:upack_2.13:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:upickle-core_2.13:2.0.0", "org.scala-lang:scala-library:2.13.11" ], @@ -3185,7 +3231,7 @@ { "coord": "com.lihaoyi:upack_2.13:jar:sources:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", "org.scala-lang:scala-library:jar:sources:2.13.11" ], @@ -3204,11 +3250,11 @@ { "coord": "com.lihaoyi:upickle-core_2.13:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "org.scala-lang:scala-library:2.13.11" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "org.scala-lang:scala-library:2.13.11" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/upickle-core_2.13/2.0.0/upickle-core_2.13-2.0.0.jar", @@ -3225,11 +3271,11 @@ { "coord": "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "org.scala-lang:scala-library:jar:sources:2.13.11" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "org.scala-lang:scala-library:jar:sources:2.13.11" ], "file": "v1/https/repo1.maven.org/maven2/com/lihaoyi/upickle-core_2.13/2.0.0/upickle-core_2.13-2.0.0-sources.jar", @@ -3243,7 +3289,7 @@ { "coord": "com.lihaoyi:upickle-implicits_2.13:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:upickle-core_2.13:2.0.0", "org.scala-lang:scala-library:2.13.11" ], @@ -3265,7 +3311,7 @@ { "coord": "com.lihaoyi:upickle-implicits_2.13:jar:sources:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", "org.scala-lang:scala-library:jar:sources:2.13.11" ], @@ -3284,7 +3330,7 @@ { "coord": "com.lihaoyi:upickle_2.13:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:ujson_2.13:2.0.0", "com.lihaoyi:upack_2.13:2.0.0", "com.lihaoyi:upickle-core_2.13:2.0.0", @@ -3310,7 +3356,7 @@ { "coord": "com.lihaoyi:upickle_2.13:jar:sources:2.0.0", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:ujson_2.13:jar:sources:2.0.0", "com.lihaoyi:upack_2.13:jar:sources:2.0.0", "com.lihaoyi:upickle-core_2.13:jar:sources:2.0.0", @@ -15135,11 +15181,11 @@ { "coord": "org.scalameta:fastparse-v2_2.13:2.3.1", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0" ], "file": "v1/https/repo1.maven.org/maven2/org/scalameta/fastparse-v2_2.13/2.3.1/fastparse-v2_2.13-2.3.1.jar", @@ -15156,11 +15202,11 @@ { "coord": "org.scalameta:fastparse-v2_2.13:jar:sources:2.3.1", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0" ], "directDependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0" ], "file": "v1/https/repo1.maven.org/maven2/org/scalameta/fastparse-v2_2.13/2.3.1/fastparse-v2_2.13-2.3.1-sources.jar", @@ -15263,7 +15309,7 @@ { "coord": "org.scalameta:trees_2.13:4.7.8", "dependencies": [ - "com.lihaoyi:geny_2.13:0.7.1", + "com.lihaoyi:geny_2.13:1.1.1", "com.lihaoyi:sourcecode_2.13:0.3.0", "com.thesamet.scalapb:scalapb-runtime_2.13:0.11.8", "org.scala-lang:scala-library:2.13.11", @@ -15302,7 +15348,7 @@ { "coord": "org.scalameta:trees_2.13:jar:sources:4.7.8", "dependencies": [ - "com.lihaoyi:geny_2.13:jar:sources:0.7.1", + "com.lihaoyi:geny_2.13:jar:sources:1.1.1", "com.lihaoyi:sourcecode_2.13:jar:sources:0.3.0", "com.thesamet.scalapb:scalapb-runtime_2.13:jar:sources:0.11.8", "org.scala-lang:scala-library:jar:sources:2.13.11",